ECRにイメージが複数作成されてしまい、Lambdaにデプロイできない問題

# 起こった事象
ローカル環境でDocker Imageを1つ作成してECRにプッシュしたところ、以下のように3つのイメージがアップロードされてしまいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/77d1aa72-45df-43cb-92e6-76e4ceb94924.png)

さらに、このイメージに付与されている**v7のタグ**を使ってLambda関数をデプロイ使用としたところ、
>ソースイメージv7のイメージマニフェスト、構成、またはレイヤーメディアタイプはサポートされていません

というエラーが発生してしまいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/376d9cad-234a-4ffa-a44f-7389cf9e52ae.png)


# 対処法
これはDocker Imageのビルド方法(コマンド)次第で解消できます。
ひとまず私が確認できた2種類の方法をご紹介します。

## docker buildコマンド
単一のDockerfileを用いて、docker buildコマンドを実行する場合は以下のように `--provenance=false` を指定しましょう。
```bash
$ docker build --provenance=false -t my-app .
```
## docker compose buildコマンド
Dockerfile + compose.yml(docker-compose.yml)を用いてdocker compose buildコマンドを実行する場合は、以下コマンドを実施しましょう。
```bash
# docker compose buildコマンドの前に環境変数を設定
$ export BUILDX_NO_DEFAULT_ATTESTATIONS=1
$ docker compose build
```

:::note
未検証ですが、以下の手法でもできそうです。

1.以下を設定して docker compose buildを実行する
```compose.yml
services:
  your-service:
    build:
      context: .
      args:
        - BUILDX_NO_DEFAULT_ATTESTATIONS=1
```

2.コマンドラインで直接ビルド引数を渡す
```bash
$ docker compose build --build-arg BUILDX_NO_DEFAULT_ATTESTATIONS=1
```
:::

これで実行したところ、ECRへプッシュされるDocker Imageは1つだけになっていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/43582191-6324-4cb3-aba0-a1ced7e60924.png)

これであれば、Lambdaがデプロイできるようになっていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/c4f9eb93-ac1d-4cab-8a12-354f7460dfe1.png)

イメージはv8になっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/b3b62c14-c25c-4346-b114-c77b1ec367bc.png)

ということで、無事にビルド・プッシュ・デプロイまでできるようになりました！
どなたかの問題解決につながれば幸いです。
<br>
**以降、本件の調査結果です。ご興味のある方はご覧ください。**

# provenanceオプションとは
以下ページが非常によくまとまっていてわかりやすかったです。

https://chroju.dev/blog/docker_buildx_slsa_provenance

https://dev.classmethod.jp/articles/understanding-untaged-small-images-created-when-pushing-to-ecr/

概念をざっくり解説すると、以下の感じのようです。
- provenanceとは、「イメージがどこでどのように構築されたか」を示す証明情報
    - ビルドタイムスタンプ
    - ビルドパラメータと環境変数
    - ソースコードやスクリプト
    - バージョン管理のメタデータ
