アットランタイム

buildx のビルドキャッシュにリモートレジストリを指定できる話

はじめに

buildx のビルドキャッシュを ECR レジストリに保存してビルドを高速化する AWS のブログが出ていた。

Reduce Docker image build time on AWS CodeBuild using Amazon ECR as a remote cache

そんな機能があるのかと思い Docker のドキュメントを見てみると ECR が Image Index に対応していない文を見つけた。ECR は OCI Image と Distribution 1.1(reference API に対応)していて、手元でも application/vnd.oci.image.index.v1+json イメージが存在していることを確認した。 しかし、この機能を image-manifest=false で実行すると失敗したので ECR の動作とあわせて調べてみた。

Cache storage backends

By default, the OCI media type generates an image index for the cache image. Some OCI registries, such as Amazon ECR, don’t support the image index media type: application/vnd.oci.image.index.v1+json. If you export cache images to ECR, or any other registry that doesn’t support image indices, set the image-manifest parameter to true to generate a single image manifest instead of an image index for the cache image:

結論

image-manifest=true とすると、Image Index(Manifest List) の manifests 内に、キャッシュ用のイメージレイヤーを含めてプッシュする動作となっている。 ECR は manifests の descriptor を manifest ダイジェストであることを想定しているので、Image Index の PUT がエラーとなる。

buildx が作成した Image Index は次のようになっていた。

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:1eb4bf5151dc6318af0ff6b9572ca3f7f0c39a2099e7d0dbf52bdf32825e8203",
      "size": 98015748,
      "annotations": {
        "buildkit/createdat": "2025-10-07T07:14:58.01557036Z",
        "containerd.io/uncompressed": "sha256:4171bd4dcf13a5b4a58216130e86f145f7d3dcadd4b702cd76dcf71479c8f489"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:204a353b39926464a283152a0636a58ea0fc6e1cecc3962930d644b782e95d72",
      "size": 36889059,
      "annotations": {
        "buildkit/createdat": "2025-10-07T07:14:57.938472005Z",
        "containerd.io/uncompressed": "sha256:231ae437bba4fef7a6bd006213b76f9cd126623c6dab224009c6cedacb2ea5a4"
      }
    },
    {
      "mediaType": "application/vnd.buildkit.cacheconfig.v0",
      "digest": "sha256:044f27e7471d4dce05d841b3c3e51cad7775780e038e4b055ff1862f0239a554",
      "size": 4978
    }
  ]
}

マルチアーキテクチャイメージのように Image Manifest が Image Index をラップするのかと思ったが違った。 マルチアーキテクチャのビルドでマルチアーキテクチャのキャッシュが作成できるなどの用途などをイメージしていた。

Image Spec をみると a list of manifests とあるので通常の使い方ではない印象である。

The OpenContainers Image Index Spec

manifests array of objects

This REQUIRED property contains a list of manifests for specific platforms. While this property MUST be present, the size of the array MAY be zero.

Each object in manifests includes a set of descriptor properties with the following additional properties and restrictions:

上記より ECR は Image Index に対応しているが、buildx が作成する Image Index が一般的な使い方とは異なると言えそうである。

image-manifest=false で実行時のエラー

イメージレイヤーのダイジェストが unknown と言われている。

$ docker build --cache-to type=registry,ref=$ECR_REPO:demo-cache,oci-mediatypes=true,image-manifest=false --cache-from type=registry,ref=$ECR_REPO:demo-cache --tag $ECR_REPO:demo --builder=containerd .

(snip)
ERROR: failed to solve: error writing manifest blob: failed commit on ref "sha256:237e3cdd46b5e9c427e40a2eff65c51438730853304f4d130d1d617e07a95401": unexpected status from PUT request to https://アカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/v2/amazon_linux_codebuild_image/manifests/demo-cache: 400 Bad Request
unknown: Images with digests '[sha256:1eb4bf5151dc6318af0ff6b9572ca3f7f0c39a2099e7d0dbf52bdf32825e8203, sha256:15b6b8c7ba8b3f0e53e4871384a1030995aa4bfd8a7dc81bc0f9a91a5bcd6f1f, ....] required for pushing image into repository with name 'amazon_linux_codebuild_image' in the registry with id 'アカウント ID' do not exist

