2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Amazon ECRのイミュータブルタグの除外設定でDockerのリモートキャッシュを利用する

2
Posted at

はじめに

CodeBuild等のCIでコンテナをビルドすると基本的にローカルのイメージキャッシュの利用が期待できません。ビルドのたびに環境が新規構築されてしまうためです。

Dockerのリモートキャッシュを利用すると、リモートリポジトリの既存のイメージをキャッシュとして使用することができます。

具体的にはイメージのビルド時にリモートキャッシュを利用するオプションを指定すると、あらかじめ指定したタグを持つキャッシュ用のイメージをビルド開始時にpullし、ビルド終了時にpushします。マルチステージビルドにも対応しており、Amazon ECRでもサポートされています。

このようにリモートキャッシュは便利な機能なのですが、リポジトリのタグのImmutabilityの設定と相性が良くないという課題があります。キャッシュに利用するイメージタグの値は基本的に固定のため、ビルド終了時、最新のキャッシュを保存する際にpushが弾かれてしまいます。

そんな中、Amazon ECRに特定のタグをImmutablityの対象から除外する機能が2025/7にリリースされました。

これを使えば、リモートキャッシュとタグのImmutabilityが共存できそうです。実際に検証して可能なことを確認したので共有します。また、リモートキャッシュに関連するBuildKitの設定について記事内で解説します。

検証構成

remotecache.drawio.png

  • Amazon ECR
    • リポジトリawsomerepoを作成
      • 初期状態はイメージがない状態
      • タグのImmutabilityを有効化
      • リモートキャッシュのため、tag: cacheを対象から除外
  • AWS CodeBuild
    • awsomerepoに対してイメージをpushする。push時のタグはビルドごとに異なる。
    • イメージのビルド時にawsomerepoからtag: cacheのイメージをpullおよびpushする。

設定内容

Amazon ECR

Amazon ECRのプライベートリポジトリからリポジトリを作成をクリック。

image.png

リポジトリ名を入力、イメージタグ設定をImmutableに。
変更不可のタグ除外に"cache"を指定しフィルターを追加。最下部までスクロールし作成

image.png

image.png

タグのイミュータビリティが「イミュータブル+除外」のプライベートリポジトリが作成されたことを確認

image.png

AWS CLIの場合、コマンドは以下です

aws ecr create-repository \
    --repository-name awsomerepo \
    --image-tag-mutability IMMUTABLE_WITH_EXCLUSION \
    --image-tag-mutability-exclusion-filters filterType=EXACT,filter=cache

AWS CodeBuild

手で設定するのが大変なのでCloudFormationで構築します。CloudShellなどのAWS CLIが実行可能な環境で以下のコマンドを実行します。AwsAccountIdregionなどは環境に合わせて指定します。

aws cloudformation deploy \
  --region us-west-2 \
  --stack-name awsomerepo-codebuild \
  --template-file codebuild-awsomerepo.yml \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    AwsAccountId=123456789012 \
    ImageRepoName=awsomerepo

コマンド中で指定しているテンプレートはこちらです。

codebuild-awsomerepo.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: CodeBuild project and IAM role for awsomerepo Docker image build
Parameters:
  AwsAccountId:
    Type: String
    Description: AWS Account ID
  ImageRepoName:
    Type: String
    Default: awsomerepo
    Description: ECR repository name
