はじめに

最近 CDK を業務で書くことが多いです。CDK の強みはいろいろありますが、そのひとつに Asset の概念があると思います。これはざっくりいうと以下のような機能です。

  • デプロイ対象のアセット (Lambda のコードなど) を CDK のコードと一緒に管理できる
  • CDK がファイルのハッシュ値を計算し、変更があったアセットだけをデプロイ対象に含めてくれる

CDK はインフラとアプリのコードをひとつの CDK アプリとして取り扱うという思想を持っています。アプリ開発のベストプラクティスをインフラに持ち込むために、インフラとアプリのコードを一緒にバージョン管理して一緒にデプロイするという考え方です。

この思想に対して組織の考え方がフィットしない場合、CDK は採用すべきではないのかもしれません。インフラとアプリを管理するチームが完全に分離されており、アプリのリリースがインフラに影響を及ぼすことがあってはならないというような組織体制である場合です。
ただ、そこまでガチガチではなくても、アプリのコードはインフラのコードとは分離された状態で個別にデプロイしたい (逆もまた然り) といったニーズは一般的な考え方としてあるとも思います。

前置きが長くなりましたが、今回はこのようなニーズがある中で、特に Lambda 関数のデプロイについて、どう CDK から分離したかを紹介します。あくまでどう対処したかのプラクティスとして捉えていただければと思います (もっといい方法があるかもしれません)

整理すると以下となります。

  • インフラは CDK で管理
  • CI/CD パイプラインも CDK で管理
  • Lambda 関数のリソース定義も CDK で管理
  • Lambda 関数のコード自体は CDK から分離して個別に管理

リポジトリ構成

リポジトリは以下のような構成とします。

.
├─ backend
│   ├─ lambda1
│   └─ lambda2
│─ frontend
└─ infra

これに対して以下のパイプラインを構成するとします (infra はいったんおいておきます)

  • backend-lambda1-pipeline
  • backend-lambda2-pipeline
  • frontend-pipeline

なお、これらのパイプラインの起動をコード変更が発生したファイルパスによって制御する方法は別記事で紹介しているので割愛します (CodeCommit の場合)

モノレポ構造の CodeCommit でパイプラインの実行を制御する Lambda 関数を Go 言語で書いてみた

Lambda コードの分離

要するに Lambda 関数を定義する際に Asset を使わなければいいわけです。通常、以下のように fromAsset を使うことが多いと思います。

const testFunction = new lambda.Function(this, "testFunction", {
  functionName: "test-function",
  code: lambda.Code.fromAsset("../backend/lambda1"),
  handler: "index.handler",
  architecture: lambda.Architecture.X86_64,
  runtime: lambda.Runtime.NODEJS_18_X,
});

これを fromBucket を使って直接 S3 バケットから取得するようにすれば、アセットとして管理されずハッシュ値の計算は走らないので、Lambda コードの変更が CDK の差分として検出されることはありません。

const testFunction = new lambda.Function(this, "testFunction", {
  functionName: "test-function",
  code: lambda.Code.fromBucket("test-lambda-packages-bucket", "backend/lambda1/function.zip"),
  handler: "index.handler",
  architecture: lambda.Architecture.X86_64,
  runtime: lambda.Runtime.NODEJS_18_X,
});

デメリット

この場合 CDK は初回デプロイにおいて特定の S3 バケットに Lambda パッケージが配置されていることを期待するので、初回デプロイの前に以下を済ませておく必要があります。

  • S3 バケットの作成
  • Lambda パッケージのアップロード

つまり CDK のスコープから外れた S3 バケットを作成しておく必要があります。Lambda コードは zip で固めて配置する必要がありますが、初回は書き捨てのシェルスクリプトなどで対応すればよいでしょう。
私の場合、cdk.json からバケット名など必要な情報を読み込んで処理するスクリプトを書いて対応しました (STEP 2, 3 が該当)

メリット

あくまでこの要件でのメリットですが、インフラの変更に対して cdk deploy する際、Lambda コードの変更が検出されなくなります。ただし、メモリやタイムアウト値など、設定は CDK で書くので引き続き CDK の差分として検出されます。

パイプラインでの Lambda デプロイ

パイプラインからアップロードする場合、以下のような感じになると思います (参考)
切り戻しできるように、バージョンとエイリアスも設定しておくとよいです。

#!/usr/bin/env bash

echo "PROCESS: Installing node modules."
npm install

echo "PROCESS: Packaging lambda function."
zip -r "$FUNCTION_PACKAGE_NAME" . &>/dev/null

echo "PROCESS: Uploading lambda function."
aws s3 cp "$FUNCTION_PACKAGE_NAME" s3://"$BUCKET_NAME/$TARGET_PATH"/

echo "PROCESS: Updating lambda function."
new_version=$(
  aws lambda update-function-code --function-name "$FUNCTION_NAME" --s3-bucket "$BUCKET_NAME" --s3-key "$TARGET_PATH/$FUNCTION_PACKAGE_NAME" --publish --query "Version" --output text || {
    echo "ERROR: Failed to update function for $FUNCTION_NAME."
    exit 1
  }
)

echo "PROCESS: Updating lambda function alias."
aws lambda update-alias --function-name "$FUNCTION_NAME" --name "$FUNCTION_ALIAS" --function-version "$new_version" || {
  echo "ERROR: Failed to update function alias for $FUNCTION_NAME."
  exit 1
}

exit 0

おわりに

今回は比較的簡単な方法で要件を達成することができました。
CDK に限った話ではありませんが、その仕組みが組織のポリシーと合わずに逆に辛くなってしまうことが想定される場合、無理やり使う必要はありません。