nodeのプロジェクトにESLintを導入してみる
undersourcecode.hatenablog.com
以前こんなのを書いていたが、さすがに2年以上前だと古くなっているので書き直してみます。
とはいっても、今回はLambdaは関係ないし、ESLint公式のGetting Startedそのままです。
まあ自分宛の備忘録ってことで。
ESLint Getting Started
Getting Started with ESLint - ESLint - Pluggable JavaScript linter
プロジェクト作成
node、npmは入っている前提で。
$ npm install eslint --save-dev $ npm init $ npx eslint --init ? How would you like to use ESLint? To check syntax and find problems ? What type of modules does your project use? JavaScript modules (import/export) ? Which framework does your project use? None of these ? Does your project use TypeScript? No ? Where does your code run? Node ? What format do you want your config file to be in? JavaScript ・・・
今回は画面がないJavaScriptファイルだけのプロジェクトだが、画面があるときなどは「$ npx eslint --init」の選択肢は変わってくるでしょう。
.eslintrc.js
ESLintの設定項目の一覧は以下を参照。
List of available rules - ESLint - Pluggable JavaScript linter
取り敢えずrules欄に2つだけ追加してみました。
"rules": { "semi": ["error", "always"], "quotes": ["error", "double"], }
実行コマンド
以下のコマンドでESListのチェックと、main.jsの実行をします。
$ npx eslint ./*.js $ node main.js
Gorillaのcontextとmuxを触ってみる
Gorilla, the golang web toolkit
GorillaというGolangのWeb向けツールキットについて調べ始めました。
Webアプリ用に色々なものが用意されているのですが、フレームワークではなく、あくまで必要なものを自分で取捨選択してつかう「ツールキット」です。
今回はこのGorillaの中から、contextとmuxを触ってみました。以下、公式のサンプルを参考に自分が書いてみたソースのメモ書きです。
context
context - Gorilla, the golang web toolkit
contextはリクエストの有効期間中、値を保存しておく機能です。何のこっちゃという気もしますが、ソースを見た方が手っ取り早いかと思います。
package main import ( "fmt" "log" "net/http" "time" ) const key1 Key = "key1" const key2 Key = "key2" // Key reprisents context key. type Key string // GetContext returns a value for this package from the request values. func GetContext(r *http.Request, key Key) string { if rv := context.Get(r, key); rv != nil { return rv.(string) } return "" } // SetContext sets a value for this package in the request values. func SetContext(r *http.Request, key Key, val string) { context.Set(r, key, val) } func contextHandler(w http.ResponseWriter, r *http.Request) { urlValue := r.URL.Path[1:] SetContext(r, key1, urlValue) SetContext(r, key2, time.Now().Format("2006/01/02 15:04:05")) // do something... value1 := GetContext(r, key1) value2 := GetContext(r, key2) fmt.Fprintf(w, "get key1 = %s\n", value1) fmt.Fprintf(w, "get key2 = %s\n", value2) } func main() { http.HandleFunc("/context/", contextHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
contextHandler内で、「http://localhost:8080/context/sample」のようなURLから「context/sample」の部分を抜き出し、キーを「key1」としてcontextに保存しています。
また現在日時を「key2」に保存しています。
「// do something...」で何らかの処理を行ったと仮定し、その後でcontextより値を取得して出力しています。
上記をビルドして実行し、curlを叩いてみると以下のように出力されます。(現在日時の部分は異なってきますが・・・)
$ curl http://localhost:8080/context/sample get key1 = context/sample get key2 = 2020/03/11 21:54:08
contextに設定した値が、無事取得できているようです。
mux
mux - Gorilla, the golang web toolkit
muxはザックリと書くと、URLのルーティング周りの機能を提供しています。
上記の公式が分かりやすいのですが、
- URLパラメータを含むURLのルーティング
- URLパラメータは正規表現で指定できる
- サブルーティングでグループ化も可能
- 静的ページの格納先パスを指定
- middlewareによる共通処理の実装
などです。
これらのサンプルを書いてみました。
標準的な使い方
先に書いた
- URLパラメータを含むURLのルーティング
- URLパラメータは正規表現で指定できる
- サブルーティングでグループ化も可能
を実装しています。
package main import ( "fmt" "log" "net/http" "github.com/gorilla/mux" ) func productHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) key := vars["key"] fmt.Fprintf(w, "get key = %s\n", key) } func articlesCategoryHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) category := vars["category"] sort := vars["sort"] fmt.Fprintf(w, "get category = %s, sort = %s\n", category, sort) } func articleHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] fmt.Fprintf(w, "get id = %s\n", id) } func main() { r := mux.NewRouter() r.HandleFunc("/products/{key}", productHandler).Methods("GET") s := r.PathPrefix("/articles").Subrouter() s.HandleFunc("/{category}/{sort:(?:asc|desc|new)}", articlesCategoryHandler).Methods("GET") s.HandleFunc("/{category}/{id:[0-9]+}", articleHandler).Methods("GET") http.Handle("/", r) log.Fatal(http.ListenAndServe(":8080", nil)) }
「/products/」はURLパラメータを、「/category/」はURLパラメータの正規表現での定義、およびサブルーディングでグループ化してURLパラメータが「asc」「desc」「new」の場合と数値の場合とで別々のハンドラを指定しています。
ビルドしてcurlで実行すると以下のようになります。
$ curl http://localhost:8080/products/xxx get key = xxx $ curl http://localhost:8080/articles/ccc/asc get category = ccc, sort = asc $ curl http://localhost:8080/articles/ccc/10 get id = 10
静的ページ
cssや画像ファイルなど静的ページを格納することがあるかと思います。その静的ページの格納先の例となります。
今回は「static」というフォルダ内の「hello.html」という静的なHTMLを用意し、それを呼び出すサンプルを書いてみました。
package main import ( "log" "net/http" "github.com/gorilla/mux" ) const staticDir = "/static/" func main() { r := mux.NewRouter() r.PathPrefix(staticDir). Handler(http.StripPrefix(staticDir, http.FileServer(http.Dir("."+staticDir)))) http.Handle("/", r) log.Fatal(http.ListenAndServe(":8080", nil)) }
実行してcurlでこんな感じで呼び出すと、hello.htmlの内容が返却されるはずです。
$ curl http://localhost:8080/static/hello.html
middleware
各ハンドラに共通の処理を実装したい場合、middlewareという機能を使い、その中に共通処理を実装する方法があります。basic認証を共通処理として実装するサンプルを作ってみました。
package main import ( "fmt" "log" "net/http" "time" "github.com/gorilla/mux" ) // User ... type User struct { Name string Password string } // BasicAuthMiddleware ... type BasicAuthMiddleware struct { Users []User } // NewBasicAuthMiddleware ... func NewBasicAuthMiddleware() BasicAuthMiddleware { user1 := User{"test", "pass"} user2 := User{"hello", "world"} users := []User{user1, user2} return BasicAuthMiddleware{users} } // Authenticate ... func (mwr *BasicAuthMiddleware) Authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { username, password, _ := r.BasicAuth() log.Println("Middleware Authenticated.") // Authenticate logic exist := false for _, u := range mwr.Users { if u.Name == username && u.Password == password { exist = true break } } if exist { next.ServeHTTP(w, r) } else { http.Error(w, "Forbidden", http.StatusForbidden) } }) } func helloHandler(w http.ResponseWriter, r *http.Request) { timeStr := time.Now().Format("2006/01/02 15:04:05") fmt.Fprintf(w, "Hello, now is %s.\n", timeStr) } func main() { r := mux.NewRouter() r.HandleFunc("/hello", helloHandler) mwr := NewBasicAuthMiddleware() r.Use(mwr.Authenticate) http.Handle("/", r) log.Fatal(http.ListenAndServe(":8080", nil)) }
パスワードを平文 + べた書きで保持していますが(笑)、サンプルなので適当にやっています。本来であればデータベースなどにハッシュ化して保持するべきでしょう。。。
「helloHandler」というハンドラに対して、BasicAuthMiddleware 構造体の「Authenticate」メソッドを共通処理として事前に実行するようにしています。
実行してcurlを叩くと、ユーザとパスワードが正しいかを判定していることが分かるかと思います。
$ username=test $ password=pass $ curl -i --basic -u $username:$password http://localhost:8080/hello
ざっと書いたサンプルを上げてみましたが、以上です。
「TypeScriptネットワークプログラミング」を読んだ
TypeScriptに興味が出てきたのと、タイトルが気になったので
「HTML5/WebSocket/WebRTCによる TypeScriptネットワークプログラミング」を読んでみました。
CUTT System:TypeScriptネットワークプログラミング
物凄く短いですが、読書記録代わりの感想などを書いてみます。
感想
「TypeScriptは何をしてくれるのか?」という項目があり、TypeScriptのメリットなどが書かれています。
TypeScriptについて解説したあと、解説した機能を使ったプログラムのサンプルが記載されているところが分かりやすかったです。
またチャットやSkype風のビデオ通話など身近なものを題材にしている点も、ネットワークプログラミングの理解を助けてくれると感じました。
WebRTCやP2Pなどネットワークに関する概要説明もあり、入門にはいいと思いました。
ただif文やfor文などの説明や、データベースのInsert文も出てくることなどから、全くの初心者というよりは、何らかのプログラムを組んだことがある人がTypeScriptやネットワークプログラミングに入門するときに読む本なのかなとも思いました。
小並な感想ですが、以上です。
aws cdk でGolangのLambdaをデプロイしてみる
前回に引き続き、aws cdkについてです。今回はGolangのLambda FunctionをAWS上にデプロイしてみました。
ソースは以下に上げてあります。
GitHub - SrcHndWng/cdk-lambda
プロジェクトはnpxを使って作成したので、プロジェクトの作成などは以前の下記記事を参照ください。
aws-cdkを触ってみた - ソースコードから理解する技術-UnderSourceCode
以下、ポイントについて説明していきます。
実装のポイント
フォルダ構成について
プロジェクト直下に「lambda」フォルダを作成し、その中にGolangのLambdaのソースを入れました。以下のようになります。(一部だけですが)
- lambda/ ・・・ 新規で作るフォルダ - bin/ - main ・・・ main.goをビルドしたバイナリ。後述します。 - main.go ・・・ Lambdaのソース - lib/ - cdk-lambda-stack.ts - node_modules/ - README.md
Lambdaについて
先に書いたように、「lambda」フォルダを作成して、その中にGolangのLambdaを実装しました。ソースは以下のようになります。
lambda/main.go
package main import ( "context" "fmt" "time" "github.com/aws/aws-lambda-go/lambda" ) const timeFormat = "2006-01-02 15:04:05" const messageFormat = "Hello, now is %s!" // MyEvent ... type MyEvent struct { Name string `json:"name"` } // HandleRequest ... func HandleRequest(ctx context.Context) (string, error) { t := time.Now() message := createMessage(t) return message, nil } func createMessage(t time.Time) string { return fmt.Sprintf(messageFormat, t.Format(timeFormat)) } func main() { lambda.Start(HandleRequest) }
単純に現在時刻と挨拶を返すだけの処理です。GolangのLambdaはバイナリでデプロイするため、「lambda」フォルダに移動して以下のコマンドでビルドします。
$ go build -o bin/main
ビルドすると「lambda/bin/main」が作成されます。が、後述するようにcdkのプロジェクトのビルド時に、このコマンドも実行してGolangのビルドもできるようにします。
cdk-lambda-stack.ts
cdkのメイン処理です。ソースは以下のようになります。
lib/cdk-lambda-stack.ts
import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; export class CdkLambdaStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here new lambda.Function( this, 'GoFunction', { functionName: 'GoFunction', runtime: lambda.Runtime.GO_1_X, handler: 'main', code: lambda.Code.asset('./lambda/bin') }) } }
「new lambda.Function」がデプロイするLambdaの定義です。Lambda Function名、Runtimeを指定した後
を指定しています。
実行
Lambdaのデプロイをする際には、「bootstrap」も必要になるようです。ビルドも、以下のコマンドでデプロイします。
$ npm run build $ npx cdk bootstrap $ npx cdk deploy
aws cdk でVPCの中にEC2を立ててみた
aws cdkを使い、VPCの中にEC2を立ててみました。よくあるパターンなので既に色んな記事が書かれていますが、以下の点を工夫してみました。
※少し改修して記事にも反映しました
- セキュリティグループを新規に作るが、Ingressはデフォルトでは何も許可しない(EC2に接続するときに手動で自分のIPのみ許可する運用を想定)
- キーペアは別に作っておき、定義ファイルにてキーペア名を指定
- デプロイするstage(dev、prdなど)毎に定義ファイルを用意する
- EC2のベースとなるAMIのIDを定義ファイルで指定
- キーペアも定義ファイルで指定
- 実行時、デプロイ対象のstage、~/.aws/credentials のprofileを指定する
以下、CDKのソースについてです。またこちらにも上げてあります。
GitHub - SrcHndWng/cdk-vpc-ec2 at v1.1.0
実装について
CDKのプロジェクトを作り、変更したのは
です。それぞれのソースを表示します。
lib/cdk-vpc-ec2-stack.ts
import cdk = require('@aws-cdk/core'); import ec2 = require('@aws-cdk/aws-ec2') import fs = require('fs'); export class CdkVpcEc2Stack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here const stage: string = this.node.tryGetContext("stage") ? this.node.tryGetContext("stage") : 'dev'; const config = JSON.parse(fs.readFileSync(`.config/config-${stage}.json`, {encoding: 'utf-8'})); const vpc = new ec2.Vpc(this, 'myVpc', { cidr: '10.0.0.0/16', }) // vpc has no inbound rules. const securityGroup = new ec2.SecurityGroup(this, 'mySecurityGroup', { vpc: vpc, securityGroupName: 'mySecurityGroup', }) const instance = new ec2.CfnInstance(this, 'myEC2', { imageId: config.ami, instanceType: 't2.micro', keyName: config.keyName, subnetId: vpc.publicSubnets[0].subnetId, securityGroupIds: [securityGroup.securityGroupId] }) new cdk.CfnOutput(this, 'stage', { value: stage }) new cdk.CfnOutput(this, 'VPC', { value: vpc.vpcId }) new cdk.CfnOutput(this, 'Security Group', { value: securityGroup.securityGroupId }) new cdk.CfnOutput(this, 'EC2 PublicIP', { value: instance.attrPublicIp }) } }
こんな感じとなりました。VPC、SecurityGroup、EC2の順に作成しているのが分かるかと思います。またstageを取得し(デフォルトは「dev」として)、config-{stage}.jsonを読み込んでいることも分かるかと思います。
実行
http://undersourcecode.hatenablog.com/entry/2019/12/12/212936
こちらの前回と同様に、npxでインストールしたので、実行時にもnpxを指定します。
ビルド、CloudFormationの作成、デプロイの順で、以下のコマンドで実行できます。stage、~/.aws/creadentialsのプロファイルを指定します。
$ npm run build $ npx cdk synth -c stage=dev --profile your_profile $ npx cdk deploy -c stage=dev --profile your_profile
デプロイし、セキュリティグループに自分のIPを許可し、SSHなどで接続できることが確認できたら成功です。
Visual Studio CodeのExtensionを作り、ローカルのWSLで実行する
Visual Studio Codeの拡張機能(Extension)を作り、ローカルで実行してみました。
手順については公式や様々なサイトに書かれていますが、自分がやったことを纏めておきます。
やったこと
- Extensionをローカルで作成する
- ローカルで動かしてみる(Windows)
- packageを作り、WSLで動かしてみる
Extensionをローカルで作成する
こちらは公式のチュートリアル通りに雛形をつくり、任意の機能を追加しました。
Your First Extension | Visual Studio Code Extension API
ただ「generator-code」はグローバル領域ではなくローカルにインストールしたので、いくつかコマンドが異なりました。
インストール
$ npm install yo generator-code
プロジェクト作成
$ npx yo code
ローカルで動かしてみる(Windows)
私はWindowsで開発したのですが、ローカルで動かす場合は作成したプロジェクトのフォルダ毎
「%USERPROFILE%.vscode\extensions」にコピーすることで動きました。
packageを作り、WSLで動かしてみる
Visual Studio CodeをWindows上で実行する際には、先に書いたようにプロジェクトフォルダをコピーすることで動かすことはできたのですが
WSL上で動かすにはフォルダのコピーでは出来ませんでした。。。
「vsce」というVisual Studio Code Extensions のコマンドラインツールを使い、公開用のファイル(.vsix)を作り、Visual Studio CodeのWSLモードでExtensionsとしてインストールする必要があります。
以下、簡単な作業の流れとなります。
Azure DevOpsにアカウントを作り「Personal Access Token」を取得する。
Create a new organization, project collection - Azure DevOps | Microsoft Docs
Publishing Extensions | Visual Studio Code Extension API
この辺りを参考に行う必要があります。注意点としては「Personal Access Token」の「Organization」を「All accessible organizations」とする必要があります。
(これをしないと次の「$ npx vsce create-publisher」で401エラーなどになります)
vsceのインストール
これもグローバル領域ではなくローカルにインストールします。
$ npm install vsce
$ npx vsce --version
packageを作り、インストール
vsceを使いpackageを作ります。package作成時に作られる公開用ファイル(.vsix)をVisual Studio CodeのExtensionとしてインストールし、WSL上で使ってみます。
Publisherの登録
Extensionを公開しなくても必要なようです。
$ npx vsce create-publisher YOUR_PUBLISHER_NAME
packageの作成
以下を実行すると公開用ファイル(.vsix)が作成され、フルパスが表示されます。
$ npx vsce package
Extensionのインストール
Visual Studio CodeをWSLモードで起動し、 Extensionsパネルの右上の「...」より「Install from VSIX...」から、先に作成した公開用ファイル(.vsix)を読み込みます。
aws-cdkを触ってみた
少し(かなり?)遅れた感はありますが、aws-cdkを触ってみました。
https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/getting_started.html#hello_world_tutorial
こちらの公式の「Hello World Tutorial」をほぼなぞっただけですが、いくつか独自にアレンジしたところもあるので、メモ書き程度に残しておきます。
※ aws-cdkのインストールはローカルにインストールからnpxを使ったインストールに変更しました
変更したところ
- (TypeScriptでやったのだが) aws-cdkのモジュールはnpxにインストールした
- Package.jsonの「scripts」にコマンドを用意してaws-cdkを各操作を出来るようにした
- プログラムで構築できることを検証するため、動的にS3のバケット名を生成してみた
以下、変更したところの詳細です。
aws-cdkを使ってのプロジェクト作成
「Hello World Tutorial」ではaws-cdkをグローバル領域にインストールしていますが、バージョンアップなどされることも考慮して、今回はnpxでプロジェクトを作成することにしました。
プロジェクト名は「hello-cdk-npx」とし、プロジェクト名と同名のフォルダを作成して以下のように実行します。
$ mkdir hello-cdk-npx $ cd hello-cdk-npx $ npx cdk init app --language=typescript $ npx cdk --version
package.json
良く使うコマンドは全てpackage.jsonの「Scripts」に纏めました。
こうすれば「$ npm run deploy」のように「$ npm run ~」で統一して実行できます。以下、今回作成したpackage.jsonの「scripts」です。
"scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "cdk": "cdk", "synth": "npx cdk synth", "diff": "npx cdk diff", "deploy": "npx cdk deploy", "destroy": "npx cdk destroy" },
動的にS3のバケット名を生成
TypeScriptのプログラムで、日付を動的に取得してバケット名として指定してみました。
lib/hello-cdk-npx-stack.ts は以下のようになりました。
import core = require('@aws-cdk/core'); import s3 = require('@aws-cdk/aws-s3'); export class HelloCdkNpxStack extends core.Stack { constructor(scope: core.Construct, id: string, props?: core.StackProps) { super(scope, id, props); // The code that defines your stack goes here const now = new Date(); const yyyymmdd = now.getFullYear().toString() + (now.getMonth()+1).toString() + now.getDate().toString(); const myBucketName = "hello-cdk-" + yyyymmdd; new s3.Bucket(this, 'MyFirstBucket', { versioned: true, bucketName: myBucketName }); } }
バケット名を指定している以外は、以下の「Hello World Tutorial」から変わっていないです。
https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/getting_started.html#hello_world_tutorial
実行
ここまで作成すれば、Makefileを使用して以下のコマンドでビルド、デプロイ、破棄を行うことができます。
$ npm run build $ npm run synth $ npm run deploy $ npm run destroy
以上です。