見出し画像

CDK にアプリコード変更を検出させつつ、ECS へデプロイ

はじめに

こんにちは、build サービスチームで Platform エンジニアをやっている t.g. です。

以前、「CDK を使った ECS アプリコードの変更検出のやり方」をご紹介しました。その記事ではECR へのイメージ push 止まりでしたので、今回は、ECS (Fargate)にデプロイする部分について解説したいと思います。

前回のサマリ

CloudFormation + CLI デプロイ
メリット: 単純
デメリット: アプリコードの変更検出を自前で作成する必要

ecs.ContainerImage.fromAsset 利用
メリット: アプリコードの変更検出
デメリット: ECR, ContainerImageタグを変えられない

cdk-docker-image-deployment 利用
メリット: アプリコードの変更検出, 自前ECR/タグ可能
デメリット: 少し大掛かり(CodeBuildがバックで動作)

CDK + ECS + ECR の構成で、自前の ECR レポジトリを使いたいならcdklabs/cdk-docker-image-deployment が便利という結果でした。
詳細をお知りになりたい方は、CloudFormation+ECS で困っていたアプリコード変更検出をCDKで実施 を御覧ください。


ECS デプロイに満たしてほしい要件

前回同様、アプリコードに変更があったときのみタスクを更新してもらいたい、という要件で調査しました。

以下、各種の実験結果になります。

案1: タグを指定せずにデプロイ

はじめにトライしてみたテンプレートが以下になります。
- ECR を作成
- cdklabs/cdk-docker-image-deployment で 指定リポジトリに push
- Fargate をデプロイ

    // ECR を作成
    const repository = new ecr.Repository(this, 'Repository', {
      repositoryName: 'cdk-ecs-test4',
      autoDeleteImages: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // Docker Image を ECR に push
    const newImage = new imagedeploy.DockerImageDeployment(this, 'DeployImage', {
      source: imagedeploy.Source.directory(
        path.join(__dirname, '../', 'app')
      ),
      destination: imagedeploy.Destination.ecr(repository, {}),
    });

    // そのまま ECS Fargate をデプロイ
    const applicationLoadBalancerFargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'FargateService', {
      cluster: new ecs.Cluster(this, 'Cluster', {
        clusterName: 'cdk-ecs-test',
      }),
      memoryLimitMiB: 512,
      cpu: 256,
      desiredCount: 1,
      taskImageOptions: {
       image: ecs.ContainerImage.fromEcrRepository(repository), // latest タグを探すのでエラー
      },
    });

すると、ECS タスクのデプロイ中に、タスクに以下のエラーが発生しました。

ECS: latest がないエラー

applicationLoadBalancerFargateService の taskImageOptions オプションにタグを指定しないと、下記のようなエラーが発生してタスクが起動しません。
これは、タグ未指定の場合 `latest` を探すようになっていることが原因です

案2: タグを指定してデプロイ

cdklabs/cdk-docker-image-deployment の imagedeploy で push したDocker イメージのタグが見つかれば良いのですが、ちょっとわからず。。

ここで、前回使っていたアレを思い出しました。

const dummyAsset = new ecrAssets.DockerImageAsset(this, 'dummy-image', {
  directory: path.join(__dirname, "../", "app"),
});

前回使用を諦めた aws_ecr_assets です。
こちらは、cdk 管理の ECR リポジトリにイメージが push されるため使用を見送っていましたが、以下の利点があります。

  • HASH tag を作ってくれる

  • HASH tag を output してくれる <-- ほしい!

cdklabs/cdk-docker-image-deployment が内部的に aws_ecr_assets を使っていることから、作成される HASH tag は同一のものになります。

ECR に push されたイメージ。
上: cdk-docker-image-deployment が push
下: aws_ecr_assets が push

この結果、定義したテンプレートが以下になります。
- ECR を作成
- aws_ecr_assets でイメージをpush
- cdklabs/cdk-docker-image-deployment で 指定リポジトリに push
- Fargate をデプロイ
- tagとして、aws_ecr_assets の値を指定

    // ECR 作成
         (snip)

    // Docker Image を ECR に push
    const newImage = new imagedeploy.DockerImageDeployment(this, 'DeployImage', {
      source: imagedeploy.Source.directory(
        path.join(__dirname, '../', 'app')
      ),
      destination: imagedeploy.Destination.ecr(repository, {}),
    });

    // ダミーで asset を作成。使わないけどTAGが欲しいので.
    const dummyAsset = new ecrAssets.DockerImageAsset(this, 'dummy-image', {
      directory: path.join(__dirname, "../", "app"),
    });


    // そのまま ECS Fargate をデプロイ
    const applicationLoadBalancerFargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'FargateService', {
      cluster: new ecs.Cluster(this, 'Cluster', {
        clusterName: 'cdk-ecs-test',
      }),
      memoryLimitMiB: 512,
      cpu: 256,
      desiredCount: 1,
      taskImageOptions: {
        // image: ecs.ContainerImage.fromEcrRepository(repository), // latest タグを探すのでエラー
        image: ecs.ContainerImage.fromEcrRepository(repository, dummyAsset.assetHash), // こうすると、指定したタグが使われる
      },
    });

ポイントは、イメージの HASH をアウトプットしてくれる aws_ecr_assets (dummyAsset.assetHash)を利用しているところです。
同一イメージが異なるリポジトリに重複して push されるため、ビルド時間が余計にかかるわけではありません。

唯一考えられる弱点は、重複分 ECR ストレージを余計に消費することでしょうか

案3: 自前で hash を計算する

同僚から、「cdk synth 前に自前でアプリフォルダの hash を計算し、タグを明示的に指定するといいのでは?」と指摘をもらいました。

たしかに、これなら余分な ECR も使わないし、hash 計算にかかる計算コストなんて微々たるものなのでいいですね。

package.json の scripts に `cdk build` と一緒に書いておくか、CICD でビルド前のプリタスクとして定義すればできそうです。

案4: 他の手は?

カスタムリソースを作るのも手だと思います。
ただ、依存関係の解決が難しそうなのと、moving parts を最小限にしたいという観点から、これ以上の深追いはやめました



まとめ

DockerImageAsset を利用することで、ECS Fargate へのデプロイもこちらの意図したとおりになりました。

アプリコードに変更がない場合とある場合で、cdk deploy したときの挙動は以下のようになりました。

[コードに変更がない場合]
- ECR のイメージは更新されない
- ECS のタスク定義そのまま -> タスクもそのまま

[コードに変更がある場合]
- ECR のイメージを更新
- ECS の タスク定義が更新 -> タスク更新

ひと工夫で思い通りのクリーンな状況になって御の字です。
プロジェクトでも使ってみていますが、今のところおかしな挙動もなく動いてますね。
みなさまも使ってみてください