make は強いタスクランナーだった。Lambda Function のライフサイクルを Makefile でまわす
Lambda Function のローカル開発を考察したとき に、不十分だったタスクランナーの導入を今回やりました。make についての細かい解説は本稿では行いません。Lambda Function 開発の要件を満たすためにタスクランナーとしてどのような Makefile の書き方があるかを示します。
Lambda Function をテストし、デプロイするためのツールはありますが、
- 複数の Function を全部プロイしたり
- 逆にひとまとまりの Lambda だけデプロイしたり
- 環境を指定してデプロイしたり
ということを考えると工夫が必要になります。そこで登場するのがタスクランナーです。たくさんのタスクランナーが候補としてあり、一長一短あるところ、make が要件を満たすのではないかということで Makefile を書いていきます。ちなみに私自身これまで Makefile を書いたことはありません。難しそうなイメージでしたが、考え方を理解するとタスクランナーとしてむしろ書きやすいという感想です。
結論から
make はC言語系向けとあって、考え方を理解するのに時間がかかりました。タスクは、基本的にファイルのことで、依存タスクとはファイルが存在するかどうかを確認することを指す、といったことです。一度このあたりを把握すると、タスクランナーとして強力に利用できると思います。どの環境でもほぼインストールなしで最初から使えるというもの良いです。結局、
1 2 | make upload-heroes # heroesにかかわる Lambda Function だけをアップロードする make deploy env =prd # プロダクション環境にすべての Lambda Function をデプロイする |
といったように、リソースごと、環境ごとにデプロイできるようになりました。これで Lambda Function の開発をさらに加速できます。
↓S3バケットに heroes のためのソースだけがアップロードされた様子。
↓CloudFormation に heroes と offices のスタックがデプロイされている様子。ここから Lambda Function も展開されている。
前提の確認
ある程度の規模(人・物・金のうち二種類以上を扱う)のアプリケーションで Lambda Function を開発すると、リソース単位で分けて開発/デプロイするシーンに遭遇します。以下のようなプロジェクトを考えてみます。
概要
ヒーローを管理するAPIを、API Gateway と AWS Lambda, DynamoDB で開発します。
扱うリソース
リソース名 | 役割 |
---|---|
heroes | ヒーローの名前や所属事務所を管理する |
offices | 事務所の名前や住所を管理する |
開発ライフサイクル
対象 | どうやるか |
---|---|
コーディング作業 | ローカルで Python を使う。Intellij でも Atom でもエディタは問わない。 |
Unit Test | pytest。ただしAWSサービスへのリクエスト部分は対象としない。 |
結合テスト | SAM Local、LocalStack、batsを組み合わせて。 |
ZIPアプロード | Lambda の ZIPファイルをアップロードする。 |
デプロイ | Lambda の ZIPファイルを、AWS SAM によってデプロイする。 |
CI/CD | AWS CodeBuild (buldspec.yml) で行う。 |
利用したツールとバージョン
ツール | バージョン |
---|---|
Python | 3.6.2 |
pytest | 3.4.2 |
aws-cli | 1.11.150 |
AWS SAM Local | 0.2.8 |
LocalStack ( Docker ) | latest |
Bats - Bash Automated Testing System | 0.4.0 |
タスクランナーに求める要件を整理する
Makefile を書き始めるまえに、タスクランナーとしてどんなことを実現したいのかを考えます。今回恩恵をあずかりたいのは、「ZIPファイルのアップロード」と「デプロイ」の部分です。単純なスクリプトだとどうしても複雑になりがちなのが、ターゲットを指定する という部分ですね。具体的には、
- 環境(Environment): test, stage, production, など。Lambda Function を環境別にデプロイしたい
- リソース:heroes だけ、 offices だけ、全部というようにデプロイしたい
ということを考えます。コマンドにすると以下のようなことができるイメージ。
1 2 | make upload-heroes # heroesにかかわる Lambda Function だけをアップロードする make deploy env =prd # プロダクション環境にすべての Lambda Function をデプロイする |
これをタスクランナーの要件に落とし込むと…
- 特定のタスクで、 env 変数がセットされていることを強要できるようにしたい
- 特定のタスクは、名前を指定することでリソース個別に実行できるし、指定しなければすべてのリソースに対して実行するようにしたい
ということになります。
makeの他に検討したタスクランナー
Bazel
Google が開発するビルドツールです。多くの言語をサポートしているようですが、目的に対して新しい記法をゼロから学ぶ元気がなく諦めました。すいません。
invoke
Python で書けるビルドツールです。途中まで触ってみて、「別にタスク定義を Python で書きたいわけではない」と気づいてやめました。
Rake
私が Ruby を触れないのと、すでにプロジェクトに Node.js と Python の環境をインストールしているためこれ以上インストールする言語環境を増やしたくないと理由でやめました。CIに時間がかかるので。
task, robo
YAMLベースでタスク定義ができるタスクランナーです。シンプルで使いやすいのですが、リソースごとにタスクを走らせたり、ワイルドカードを指定したりといった面がすんなりいきませんでした。また、変数の定義がすべて静的に評価されてしまい、ファイルハッシュの取得など動的な評価はすべてスクリプトにおこさなくてはならないといった面もありました。シンプルな用途にはマッチするツールだと思います。
結局、弊社中山や齋藤の勧めもあり、make を使うことにしました。
作った Makefile のタスク一覧
タスク名 | 役割 |
---|---|
all | clean, test-unit, test-integ, dist, upload, deploy を実行する。 |
install | pip で requirements.txt をインストールする。 |
localstack-up | docker-compose.yml で定義している LocalStack を起動する。 |
localstack-stop | LocalStack を停止する。 |
test-unit | ユニットテストを実行する。 |
test-integ | 結合テストを実行する。 |
clean | デプロイのために必要になる zip ファイルなどを削除する。 |
dist | アップロード用の zip ファイルを作成する。 |
upload | すべてのリソースに対してアップロードを実行する。 |
upload-% | リソースを指定し、そのリソースについてアップロードする。 |
deploy | すべてのリソースに対してデプロイ実行する。 |
deploy-% | リソースを指定し、そのリソースに対してデプロイ実行する。 |
guard-% | タスクの依存関係に記載することで、%で指定した変数がセットされていることを強要する。 |
要件にかかわるところを詳しく見ていきます。
guard-%
他のタスクに指定することで、%に指定した変数が make 実行時にセットされていることを強要します。
1 2 3 4 5 | guard-%: @ if [ "${${*}}" = "" ]; then \ echo "Environment variable $* not set" ; \ exit 1; \ fi |
以下のように使います。
1 | deploy: guard- env |
deploy
を実行するときに、env がセットされていないと先に進めないようになります。
1 2 3 4 5 6 7 8 9 | $ make deploy + find test -name '*.bats' + git log -n 1 -- format =%h + '[' '' = '' ']' + echo 'Environment variable env not set' Environment variable env not set + exit 1 make : *** [guard- env ] Error 1 |
upload, upload-%
upload-%
によって個別にアップロードできるようにし、upload
では src/functions/
以下をさがしてアップロード対象をすべて抽出してから、すべてのアップロードを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | # src/functions以下をさがして、upload-heroes upload-offices という配列を作成 BASE := src /functions DIR := $( sort $( dir $(wildcard $(BASE)/*/))) TARGETS := $(patsubst $(BASE)/%/, %, $(DIR)) UPLOAD_TASK := $(addprefix upload-, $(TARGETS)) # $(UPLOAD_TASK) を展開すると upload-heroes upload-offices となる upload: guard- env clean dist $(UPLOAD_TASK) @ echo $(TARGETS) @ echo $(UPLOAD_TASK) @ echo $(DEPLOY_TASK) # % に指定された名前を使ってターゲットを特定しアップロード upload-%: guard- env $(UPLOAD_FILE) @ if [ "${*}" = "" ]; then \ echo "Target is not set" ; \ exit 1; \ elif [ ! -d "$(BASE)/${*}" ]; then \ echo "Target directory $(BASE)/$* does not exists." ; \ exit 1; \ else \ s3_keyname= "${*}/$(ZIP_FILE)" && \ echo $${s3_keyname} && \ aws s3 cp $(UPLOAD_FILE) $(S3_BUCKET_URL)/$${s3_keyname} ; \ fi |
deploy, deploy-%
考え方は uploadと同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | # src/functions以下をさがして、deploy-heroes deploy-offices という配列を作成 BASE := src /functions DIR := $( sort $( dir $(wildcard $(BASE)/*/))) TARGETS := $(patsubst $(BASE)/%/, %, $(DIR)) DEPLOY_TASK := $(addprefix deploy-, $(TARGETS)) # $(DEPLOY_TASK) を展開すると deploy-heroes deploy-offices となる deploy: guard- env $(DEPLOY_TASK) @ echo $(TARGETS) @ echo $(UPLOAD_TASK) @ echo $(DEPLOY_TASK) # % に指定された名前を使ってターゲットを特定しデプロイ deploy-%: template_%.yaml guard- env @ if [ "${*}" = "" ]; then \ echo "Target is not set" ; \ exit 1; \ elif [ ! -d "$(BASE)/${*}" ]; then \ echo "Target directory $(BASE)/$* does not exists." ; \ exit 1; \ else \ s3_keyname= "${*}/$(ZIP_FILE)" && \ aws cloudformation package \ --template- file template_${*}.yaml \ --s3-bucket $(S3_BUCKET_URL) \ --output-template- file packaged-${*}.yaml && \ aws cloudformation deploy \ --template- file packaged-${*}.yaml \ --stack-name $${ env }-${*}-lambda \ --capabilities CAPABILITY_IAM \ --parameter-overrides \ Env=$${ env } \ CodeKey=$${s3_keyname} ; \ fi |
あとは実行していくだけです。わかりづらかったスクリプトがすっきり。
buldspec.yaml
ビフォーアフター
make によって 以下のように buildspec.yaml
が改善しました。
1 2 3 4 5 6 7 8 9 10 11 | pre_build: commands: - pip install -r requirements.txt - docker-compose up -d build: commands: - python -m pytest - ./integration_test.sh post_build: commands: - ./deploy.sh |
1 2 3 4 5 6 7 8 9 10 11 12 | pre_build: commands: - make install - make localstack-up build: commands: - make test-unit - make test-integ post_build: commands: - make upload env=$ENV - make deploy env=$ENV |
なにをやっているのかわかりやすくなりましたね。
他のリソースを追加したいときは
ヒーロー、事務所に加えて、「スポンサー会社」を追加したいとしましょう。ソースツリーは以下のようになるはずです。
1 2 3 4 5 | src └── functions ├── heroes ├── offices └── sponsors # 追加 |
デプロイのために template_sponsors.yaml
を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | AWSTemplateFormatVersion: '2010-09-09' Transform: AWS : : Serverless-2016-10-31 Description: Simple CRUD webservice. State is stored in a SimpleTable (DynamoDB) resource. Parameters: Env: Type: String Default: local DynamoDBEndpoint: Type: String Default: https : //dynamodb.ap-northeast-1.amazonaws.com/ SponsorTableName: Type: String Default: CM-Sponsors BucketName: Type: String Default: hero-lambda-deploy CodeKey: Type: String Default: sponsor/0000.zip Resources: GetSponsors: Type: AWS : : Serverless : : Function Properties: FunctionName: Fn: : Sub : $ { Env } -heroes-GetSponsors Handler: src/functions/sponsors/index.get Runtime: python3.6 CodeUri: Bucket: !Ref BucketName Key: !Ref CodeKey Policies: AmazonDynamoDBReadOnlyAccess Environment: Variables: DYNAMODB_ENDPOINT: !Ref DynamoDBEndpoint SPONSOR_TABLE_NAME: Fn: : Sub : $ { Env } -$ { SponsorTableName } PutSponsors: Type: AWS : : Serverless : : Function Properties: FunctionName: Fn: : Sub : $ { Env } -heroes-PutSponsors Handler: src/functions/sponsors/index.put Runtime: python3.6 CodeUri: Bucket: !Ref BucketName Key: !Ref CodeKey Policies: AmazonDynamoDBFullAccess Environment: Variables: DYNAMODB_ENDPOINT: !Ref DynamoDBEndpoint SPONSOR_TABLE_NAME: Fn: : Sub : $ { Env } -$ { SponsorTableName } |
この後デプロイをしたいとします。このとき、 Makefile を編集する必要はありません。ディレクトリからターゲットをさがしてくれるので、我々は make タスクを実行するだけです。
1 2 3 4 5 6 7 8 9 10 | bash -3.2$ make upload-sponsors sponsors /6a3da43 .zip + aws s3 cp deploy /6a3da43 .zip s3: //hero-lambda-deploy/sponsors/6a3da43 .zip upload: deploy /6a3da43 .zip to s3: //hero-lambda-deploy/sponsors/6a3da43 .zip bash -3.2$ make deploy-sponsors env =prd ...中略*... Waiting for changeset to be created.. Waiting for stack create /update to complete Successfully created /updated stack - prd-sponsors-lambda |
Makefile を編集することなく、スポンサーの Lambda をデプロイするための CloudFormation スタックが作成されました。
まとめ
Lambda Function 開発のためのタスクランナーとして、make を使いました。デプロイ先の環境 であったり、ヒーローや事務所など Lambda Function のグループ を考える場合は、タスクランナーとしての機能が強力です。Lambda Function の数が多くなってきてどう管理したものかという段階になってきた方々の参考になれば幸いです。