Resources:
  # ----------------------------------------
  # IAM Role
  # ----------------------------------------
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "CodeBuild${ImageRepoName}ServiceRole"
      Description: !Sub "Service role for CodeBuild project ${ImageRepoName}"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: !Sub "CodeBuild${ImageRepoName}Policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: ECRAuthToken
                Effect: Allow
                Action:
                  - ecr:GetAuthorizationToken
                Resource: "*"
              - Sid: ECRImagePushPull
                Effect: Allow
                Action:
                  - ecr:BatchCheckLayerAvailability
                  - ecr:GetDownloadUrlForLayer
                  - ecr:BatchGetImage
                  - ecr:InitiateLayerUpload
                  - ecr:UploadLayerPart
                  - ecr:CompleteLayerUpload
                  - ecr:PutImage
                Resource: !Sub "arn:aws:ecr:${AWS::Region}:${AwsAccountId}:repository/${ImageRepoName}"
              - Sid: CloudWatchLogs
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !Sub "arn:aws:logs:${AWS::Region}:${AwsAccountId}:log-group:/aws/codebuild/*"
  # ----------------------------------------
  # CodeBuild Project
  # ----------------------------------------
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub "${ImageRepoName}-build"
      Description: !Sub "Build and push Docker image for ${ImageRepoName}"
      ServiceRole: !GetAtt CodeBuildServiceRole.Arn
      Source:
        Type: NO_SOURCE
        BuildSpec: |
          version: 0.2
          phases:
            pre_build:
              commands:
                - echo Logging in to Amazon ECR...
                - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
                - |
                  cat << 'EOF' > package.json
                  {
                    "name": "app",
                    "version": "1.0.0",
                    "dependencies": {
                      "express": "^4.18.2"
                    }
                  }
                  EOF
                - mkdir -p src
                - |
                  cat << 'EOF' > src/index.js
                  const express = require('express');
                  const app = express();
                  app.get('/', (req, res) => {
                    res.send('Hello World!!!\n');
                  });
                  app.listen(3000, () => console.log('listening on 3000'));
                  EOF
                - |
                  cat << 'EOF' > Dockerfile
                  FROM public.ecr.aws/docker/library/node:25-alpine AS builder
                  WORKDIR /app
                  COPY package*.json ./
                  RUN npm install
                  COPY src/ ./src/
                  FROM public.ecr.aws/docker/library/node:25-alpine AS runner
                  WORKDIR /app
                  COPY --from=builder /app/node_modules ./node_modules
                  COPY src/ ./src/
                  CMD ["node", "src/index.js"]
                  EOF
                - echo Setting up docker buildx...
                - docker buildx create --name mybuilder --driver docker-container --bootstrap --use
                - export IMAGE_TAG=$(echo $CODEBUILD_BUILD_ID | cut -d: -f2)
            build:
              commands:
                - echo Build started on `date`
                - |
                  docker buildx build \
                    --builder mybuilder \
                    --provenance=false \
                    --cache-from type=registry,ref=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:cache \
                    --cache-to type=registry,ref=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:cache,mode=max \
                    --tag $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG \
                    --push \
                    .
            post_build:
              commands:
                - echo Build completed on `date`
      Artifacts:
        Type: NO_ARTIFACTS
      Environment:
        Type: LINUX_CONTAINER
        Image: aws/codebuild/standard:7.0
        ComputeType: BUILD_GENERAL1_SMALL
        PrivilegedMode: true
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Value: !Ref AWS::Region
            Type: PLAINTEXT
          - Name: AWS_ACCOUNT_ID
            Value: !Ref AwsAccountId
            Type: PLAINTEXT
          - Name: IMAGE_REPO_NAME
            Value: !Ref ImageRepoName
            Type: PLAINTEXT
      LogsConfig:
        CloudWatchLogs:
          Status: ENABLED
          GroupName: !Sub "/aws/codebuild/${ImageRepoName}-build"
Outputs:
  CodeBuildProjectName:
    Value: !Ref CodeBuildProject
    Description: CodeBuild project name
  CodeBuildServiceRoleArn:
    Value: !GetAtt CodeBuildServiceRole.Arn
    Description: IAM role ARN for CodeBuild

CodeBuildのBuildSpecは以下のようになります。長くなっていますが、リモートキャッシュの利用にあたってみるべきは2か所です。

version: 0.2
phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
      - |
        cat << 'EOF' > package.json
        {
          "name": "app",
          "version": "1.0.0",
          "dependencies": {
            "express": "^4.18.2"
          }
        }
        EOF
      - mkdir -p src
      - |
        cat << 'EOF' > src/index.js
        const express = require('express');
        const app = express();
        app.get('/', (req, res) => {
          res.send('Hello World!!!\n');
        });
        app.listen(3000, () => console.log('listening on 3000'));
        EOF
      - |
        cat << 'EOF' > Dockerfile
        FROM public.ecr.aws/docker/library/node:25-alpine AS builder
        WORKDIR /app
        COPY package*.json ./
        RUN npm install
        COPY src/ ./src/
        FROM public.ecr.aws/docker/library/node:25-alpine AS runner
        WORKDIR /app
        COPY --from=builder /app/node_modules ./node_modules
        COPY src/ ./src/
        CMD ["node", "src/index.js"]
        EOF
      - echo Setting up docker buildx...
      - docker buildx create --name mybuilder --driver docker-container --bootstrap --use
      - export IMAGE_TAG=$(echo $CODEBUILD_BUILD_ID | cut -d":" -f2)
  build:
    commands:
      - echo Build started on `date`
      - |
        docker buildx build \
          --builder mybuilder \
          --provenance=false \
          --cache-from type=registry,ref=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:cache \
          --cache-to type=registry,ref=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:cache,mode=max \
          --tag $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG \
          --push \
          .
  post_build:
    commands:
      - echo Build completed on `date`

その1: docker buildx create

docker-containerのビルダインスタンスを生成して利用可能な状態にしています。

