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
以上です。
AnsibleでEC2にSSH接続してファイルをアップロードする
仕事でAnsibleを少し触る機会があったのですが、初見でさっぱり分からなかったので
復習を兼ねてインストールからやってみました。
タイトルにも書きましたが、やったことは
です。
以下、やったことのメモとなります。
Ansibleのインストール
ローカルマシンにAnsibleをインストールします。私はWSLのUbuntuを使ったので、公式の以下の手順を参考にしました。
https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#latest-releases-via-apt-ubuntu
Ansibleはエージェントレスで動き、先にも書いたようにSSHで接続して操作を行うため、EC2に何かをインストールする必要はありません。
プロジェクト作成
ローカルの任意のフォルダをプロジェクトフォルダとし、以下のファイルを用意します。
- ssh_config ・・・ SSHでの接続先を定義
- inventory.ini ・・・ ssh_configで定義した接続先を指定
- ansible.cnf ・・・ SSHの接続オプションを指定
- sample.yml ・・・ 実行するタスク(今回はフォルダの作成、ファイルのアップロード)を定義
- test.txt ・・・ アップロードするファイル
以下、各ファイルについてです。
ssh_config
SSHの接続先を定義します。今回はEC2にキーペアで接続しました。
こんな感じとなります。(~/.ssh/configを使ったSSH接続と同じ内容です)
Host my-ec2 HostName ec2-xx-xxx-xxx-xxx.ap-northeast-1.compute.amazonaws.com User ec2-user IdentityFile your_pem_fullpath.pem ServerAliveInterval 15
用意したらこのファイル単体でSSH接続できるかを確認します。
$ ssh -F ssh_config my-ec2
sample.yml
実行するタスクを定義します。今回は
- /home/ec2-user に workフォルダを作成
- workフォルダにローカルのtext.txtをアップロード
を行っています。
- hosts: my-ec2 tasks: - name: make dir file: path: /home/ec2-user/work state: directory mode: 0755 - name: file copy copy: src: ./test.txt dest: /home/ec2-user/work/ mode: 0755
test.txt
特にこだわりはないです。適当なテキストファイルを用意します。
実行
ここまで用意したら実行してみます。
AnsibleはDry Runが可能なので、まずはDry Runで確認してみます。
$ ansible-playbook ./sample.yml -i inventory.ini --check
「--check」を付けることでDry Runとなります。
本番実行は以下のようになります。
$ ansible-playbook ./sample.yml -i inventory.ini
参考
以下を参考にさせていただきました。ありがとうございました。
https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#latest-releases-via-apt-ubuntu
https://dev.classmethod.jp/server-side/ansible/enable_ssh_conf_using_via_ansible-cnf/
https://qiita.com/okoshi/items/9c11fa7e5f2eb76896a1
https://tekunabe.hatenablog.jp/entry/2019/03/03/ansible_file_intro
https://qiita.com/brighton0725/items/7754c3ea462d3b06e5d3
「Go言語によるWebアプリケーション開発」の感想など
O'Reilly Japan - Go言語によるWebアプリケーション開発をのんびりと写経してきて、一通り終わりました。
全ての章のものではないですが、写経と読書中にメモした感想などを備忘録として残しておきたいと思います。
全体の感想
「Go言語によるWebアプリケーション開発」というタイトルですが、
本書に書かれているプログラムはWebアプリケーションとコンソールアプリケーションの両方になります。
どちらもGo言語での開発に即、実践可能なノウハウが書かれているので
「Webアプリケーション開発」に限らず、Go言語でのあらゆるアプリケーション開発に従事する人におススメできるかと思います。
(例え小さなツールを作るだけの場合などでも。)
また、ある程度Go言語が分かった状態で始めた方が良さそうだとも感じました。
goroutineやスライスなど、Go言語での開発でよく使う機能が取り上げられていますが、これらを知らないで本書を読むと、唐突に出来てたような印象を受けそうだと思います。
各章の「まとめ」には、書かれている内容が「まと」められているので、後日必要になった時に読み返す箇所を探すのに役立ちそうです。
3章
- インターフェースを使った実装の切り替え
- 型の埋め込み(type embedding)とインターフェース
について取り扱われており、大変参考になりました。
インターフェースの定義と具象の実装と切り替え、これに型の埋め込みを組み合わせて
どのように使うかの具体例が示されてていることが参考になります。
4章
一転してコンソールアプリで、小さなアプリを組み合わせ、それぞれのアウトプットを標準出力で繋ぐことで目的を達成している具体例です。
小さなアプリの組み合わせているところは、「UNIXという考え方」で書かれていた考えに近い感じがします。
この章も、ツールやバッチを作る時などにアプリ構成を考えるのに参考になるかと思います。
5章
分散システム。システムを複数のサブシステム(今でいうマイクロサービスに近い?)に分割することによって、サブシステムごとにスケールアウトできるメリットについて言及しています。
また複数のgoroutineの使用例、mapとlockを使って複数goroutineでmapを破壊しない方法、チャネルを使ったシグナルの送受信、Graceful Shutdownについても記載されています。
6章
5章で作ったシステムのデータをWebで公開する仕組みを作成します。
独立したAPIと、Webサイトをそれぞれ分ける仕組みです。
APIの方は実案件では何らかのフレームワーク使うかな、という感じはしました。
7章
WebAPIの作成。ここまでやってきたことの応用例という感じです。
8章
コンソールアプリでファイルのバックアップシステムを作成します。
これもここまでやってきたことの応用例という感じです。
以上です。
watermillを触ってみた
前回に引き続きメッセージを扱うライブラリを触ってみました。
github.com
「watermill」というもので、メッセージストリームを効率的に扱うためことを目的としているようです。
手始めにREADMEにある「Getting Started Guide」(https://watermill.io/docs/getting-started/)から始めてみたのですが、一番最初にあるサンプルを改変してみたので上げておきたいと思います。
ポイントとしては
- メッセージを受信するためのChannelを複数(2つ)最初に定義する(messagesA、messagesB)
- それぞれのChannel(messagesA、messagesB)にはトピック名を定義する
- メッセージを受信するreceiveMessages()をgoroutineとして実行する
- publishMessages()でトピック名を指定してメッセージを送信する
- 最後に終了のログ(finish)も出している
点です。ソースは以下のようになります。
GitHub - SrcHndWng/go-watermill-simple-sample
package main import ( "context" "log" "time" "github.com/ThreeDotsLabs/watermill" "github.com/ThreeDotsLabs/watermill/message" "github.com/ThreeDotsLabs/watermill/pubsub/gochannel" ) func main() { pubSub := gochannel.NewGoChannel( gochannel.Config{}, watermill.NewStdLogger(false, false), ) messagesA, err := pubSub.Subscribe(context.Background(), "example.topic.A") if err != nil { panic(err) } messagesB, err := pubSub.Subscribe(context.Background(), "example.topic.B") if err != nil { panic(err) } go receiveMessages(messagesA) go receiveMessages(messagesB) publishMessages(pubSub) log.Println("finish!") } func publishMessages(pubSub *gochannel.GoChannel) error { publishMessage := func(publisher message.Publisher, topic string, msgStr string) error { msg := message.NewMessage(watermill.NewUUID(), []byte(msgStr)) return publisher.Publish(topic, msg) } for i := 0; i < 5; i++ { if err := publishMessage(pubSub, "example.topic.A", "Hello, message A!"); err != nil { return err } if err := publishMessage(pubSub, "example.topic.B", "Hello, message B!"); err != nil { return err } time.Sleep(time.Second) } return nil } func receiveMessages(messages <-chan *message.Message) { for msg := range messages { log.Printf("received message: %s, payload: %s", msg.UUID, string(msg.Payload)) msg.Ack() // Ack sends message's acknowledgement. } }
動かしてみると分かりますが、A・Bのメッセージが受信される順番は、送信した順になるとは限らないようです。
メッセージを受信したら、「Ack()」で受信済である旨を通知することもポイントかと思います。
これ以外は、goroutineを使っていること、pushish~、subscribe~など一般的な用語がそのままメソッド名になっているため、分かりやすいライブラリかなと思います。