Makefile で複数の Lambda 関数をデプロイするメモ

複数の Lambda 関数で構成される環境を、基本的な構成は Terraform で管理しつつ、Lambda 関数のコードは make で aws cli を呼び出してデプロイするメモ。

Lambda 関数のコードも含めてすべてを Terraform で管理することもできると思いますが・・・それだとコードを修正したときのデプロイに無駄に時間が掛かりすぎて辛いので却下。

Terraform での Lambda 関数の定義

Lambda 関数の作成は Terraform でやりますが、コードの更新は aws cli を使うので、Terraform でコードの内容の変化は無視しておく必要があります。

locals {
  # ダミーのファイル(空で良い)
  dummy_file = {
    filename         = "dummy.zip"
    source_code_hash = filebase64sha256("dummy.zip")
  }
}

resource "aws_lambda_function" "aaa" {
  function_name = "aaa"
  role          = aws_iam_role.lambda.arn
  handler       = "index.aaa"
  runtime       = "nodejs12.x"

  filename         = local.dummy_file.filename
  source_code_hash = local.dummy_file.source_code_hash

  # filename と source_code_hash を無視する
  lifecycle {
    ignore_changes = [
      filename,
      source_code_hash,
    ]
  }
}

Makefile

make で Lambda 関数のコードのアップロードや、ログの表示ができます。

# Lambda 関数にアップロードするアーカイブを作成
make build

# すべての Lambda 関数にアーカイブをアップロード
make deploy

# 特定の Lambda 関数にアーカイブをアップロード
make deploy/aaa

# すべての Lambda 関数のログを aws logs tail --follow で表示
# 複数のロググループを並列に tail --follow するために -j で並列処理させる
make logs -j

# 特定の Lambda 関数のログを aws logs tail --follow で表示
make logs/aaa

make logs/aaa などで指定している aaa は make のターゲットで指定しやすくするためのエイリアスのようなもので envs/dev.mk などの環境ごとのファイルで次のように定義しています。IAM クレデンシャルのプロファイルを切り替えるために aws コマンドもここで指定しています。

# aws コマンド(対象となる環境によってプロファイルを切り替える)
AWS := aws --profile oreore

# make のターゲットで使用する名前のリスト
TARGETS := aaa bbb ccc

# ↑のターゲット名と実際の Lambda 関数の名前のマッピング
LAMBDA_aaa := test-dev-lambda-aaa
LAMBDA_bbb := test-dev-lambda-bbb
LAMBDA_ccc := test-dev-lambda-ccc

Makefile は次のような内容です。

# 対象となる環境ごとの定義ファイルをインクルード
include envs/$(APP_ENV).mk

# ソースファイル
# 直下の *.js と src ディレクトリ内の *.js
# node_modules は含めません
SRC_FILES := $(wildcard *.js) $(shell find src -name '*.js')

# ビルド用ディレクトリ
# アップロード用のアーカイブやデプロイ結果が保存されます
BUILD_DIR     := .build
BUILD_ENV_DIR := $(BUILD_DIR)/$(APP_ENV)

.PHONY: all
all: deploy

.PHONY: build
build: $(BUILD_DIR)/package.zip

# lambda にアップロードするアーカイブを作成します
# node_modules だけ含むアーカイブをコピーしてソースファイルを追加します
# 変数 $@ や $< で書いたら見辛い気がしたのであえて使ってません
$(BUILD_DIR)/package.zip: $(BUILD_DIR)/node_modules.zip $(SRC_FILES)
mkdir -p $(BUILD_DIR)
    cp $(BUILD_DIR)/node_modules.zip $(BUILD_DIR)/package~.zip
    zip -r $(BUILD_DIR)/package~.zip $(SRC_FILES)
    mv $(BUILD_DIR)/package~.zip $(BUILD_DIR)/package.zip