ECR の動作

Image Manifest からイメージレイヤーのダイジェストを取得し、その値を Image Index としてプッシュすると同様のエラーとなる。 イメージレイヤーは ECR に存在するにもかかわらずエラーになるので、ECR は manifests 内のダイジェストを、Image Manifest のダイジェストを想定してバリデーションしているのだろう。ReferencedImagesNotFoundException というエラーからもそのように判断できる。この処理は妥当なバリデーションと言える。

$ aws ecr batch-get-image --repository-name nginx --image-ids imageDigest=sha256:8fb9eb820272147b0cb1ba2e547231ff3aeb4eca98f007ee8c3c6170acaa892b --query 'images[].imageManifest' --output text
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:aa8263c9be9ab91244fbaf41ca89d7997742fbe04b404d175f8074eb92f3c0be",
    "size": 241
  },
  "layers": [
    {
      "mediaType": "application/vnd.in-toto+json",
      "digest": "sha256:e4c1e9b9a4c966b1a6be37e4a75e969a12caab48d1097356ffefe1e1ebf6cb6a",
      "size": 2956060,
      "annotations": {
        "in-toto.io/predicate-type": "https://spdx.dev/Document"
      }
    },
    {
      "mediaType": "application/vnd.in-toto+json",
      "digest": "sha256:917435ed7931e7e44d3ffff791e119a91194cab7d8853202b557e276a71af15d",
      "size": 34619,
      "annotations": {
        "in-toto.io/predicate-type": "https://slsa.dev/provenance/v0.2"
      }
    }
  ]
$ aws ecr put-image --repository-name nginx --image-tag latest --image-manifest '{
    "manifests": [
        {
            "mediaType": "application/vnd.in-toto+json",
            "digest": "sha256:e4c1e9b9a4c966b1a6be37e4a75e969a12caab48d1097356ffefe1e1ebf6cb6a",
            "size": 2956060,
            "annotations": {
                "in-toto.io/predicate-type": "https://spdx.dev/Document"
            }
        },
        {
            "mediaType": "application/vnd.in-toto+json",
            "digest": "sha256:917435ed7931e7e44d3ffff791e119a91194cab7d8853202b557e276a71af15d",
            "size": 34619,
            "annotations": {
                "in-toto.io/predicate-type": "https://slsa.dev/provenance/v0.2"
            }
        }
    ],
    "mediaType": "application/vnd.oci.image.index.v1+json",
    "schemaVersion": 2
}'

An error occurred (ReferencedImagesNotFoundException) when calling the PutImage operation: Images with digests '[sha256:e4c1e9b9a4c966b1a6be37e4a75e969a12caab48d1097356ffefe1e1ebf6cb6a, sha256:917435ed7931e7e44d3ffff791e119a91194cab7d8853202b557e276a71af15d]' required for pushing image into repository with name 'nginx' in the registry with id 'アカウントID' do not exist

ソースコード

Image Index(Manifest List) の場合、manifests にイメージレイヤーの descriptor を追加している。 https://github.com/moby/buildkit/blob/master/cache/remotecache/export.go#L132

func (ec *ExportableCache) AddCacheBlob(blob ocispecs.Descriptor) {
	if ec.CacheType == ManifestList {
		ec.ExportedIndex.Manifests = append(ec.ExportedIndex.Manifests, blob)
	} else {
		ec.ExportedManifest.Layers = append(ec.ExportedManifest.Layers, blob)
	}
}

なお oci-mediatypes はマニフェストやイメージレイヤーとして OCI Image Spec を使用するか、Docker Image Spec を使用するかの分岐フラグである。

https://github.com/moby/buildkit/blob/master/cache/remotecache/export.go#L81C1-L95C3

