AWS CDKとlambrollを組み合わせてLambdaをデプロイしたいので改めて調査をする
本記事は『AWS CDK Advent Calendar 2024』3日目の記事です。
(記事公開しようとしたらたまたま空いていたので…せっかくなので…)
AWS CDKが大好きだけど、ふいんきで触ったり触らなかったりしているちゃちいです。最近またCDKを案件で使うことになりましたが、悩みがありました。
去る 2024-11-30、紅白ぺぱ合戦にて、Lambroll作者の @fujiwaraさんとお目にかかれて、タイトルの内容で困っていたので二次会で開口一番、質問させていただきました。fujiwaraさん、その節はありがとうございました…!
なぜ困っているのかと質問へのお返事についての前に、背景と改めてCDKの挙動を手で確認する必要があると感じたので、せっかくなのでまとめながら。
背景
CDKには、アプリケーションコードのディレクトリを指定するだけで Docker イメージのビルドをしてくれる DockerImageCode.fromImageAsset()
と、それを使って Lambda をデプロイする DockerImageFunction
があります。
とても便利ですが、息が長くなりそうなプロダクトに使うのは辛いモノがあります。
これについてはアイレットさんの記事が参考になります。こちらの記事ではコンテナイメージではありませんが、だいたい同じ理由で、アプリケーションのデプロイはCDKの管轄外としたいんですね。
ちなみに、なぜコンテナイメージにするかというと、アプリケーションコードが合計すると大きくなるためです。Lambda…というか、FaaSは一般的には細かい仕事をさせるものでしょうけれど、ぼくはLambdaのことを「手間の掛からない実行環境」として選んでいます。なので、LambdaでPHPを実行できるようにする Bref を使い、SymfonyやLaravelといったフレームワークを載せます。LambdaにWebアプリケーションフレームワークを載せることについては、@seike460さんの以下発表が参考になります。
まあ、つまりLambdaで実行したいアプリケーションコードがそもそも大きいので、CDKの便利機能でビルド・デプロイするのは責務を明確に分けるべきということです。
インフラとアプリを分ける…ecspresso…
ecspressoという素晴らしいツールがありまして、特にクラスメソッドさんの記事が詳しいですね。
ぼくも以前、Fargateを使う案件でCDKとecspressoの組み合わせで構築した経験はあります。
となれば、同じ思想で作られたLambdaをデプロイするツール、lambrollを使おうとするのは自然な流れですよね。
なので、CDKとlambrollを組み合わせてインフラ、アプリ、デプロイフローを構築しようとしています。
ざっくり構成
- API Gateway
- Lambda
- ECRリポジトリ
- GitHub Actions / ローカル からデプロイ
- インフラ(CDK)とアプリ(PHP)でGitリポジトリを分ける
どうすればいいんですか?
ecspressoの場合、CDKでFargateクラスターを、ecspressoでサービスとタスクをデプロイしていました。
ecspressoのデプロイに必要な値は、CDKでssmに保存し、それを参照するようにしています。
では、lambrollでデプロイしたLambdaを、CDKを使って API GatewayのLambda統合が…おや?どうやって「このLambdaを使う」と決められる…?この方法に悩んでいました。
Terraformは未習得ですが、これはできるらしいですね。でもぼくはCDKを使っていきたいんです…。
あらためて、冒頭のfujiwaraさんへの質問を簡潔に書き表すと
「インフラをCDK、Lambdaをlambrollでデプロイしたい時、API GatewayはCDK管轄になるが、lambrollでデプロイしたLambdaをどのように統合するか?」
ということです。
もらったお返事
「CDK使ってないからなあ」「Terraformならできるけれど」とおっしゃっていましたが、「ssmで渡せないかな」とお話ししてくださいました。
あと、「CDK側がLambdaが更新されていたら変更を検知してしまう?」と聞かれて、「そうですねえ」と返したんですが、そういえば自分でちゃんと調べてないなと思ったので…
調査: CDKでデプロイしたコンテナイメージを使ったLambdaを外部(lambroll)で変更したらどうなるか?
まずCDKでLambdaをデプロイ
new aws_lambda.DockerImageFunction(this, "TestLambda", {
timeout: Duration.seconds(30),
memorySize: 1024,
functionName: "test-lambda",
role: testLambdaRoll,
code: aws_lambda.DockerImageCode.fromImageAsset(
"./test_src",
{
platform: Platform.LINUX_AMD64,
},
),
});
`test_src`の中身
特に動作確認はしていません。Dockerイメージがビルドできればよいので。
FROM public.ecr.aws/lambda/nodejs:20
# Copy function code
COPY index.js ${LAMBDA_TASK_ROOT}
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "index.handler" ]
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
cdk deploy
すると、ECRにリポジトリ cdk-hnb659fds-container-assets-{account_id}-{region}
ができ、イメージがプッシュされます。そして、そのイメージを使ったLambda関数が作成されます。
lambrollで取り込み
lambroll init --function-name=test-lambda
生成された function.json
は以下のようになりました。
function.json
(隠したい文字列は{}
に置換しています)
{
"Architectures": [
"x86_64"
],
"Code": {
"ImageUri": "{account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/cdk-hnb659fds-container-assets-{account_id}-{region}:{container_image_tag}"
},
"EphemeralStorage": {
"Size": 512
},
"FunctionName": "test-lambda",
"LoggingConfig": {
"LogFormat": "Text",
"LogGroup": "/aws/lambda/test-lambda"
},
"MemorySize": 1024,
"PackageType": "Image",
"Role": "arn:aws:iam::{account_id}:role/{stack_name}-{constructor_name}TestLambdaRole98F59437-YCWLx7fcxsDI",
"SnapStart": {
"ApplyOn": "None"
},
"Tags": {
"aws:cloudformation:logical-id": "{constructor_name}TestLambdaB1BA23AA",
"aws:cloudformation:stack-id": "arn:aws:cloudformation:{region}:{account_id}:stack/{stack_name}/{stack_id}",
"aws:cloudformation:stack-name": "{stack_name}"
},
"Timeout": 30,
"TracingConfig": {
"Mode": "Active"
}
}
LambdaのDockerイメージをCDKの外から変更
では、lambrollを使って、参照するイメージを変更してみましょう。
docker build
でビルドし、ECRの別リポジトリにプッシュします。
コマンドについては、AWSコンソール内、ECRリポジトリの画面にある「プッシュコマンドを表示」でそのままコピペして実行しました。便利ですね。
ローカルからのイメージプッシュが完了したあと、イメージURIを取得します。
そして、function.json
の Code.ImageUri
を書き換えて lambroll deploy
を実行したところ、無事デプロイできました。
確認: Dockerイメージが変わったことをCDKが変更とみなすか?
cdk diff
を実行してみます。
# (略)
Stack {stack_name}
There were no differences
✨ Number of stacks with differences: 0
差分は無いようです。ということは、デプロイしてもLambdaが変更されない(=CDK外で更新したLambdaに影響が無い)のでは?
✅ {stack_name} (no changes)
ということで、参照するイメージをCDK外で変更しても、CDKは差分を検出しない(=期待していた挙動)とわかりました。
fromImageAssetで参照するディレクトリ内に変更があった場合
前出の通り、インフラとアプリケーションはGitリポジトリを分けています。"./test_src"
ディレクトリは本来のアプリケーションコードではありませんが、もし、ここに変更があると、差分ありとなってしまいます。
回避策として、Lambdaの初回デプロイ用として、CDK側プロジェクトに「絶対に変更してはならないDockerイメージをビルドするためのディレクトリ」を用意する…でしょうか。
より安全にするなら、GitHub Actionsで、"./test_src"
配下に変更があった場合は必ずfailさせる…とか…?
小手先感があって、あまりしっくりはしていませんが…。
Lambdaの設定値をCDKの外から変更
たとえば、lambrollでMemorySize
を変更してみましょう。(1024
=> 512
)
その上でlambroll deploy
してみます。AWSコンソール上でも、メモリが512MBになったのを念の為確認。
確認: 設定値が変わったことをCDKが変更とみなすか?
CDKのコード上は、memorySize: 1024
のままですが、cdk diff
では差分なしとなります。外部で変更されたことは検知できません。
ということは、この状態でcdk deploy
しても、メモリサイズはlambroll側で変更した512MB
のままとなります。
lambrollの設定値が生きるので、期待している挙動をしてくれます。
(なお、CloudFormationのドリフト検出では当然検出されます)
(余談ですが、ドリフトについて気になるものをConstruct Hubで見つけました)
CDKとlambrollで設定値が食い違うのは正したい
lambrollでメモリ512MBに変更したため、CDK側の memorySize: 1024
が食い違ってしまいます。そこで、CDK側も512
の指定にしてみます。
当然、今度はcdk diff
で差分が発生します。MemorySizeのみなので、アプリケーション(ImageUri
)は変更されません。
└─ [~] MemorySize
├─ [-] 1024
└─ [+] 512
lambrollでの変更を、後からCDKでも書き換えたとしても、Lambdaの動作に影響は無いとわかりました。
結果: CDK外でLambdaを変更してわかったこと
- CDKでデプロイしたLambdaのアプリケーション(Dockerイメージ)をlambrollで変更してもCDKは差分は発生しない
- 設定値をlambrollで変更されてもCDKは差分は発生しない
- CDKでは最低限のLambdaをデプロイ(初期デプロイ)
- lambrollでそのLambdaに対してデプロイ
設定値はインフラの話だからlambroll管理にしたくない
lambrollはJsonnetという、JSONの拡張フォーマットが使えます。これにより、設定値を動的に読み込むことができます。さらに、Jsonnet内で使える {{ ssm }}
構文により、SSMに保存されている値を取得できます。
これを使い「Lambdaの設定値をCDKでSSMに保存」し「CDKでのLambda初期デプロイ時」と「lambrollによるデプロイ時」に設定値を参照するのはどうか?となりました。
先ほどの例と同じように、Lambdaに割り当てるメモリサイズで試してみます。
検証: SSMに設定値を保存する場合、設定値の変更・アプリケーションコードの変更が問題無く行えるか?
CDKでSSMに設定値を保存し、初期デプロイする
const memorySizeParamPath = "/test-lambda/params/memory-size";
const memorySize = 2048; // 実際は外部の設定ファイルから取得する
new aws_ssm.StringParameter(this, "TestSsmLambdaMemorySize", {
parameterName: memorySizeParamPath,
stringValue: memorySize.toString(),
});
new aws_lambda.DockerImageFunction(this, "TestLambda", {
timeout: Duration.seconds(30),
memorySize: memorySizeValue,
functionName: "test-lambda",
role: testLambdaRoll,
code: aws_lambda.DockerImageCode.fromImageAsset(
"./test_src",
{
platform: Platform.LINUX_AMD64,
},
),
});
lambrollでSSMの設定値を参照しデプロイする
local ssm = std.native('ssm');
{
// (略)
"MemorySize": std.parseInt(ssm('/test-lambda/params/memory-size')),
問題無くlambroll deploy
が完了します。
CDKで設定値を変更しデプロイする
2048
=> 512
へ変更し、cdk deploy
します。
const memorySize = 512;
lambrollでアプリケーションコード(イメージ)を変更してデプロイする
lambroll側で、使用するDockerイメージを変更しデプロイしてみます。ここで、メモリサイズについては前項で設定した512MBのまま、イメージだけが変更されることを期待しています。
Dockerイメージをビルドし、latest
タグを付け替えてECRリポジトリにプッシュします。
その後、lambroll deploy
を実行。
無事、期待通りの挙動になりました。
結果: SSMを使って設定値を管理できる
- Dockerイメージ(
latest
)は新しくプッシュしたイメージのsha256 digestを参照している - メモリサイズは512MBのまま
- 問題は無さそう
ついでにAPI Gatewayを立ててみる
とくに問題は起きないと思いますが、ついでの検証として、CDKでAPI Gatewayをデプロイしてみます。
new aws_apigatewayv2.HttpApi(this, "TestApi", {
defaultIntegration: new HttpLambdaIntegration(
"DefaultIntegration",
testLambda,
),
});
API Gatewayがデプロイできたら、HTTPリクエストをして index.js
のとおりのbodyがレスポンスされることを確認。
続けて、bodyの内容を変更してDockerイメージのビルド&プッシュ、lambrollのデプロイ実行をします。
再度、HTTPリクエストをして、レスポンスが変更されていることを確認します。
…ということで、ようやく実装に取りかかれそうです。
まとめ
- CDKでLambdaをデプロイしてもlambrollで更新できる
- ただし、CDKで最初にデプロイしたアプリケーションコードを変更してはならない
- Lambdaの設定値はSSMで共通化できる
- (手を動かして調査するの大事)
- fujiwaraさん、ありがとうございました
Discussion