この記事はWanoグループ Advent Calendar 2021 / 7日目の記事になります。。
terraformユーザーの皆様は Lambda や StepFunctions などのデプロイどうしてますか?
プロジェクト構成によっていろんなパターンがあるかと思いますが、少し前までは結構悩んでるユーザーが多かったように記憶しています。
特に弊社ではデプロイ間の依存や順番、リポジトリ構成の都合で大いに悩みました。
弊社でいままで使用/検討したものを振り返りつつ、プロジェクトで固まってきた運用を紹介します。
いままでにやってきた/検討したデプロイ
現行関わっているプロジェクトの特徴として、アプリ/ビジネスロジック系リポジトリとIaC専用リポジトリが別である ということがあります。
運用が決まるまでの苦しみや試行錯誤もこの要件にまつわるものである点にご留意ください。
1. そのままterraformのLambdaモジュールを使い、IaCリポジトリ側に置く (local code)
愚直にterraformのlambdaモジュールを学んでいくと、こうなると思います。
ハッシュ値やzip関数などを使ってローカルのアプリケーションコードを常に同梱するパターンです。
data "archive_file" "layer_zip" {
type = "zip"
source_dir = "lambda"
output_path = "lambda.zip"
}
resource "aws_lambda_function" "test_lambda" {
filename = "lambda.zip"
function_name = local.lambda_name
role = aws_iam_role.iam_for_lambda.arn
handler = "index.handler"
runtime = "nodejs12.x"
timeout = 40
source_code_hash = data.archive_file.layer_zip.output_base64sha256
}
Pros
- 常にTerraform経由でIaCできるので、IAMや物理id/permission/event当てなど、他terraformリソースとの連携が簡単
- IaCの経路がこれだけなのでわかりやすい
Cons
- アプリケーションのコードが別リポジトリだったりすると、ビルドやCIの経路に悩むことになる
検討したこと
アラート通知やインフラ監視など、ビジネスロジックそのものとあまり関係のないlambdaコードなどは弊社でもこのパターンを 今も採用しており、先述のIaCリポジトリの中にコードの実体も同梱しています。
弊社で問題になったのはアプリ/ビジネスロジック系リポジトリとIaC専用リポジトリが別であるパターンです。
Web3層アプリ程度ならInfra/アプリのリポジトリ分離の思想はうまくいっていたのですが、LambdaやStepFunctionsで実現できるような「ビジスロジック/アプリケーション寄り」のインフラコンポーネントを置くときに、どこでデプロイされるべきか悩み始めました。
特にLambdaやStepFunctionsの初回デプロイは「中身のロジック」と一緒でなくてはならないため、インフラリポジトリでLambdaをデプロイするときには同時にコードもなければいけません。
2. そのままterraformのLambdaモジュールを使い、IaCリポジトリ側に置く (s3/docker)
上記と同じパターンで、Lambdaのロジック置き場として s3 object/dockerを指定する場合です。
resource "aws_lambda_function" "image_resize_api" {
function_name = local.lambda_name
s3_bucket = local.lambda_code_bucket
s3_key = "image_resize_api/lambda.zip"
role = module.s3_image_resize_api_role.iam_role_arn
handler = "main"
runtime = "go1.x"
timeout = 40
memory_size = 6000
}
Pros
- S3のobject更新とコード更新の分離が簡単
- 常にTerraform経由でIaCできるので、IAMや物理id/permission/event当てなど、他terraformリソースとの連携が簡単
Cons
- アプリケーションのコードが別リポジトリだったりすると、ビルドやCIの経路に悩むことになる(同じ)
- 別にS3リポジトリ/ECR設定が要る
- 初回デプロイにS3オブジェクトや少なくとも少なくとも一つのdockerimageの実体が必要
- 別リポジトリのCIでs3 object/docker image の更新や
aws lambda update-function-code
が必要
検討したこと
はじめのパターンより分離が可能になりました。初回デプロイで必ずコード実体としてのS3 Objectが必要なのがいまいちなくらい、です。 Lambdaのビジネスロジックが更新された場合、更新を伝えてあげるスクリプトが別途アプリのCI/CD時に必要になります。
3. 旧apexやlambrollなどのLambda専用デプロイツールを使い、アプリのデプロイとサイクルを合わせる
apex (メンテされていない) や lambroll など専用ツールを使っていたこともあります。
これらはlambda関数そのものをデプロイするものであり、remote state
が読み込み可能だったりすることが特徴です。
Pros
- Lambda関数「以外」との分離が可能
- ローカルのtfやterraform remote state が使える
Cons
- 実行permissionなどで結局インフラリポジトリ側でLambda本体への参照が必要になる
検討したこと
これも結局インフラリポジトリが分かれている弊社プロジェクトならではの事情になるのですが、Lambdaそのもののデプロイ自体もterraformにやらせたほうが何かと都合がよかったのもあり、採用しなくなりました。
具体的には、LambdaのARNが必要なコンポーネントを宣言しておきたいときです。
resource "aws_cloudwatch_event_target" "sfn_event_to_lambda_1" {
rule = aws_cloudwatch_event_rule.fail_event.name
target_id = "${local.env}_sfn_fail_to_runtime_error"
arn = aws_lambda_function.litetask.arn
input_transformer {
input_paths = {
"detail" = "$.detail"
}
input_template = <<-JSON
{
"task_type" : "/on_failed",
"payload" : {
"detail" : <detail>
}
}
JSON
}
}
これ系はLambdaのARNが初回デプロイ時点で存在しなくとも特にエラーが出ないものも多いので、目をつむれるといえばつむれるのですが、そうでもないものがあると急にインフラが先にあるべきか、Lambdaが先にあるべきか....のデプロイ順序が難しくなるので、その辺りの判断がけっこうめんどくさいです。
計画的/明確に
- iamだけのデプロイ層やイベント層をIaCのデプロイサイクル上分けている
- コンポーネントをつなぐ層をIaCのデプロイサイクル上分けている
パターンだと有効かもしれません。
4. 部分的に SAM や ServerlessFramework などのサーバレス/アプリ層寄りのIaCツールをつかい、アプリのデプロイとサイクルを合わせる
別のIaCを使うパターンです。
Pros
- S3のobject更新とコード更新の分離が簡単
- 得意なものがあるツールに縦割りで任せられる
Cons
- インフラリポジトリ側で宣言したものとの受け渡しが大変
- ツールが分散する
検討したこと
「アプリ/ビジネスロジックよりのコンポーネントってstatefulなAWS Componentとわけておきたいよね」
みたいな肌感が最初からあると、これは有効かもしれません。
得意なことは得意なツールに任せる。
ただしこれも結局デプロイ単位の綿密な設計がいる話ですので、なかなか使いどころが難しかったです。
また、IaCツールが分かれると、**自動払い出しされるタイプのARNなどでいちいちParameter Storeを介した受け渡し方法を考えなきゃいけなかったりして、**数が増えるとなかなか管理が難しかったりします。
採用しているパターン
5. そのままterraformのLambdaモジュールを使ってIaCリポジトリ側で管理 + ignore_changes + 別途 aws-cli
結局たどりついた雑なパターンが以下になります。
IaCリポジトリ側
resource "aws_lambda_function" "image_resize_api" {
function_name = "api_server"
s3_bucket = "${local.env}_terraform_lambda_source" // dummy.実際には他のアプリ側リポジトリのCIで更新
s3_key = "terraform_dummy_lambda/lambda.zip" // dummy.実際には他のアプリ側リポジトリのCIで更新
role = module.s3_image_resize_api_role.iam_role_arn
handler = "main"
runtime = "go1.x"
timeout = 40
memory_size = 6000
// 実際には他のアプリ側リポジトリのCIで更新するのでignore
lifecycle {
ignore_changes = [
s3_bucket, s3_key
]
}
}
アプリのリポジトリ側
PROJECT_ROOT=${PWD}/../../../
build:
mkdir -p .build/api_server
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags "-X main.version=$(git describe --tags --abbrev=0) \
-X main.revision=$(git rev-parse --short HEAD)" \
-o .build/api_server/main ${PROJECT_ROOT}/cmd/api_server
deploy:
cd ${PWD}/.build/api_server && zip lambda.zip main && \
aws lambda update-function-code --function-name api_server --zip-file fileb://lambda.zip
(1) あらかじめ共通のダミーS3 Objectを決めておく
(2) s3_bucket, s3_keyはignore_changeに入れる
(3) lambdaや周辺IaCコードは先に初回デプロイ(エラーが出ない)
(4) アプリのロジックを持っている側のリポジトリのCIでs3_bucket, s3_keyやコードを更新
Pros
- インフラだけ先にデプロイできる
- アプリのコード更新くらいならterraform側のデプロイがいらない
- だいたい対応できる
Cons
- s3 objectのzip化などのコードはアプリ側CIで書かないといけない
- ダミーのオブジェクト自体はプロジェクトのはじめに決めでおかないといけない(若干かっこわるい)
- 環境変数など外部依存が増えるときはIaCリポジトリ側を更新しないとダメ
- ignore_changesがあるterraformならではの手法?
検討したこと
要は初回デプロイでいったりきたりするのをなるべくやめたかったのがメインなので、アプリのロジック/コードは別リポジトリのCIに完全に任せきり、Lambda関数そのものはterraformで構成管理に集中させます。
IaC側リポジトリの構成管理の更新にLambdaのコードが巻き込まれないように、s3objectやdocker imageタグ指定まわりにはignore_changes
を利用しています。
また、アプリ側CIでは zip化/update function-code のコードが必要になりますが、まあ一度書いちゃえば慣れるものなので、特別なツールを使わずシェルで更新しています。
ちなみに、AWS StepFunctionsでかなりビジネスロジック寄りのStateMachineを書くときも、この ignore_changes
を使ってダミーコードの初回デプロイと他リポジトリでのロジック更新を分離しています。Lambdaに限らず「一概にインフラとは言えない!」という気持ちになるコンポーネントを使う際にはわりと便利な仕組みだなあと感じています。
まとめ
terraform x lambda x polyrepoなデプロイの試行錯誤の一端をお見せしました。
既存プロジェクトの事情によってこのへんの縦割り/横割りの構成管理の思想ってけっこうバラバラかとは思いますが、「これが最強だ」みたいなのはどんどん知っていきたいですね。