func NewExportableCache(oci bool, imageManifest bool) (*ExportableCache, error) {
	var mediaType string

	if imageManifest {
		mediaType = ocispecs.MediaTypeImageManifest
		if !oci {
			return nil, errors.Errorf("invalid configuration for remote cache, OCI mediatypes are required for image-manifest cache format")
		}
	} else {
		if oci {
			mediaType = ocispecs.MediaTypeImageIndex
		} else {
			mediaType = images.MediaTypeDockerSchema2ManifestList
		}
	}
    (snip)

https://github.com/moby/buildkit/blob/master/util/compression/compression.go#L185C1-L224C2


var toDockerLayerType = map[string]string{
	ocispecs.MediaTypeImageLayer:                     images.MediaTypeDockerSchema2Layer,
	images.MediaTypeDockerSchema2Layer:               images.MediaTypeDockerSchema2Layer,
	ocispecs.MediaTypeImageLayerGzip:                 images.MediaTypeDockerSchema2LayerGzip,
	images.MediaTypeDockerSchema2LayerGzip:           images.MediaTypeDockerSchema2LayerGzip,
	images.MediaTypeDockerSchema2LayerForeign:        images.MediaTypeDockerSchema2LayerForeign,
	images.MediaTypeDockerSchema2LayerForeignGzip:    images.MediaTypeDockerSchema2LayerForeignGzip,
	ocispecs.MediaTypeImageLayerNonDistributable:     images.MediaTypeDockerSchema2LayerForeign,     //nolint:staticcheck // ignore SA1019: Non-distributable layers are deprecated, and not recommended for future use.
	ocispecs.MediaTypeImageLayerNonDistributableGzip: images.MediaTypeDockerSchema2LayerForeignGzip, //nolint:staticcheck // ignore SA1019: Non-distributable layers are deprecated, and not recommended for future use.
	ocispecs.MediaTypeImageLayerZstd:                 mediaTypeDockerSchema2LayerZstd,
	mediaTypeDockerSchema2LayerZstd:                  mediaTypeDockerSchema2LayerZstd,
}

var toOCILayerType = map[string]string{
	ocispecs.MediaTypeImageLayer:                     ocispecs.MediaTypeImageLayer,
	ocispecs.MediaTypeImageLayerNonDistributable:     ocispecs.MediaTypeImageLayerNonDistributable,     //nolint:staticcheck // ignore SA1019: Non-distributable layers are deprecated, and not recommended for future use.
	ocispecs.MediaTypeImageLayerNonDistributableGzip: ocispecs.MediaTypeImageLayerNonDistributableGzip, //nolint:staticcheck // ignore SA1019: Non-distributable layers are deprecated, and not recommended for future use.
	ocispecs.MediaTypeImageLayerNonDistributableZstd: ocispecs.MediaTypeImageLayerNonDistributableZstd, //nolint:staticcheck // ignore SA1019: Non-distributable layers are deprecated, and not recommended for future use.
	images.MediaTypeDockerSchema2Layer:               ocispecs.MediaTypeImageLayer,
	ocispecs.MediaTypeImageLayerGzip:                 ocispecs.MediaTypeImageLayerGzip,
	images.MediaTypeDockerSchema2LayerGzip:           ocispecs.MediaTypeImageLayerGzip,
	images.MediaTypeDockerSchema2LayerForeign:        ocispecs.MediaTypeImageLayerNonDistributable,     //nolint:staticcheck // ignore SA1019: Non-distributable layers are deprecated, and not recommended for future use.
	images.MediaTypeDockerSchema2LayerForeignGzip:    ocispecs.MediaTypeImageLayerNonDistributableGzip, //nolint:staticcheck // ignore SA1019: Non-distributable layers are deprecated, and not recommended for future use.
	ocispecs.MediaTypeImageLayerZstd:                 ocispecs.MediaTypeImageLayerZstd,
	mediaTypeDockerSchema2LayerZstd:                  ocispecs.MediaTypeImageLayerZstd,
}

最後に

イメージレイヤーをリモートレジストリや S3 に保存する方法する仕組みについて、ビルドをする時間に対して、リモートからダウンロードする時間(+新しいキャッシュをアップロードする時間)どちらが早いかが一般的な争点になりそうである。 レイヤーのダウンロードやアップロードには圧縮の処理もあるがネットワーク時間に対して大体無視できそうである。 同じイメージのビルドで 25% とのことなので、ユースケースは選びそうであるか。