はじめに

CDK Docker Image Deployment (cdk-docker-image-deployment) という CDK ライブラリがリリースされていることを知りました。

これは CDK で構成する Docker イメージを任意のリポジトリに保存できるようにするもので、将来的には公式に aws-cdk-lib に取り込まれことも想定して作られたもののようです。

これから、このライブラリによってどんな問題が解決できるかについて順番に確認しながら、実際に使ってみたいと思います。

CDK で Docker イメージを構築し ECR にプッシュするときの問題

ContainerImage.fromAsset でイメージをビルド&プッシュ

例えば、以下のように1つのプロジェクトにアプリケーションと CDK のコードがあるとします。

.
├── app
│   ├── Dockerfile
│   └── index.js
└── cdk
    ├── bin
    ├── cdk.json
    ├── cdk.out
    ├── jest.config.js
    ├── lib
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    └── tsconfig.json

CDK でアプリケーションの Docker イメージをビルドをして ECS で動かしたいときには、ContainerImagefromAsset を使ってローカルパスの Docker イメージをビルドし、aws_ecs_patterns のコンストラクトを利用するのが一番手軽でしょう。

//**************************************************** */
// Task Definition
//**************************************************** */
const taskDefinition = new ecs.FargateTaskDefinition(
  this,
  "TaskDefinition",
  {
    runtimePlatform: {
      operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
      cpuArchitecture: ecs.CpuArchitecture.ARM64,
    },
  }
);

// Add container to task definition
taskDefinition.addContainer("AppContainer", {
  image: ecs.ContainerImage.fromAsset(
    path.resolve(__dirname, "../..", "app")
  ),
  portMappings: [
    {
      containerPort: 3000,
    },
  ],
});

//**************************************************** */
// ECS Fargate Service
//**************************************************** */
new ecsPatterns.ApplicationLoadBalancedFargateService(this, "Service", {
  taskDefinition,
  vpc,
});

これをデプロイすると、CDK でリソースがデプロイされるのと同時に、アプリケーションのイメージのビルドと ECS へのデプロイが実行されます。今のところ期待通りの挙動です。

しかし、ビルドしたイメージはどこに push されているのでしょうか?

ECR を見てみると、cdk-hogehoge-container-assets-000000000000-ap-northeast-1 のような名前のリポジトリが自動的に作られており、イメージタグもランダムな文字列になっています。

つまり、ContainerImage.fromAsset で作られたイメージは CDK の処理の流れの中でのみ使われることを意図していて、イメージを特定することが非常に困難なので、再利用することができません。

これは、イメージの格納先を意識しなくてもデプロイできるように意図された仕様です。ただ、ごく小規模なプロジェクトであればこれでも十分かもしれませんが、緊急時の切り戻しなどを考えると、本番で利用するのはちと怖い仕組みになってしまっています。

DockerImageAsset でイメージをビルド&プッシュ

それなら、事前に ECR リポジトリを作って、そこにプッシュするように構成すればどうでしょうか。

イメージとしては以下のようなコードになっていきそうな雰囲気です。ちなみに先に言っておくとこのコードは意味のないコードになっています。

const repository = new ecr.Repository(this, "Repository", {
  repositoryName: "example-repository",
});

const dockerImageAsset = new ecrAssets.DockerImageAsset(
  this,
  "DockerImageAsset",
  {
    directory: path.join(__dirname, "../..", "app"),
  }
);

// 略

// Add container to task definition
taskDefinition.addContainer("AppContainer", {
  image: ecs.ContainerImage.fromEcrRepository(repository),
  portMappings: [
    {
      containerPort: 3000,
    },
  ],
});

コードをよく見るとおかしな点に気づくと思いますが、DockerImageAsset でビルド&プッシュしたイメージがどこにも使われていません。もしこれをこのままデプロイしたら、example-repository にプッシュされたイメージが存在しないので、ECS のタスク起動がずっとエラーになり続けることでしょう。

本当は、DockerImageAsset でビルドしたイメージは Repository コンストラクトで構成した ECR リポジトリにプッシュされてほしくて、ECS でそのイメージを使って欲しいのですが、DockerImageAsset にはリポジトリ名やタグを指定できないので、やりたいことが実現できません。詰みました…

cdklabs/cdk-ecr-deployment というライブラリ

さて、この困った状況を何とかするために作られたのが、cdklabs/cdk-ecr-deployment です。

CDK の公式ドキュメントにも記載のあるもので、ここに、まさに今直面している問題が意図された仕様によるものであることと、そして、それを回避するツールとして cdklabs/cdk-ecr-deployment が利用できることが書かれています。

DockerImageAsset is designed for seamless build & consumption of image assets by CDK code deployed to multiple environments through the CDK CLI or through CI/CD workflows. To that end, the ECR repository behind this construct is controlled by the AWS CDK. The mechanics of where these images are published and how are intentionally kept as an implementation detail, and the construct does not support customizations such as specifying the ECR repository name or tags.