- Provenance Attestation(出生証明書/来歴証明書)と言われることもある
- Dockerのビルドにおいては[Buildkit v0.11.0](https://github.com/moby/buildkit/releases/tag/v0.11.0)にてProvenance Attestation対応
    - `docker buildx build --provenance true`を実行することで、Provenanceを生成してDocker Imageに添付できる
- その後、[docker/build-push-actionのv3.3.0](https://github.com/docker/build-push-action/releases/tag/v3.3.0)にて追加された
    - これによって、`docker build`コマンドでも対応
    - デフォルトでは有効化、つまり`true`になっている

## SLSA(Supply-chain Levels for Software Artifacts)
Provenance AttestationはSLSAフレームワークというものに基づいて作成されます。

SLSAは簡単に言うと、「開発・ビルド・デプロイという一連の過程(ソフトウェアのサプライチェーン)において、外部の攻撃から守るためのフレームワーク」のことです。
これがあることによって、ソフトウェア成果物の整合性を確保することができるようになります。

https://www.legitsecurity.com/blog/what-is-slsa-slsa-explained-in-5-minutes

4段階のレベルに分かれており、その中のレベル1にて必要とされているのが、ソフトウェアのビルドプロセスに関するProvenanceとなっています。
すなわち、ソフトウェア自身の来歴証明書をメタデータとしてアーティファクトに添付することが求められています。
- レベル1: ビルドプロセスの文書化
    - 例: 署名なしのProvenance
- レベル2: ビルドサービスの改ざん耐性
    - 例: ホストされたソース/ビルド、署名付きProvenance
- レベル3: 特定の脅威に対する追加耐性
    - 例: ホストのセキュリティ制限、偽造不可能なProvenance
- レベル4: 最高レベルの信頼性
    - 例: 二者によるレビュー+密閉的ビルド

## Attestation Storage
Provenance Attestationをイメージに添付する際は、Attestation Storageという仕組みが使われています。

https://github.com/moby/buildkit/blob/master/docs/attestations/attestation-storage.md

Attestation Storageとは、簡単に言うと「このソフトウェアはこのように作られました」という証明書をイメージと一緒に保存し、配布するための仕組みです。

DockerやOCIイメージに関連する証明情報(ProvenanceやSBOMなど)をイメージインデックス内のマニフェストオブジェクトとして保存し、イメージ自体とともに配布できるようなものになっています。

## SLSAとAttestation Storageの関係
基本的な関係としては、以下の形です。
- SLSA: ソフトウェアサプライチェーンのセキュリティを確保するフレームワーク
    - 信頼できるソフトウェアに必要な証明を定義する
- Attestation Storage: SLSAなどで生成された証明情報をイメージとともに保存・配布するための方法
    - 証明書を保管・配布するための入れ物を提供する
- 証明の生成と保存
    - SLSAは、ソフトウェアの作成方法や条件に関する証明(Provenance)を生成するよう要求する
    - Attestation Storageは、生成された証明情報をコンテナイメージとともに標準化された形式で保存する(という仕組みを提供する)
- 検証の仕組み
    - SLSAは、1~4のレベルで証明の検証方法を定義する
    - Attestation Storageは、それらの証明が検証可能な形でイメージに添付される方法を提供する
- フォーマットの共通化
    - SLSAは、[in-totoベースの証明フォーマット](https://qiita.com/kannkyo/items/b823761fd7861103bf9f#in-toto-%E3%81%A8%E3%81%AF)を推奨する
    - Attestation Storageは、そのフォーマットを保存・配布するための標準化された方法を実装している

実際の流れとしては、以下の形になります。
```
1. ビルドプロセスでSLSAに準拠したProvenance Attestation(プロべナンス証明)が生成されます
2. この証明はAttestation Storageの仕組みを使って、コンテナイメージに添付されます
3. コンテナイメージがレジストリにプッシュされる際、証明も一緒に保存されます
4. 利用者がイメージをプルする際、証明も一緒に取得できます
5. 利用者はこの証明を使って、SLSAのガイドラインに従い信頼性を検証できます
```

# コンテナイメージにおける具体的な階層構造
わかりづらいので、もう少し具体化しましょう。

DockerやOCIによるコンテナの仕様として、イメージインデックス・イメージマニフェストというものがあります。
(Dockerでは、イメージインデックスをマニフェストリストと言うのが正確らしいです)

以下のような階層構造で、イメージインデックスは構成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/d41aa18f-a697-464a-b982-ead269c27729.png)

以下、図の中の吹き出しを文字ベースに起こしておきます。
- Image Index(イメージインデックス)
    - コンテナイメージの目次的な役割
    - マニフェストのリストと、それらに紐づくプラットフォーム情報(OS/CPUアーキテクチャ)を管理
    - OCIフォーマットでは通常「application/vnd.oci.image.index.v1+json」というメディアタイプを保持
- Image Manifest(イメージマニフェスト)
    - コンテナイメージの構成情報を管理
    - 具体的には、イメージ構成とイメージレイヤー(後述)
    - 実行可能なコンテナイメージの内容を記述
    - OCIフォーマットでは通常「application/vnd.oci.image.manifest.v1+json」というメディアタイプを保持
- Image Layer(イメージレイヤー)
    - 実際のファイルシステムの内容を表す
    - レイヤーを順番に積み重ねることで、コンテナのファイルシステムを構築する
- Image Config(イメージ構成)
    - コンテナ実行時に必要なメタデータを含む
    - イメージのルートファイルシステムに対する変更セット(Dockerfile内のコマンドライン命令に相当)を管理
- Attestation Manifest(証明マニフェスト)
    - イメージマニフェストと同じフォーマット
    - 通常の実行目的ではなく、証明を格納する目的で用いられる
    - 1つまたは複数の証明プロブをレイヤーとして参照する
    - プラットフォームは「unknown/unknown」と設定され、コンテナランタイムに実行されないようになっている
- Attestation Blob(証明ブロブ)
    - 実際の証明データ(ProvenanceやSBOMなど)が含まれる
    - 複数の証明ブロブを単一の証明マニフェストに含めることが可能
    - 通常「application/vnd.in-toto+json」などのメディアタイプを保持

## ECR上のイメージと比較する
では実際に、先程プッシュしたECR上のコンテナイメージを確認してみましょう。

まずv7のタグが付いたイメージです。よく見ると、「Image Index」と記載されています。
つまり、こいつが目次ですね。このインデックスの配下にタグ無しの2つのImageが存在している、ということになりそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/b697f29b-95c1-4ab6-a8b7-42dcf2d8b9a6.png)

タグのついていない、2つのImageを見てみましょう。
Configの設定が記載されているので、恐らくこの実態としてはイメージマニフェストです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/48ba92bf-de28-400b-ad7f-da91cd952e7b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/bf1edb44-160f-43ec-b0d1-2db5656fdfc4.png)

