mazyu36の日記

某SIer所属のクラウドエンジニアのブログ

AWS CDKでECSタスク定義を作成後、アプリを更新するとイメージタグがずれる問題への対応

CDKでECSのタスク定義を作成すると、必ず遭遇する(と思う)コンテナのイメージタグがCDKと実態でズレる問題への対応についてです。

以下のスライドでまさに言及されているものです。

AWS CDKでECS on FargateのCI/CDを実現する際の理想と現実 / ideal-and-reality-when-implementing-cicd-for-ecs-on-fargate-with-aws-cdk - Speaker Deck

過去試行錯誤したので、自分なりに整理してみます。

事象の内容

まず内容と流れについて整理します。なおイメージタグに使用するのはコミットハッシュ等が一般的ですが、説明の中では単純化のためv1, v2などを用います。

前提

コンテナのイメージタグでlatest等固定のタグを使わずに、イミュータブルなタグ運用を行う場合に発生します。よくあるのはコミットハッシュをタグに付与するなどのやり方です。以下はCodeBuildでやる場合の例。

github.com

latest固定(いわゆるLatestタグ運用)は一般的にアンチパターンと言われています。タイミングによってlatestが指しているものがズレるため、後からトレースが困難になる、資材が混ざる可能性がある、などの問題があります。

progret.hatenadiary.com

vsupalov.com

またアプリとインフラ(CDK)のリポジトリが別になっており、ライフサイクルが別れている時に起こります。そこそこ大規模なプロジェクトでは分かれていることが多いんじゃないかなという気がします。

上記の前提踏まえ、事象について流れを以下に記載します。

(1). CDKでタスク定義をデプロイ

CDK上でv1のタグを指定してタスク定義をデプロイします。

    taskDefinition.addContainer('TestContainer', {
      containerName: 'test',
      image: ecs.ContainerImage.fromEcrRepository(props.ecrConstruct.ecrRepository, 'v1'), // タグを指定
    })

この時点では実際のタスク定義とCDKのタグはv1で一致しています。

(2). アプリの更新が入り、CICDでタスク定義が更新される

その後CDKとは別リポジトリのアプリが更新され、CICDによりタスク定義のバージョンがv2に変わります。

この時CDK上のタスク定義のバージョンv1とズレた状態になります。

(3). CDK上でタスク定義を更新しデプロイ

問題になるのはこのタイミングです。CDK上のタスク定義のバージョンv1をそのままの状態で、CPU等変更を加えてデプロイするとタグのバージョンがv1に戻ってしまいます。

AppとCDKが別リポジトリになっており、ライフサイクルが分かれていることにより発生します。

対応策

以下考えた対応先を5つ記載します。なおAWS CDK以外の外部ツールなどは使わないことを前提としています。

1. latestタグを使う

最も単純なのはlatestタグを使うやり方です。latestタグを使うとApp, CDKどちらでも常にlatestなので、タスク定義とCDKの実装上でズレることがありません。

ただし先述の通りアンチパターンなので、基本は避けた方が良いかと思います。

2. CDKは初期生成のみで、更新はマネコン等別で行う。

タスク定義の初期生成のみはCDKで行い、以降の更新は別のツール(マネコン、CLI、etc...)で行うというやり方です。一度でも更新が入るとCDKと実態が乖離し続けることになります。

個人的にはこれも避けた方が良いかと思います。CDKのメリットとして、コードを読めば設定内容が確認できること、また設定値のテストができること、だと思っているので、そのメリットを捨ててしまうことになるからです。

3. CDK上で変更を加える場合は最新のタグを設定する

運用でカバーする案です。CDKでタスク定義の更新をかける際は、実装上のタグを修正してからデプロイして、先祖返りしないようにします。

実装イメージは以下です。最初はv1となっていたところをv2に変えたのみです。

    taskDefinition.addContainer('TestContainer', {
      containerName: 'test',
      image: ecs.ContainerImage.fromEcrRepository(props.ecrConstruct.ecrRepository, 'v2') // タグを更新
    })

タスク定義の数が少なければ、これでもカバーできるかと思います。実際に過去使っていたものはこれでした。

ただしCDK上のタグの更新を忘れると先祖返りする可能性があるので注意が必要です。

4. SSM Parameter Storeを使う

これは以下を参考にさせていただきました。3をSSM Parameter Storeを活用して自動化するイメージです。

下流れを実装イメージと合わせて記載します。

4-1. SSM Parameter Storeにタグを保存するパラメータを手動で初期生成