If you are looking for a way to publish image assets to an ECR repository in your control, you should consider using cdklabs/cdk-ecr-deployment, which is able to replicate an image asset from the CDK-controlled ECR repository to a repository of your choice.

cdklabs/cdk-ecr-deployment を使うと、任意の ECR リポジトリにイメージをコピーできるため、コピー先のリポジトリを管理されたリポジトリとして扱って運用することが可能になります。CDK のコードだと、以下の追記箇所がその処理になります。

const repository = new ecr.Repository(this, "Repository", {
  repositoryName: "example-repository",
});

const dockerImageAsset = new ecrAssets.DockerImageAsset(
  this,
  "DockerImageAsset",
  {
    directory: path.join(__dirname, "../..", "app"),
  }
);

// !! 追記: cdk-ecr-deployment !!
new ecrdeploy.ECRDeployment(this, "DeployDockerImage", {
  src: new ecrdeploy.DockerImageName(dockerImageAsset.imageUri),
  dest: new ecrdeploy.DockerImageName(
    `${awsAccount}.dkr.ecr.ap-northeast-1.amazonaws.com/example-repository:latest`
  ),
});

// 略

// Add container to task definition
taskDefinition.addContainer("AppContainer", {
  image: ecs.ContainerImage.fromEcrRepository(repository, "latest"),
  portMappings: [
    {
      containerPort: 3000,
    },
  ],
});

これは期待通りに動作します。cdklabs/cdk-ecr-deployment によって example-repository にイメージがコピーされているので、このイメージを使って ECS へのコンテナのデプロイが実現できています。

ここではタグに latest を使っていますが、切り戻しなどを考慮するなら、Git コミットハッシュなどをタグとして設定できるような工夫をすることも可能です。

これで問題が解決しました!

そして、cdk-docker-image-deployment

…はい、ここまで長い長い前振りでした。ようやく本題になります。

先日、本記事で説明してきた課題についての issue を見ていたらこのコメントがありました。

https://github.com/aws/aws-cdk/issues/12597#issuecomment-1243879965

どうやら、cdk-docker-image-deployment という、cdklabs/cdk-ecr-deployment と同じような問題を解決するための新しいライブラリができたようです。

https://github.com/cdklabs/cdk-docker-image-deployment

そして、これは現在は cdklabs 配下のプロジェクトですが、CDK チームによってメンテされていて、人気があれば本家 aws-cdk-lib に同梱することも検討するとのこと。

これは使ってみるしかありません。

やってみた

とりあえず README の通りにやってみます。

const repository = new ecr.Repository(this, "Repository", {
  repositoryName: "example-repository",
});

new imagedeploy.DockerImageDeployment(
  this,
  "ExampleImageDeploymentWithTag",
  {
    source: imagedeploy.Source.directory(
      path.join(__dirname, "../..", "app")
    ),
    destination: imagedeploy.Destination.ecr(repository, {
      tag: "myspecialtag",
    }),
  }
);

// 略

// Add container to task definition
taskDefinition.addContainer("AppContainer", {
  image: ecs.ContainerImage.fromEcrRepository(repository, "myspecialtag"),
  portMappings: [
    {
      containerPort: 3000,
    },
  ],
});

まず、かなり記述量が減ってスッキリとしたコードになっているところがいいですね。また、cdklabs/cdk-ecr-deployment の場合にはコピー先を文字列で指定する必要があったのですが、Repository クラスを渡す API なので TypeScript の型チェックが効くのも嬉しいポイントです。

実際試してみた結果ですが、期待通りの処理を実現することができました。つまり、CDK で ECR リポジトリを構成し、ビルドしたイメージをそのリポジトリにタグ付けしてプッシュ、そしてそのイメージを ECS のコンテナとしてデプロイする、ということが CDK のデプロイだけで可能です。

具体的に裏側でどのような処理についても README に詳しく書いてありますが、コピー元からの docker pull とコピー先への docker push を行うことで ECR リポジトリ間のコピーをする CodeBuild のプロジェクトが中心的な役割をになっているようです。実際にデプロイ後に CodeBuild を確認すると、まさにそれだけをやっているビルドプロジェクトが作られていました。

おわりに

公式に cdk-docker-image-deployment がリリースされたことで、CDK で ECS を扱うことが更にやりやすくなって、業務などでの本番利用でも CDK でアプリケーションデプロイをすることがより現実的な選択肢となるように思います。ぜひ aws-cdk-lib に入れて欲しいです。

一方、できたばかりのプロジェクトなので、今後破壊的変更が入ったり、まだ何か問題などもあるかもしれません。実際、最初試したときに README のサンプルコードをコピペしたらいきなりシンタックスエラーになってマジかよって思いました (なのでそれは pull request を出しておいた)。とはいえ、多くの人が使ってフィードバックしてアピールしていかないと本家にも取り込まれないだろうから、これからは積極的に使って行きたいと思います。皆さんもぜひ使ってみてください。

最後に、こちらが今回試した CDK のサンプルコードになります。

https://github.com/5t111111/docker-image-deployment-example