• 作成:
  • 更新:

AWS CDKでECSにデプロイするDockerイメージのマシな管理方法

大前提の問題

AWS CDKを使ってECSへの継続的なデプロイを行う場合、単純にイメージをfromEcrRepositoryなどで指定すると、 Dockerイメージのみが変更された時、素直にイメージを指定しているだけだと変更が検知されないのでECSタスクの更新が行われません。

主な解決策

3つあります。

  • ContainerImage.fromAssetを使う
  • イメージのダイジェストをイメージのタグとして付与する
  • gitコミットハッシュをイメージのタグとして付与する

それぞれ利点や問題点を書いていきます。

ContainerImage.fromAssetを使う

一見一番良さそうに思える方法です。

aws-ecr-assets module · AWS CDK にある class ContainerImage · AWS CDK のメソッドを利用します。

ContainerImage.fromAssetContainerImage.fromDockerImageAssetの両方ありますが、 ContainerImage.fromAssetで基本的に良いでしょう。イメージを共有して複数箇所に配置するとかなら後者の出番もあるかもしれません。

ContainerImage.fromAssetにDockerfileのあるディレクトリを指定するだけで、 ECRリポジトリの作成、イメージのビルド、プッシュ、全て自動で行ってくれます。

これ最高では? experimental扱いですが… と思ったのですが、 aws-cdkみたいなリポジトリが出来てしまいます。ではリポジトリ名を指定すれば良いのでは? と思ったら引数のrepositoryNameが非推奨で、書き換え先もまた非推奨です。

何故だろうなと思って調べた所、

Confusing Deprecations: IDeploymentEnvironment / Stack.addDockerImageAsset / DockerImageAsset.repositoryName · Issue #8483 · aws/aws-cdk で議論されていて、 aws-ecr-assetsが作るリポジトリは一時的な領域であることが分かりました。だからタスクがイメージを読み込み終えたらイメージが即座に消失するのですね。

これはcontextでassets-ecr-repository-nameを指定すれば良いとかそういう次元の問題ではありませんね。もしcdk deployした後問題が起きてとりあえずrollbackしたくなった場合、イメージが消えていたらrollback出来ません。

なので、社内向けとかそういう手軽な用途ならともかく、広く一般に公開するときの手法としては適さないでしょう。

イメージのダイジェストをイメージのタグとして付与する

ECS(Fargate)のServiceをCDKで構築・デプロイしてみた | Developers.IO

で使われている方法です。

CloudFormationのImageではtagはサポートされていてもdigestはサポートされていないため、泥臭くタグに移し替えす必要があります。

これをもうちょっと洗練させて、 Cannot reference EcrImage by digest instead of tag · Issue #5082 · aws/aws-cdk の方が貼っているDigestibleEcrImageを使うと少しスッキリするかもしれません。

しかしこれにも問題があります。それはリモートリポジトリのlatestのdigestを見ているため、複数ブランチで開発した時に間違ったイメージを見てしまう可能性が出来るという点です。

例えばステージをdeve, stagと分けていた場合、 deveで実験しているつもりがstagの実験のためのイメージを読み込んでしまうことなどが考えられます。

またせっかくgit resetでリビジョンを戻しても、 docker pushしていなかったら元のイメージがpushされてしまいます。

gitコミットハッシュをイメージのタグとして付与する

コミットハッシュを付与するのが一番マシなようです。

ECRへのプッシュは以下のようなシェルスクリプトで別個に行いましょう。

#!/usr/bin/env bash

set -eux

docker build -t foo .
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin bar.dkr.ecr.ap-northeast-1.amazonaws.com
docker tag foo:latest bar.dkr.ecr.ap-northeast-1.amazonaws.com/foo:$(git rev-parse HEAD)
docker push bar.dkr.ecr.ap-northeast-1.amazonaws.com/foo:$(git rev-parse HEAD)

ただこれだと、 CDKのデプロイ時にリポジトリに指定のタグのイメージがあるか分かりません。無くてもそのままデプロイ実行出来てしまって、 ECSがイメージを取得できないと言うエラーが出現します。

仕方がないのでaws cliを使ってチェックしましょう。

見つかりませんでした。

/** デプロイ対象のDockerイメージのタグを返します */
export function taskImageTag(): string {
  return spawnSync("git", ["rev-parse", "HEAD"]).stdout.toString().trim();
}

/** デプロイ対象のDockerイメージが存在するかチェックします */
function ecrImageExist(): boolean {
  return (
    spawnSync("aws", [
      "ecr",
      "describe-images",
      "--repository-name=foo",
      `--image-ids=imageTag=${taskImageTag()}`,
    ]).status === 0
  );
}

aws cliはcdkの--profile引数を当然読まないので、 AWS_PROFILE環境変数にAWSのプロファイルを入れてプッシュスクリプトごと実行するのが良いでしょう。

あるいはもうCDK内部でプッシュスクリプトを呼び出しても良いかもしれませんね。

スナップショットテストを実行しているとコミットごとにイメージタグが変わるため、毎回差分が発生して必ずテスト失敗になります。よって、ダミーのタグ値を引数で入れられるように設定しておきましょう。

何かもっと良い解決策がある気がするのですが、現状これが一番マシなので仕方なくこれを使っていきます。

より良い方法を常に求めています。

2021-02-06 追記: AWS SDKを使えば良さそうです

イメージが存在するかチェックする方法は、 DescribeImagesCommand | @aws-sdk/client-ecr などを使うのが可読性が高そうです。

aws cliとそこまで変わっているかと言うとそうでもありませんが。

2021-03-01 追記: AWS SDKを使ってチェックする方法

/** デプロイ対象のDockerイメージが存在するかチェックします */
async function ecrImageExist(): Promise<boolean> {
  try {
    await ecrClient.send(
      new DescribeImagesCommand({
        repositoryName: "foobar",
        imageIds: [{ imageTag: await taskImageTag() }],
      })
    );
    return true;
  } catch (e) {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    if (e?.name === "ImageNotFoundException") {
      // eslint-disable-next-line no-console
      console.info("ECRにデプロイ指定するためのイメージが見つかりませんでした");
      return false;
    }
    // eslint-disable-next-line no-console
    console.error(e);
    return false;
  }
}