ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

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を指定した後

  • handler ・・・ ビルドしたGolangのバイナリ名
  • code ・・・ lambda.Code.assetの引数として、ビルドしたGolangのバイナリ格納先フォルダパス

を指定しています。

package.json

cdkのビルド時にGolangもビルドできるよう、「npm run build」で実行するコマンドを修正します。

(中略)
  "scripts": {
    "build": "tsc; cd lambda; go build -o bin/main",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },
(中略)

実行

Lambdaのデプロイをする際には、「bootstrap」も必要になるようです。ビルドも、以下のコマンドでデプロイします。

$ npm run build
$ npx cdk bootstrap
$ npx cdk deploy

削除について

「$ npx cdk destroy」でデプロイしたLambdaは削除されますが、「bootstrap」で作られたCloudFormationは残ってしまいます。このCloudFormationをマネージメントコンソールから削除しようとすると、S3の「cdktoolkit~」バケットが削除できない旨のエラーとなります。

対応としては、「cdktoolkit~」バケットの中身を手動で削除し、CloudFormationを削除することで完全に削除することが出来そうです。

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 ・・・ 変更
  • .config/config-dev.json ・・・ 追加

です。それぞれのソースを表示します。

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を読み込んでいることも分かるかと思います。

.config/config-dev.json

{
    "ami": "your-favorite-ami-id",
    "keyName": "your-key-name"
}

こちらは「.config」フォルダを作り、新規に作成しました。AMIのIDと、EC2のキーペア名を定義します。ファイル名の「-dev」の部分は、stage名としてください。例えば本番用なら「config-prd.json」として、後述する実行時にstageを指定します。

実行

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」にコピーすることで動きました。

MacLinuxであれば「$HOME/.vscode/extensions」にコピーすれば動くようです。

※参考
Visual Studio Code の拡張機能を作ってみた - Qiita

packageを作り、WSLで動かしてみる

Visual Studio CodeWindows上で実行する際には、先に書いたようにプロジェクトフォルダをコピーすることで動かすことはできたのですが
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.jsonの「publisher」を定義

package.jsonの「publisher」項目を追加し、先のPublisherの登録時に使ったPublisher名を記載します。

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

以上です。