はじめに
CodeBuild等のCIでコンテナをビルドすると基本的にローカルのイメージキャッシュの利用が期待できません。ビルドのたびに環境が新規構築されてしまうためです。
Dockerのリモートキャッシュを利用すると、リモートリポジトリの既存のイメージをキャッシュとして使用することができます。
具体的にはイメージのビルド時にリモートキャッシュを利用するオプションを指定すると、あらかじめ指定したタグを持つキャッシュ用のイメージをビルド開始時にpullし、ビルド終了時にpushします。マルチステージビルドにも対応しており、Amazon ECRでもサポートされています。
このようにリモートキャッシュは便利な機能なのですが、リポジトリのタグのImmutabilityの設定と相性が良くないという課題があります。キャッシュに利用するイメージタグの値は基本的に固定のため、ビルド終了時、最新のキャッシュを保存する際にpushが弾かれてしまいます。
そんな中、Amazon ECRに特定のタグをImmutablityの対象から除外する機能が2025/7にリリースされました。
これを使えば、リモートキャッシュとタグのImmutabilityが共存できそうです。実際に検証して可能なことを確認したので共有します。また、リモートキャッシュに関連するBuildKitの設定について記事内で解説します。
検証構成
- Amazon ECR
- リポジトリ
awsomerepoを作成- 初期状態はイメージがない状態
- タグのImmutabilityを有効化
- リモートキャッシュのため、
tag: cacheを対象から除外
- リポジトリ
- AWS CodeBuild
-
awsomerepoに対してイメージをpushする。push時のタグはビルドごとに異なる。 - イメージのビルド時に
awsomerepoからtag: cacheのイメージをpullおよびpushする。
-
設定内容
Amazon ECR
Amazon ECRのプライベートリポジトリからリポジトリを作成をクリック。
リポジトリ名を入力、イメージタグ設定をImmutableに。
変更不可のタグ除外に"cache"を指定しフィルターを追加。最下部までスクロールし作成。
タグのイミュータビリティが「イミュータブル+除外」のプライベートリポジトリが作成されたことを確認
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が実行可能な環境で以下のコマンドを実行します。AwsAccountIdやregionなどは環境に合わせて指定します。
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度目のビルドを実行し、キャッシュイメージが生成されることを確認します。
ビルドが完了すると、ECRにはアプリのイメージに加えて、tag: cacheのイメージが生成されていました。
CodeBuildのログを見ると、docker buildx build時にキャッシュとなるイメージのpullを試行して失敗した様子が見て取れました。このエラーはキャッシュイメージが存在しないことによるpullの失敗であり、ビルド自体は正常に続行されます。--cache-fromで指定したイメージが存在しない場合、BuildKitはキャッシュなしのフルビルドにフォールバックする仕様のため、初回ビルドでは無視して問題ありません。
2度目のビルド
では2度目のビルドです。2度目のビルドは、アプリのソースコードを変更してビルドしたいので上書きビルドでCodeBuildを開始します。
ソースコードに「New」という文字列を加えています。
ビルドが完了してECRを見に行くと以下のようになりました。tag: cacheのイメージの作成時刻が前回(17:45:23)から更新されていることからImmutabilityの除外が効いています。また、untaggedなイメージの「最後にプルされた時刻」が2度目のビルド時のタイミングであることからキャッシュのpullも行われています!
キャッシュが利用されていることはCodeBuildのdocker buildx build時のログでもCACHEDという文字列から確認できました。

まとめ
イミュータブルタグの除外設定でDockerのリモートキャッシュを利用することができることを確認できました。
なお、実際運用する場合は、ECRのライフサイクルポリシーでuntaggedイメージを定期削除する設定を入れておくとよいでしょう。mode=maxでキャッシュをpushすると、それまでcacheタグが付いていたイメージがuntaggedになります。これはビルドのたびに発生するため、放置するとuntaggedイメージが蓄積しストレージコストが増加します(約$0.10/GB/月)。過去のキャッシュは基本利用されませんし、イメージ一覧の見通しが悪くなるので削除してしまいましょう。
また、昨年CodeBuildのDocker Server機能がリリースされています。ビルドを高速化したい場合はこちらも選択肢として検討するとよいでしょう。こちらは時間単位でサーバの課金が発生する分、より頻繁にビルドする場合に向いていそうです。もしどちらか迷うようならまずは安価なリモートキャッシュから始めるのが良いと思います。
ここまで読んでいただきありがとうございました。本記事が何かのお役に立てば幸いです。