事前に手動でSSM Parameter Storeに値を登録しておきます。

CDKで初期生成しても良いかと思いましたが、初期値が実装上残るので別管理にした方がシンプルかと思いこのようにしています。

const ssmParameter = new ssm.StringParameter(this, 'mySsmParameter', {
   parameterName: 'mySsmParameter',
   stringValue: 'mySsmParameterValue',
});

注意点として、この時点では登録するタグの値が存在する(コンテナイメージが1つ以上すでに格納されていること)が前提となっています。

もしまだ存在しない場合は一旦適当な値を埋めておくことでも可能ですが、その状態でECSのサービスをデプロイしようとすると存在しないタグを引っ張ろうとしてしまいます。その場合はタスク数を一旦0にしておいて、存在するタグの値がSSM Parameter Storeに格納されたら以降タスク数を1以上にするといったことが必要です。

4-2. CDKデプロイ時にSSM Parameter Storeの値を読み取る

CDKからタスク定義をデプロイする際に、タグはSSM Parameter Storeから読み取るようにします。

実装イメージは以下です。ssm.StringParameter.valueForStringParameterを使用すると、CDKのデプロイ時にSSM Parameter Storeから値が取得されます。

    // SSMから値を取得
    const tag = ssm.StringParameter.valueForStringParameter(scope, 'tag');

    taskDefinition.addContainer('TestContainer', {
      containerName: 'test',
      image: ecs.ContainerImage.fromEcrRepository(props.ecrConstruct.ecrRepository, tag),
    })

4-3. AppのCICDにおいてイメージプッシュ時はSSM Parameter Storeの値も更新する

アプリのCICDにおいて、コンテナイメージを更新した際はSSM Parameter Storeの値も更新するようにします。

CodeBuildで行う場合のイメージは以下です(CDKのcodebuild.BuildSpec.fromObjectに渡す形式で記載)。

post_buildでイメージプッシュ後にSSM Parameter Storeの値をCLIで更新しています。

    const buildSpecObject = {
        version: 0.2,
        env: {
            variables: {
                'AWS_REGION_NAME': 'ap-northeast-1',
                'ECR_REPOSITORY_NAME': `${repositoryName}`,
            }
        },
        phases: {
            pre_build: {
                commands: [
                    'AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query \'Account\' --output text)',
                    'aws ecr --region ap-northeast-1 get-login-password | docker login --username AWS --password-stdin https://${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION_NAME}.amazonaws.com/${ECR_REPOSITORY_NAME}',
                    'REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION_NAME}.amazonaws.com/${ECR_REPOSITORY_NAME}',
                    '# Use Git Commit hash for image tag',
                    'IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION})',
                ]
            },
            build: {
                commands: [
                    'docker image build -t ${REPOSITORY_URI}:${IMAGE_TAG} .'
                ]
            },
            post_build: {
                commands: [
                    'echo Build completed on $(date)',
                    // コンテナイメージをプッシュ
                    'docker image push ${REPOSITORY_URI}:${IMAGE_TAG}',
                    // SSM Parameter Storeにタグを登録
                    'aws ssm put-parameter --overwrite --name "tag" --type "String" --value ${IMAGE_TAG}',
                    'printf \'[{"name":"%s","imageUri":"%s"}]\' $ECR_REPOSITORY_NAME $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json',
                ]
            }
        },
        artifacts: {
            files: [
                'imagedefinitions.json',
            ]
        }
    }

4-4. CDK上からタスク定義の更新をかける

4-3の後にCDK上でタスク定義の更新が入ったとします。しかしデプロイ時にSSM Parameter Storeから最新のイメージタグを取得するので、先祖返りを防ぐことができます。

5. TagParameterContainerImageを使用する

公式のReferenceでTagParameterContainerImageとCodePipelineを使用する対応案が示されています。

ですが仕組みとしては難解です。過去一度導入を試したことはありますが、維持できそうになく断念しました。

ここでは紹介のみにとどめます。

docs.aws.amazon.com

まとめ

以下5つの対応策をご紹介しました。

  1. latestタグを使う
  2. CDKは初期生成のみで、更新はマネコン等別で行う。
  3. CDK上で変更を加える場合は最新のタグを設定する
  4. SSM Parameter Storeを使う
  5. TagParameterContainerImageを使用する

個人的に過去は3を使うことが多かったのですが、今後行う場合は4がファーストチョイスになりそうです。

また今回はAWS CDKのみという縛りでしたが、他ツールも含めた運用方法は模索していきたいと思います。