dockerでは2023/2リリースのDocker 23.0からコンテナのビルド時に、BuildKitと呼ばれるビルドエンジンをデフォルトで使うようになりました。従来から存在するdocker buildコマンドも、本タイミングからBuildKitが利用されるよう変更されています。

BuildKitは従来のビルドエンジンと比較してパフォーマンス改善や機能強化などが図られています。BuildKitのバックエンドをどこで動かすのか(=コンテナを実際にどこでビルドするのか)には4つの選択肢があり、選択肢によって機能が異なります。

選択肢 説明
docker DockerデーモンにバンドルされたローカルのBuildKitを使用(default)
docker-container BuildKitコンテナをDocker上で動かして使用
kubernetes BuildKit podをKubernetes上で動かして使用
remote 別の場所のBuildKitに接続
機能 docker docker-container kubernetes remote
イメージの自動ロード
キャッシュのエクスポート ✅*
tar形式の出力
Multi-arcイメージ
BuildKitの設定 Managed externally

デフォルトの選択肢であるdockerでは今回紹介しているリモートキャッシュを実現できないため、mybuilderという名前でdocker-containerのビルダインスタンスを立ち上げています。

その2: docker buildx build

コンテナをビルドしてリポジトリにpushしています。docker buildコマンドではなく、docker buildx buildコマンドを利用しているのはdocker buildコマンドではリモートキャッシュのオプションを指定できないためです。

Buildspec内のdocker buildx buildコマンドの各オプションは以下を意味しています。

オプション 意味
--builder mybuilder ビルダインスタンスとしてmybuilderを使用する。
--provenance=false Provenance attestation を作成しない。Inspectorのスキャン対象となるためprovenanceをfalseを推奨。
--cache-from type=registry,ref=REPO_URL:cache ビルド開始時にawsomerepoからキャッシュイメージ(tag: cache)を取得する。
--cache-to type=registry,ref=REPO_URL:cache,mode=max ビルド終了時にawsomerepoからキャッシュイメージ(tag: cache)をpushする。イメージにはすべてのステージを含める(mode=max)。マルチステージビルドでは基本maxでよい。
--tag ビルドしたイメージに付与するタグ
--push ビルド時にpushも行う

検証

1度目のビルド

それでは1度目のビルドを実行し、キャッシュイメージが生成されることを確認します。

image.png

ビルドが完了すると、ECRにはアプリのイメージに加えて、tag: cacheのイメージが生成されていました。

image.png

CodeBuildのログを見ると、docker buildx build時にキャッシュとなるイメージのpullを試行して失敗した様子が見て取れました。このエラーはキャッシュイメージが存在しないことによるpullの失敗であり、ビルド自体は正常に続行されます。--cache-fromで指定したイメージが存在しない場合、BuildKitはキャッシュなしのフルビルドにフォールバックする仕様のため、初回ビルドでは無視して問題ありません。

image.png

2度目のビルド

では2度目のビルドです。2度目のビルドは、アプリのソースコードを変更してビルドしたいので上書きビルドでCodeBuildを開始します。

image.png

ソースコードに「New」という文字列を加えています。

image.png

ビルドが完了してECRを見に行くと以下のようになりました。tag: cacheのイメージの作成時刻が前回(17:45:23)から更新されていることからImmutabilityの除外が効いています。また、untaggedなイメージの「最後にプルされた時刻」が2度目のビルド時のタイミングであることからキャッシュのpullも行われています!

image.png

キャッシュが利用されていることはCodeBuildのdocker buildx build時のログでもCACHEDという文字列から確認できました。
image.png

まとめ

イミュータブルタグの除外設定でDockerのリモートキャッシュを利用することができることを確認できました。

なお、実際運用する場合は、ECRのライフサイクルポリシーでuntaggedイメージを定期削除する設定を入れておくとよいでしょう。mode=maxでキャッシュをpushすると、それまでcacheタグが付いていたイメージがuntaggedになります。これはビルドのたびに発生するため、放置するとuntaggedイメージが蓄積しストレージコストが増加します(約$0.10/GB/月)。過去のキャッシュは基本利用されませんし、イメージ一覧の見通しが悪くなるので削除してしまいましょう。

また、昨年CodeBuildのDocker Server機能がリリースされています。ビルドを高速化したい場合はこちらも選択肢として検討するとよいでしょう。こちらは時間単位でサーバの課金が発生する分、より頻繁にビルドする場合に向いていそうです。もしどちらか迷うようならまずは安価なリモートキャッシュから始めるのが良いと思います。

ここまで読んでいただきありがとうございました。本記事が何かのお役に立てば幸いです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?