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

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

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

以上です。

AnsibleでEC2にSSH接続してファイルをアップロードする

仕事でAnsibleを少し触る機会があったのですが、初見でさっぱり分からなかったので
復習を兼ねてインストールからやってみました。

タイトルにも書きましたが、やったことは

  • Ansibleにて既存のEC2にSSH接続する
  • SSH経由でEC2内にフォルダを作る
  • 同じくSSH経由でファイルをアップロードする

です。

以下、やったことのメモとなります。

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

inventory.ini

ssh_configで定義した接続先を指定します。

[mysection]
my-ec2

ansible.cnf

SSHの接続オプションを指定します。

ssh_args = -F ssh_config

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

「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~など一般的な用語がそのままメソッド名になっているため、分かりやすいライブラリかなと思います。