# node_modules だけ含むアーカイブを作成します
# package.json や package-lock.json が更新されたときだけリビルドします
# devDependencies は含めたくないので npm ci --prod してアーカイブ化した後に npm i で戻します
$(BUILD_DIR)/node_modules.zip: package.json package-lock.json
    mkdir -p $(BUILD_DIR)
    npm ci --prod
    rm -f $(BUILD_DIR)/node_modules.zip
    zip -r $(BUILD_DIR)/node_modules.zip node_modules
    npm i

# すべての Lambda 関数をデプロイ、次のように展開されます
#   deploy: deploy/aaa deploy/bbb deploy/ccc
.PHONY: deploy
deploy: $(TARGETS:%=deploy/%)

# 特定の Lambda 関数をデプロイ、次のように展開されます
#   deploy/aaa: .build/dev/aaa/deploy.json
#   deploy/bbb: .build/dev/bbb/deploy.json
#   deploy/ccc: .build/dev/ccc/deploy.json
# ただのワイルドカードパターンだといろいろ不都合があったので、
# 静的パターンルールで記述します
.PHONY: $(TARGETS:%=deploy/%)
$(TARGETS:%=deploy/%): deploy/%: $(BUILD_ENV_DIR)/%/deploy.json

# .build/dev/aaa/deploy.json のようなターゲット名から、
# ワイルドカード部分の aaa を取り出して TARGET 変数に入れます
$(BUILD_ENV_DIR)/%/deploy.json: TARGET = $*
# LAMBDA_aaa などの変数から Lambda 関数名を取得して LAMBDA 変数に入れます
$(BUILD_ENV_DIR)/%/deploy.json: LAMBDA = $(LAMBDA_$(TARGET))
# アップロードを実行して成功したら .build/dev/aaa/deploy.json のようなファイルを作成します
$(BUILD_ENV_DIR)/%/deploy.json: $(BUILD_DIR)/package.zip
    mkdir -p $(@D)
    $(AWS) lambda update-function-code --function-name $(LAMBDA) --zip-file fileb://$< > $@~
    mv $@~ $@

# すべての Lambda 関数のログを tail -f します
# make -j で並列実行させないと一つのログしか tail -f できません
.PHONY: logs
logs: $(TARGETS:%=logs/%)

.PHONY: $(TARGETS:%=logs/%)
# logs/aaa のような名前から TARGET に aaa を入れます
logs/%: TARGET = $*
# LAMBDA_hoge などの変数から Lambda 関数名を取得して LAMBDA 変数に入れます
logs/%: LAMBDA = $(LAMBDA_$(TARGET))
# 特定の Lambda 関数のログを tail -f します(Ctrl+C するまで続行)
# ただのワイルドカードパターンだと .PHONY にできないので、
# 静的パターンルールで記述します
$(TARGETS:%=logs/%): logs/%: deploy/%
    $(AWS) logs tail "/aws/lambda/$(LAMBDA)" --follow --format=short --since 1s

# tmux でペイン分割して複数のログを tail -f します
# Lambda 関数が多くなると見辛いので基本的には↑を使ってます
# 動的にコマンドを生成するために Makefile の foreach 関数使ってます
.PHONY: tmux-logs
tmux-logs: deploy
    tmux new : \; $(foreach t,$(TARGETS), split make logs/$(t) \; ) set sync \; select-layout main-v

.PHONY: clean
clean:
    rm -fr $(BUILD_DIR)

補足とか

Serverless Framework

複数の Lambda 関数のデプロイを管理するなら apex が便利かと思ってたんですけど こういう状況になっているので もう使わないほうが良さそうです。

代替としては Serverless Framework とか? ただこれは CloudFormation を使うようです。今回は自社管理外の AWS 環境にちょこっと Lambda 関数をデプロイするだけのものなのであまり大げさなことはやりたくありませんでした。

Lambda Layer

デプロイの都度 node_modules がまるごとアップロードされているのと、すべての Lambda 関数に同じ node_modules が含まれた状態になっていて無駄なので、node_modules は Lambda layer にして共有するのが良いかも。

さいごに

Makefile 毎回書き方ググってる気がするので未来の自分用にメモ。