🐑

AWS CDKとlambrollを組み合わせてLambdaをデプロイしたいので改めて調査をする

2024/12/03に公開

本記事は『AWS CDK Advent Calendar 2024』3日目の記事です。
(記事公開しようとしたらたまたま空いていたので…せっかくなので…)

https://qiita.com/advent-calendar/2024/aws-cdk

AWS CDKが大好きだけど、ふいんきで触ったり触らなかったりしているちゃちいです。最近またCDKを案件で使うことになりましたが、悩みがありました。

去る 2024-11-30、紅白ぺぱ合戦にて、Lambroll作者の @fujiwaraさんとお目にかかれて、タイトルの内容で困っていたので二次会で開口一番、質問させていただきました。fujiwaraさん、その節はありがとうございました…!

なぜ困っているのかと質問へのお返事についての前に、背景と改めてCDKの挙動を手で確認する必要があると感じたので、せっかくなのでまとめながら。

背景

CDKには、アプリケーションコードのディレクトリを指定するだけで Docker イメージのビルドをしてくれる DockerImageCode.fromImageAsset() と、それを使って Lambda をデプロイする DockerImageFunction があります。
とても便利ですが、息が長くなりそうなプロダクトに使うのは辛いモノがあります。

これについてはアイレットさんの記事が参考になります。こちらの記事ではコンテナイメージではありませんが、だいたい同じ理由で、アプリケーションのデプロイはCDKの管轄外としたいんですね。

https://iret.media/79297

ちなみに、なぜコンテナイメージにするかというと、アプリケーションコードが合計すると大きくなるためです。Lambda…というか、FaaSは一般的には細かい仕事をさせるものでしょうけれど、ぼくはLambdaのことを「手間の掛からない実行環境」として選んでいます。なので、LambdaでPHPを実行できるようにする Bref を使い、SymfonyやLaravelといったフレームワークを載せます。LambdaにWebアプリケーションフレームワークを載せることについては、@seike460さんの以下発表が参考になります。

https://speakerdeck.com/seike460/stepping-into-anti-patterns-serverless-php-frameworks-to-build-and-understand

まあ、つまりLambdaで実行したいアプリケーションコードがそもそも大きいので、CDKの便利機能でビルド・デプロイするのは責務を明確に分けるべきということです。

インフラとアプリを分ける…ecspresso…

ecspressoという素晴らしいツールがありまして、特にクラスメソッドさんの記事が詳しいですね。

https://dev.classmethod.jp/articles/ecspresso-ecs-deployment-tool/

ぼくも以前、Fargateを使う案件でCDKとecspressoの組み合わせで構築した経験はあります。

となれば、同じ思想で作られたLambdaをデプロイするツール、lambrollを使おうとするのは自然な流れですよね。

https://github.com/fujiwara/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イメージがビルドできればよいので。

Dockerfile
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" ]
index.js
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

(隠したい文字列は{}に置換しています)

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.jsonCode.ImageUri を書き換えて lambroll deploy を実行したところ、無事デプロイできました。

確認: Dockerイメージが変わったことをCDKが変更とみなすか?

cdk diff を実行してみます。

cdk diff
# (略)
Stack {stack_name}
There were no differences

✨  Number of stacks with differences: 0

差分は無いようです。ということは、デプロイしてもLambdaが変更されない(=CDK外で更新したLambdaに影響が無い)のでは?

cdk deploy
{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で見つけました)

https://construct-hub-testing.dev-tools.aws.dev/packages/cdk-drift-monitor/v/0.2.397?lang=typescript

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の設定値を参照しデプロイする

function.jsonnet
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