Docker Desktopで無効化するべき設定。ECR ”400 Bad Request failed commit on ref”の便り
開発PCからECRへのコンテナイメージPushがエラーになる原因調査の結果、2024年11月現在ではDocker Desktopで Use containerd for pulling and storing images
設定を無効化すると幸せになれるという話をします。マルチプラットフォームイメージに起因する事象のため、マルチプラットフォームイメージも深掘りします。
背景
IMMUTABLEなECR(イメージタグの上書き禁止設定がされたECR)へ開発PCからコンテナイメージをPushしたところfailed commit on ref "manifest-sha256:dd***b1": unexpected status from PUT request to https://${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/v2/${REPOSITORY}/manifests/${IMAGE_TAG}: 400 Bad Request
エラーが発生し、Pullできない壊れたイメージがECRに登録される事象が発生しました。同じ手順で、MUTABLEなECRへのPush、Docker HubへのPushは成功します。
開発PCはMac Book AirでDocker DesktopをインストールしDocker Engineを利用しています。エラー発生時の実行コマンドです。
Docker Desktopのバージョン等
Docker Descktop Version
Current version: 4.34.3 (170107)
$ docker version
Client:
Version: 27.2.0
API version: 1.47
Go version: go1.21.13
Git commit: 3ab4256
Built: Tue Aug 27 14:14:45 2024
OS/Arch: darwin/arm64
Context: desktop-linux
Server: Docker Desktop 4.34.3 (170107)
Engine:
Version: 27.2.0
API version: 1.47 (minimum version 1.24)
Go version: go1.21.13
Git commit: 3ab5c7d
Built: Tue Aug 27 14:15:41 2024
OS/Arch: linux/arm64
Experimental: false
containerd:
Version: 1.7.20
GitCommit: 8fc6bcff51318944179630522a095cc9dbf9f353
runc:
Version: 1.1.13
GitCommit: v1.1.13-0-g58aa920
docker-init:
Version: 0.19.0
GitCommit: de40ad0
$ docker info
Client:
Version: 27.2.0
Context: desktop-linux
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.16.2-desktop.1
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-buildx
compose: Docker Compose (Docker Inc.)
Version: v2.29.2-desktop.2
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-compose
debug: Get a shell into any image or container (Docker Inc.)
Version: 0.0.34
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-debug
desktop: Docker Desktop commands (Alpha) (Docker Inc.)
Version: v0.0.15
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-desktop
dev: Docker Dev Environments (Docker Inc.)
Version: v0.1.2
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-dev
extension: Manages Docker extensions (Docker Inc.)
Version: v0.2.25
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-extension
feedback: Provide feedback, right in your terminal! (Docker Inc.)
Version: v1.0.5
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-feedback
init: Creates Docker-related starter files for your project (Docker Inc.)
Version: v1.3.0
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-init
sbom: View the packaged-based Software Bill Of Materials (SBOM) for an image (Anchore Inc.)
Version: 0.6.0
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-sbom
scout: Docker Scout (Docker Inc.)
Version: v1.13.0
Path: /Users/shintaro.kurihara/.docker/cli-plugins/docker-scout
Server:
Containers: 3
Running: 0
Paused: 0
Stopped: 3
Images: 126
Server Version: 27.2.0
Storage Driver: overlayfs
driver-type: io.containerd.snapshotter.v1
Logging Driver: json-file
Cgroup Driver: cgroupfs
Cgroup Version: 2
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 8fc6bcff51318944179630522a095cc9dbf9f353
runc version: v1.1.13-0-g58aa920
init version: de40ad0
Security Options:
seccomp
Profile: unconfined
cgroupns
Kernel Version: 6.10.4-linuxkit
Operating System: Docker Desktop
OSType: linux
Architecture: aarch64
CPUs: 8
Total Memory: 7.655GiB
Name: docker-desktop
ID: 5a314f97-a23b-402e-825a-a09c4631fd20
Docker Root Dir: /var/lib/docker
Debug Mode: false
HTTP Proxy: http.docker.internal:3128
HTTPS Proxy: http.docker.internal:3128
No Proxy: hubproxy.docker.internal
Labels:
com.docker.desktop.address=unix:///Users/shintaro.kurihara/Library/Containers/com.docker.docker/Data/docker-cli.sock
Experimental: false
Insecure Registries:
hubproxy.docker.internal:5555
127.0.0.0/8
Live Restore Enabled: false
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
container-builder* docker-container
\_ container-builder0 \_ desktop-linux running v0.16.0 linux/arm64, linux/amd64, linux/amd64/v2, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
default docker
\_ default \_ default running v0.16.0 linux/arm64, linux/amd64, linux/amd64/v2, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64
desktop-linux docker
\_ desktop-linux \_ desktop-linux running v0.16.0 linux/arm64, linux/amd64, linux/amd64/v2, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64
$ uname -a
Darwin P-LMD0320.local 23.6.0 Darwin Kernel Version 23.6.0: Wed Jul 31 20:53:05 PDT 2024; root:xnu-10063.141.1.700.5~1/RELEASE_ARM64_T8112 arm64
$ aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}
$ docker build -t ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} .
# ここでエラー
$ docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG}
Buildx経由であれば成功します。
$ aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}
# 成功する
$ docker buildx build \
--tag ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} .
--push
結論
発生した事象の詳細は次章で解説しますが、Docker Desktopユーザーの方は少なくとも2024年11月現在では『"Use containerd for pulling and storing images" は無効化すべき』という結論に至りました。
Setting > General
と遷移しUse containerd for pulling and storing images
のチェックを外します。
設定の無効化が反映されたかどうかは以下の方法で確認可能です。
# Use containerd for pulling and storing images設定が無効化されている
$ docker info -f '{{ .DriverStatus }}'
[[Backing Filesystem extfs] [Supports d_type true] [Using metacopy false] [Native Overlay Diff true] [userxattr false]]
# Use containerd for pulling and storing images設定が有効な状態
$ docker info -f '{{ .DriverStatus }}'
[[driver-type io.containerd.snapshotter.v1]]
Use containerd for pulling and storing images
を有効だと、Docker daemonがローカルでイメージを管理するためのimages storeが、現段階では実験的なcontainerd image storeに切り替わります。containerd image storeの利用には(現段階では)以下の問題があります。
- containerd image storeの格納されたイメージからIMMUTABLEなECRへのPushが失敗する。
- containerd image storeは実験的な機能であり、Docker Community Editionのデフォルトでは無効になっており、GitHub ActionsなどのCI/CD環境との環境差異が発生する。
Use containerd for pulling and storing images
無効化は、12 facotr app - X. 開発/本番一致の原則に基づいて推奨しています。ECRへのエラーだけ回避をしたいという場合は、docker buildコマンドに--provenance false
オプションを付加することでも回避できます。チームメンバーの無効化漏れ対策としてビルドスクリプトに仕込んでもよいでしょう。
# 前述の問題コマンドを変更する例
- docker build -t ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} .
+ docker build --provenance false -t ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} .
事象の調査結果と、マルチプラットフォームイメージ深掘り
この章では本事象の調査結果を解説します。本事象はDockerのマルチアーキテクチャイメージに深く関わる事象であるため、先にマルチプラットフォームイメージの説明をします。
マルチプラットフォームイメージとは?
コンテナはホストOSのカーネルを共有して起動することから、ホストのOS、CPUアーキテクチャに依存します。そのためコンテナイメージもホストOSのCPUアーキテクチャにあわせてビルドされている必要があります。
そこで登場したのがマルチプラットフォームイメージ(マルチアーキテクチャーイメージ)で、利用者は自身のホストのアーキテクチャを意識せず、透過的にコンテナイメージを利用できる機能です。
出典: Multi-platform builds - dockerdocs
対して特定のCPUアーキテクチャー向けのみに作られたDockerイメージをシングルプラットフォームイメージ(シングルプラットフォームイメージ)と呼ばれ、図左のように単一のManifest(メタデータの様なもの)で管理されます。対して図右がマルチプラットフォームイメージで、Manifest Listが導入され、複数のシングルプラットフォームイメージを参照するIndexのレイヤーが追加になっています。
このManifest (List)はdocker manifest inspect ${NAME_SPACE}/${REPOSITORY}/${IMAGE_TAG}
で調べることができます。
シングルプラットフォームイメージのManifest。
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:483d88c9d24321ea8bc657b374fbebc3a73886bdb0e70db392d670d1eba6f87f",
"size": 964
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:a46fbb00284bdd1a1d8d80d51333abc851371a7b8d44cc781c4469b5e54119ae",
"size": 2155620
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:cd5739cb1aad4cdff30944b6df4988df566701960689d6444da4e8229b310831",
"size": 151
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:5cbc375e13707d319f3c2e1fa2ca5e4debb265b05c01f5686b91396628dd6025",
"size": 165
}
]
}
マルチプラットフォームイメージのManifest(List)。manifests
というリストが追加され、複数アーキテクチャー向けイメージへの参照が管理されていることがわかります。
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 890,
"digest": "sha256:ee600707924ab69947b4ffa45825c71de2eaa8ab90b72a04c9e42764e4ee2ae4",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 890,
"digest": "sha256:2711e22ac34f29914aa17d5fb3dff800c5e74ff857cb9f5a39ed3859333f3c1b",
"platform": {
"architecture": "arm64",
"os": "linux"
}
}
]
}
加えて、Provenance attestationsという、コンテナイメージのビルド時の情報を保持(証明)する機能がリリースされています。
The provenance attestations include facts about the build process, including details such as:
- Build timestamps
- Build parameters and environment
- Version control metadata
- Source code details
- Materials (files, scripts) consumed during the build
ドキュメント記載の通り、例えばVersion control metadata
にはGitのどのリビジョンでビルドされたかの情報が含まれます。docker buildx imagetools inspect
コマンドで中身を確認できます。
$ docker buildx imagetools inspect ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} --format '{{json .Provenance.SLSA}}'
{
...中略
"vcs": { "localdir:context": ".",
"localdir:dockerfile": ".",
"revision": "bfd4bafcd6021d51dea4a7688b6c664725ca2c49"
}
}
}
Provenance attestations
をコンテナイメージに保持するには、--provenance=mode=[min|max]
オプションを付加してビルドを行い、同イメージをPushします。
$ docker build --provenance=mode=max -t ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} .
$ docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG}
Provenance attestations
が保持されたコンテナイメージのManifestを確認してみます。
$ docker manifest inspect ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG}
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 856,
"digest": "sha256:c23721a898812f1dd7ee59404f05f7afbb635ef9d8bbede41f0013c4c7134094",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 566,
"digest": "sha256:fb81a185b6a692ca0d5e7946bbebe02fa88ebcb97dc33c6a7551674d9a76bd6c",
"platform": {
"architecture": "unknown",
"os": "unknown"
}
}
]
}
イメージがマルチプラットフォームイメージになっており、Provenance attestations
への参照(platformがunknown
)がManifest Listで管理されています。
ここで押さえておいていただきたいのが、Provenance attestations
を保持させたことで、単一のアーキテクチャ向けにビルドしたイメージであってもマルチプラットフォームイメージの形式になる点です。
containerd image store
現段階のDockerのデフォルトのimage store(ローカルでイメージを管理するコンポーネント)では、マルチプラットフォームイメージを管理できません。マルチプラットフォームイメージを管理するには、containerd image storeという機能を有効にする必要があります。
containerd image storeを有効にした状態で、docker build
コマンドを実行すると、デフォルトでProvenance attestations
が保持されます。つまりデフォルトでマルチプラットフォームイメージがビルドされます。
Docker Desktopでは、Use containerd for pulling and storing images
設定を有効にすると、containerd image storeが有効になります。
将来的には、containerd image storeがデフォルトになるようですが、現段階では実験的な機能であり、Docker Community Editionのデフォルトでは無効になっている機能です。
デフォルトのimage storeでも、Buildxであればマルチプラットフォームイメージをビルドできるじゃないか!?と思われた方もいると思いますが、Buildx経由の場合、image storeは経由せず、driver独自のキャッシュストレージでイメージは管理されます。Buildx経由であればマルチイメージプラットフォームイメージをビルドできるのはそのためです。
Unlike when using the default docker driver, images built using other drivers aren't automatically loaded into the local image store. If you don't specify an output, the build result is exported to the build cache only.
-- https://docs.docker.com/build/builders/drivers/#loading-to-local-image-store
--load
オプションをつけるとimage storeにイメージを格納しますが、containerd image store
が有効になってない場合、エラーになることがわかります。
$ docker buildx build --builder=container-builder \
--platform linux/amd64 \
--provenance=true \
--load .
[+] Building 0.0s (0/0) docker-container:container-builder
ERROR: docker exporter does not currently support exporting manifest lists
事象の調査結果
ここからは、IMMUTABLEなECRへのPushがエラーになった原因の詳細を解説します。以降の検証はcontainerd image store
が有効になっている状態が前提になります。
先に発生した事象を文章化します。
-
containerd image storeを有効にした状態で
docker build
でコンテナイメージをビルドするとProvenance attestations
がデフォルトで保持されるようになる。 - マルチプラットフォームイメージをECRにPushすると、参照先のシングルプラットフォームイメージ、
Provenance attestations
、Manifest Listがそれぞれ個別にPushされる。 - containerd image storeに格納されたイメージをECRにPushすると、全てのPushのリクエストパラメータにイメージタグが付与される(Buildx経由の場合、Manifest ListのPushのリクエストのみイメージタグが付与される)。
- ECRがIMMUTABLEな場合、イメージタグの変更だと判断してしまい、2回目のPushで
ImageTagAlreadyExistsException
が発生する。 - 結果、不完全なイメージがECRに登録される。
Docker Community Editionでも同じ挙動になります。
Docker Community Editionでもcontainers images storeを有効にすると同じ事象が発生します。
以下、Ubuntsuで検証した際の、containerd image store有効化の手順です。(公式手順書)
$ uname -a
Linux ip-172-31-46-214 6.8.0-1016-aws #17-Ubuntu SMP Mon Sep 2 13:48:07 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
$ docker version
Client: Docker Engine - Community
Version: 27.3.1
API version: 1.47
Go version: go1.22.7
Git commit: ce12230
Built: Fri Sep 20 11:40:59 2024
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 27.3.1
API version: 1.47 (minimum version 1.24)
Go version: go1.22.7
Git commit: 41ca978
Built: Fri Sep 20 11:40:59 2024
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.7.22
GitCommit: 7f7fdf5fed64eb6a7caf99b3e12efcf9d60e311c
runc:
Version: 1.1.14
GitCommit: v1.1.14-0-g2c9f560
docker-init:
Version: 0.19.0
GitCommit: de40ad0
$ sudo vim /etc/docker/daemon.json
$ cat /etc/docker/daemon.json
{
"features": {
"containerd-snapshotter": true
}
}
# Docker Engine再起動
$ sudo systemctl restart docker
# container image store有効化の確認
$ docker info -f '{{ .DriverStatus }}'
[[driver-type io.containerd.snapshotter.v1]]
改めてエラーになったコマンドを再掲します。
$ aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}
$ docker build -t ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} .
# ここでエラー
$ docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG}
先に、MUTABLEに設定したECRにPushが成功したイメージのManifestを確認します。
$ docker manifest inspect ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${MMUTABLE_REPOSITORY}/${IMAGE_TAG}
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 668,
"digest": "sha256:ff9de85e43b110ebdb9874660fc36455863d442736a56fbce415f938a89b3e90",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 565,
"digest": "sha256:01460f3421b09b70b9de765f02b2713ff344e5c864e1e702a952111c20636e1c",
"platform": {
"architecture": "unknown",
"os": "unknown"
}
}
]
}
Provenance attestations
が保持されたマルチプラットフォームイメージがPushされていることがわかります。
まったく同じイメージをIMMUTABLEなECRにPushすると、400 Bad Requestでエラーになります。
$ docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${IMMUTABLE_REPOSITORY}/${IMAGE_TAG}
The push refers to repository [${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${IMMUTABLE_REPOSITORY}/${IMAGE_TAG}]
b7a3674acad1: Pushed
8aa415ab3473: Pushed
a46fbb00284b: Layer already exists
failed commit on ref "manifest-sha256:01460f3421b09b70b9de765f02b2713ff344e5c864e1e702a952111c20636e1c": unexpected stat
us from PUT request to https://${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${IMMUTABLE_REPOSITORY}/${IMAGE_TAG}: 400 Bad Request
エラー時のCloudTrailを見ると、ecr:PutImageアクションが2回連続で呼ばれ、2回目がImageTagAlreadyExistsException
エラーになっていることがわかります。
1回目のecr:PutImage - 成功
{
"eventVersion": "1.08",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDA****AQH",
"arn": "arn:aws:iam::${AWS_ACCOUNT_ID}:user/kuritify",
"accountId": "${AWS_ACCOUNT_ID}",
"accessKeyId": "ASI******ROXO",
"userName": "kuritify",
"sessionContext": {
"sessionIssuer": {},
"webIdFederationData": {},
"attributes": {
"creationDate": "2024-10-30T11:47:06Z",
"mfaAuthenticated": "false"
}
},
"invokedBy": "ecr.amazonaws.com"
},
"eventTime": "2024-10-30T11:47:15Z",
"eventSource": "ecr.amazonaws.com",
"eventName": "PutImage",
"awsRegion": "${AWS_REGION}",
"sourceIPAddress": "ecr.amazonaws.com",
"userAgent": "ecr.amazonaws.com",
"requestParameters": {
"registryId": "${AWS_ACCOUNT_ID}",
"repositoryName": "${IMMUTABLE_REPOSITORY}",
"imageManifest": "{\n \"schemaVersion\": 2,\n \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n \"config\": {\n \"mediaType\": \"application/vnd.oci.image.config.v1+json\",\n \"digest\": \"sha256:8a3861f44d17204dec1be9457e9cf59af0d8f92b9de433b52866151f55d0a3e1\",\n \"size\": 985\n },\n \"layers\": [\n {\n \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"digest\": \"sha256:1523c6c3dc4c68bb2416b8bdbc51b5621a2d7dc445fa36cbfe7b0cdb9582b44f\",\n \"size\": 1844315\n },\n {\n \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"digest\": \"sha256:b1e091d90c73dfd0c1762ee6b26dfeec525e5c241504f02d64317bb09a61ba1d\",\n \"size\": 150\n },\n {\n \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"digest\": \"sha256:af7a626ee47b5bd7d3c03ec02313f09036f6f25e8973eb72f0f26a718ef1c1c4\",\n \"size\": 110\n }\n ]\n}",
"imageManifestMediaType": "application/vnd.oci.image.manifest.v1+json",
"imageTag": "${IMAGE_TAG}"
},
"responseElements": {
"image": {
"registryId": "${AWS_ACCOUNT_ID}",
"repositoryName": "${IMMUTABLE_REPOSITORY}",
"imageId": {
"imageDigest": "sha256:ad26e03c69470f637d5d991ebd4d9c6d1f3f1bed29587d3e91f57c8bca227d0a",
"imageTag": "${IMAGE_TAG}"
},
"imageManifest": "{\n \"schemaVersion\": 2,\n \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n \"config\": {\n \"mediaType\": \"application/vnd.oci.image.config.v1+json\",\n \"digest\": \"sha256:8a3861f44d17204dec1be9457e9cf59af0d8f92b9de433b52866151f55d0a3e1\",\n \"size\": 985\n },\n \"layers\": [\n {\n \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"digest\": \"sha256:1523c6c3dc4c68bb2416b8bdbc51b5621a2d7dc445fa36cbfe7b0cdb9582b44f\",\n \"size\": 1844315\n },\n {\n \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"digest\": \"sha256:b1e091d90c73dfd0c1762ee6b26dfeec525e5c241504f02d64317bb09a61ba1d\",\n \"size\": 150\n },\n {\n \"mediaType\": \"application/vnd.oci.image.layer.v1.tar+gzip\",\n \"digest\": \"sha256:af7a626ee47b5bd7d3c03ec02313f09036f6f25e8973eb72f0f26a718ef1c1c4\",\n \"size\": 110\n }\n ]\n}",
"imageManifestMediaType": "application/vnd.oci.image.manifest.v1+json"
}
},
"requestID": "65741d4b-d998-4dc6-b23a-0d637f1013b3",
"eventID": "6210fac9-f018-495a-afa1-7008986a339b",
"readOnly": false,
"resources": [
{
"accountId": "${AWS_ACCOUNT_ID}",
"ARN": "arn:aws:ecr:${AWS_REGION}:${AWS_ACCOUNT_ID}:repository/${IMMUTABLE_REPOSITORY}"
}
],
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "${AWS_ACCOUNT_ID}",
"eventCategory": "Management"
}
2回目のecr:PutImage - ImageTagAlreadyExistsExceptionで失敗
{
"eventVersion": "1.08",
"userIdentity": {
"type": "IAMUser",
"principalId": "AIDAR*****XQBKCAQH",
"arn": "arn:aws:iam::${AWS_ACCOUNT_ID}:user/kuritify",
"accountId": "${AWS_ACCOUNT_ID}",
"accessKeyId": "ASIAR*******ROXO",
"userName": "kuritify",
"sessionContext": {
"sessionIssuer": {},
"webIdFederationData": {},
"attributes": {
"creationDate": "2024-10-30T11:47:06Z",
"mfaAuthenticated": "false"
}
},
"invokedBy": "ecr.amazonaws.com"
},
"eventTime": "2024-10-30T11:47:15Z",
"eventSource": "ecr.amazonaws.com",
"eventName": "PutImage",
"awsRegion": "${AWS_REGION}",
"sourceIPAddress": "ecr.amazonaws.com",
"userAgent": "ecr.amazonaws.com",
"errorCode": "ImageTagAlreadyExistsException",
"errorMessage": "The image tag '${IMAGE_TAG}' already exists in the '${IMMUTABLE_REPOSITORY}' repository and cannot be overwritten because the repository is immutable.",
"requestParameters": {
"registryId": "${AWS_ACCOUNT_ID}",
"repositoryName": "${IMMUTABLE_REPOSITORY}",
"imageManifest": "{\n \"schemaVersion\": 2,\n \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n \"config\": {\n \"mediaType\": \"application/vnd.oci.image.config.v1+json\",\n \"digest\": \"sha256:5d2b088cc93d775e9bbf4c0df5e959221b63e1e897ad5b693f177aef438a2724\",\n \"size\": 167\n },\n \"layers\": [\n {\n \"mediaType\": \"application/vnd.in-toto+json\",\n \"digest\": \"sha256:d002f763f1f407ee1582e29e4ece611567327997fe04713c54108146228c68fc\",\n \"size\": 1114,\n \"annotations\": {\n \"in-toto.io/predicate-type\": \"https://slsa.dev/provenance/v0.2\"\n }\n }\n ]\n}",
"imageManifestMediaType": "application/vnd.oci.image.manifest.v1+json",
"imageTag": "${IMAGE_TAG}"
},
"responseElements": null,
"requestID": "a7f2287a-d66c-40e6-a397-3c4a0779158e",
"eventID": "46211382-1817-4359-baa4-ce339cf4df9f",
"readOnly": false,
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "${AWS_ACCOUNT_ID}",
"eventCategory": "Management"
}
それぞれのrequestParameters.imageManifest
フィールドの値を確認すると、1回目のPutImageでは実際のコンテナイメージのManifestがPutされ、2回目のPutImageではProvenance attestationsのManifestがPutされていることがわかります。
1回目のPutImage
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:8a3861f44d17204dec1be9457e9cf59af0d8f92b9de433b52866151f55d0a3e1",
"size": 985
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:1523c6c3dc4c68bb2416b8bdbc51b5621a2d7dc445fa36cbfe7b0cdb9582b44f",
"size": 1844315
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:b1e091d90c73dfd0c1762ee6b26dfeec525e5c241504f02d64317bb09a61ba1d",
"size": 150
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:af7a626ee47b5bd7d3c03ec02313f09036f6f25e8973eb72f0f26a718ef1c1c4",
"size": 110
}
]
}
2回目のPutImage
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:5d2b088cc93d775e9bbf4c0df5e959221b63e1e897ad5b693f177aef438a2724",
"size": 167
},
"layers": [
{
"mediaType": "application/vnd.in-toto+json",
"digest": "sha256:d002f763f1f407ee1582e29e4ece611567327997fe04713c54108146228c68fc",
"size": 1114,
"annotations": {
"in-toto.io/predicate-type": "https://slsa.dev/provenance/v0.2"
}
}
]
}
かつ双方のrequestParametersにimageTag
が含まれているため、IMMUTABLEな設定をされたECR場合、2回目のPutImageがタグの上書きのリクエストと判定され、ImageTagAlreadyExistsException
エラーが発生します。結果1回目のPutImageは成功しているため整合性のとれていない不完全なイメージがECRにPutされるという状態になります。
container image storeが有効な状態であっても、Buildx経由であれば成功します。
$ docker buildx build \
--push \
--platform linux/amd64 \
--provenance=true \
--tag ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} .
buildx経由の場合、PutImageは以下の3回が実行されます。
- イメージ本体のManifest
- Provenance attestationsのManifest
- Manifest List
ただし、requestParametersにimageTag
が含まれるのは3回目のManifest ListのPutImageのみになるため、IMMUTABLEなECRであっても正しくイメージをPushできる挙動になります。
結論、container image store
はまだexperimental featureであるため、ECR側は未対応なのであろうと推察されます。
出典: AWS re:Invent2023 - Dive deep into Amazon ECR
AWS re:Invent2023 - Dive deep into Amazon ECRでECRのアーキテクチャーが説明されていますが、docker cli経由でECRにアクセスする際にはProxy Serviceを経由してリクエストが実行されます。このProxy Serviceが問題なのか、Docker側の問題なのかまでは調べきれませんでした。ちなみに、cdkのissueですが、本事象と類似する事象が報告されています。
本事象の回避方法は3つあります。
- Buildxを利用する
-
container image store
を有効にしない。 -
container image store
を有効にする場合、docker build時に明示的に--provenance=false
を指定し、シングルアーキテクチャイメージとしてビルド、Pushする。
3番についてはdocker buildコマンドを以下の様に変更することで、Provenance attestationsを保持しないことで、シングルアーキテクチャイメージがビルドされ、結果docker push時にecr:PutImageは1回しか実行されないため、結果本事象を回避できます。
# 前述の問題コマンド
+ docker build --provenance false -t ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} .
- docker build -t ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY}:${IMAGE_TAG} .
まとめ
以上Docker Desktopのお勧め設定を謳いながら、ECRにPush時に発生した400 Bad Request
の調査結果の共有にお時間いただきました。私個人としてはふんわり理解していたマルチプラットフォームイメージの解像度が上がったので、同じ様な方の参考になっていれば幸いです。
Discussion