🐋

Docker Desktopで無効化するべき設定。ECR ”400 Bad Request failed commit on ref”の便り

2024/11/06に公開

開発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の利用には(現段階では)以下の問題があります。

  1. containerd image storeの格納されたイメージからIMMUTABLEなECRへのPushが失敗する。
  2. 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回が実行されます。

  1. イメージ本体のManifest
  2. Provenance attestationsのManifest
  3. 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つあります。

  1. Buildxを利用する
  2. container image storeを有効にしない。
  3. 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