echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【AWS CDK】LambdaをDockerイメージでデプロイする方法(最終編)

今回のお題

前々回、及び前回にわたり「LambdaをDockerイメージでデプロイする方法」について記載しました。
今回はその最終編ということで、前回紹介した「ハマりどころ」の続きを記載しようと思います。

なお今回触れる事項は、前々回に少し触れた「インフラ側とアプリ側でソース管理を分ける」ケースで発生するものです。

アジェンダ

  • アプリ側のimageタグの設定をCDKにも反映するには?
  • 初回のみイメージがなくてエラーになる現象を回避するには?

アプリ側のimageタグの設定をCDKにも反映するには?

インフラ側とアプリ側でソース管理を分ける場合、LambdaのDocker Imageに付与するイメージタグは、基本アプリ側に依存します。(よくあるのが、GitHubコミットハッシュの先頭数文字」)

そして、CDKを更新する場合に、CDKのLambda関数の定義にこのイメージタグを設定する必要があります。

前々回に紹介したEcrImageCodePropsのpropsに「tagOrDigest」というプロパティがあり、これのデフォルト値は「latest」です。
なのでイメージタグを指定しないと該当Lambda関数が参照するイメージタグが強制的に「latest」になってしまい、当然そんなタグが付いたイメージはECRにないのでエラーになってしまいます。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.EcrImageCodeProps.html

そのため、デプロイ時にアプリ側Lambdaのimageタグを取得する必要があります。

対策

これの対策としては「CDKとアプリ側でイメージタグを共有する仕組みを作る」方法があります。

やり方は色々ありますが「AWS SSM パラメータストアで値を共有する」方法がセキュアでよいのではないかと思います。

具体的には手順は下記の通り感じです。

  1. アプリ側でLambda関数を更新(=イメージタグを採番)する際、そのイメージタグをパラメータストアに登録する
  2. CDK側でデプロイを実施する際、1のパラメータストアの値を取得する
  3. 2の値を tagOrDigest に設定する

ポイントとしては下記の4点です。

  • パラメータストアへの登録は、AWS CLIを使うと良いです。
    • AWS CDKではパラメータストアへの登録はできません。(取得は可能)
  • パラメータストアの値は、aws-cdk-lib.aws_ssmStringParameter.valueForStringParameter() で取得可能。
  • パラメーターストアのキー値は、アプリ側とCDK側で共有しておく
    • 可能ならば環境変数などで設定しておく(CodePipelineなど)
  • 初回のCDKデプロイ時(=アプリ側がまだ存在していない)のみ、該当キーの値が存在せずエラーになるので、何か対策しておく(例えば下記)
    • 仮の値を手動でパラメーターストアに登録する
    • CDK側でもパラメータストアの値をAWS CLIで取得して、存在しない場合には仮の値を入れる *1
// 関連部分以外は省略
// パラメータストアからイメージタグタグの取得
const tag = StringParameter.valueForStringParameter(this, 'lambdaImageTag');
  
// イメージタグを適用(tagプロパティはdeprecatedなので使わないこと)
const lambdaDockerEcrFunction = new lambda.DockerImageFunction(this, 'SampleLambdaDockerEcrFunction', {
  code: lambda.DockerImageCode.fromEcr(ecrRepo, {
    tagOrDigest: tag,
  }),
});

初回のみイメージがなくてエラーになる現象を回避するには?

前々回でも触れましたが、DockerImageFunction でLambda関数を作成する場合、指定したECRに該当Lambdaのイメージがないとデプロイ時にエラーになってしまいます。
通常はそれで問題ないですが、初回デプロイ時だけはアプリ側がまだ存在していないため、そのままだと100%エラーになってしまいます。

なので、初回デプロイ時のみこの問題に対策する必要があります。

対策:

これについては「初回限定の仮イメージを登録する」ことで対処可能です。
※初回限定なので、Lambda定義さえあれば中身は何でもよいです。

で、この「初回限定イメージのECR登録」については「cdk-ecr-deployment」を使用すると非常にシンプルに実装できます。

github.com

この「cdk-ecr-deployment」を用いて、下記の処理を行うことで対応できます。

  1. パラメータストアにイメージタグの値を取得する
  2. 1の値が取得不可だったら(=初回デプロイ時のみ発生)、イメージタグに仮の値を設定する(下記ソースでは「initail」)
  3. イメージタグが仮の値だったら、cdk-ecr-deploymentを使用してECRに仮のイメージを設定する*2
import * as ecr_deployment from 'cdk-ecr-deployment';
import { aws_ecr } as ecr from 'aws-cdk-lib';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';  
import path from 'path';

const INITIAL_IMAGE_TAG = 'initial';
  
// 1および2
// getParameterStoreValueは、AWS CLIでパラメータストアからイメージタグの値を取得する関数。(なければ空文字を返す)
const imageTag = getParameterStoreValue(LATEST_IMAGE_TAG_KEY_NAME, profile) || INITIAL_IMAGE_TAG;  
  
// ECRの作成
const repo = new ecr.Repository(this, 'Repository', {
  ...(省略)
});
  
// 3(初回のみ仮イメージをECRにpush)
if (imageTag === INITIAL_IMAGE_TAG) {
 
  // 仮イメージの設定。
  // directoryにはDockerfileファイルがあるフォルダのパスを指定
  const image = new DockerImageAsset(this, 'DockerImageAssetInitialOnly', {
    directory: path.resolve(__dirname, '../docker');
  });
  new ecr_deployment.ECRDeployment(this, 'InitialOnlyImage', {
    
    // ここでは仮イメージをECRに設定
    src: new ecr_deployment.DockerImageName(image.imageUrl),
    dest: new ecr_deployment.DockerImageName(repo.repositoryUriForTag(INITIAL_IMAGE_TAG)),
    
    // なおsrcにはローカルのイメージ以外にも、ECRパブリックリポジトリやDocker Hubなどのイメージも指定可能。 
    // src: new ecr_deployment.DockerImageName(`public.ecr.aws/docker/library/nginx:mainline-alpine3.18-slim`),
  });
}

ちなみにこのあたりの「アプリとインフラを分離した際に発生する問題」に関しては、9/30(土)に開催されたJAWS FESTA 2023 in Kyusyu 直前スペシャル!! にて「AWS CDKでインフラ、アプリを分離した際に困ったこと」という内容で発表を行いましたので、よろしければそちらの資料をご参照ください。

speakerdeck.com

宣伝

11/19(日) 開催のJSConf JPにて、発表をさせて頂くことになりました。

jsconf.jp

内容としては「最近話題の(?)Bun について、実際にプロダクトワークロード(とりあえずLambda)で動かしたらどうなの?」という内容になりますので、よろしくお願いいたします。

それでは、今回はこの辺で。

*1:AWS CLIで直接値を取得すれば、cdk synthの時点で値を取得するので、try~catchによる制御が可能。 ※StringParameter.valueForStringParameterではできない

*2:なお始めは「イメージタグが存在する場合のみ、Lambda定義の処理を通るようにする」という処理をしていましたが、それだと万が一何かあった際にLambda関数が削除されてしまうため、やめました