ただこれだけでは情報が不足していて、どれがどういうイメージマニフェストなのかわかりません。

そこで、ローカル環境で`docker buildx imagetools inspect <image>`コマンドを実行します。これでより詳細にマニフェストの中身を確認できます。

```bash
# 1.認証トークンを取得し、レジストリに対して Docker クライアントを認証します
$ aws ecr get-login-password --region <リージョン> | docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com

# 2.ECRからローカル環境へ、イメージをプルする
$ docker pull <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com/<リポジトリ名>:<タグ>

# 3.マニフェストの中身を確認する
$ docker buildx imagetools inspect <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com/<リポジトリ名>:<タグ>
```

```bash
# 今回の実行結果
$  docker buildx imagetools inspect <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com/blog-service-app:v7 
Name:      <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com//blog-service-app:v7
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:*****************
           
Manifests: 
  Name:        <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com//blog-service-app:v7@sha256:*****************
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/arm64
               
  Name:        <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com/blog-service-app:v7@sha256:*****************
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    unknown/unknown
  Annotations: 
    vnd.docker.reference.digest: sha256:*****************
    vnd.docker.reference.type:   attestation-manifest
```

1つ目に表示されたマニフェストは、`Platform: linux/arm64`と記載されているので、イメージマニフェストです。つまり、こちらがイメージの実態です。
ECRコンソールで言うと、2つ目のImageが該当していそうです(イメージサイズがきちんと設定されているため)。

2つ目に表示されたマニフェストは、`Platform: unknown/unknown`かつ`reference.type: attestation-manifest`が設定されているため、証明マニフェストです。
ECRコンソールで言うと、1つ目のImageが該当していそうです(ただ証明を保存するため、イメージサイズが0となっていると推測)。

:::note warn
※以下、一部推測を含みます

前述の通り、証明マニフェストはイメージマニフェストと同じフォーマットです。
そのため、ECRコンソール上ではイメージマニフェストも証明マニフェストもImageとして同じ形で表示されていましたが、実態としては異なる、というような形になっているのだと思われます。
:::

ということで上記を踏まえると、v7のイメージの構造としては、以下のようになります。
```bash
イメージインデックス: v7タグ付き
  ├── 証明マニフェスト（Attestation Manifest）: タグなし1つ目
  └── イメージマニフェスト（実行可能イメージ）: タグなし2つ目
        ├── イメージコンフィグ
        └── イメージレイヤー
```

# --provenanceをfalseに設定する理由
ここまで読むと、セキュリティ的にProvenanceを有効化しておいたほうが良さそうですよね。

ただ、Lambdaを使う場合は、そうもいかない理由があります。
Lambda自体がLinuxベースのコンテナイメージのみをサポートしており、**マルチアーキテクチャコンテナイメージには対応していないため**です。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-create.html#images-reqs

すなわち、provenanceを有効化した状態でImageをビルドした結果生成される、複数のManifestを含むImage Indexを指定した場合、Lambdaを起動できないということです。

https://github.com/docker/buildx/issues/1533

また、冒頭のエラー文を思い出してみると、以下のように記載されていました。
>ソースイメージv7のイメージマニフェスト、構成、またはレイヤーメディアタイプはサポートされていません

これまでの調査を踏まえてこの文章を噛み砕くと、
- ソースイメージv7(Image Index)配下にImage Manifest, Image Config, Image Layerが存在する
- ただ、Lambdaへのデプロイにおいてはサポートされていない要素が含まれている
    - 通常のImage Manifestは問題ない
    - ただ、Attestation Manifestも含まれていると、マルチアーキテクチャコンテナイメージとなってしまう
    - あとはシンプルに、Attestation Manifestをデプロイすることができない

という形になっているためのエラーであることがわかります。

# まとめ
コンテナイメージを用いてLambdaを起動したい場合は、Provenanceオプションを無効化しましょう！

(ここまで読んだうえで、冒頭の解消用コマンド`BUILDX_NO_DEFAULT_ATTESTATIONS`を見ると、その意味が理解できるかも？)
```bash
# docker compose buildコマンドの前に環境変数を設定
# ビルド時のAttestationsを無効化
$ export BUILDX_NO_DEFAULT_ATTESTATIONS=1
$ docker compose build
```

# 参考
https://recruit.gmo.jp/engineer/jisedai/blog/aws-ecr-many-images/

https://chroju.dev/blog/docker_buildx_slsa_provenance

https://tech.nri-net.com/entry/what_is_an_oci_artifact

https://dev.classmethod.jp/articles/understanding-untaged-small-images-created-when-pushing-to-ecr/

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/image-manifest-formats.html

https://aws.amazon.com/jp/blogs/news/introducing-multi-architecture-container-images-for-amazon-ecr/

https://docs.docker.com/build/metadata/attestations/slsa-provenance/