[アップデート]AWS SAM CLIがLambda関数のコンテナイメージ(ImageUrl)の指定にローカルのパスをサポートしました

2024.05.23

初めに

先日AWS SAM CLIのv1.117.0がリリースされました。
今回のアップデートのでコンテナベースのLambda関数のイメージの指定にすでに生成されているローカルのイメージを指定することができるようになります。

従来でもSAMでは以下のようにMetadataにDockerfileのパス等の情報を書き込むことでSAM CLI経由でコンテナイメージのビルドおよびそのデプロイ(ECRへのpushを含む)が可能でした。

...
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./hello_world
      DockerTag: v1

SAM CLIで完結することがメリットである一方で、イメージビルド処理もSAM CLIを経由させる必要があったためすでに生成されたイメージを利用したい場合はこちらでは実現することができませんでした。

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-resource-function.html
ImageUri Lambda 関数のコンテナイメージ用の Amazon Elastic Container Registry (Amazon ECR) リポジトリの URI です。このプロパティは、PackageType プロパティが Image に設定されている場合にのみ適用され、それ以外の場合は無視されます。詳細については、AWS Lambda デベロッパーガイドの「Lambda でのコンテナイメージの使用」を参照してください。

ImageUriにはECRのリポジトリのURIを利用することですでに存在するイメージを利用可能ではありますが、この場合イメージのデプロイ(push)は別の仕組みで行い関数の更新(利用するコンテナの切り替えを含め)はSAM側で行う形となるためデプロイが一元化されず場合によっては好ましくないものとなります。
(役割が分かれているという意味では必ずしも悪いわけではないとは思いますが)

今回SAM CLIで外部イメージを取り込めるようになったことですでに存在するイメージを使う場合でもECRへのイメージのpushとlambda関数の更新をSAM CLI側で一元的に管理できる選択肢が生まれました。

試してみる

sam initで生成されるHello Worldプロジェクトのアプリをdockerコマンドで直接ビルドし、SAMテンプレートに読み込ませローカル実行およびデプロイを行います。

$ tree
.
├── Dockerfile
├── __init__.py
├── app.py
└── requirements.txt

Dockerfileは以下の通りです。

Dockerfile

FROM public.ecr.aws/lambda/python:3.12

COPY app.py requirements.txt ./

RUN python3.12 -m pip install -r requirements.txt -t .

CMD ["app.lambda_handler"]

ビルド〜イメージの指定

説明のために順番が前後してしまいますが、まずはイメージのビルドを行います。

$ docker build -t sam-local-image .
...
$ docker image ls | head -n2
REPOSITORY                                                                  TAG                 IMAGE ID       CREATED          SIZE
sam-local-image                                                             latest              051c183de1b0   33 seconds ago   532MB

ローカルのイメージの指定はエクスポート(docker save)で出力したファイル、もしくはローカルのdockerデーモン管理配下のイメージ(?と呼んでいいのでしょうか)の2パターンが可能です。

後述しますが仕組み的には最終的に後者に集約されるため今回は一旦gzにエクスポートしてそれを指定します。

まずはdocker saveを利用して先ほどのイメージをファイルに出力します。

$ docker image ls sam-local-image
REPOSITORY        TAG       IMAGE ID       CREATED          SIZE
sam-local-image   latest    051c183de1b0   36 minutes ago   532MB
$ docker save sam-local-image > sam-local-image.gz
$ ls -ltr sam-local-image.gz
-rw-r--r--  1 xxxx  staff  544971264  5 23 19:36 sam-local-image.gz

## 後の説明のためにエクスポート元のイメージは削除しておきます
$ docker rmi sam-local-image && docker image ls sam-local-image
Untagged: sam-local-image:latest
Deleted: sha256:051c183de1b050e85624e90b784922f27017dff2a0de5430a3ad7cae6ff7ee88
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

テンプレートにはエクスポートしたパスを指定します。

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
+     ImageUri: hello_world/sam-local-image.gz
      PackageType: Image
      Architectures:
        - arm64
-   Metadata:
-     Dockerfile: Dockerfile
-     DockerContext: ./hello_world
-     DockerTag: python3.12-v1

これをビルド(sam build)したところビルド後のテンプレート(.aws-sam/build/template.yml)のImageUri部分は以下のようになっていました。

  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      ImageUri: sha256:051c183de1b050e85624e90b784922f27017dff2a0de5430a3ad7cae6ff7ee88
      PackageType: Image
      Architectures:
        - arm64

ImageUriに指定される値がダイジェスト値に変換されていることが確認できます。

さてこのイメージの実態はどこにあるのか?とdocker image lsを実行してみたところ先ほどのイメージが復活しておりエクスポートしたイメージが取り込まれていることが確認できました。

$ docker image ls sam-local-image --no-trunc
REPOSITORY        TAG       IMAGE ID                                                                  CREATED          SIZE
sam-local-image   latest    sha256:051c183de1b050e85624e90b784922f27017dff2a0de5430a3ad7cae6ff7ee88   41 minutes ago   532MB

ダイジェスト値はビルド前のテンプレートにも指定可能であり、この場合は追加の取り込み処理等が発生しないだけなのですでに取り込まれているイメージをあえて一旦エクスポートする必要はありません。

デプロイ

ここまで来たらあとは通常通りsam deployを行うだけです。
デプロイのタイミングでイメージがpushされておりDeployment image repositoryにECRのURLが記載されています。

 $ sam deploy

		Managed S3 bucket: xxxxxxx
		A different default S3 bucket can be set in samconfig.toml
		Or by specifying --s3-bucket explicitly.
File with same data already exists at 6978ea41514daaaa372a5b921e0928c1.template, skipping upload
e9e3ef36b059: Layer already exists
9a98523a9a17: Layer already exists
084832a54af8: Layer already exists
6b71b95c1d63: Layer already exists
ace32358fa20: Layer already exists
6c7e850d75ee: Layer already exists
71e0447675cd: Layer already exists
2a29e3f0f094: Layer already exists
HelloWorldFunction-051c183de1b0-latest: digest: sha256:2aa7a2be5d2a780956f0d59ba6ac64a3eb9ee72561e8426e81fb81931fb2172d size: 1996

	Deploying with following values
	===============================
	Stack name                   : sam-app-image
	Region                       : ap-northeast-1
	Confirm changeset            : True
	Disable rollback             : False
	Deployment image repository  :
                                       {
                                           "HelloWorldFunction": "xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/samappimageccxxxxxx/helloworldfunctionxxxxxx"
                                       }
	Deployment s3 bucket         : aws-sam-cli-managed-xxxxxx
	Capabilities                 : ["CAPABILITY_IAM"]
	Parameter overrides          : {}
	Signing Profiles             : {}

Initiating deployment
=====================

	Uploading to xxxxxx.template  539 / 539  (100.00%)


Waiting for changeset to be created..

CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Operation                                    LogicalResourceId                            ResourceType                                 Replacement
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                        HelloWorldFunctionRole                       AWS::IAM::Role                               N/A
+ Add                                        HelloWorldFunction                           AWS::Lambda::Function                        N/A
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

実行

実行確認をしておきます。まずは実環境上の確認。

 % sam remote invoke
Invoking Lambda Function HelloWorldFunction
START RequestId: 5861d461-0c5a-4d86-84e1-e668bb473a28 Version: $LATEST
END RequestId: 5861d461-0c5a-4d86-84e1-e668bb473a28
REPORT RequestId: 5861d461-0c5a-4d86-84e1-e668bb473a28	Duration: 2.18 ms	Billed Duration: 143 ms	Memory Size: 128 MB	Max Memory Used: 32 MB	Init Duration: 140.76 ms
{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}%

次にローカル...あれ...?

% sam local invoke
Invoking Container created from sha256:051c183de1b050e85624e90b784922f27017dff2a0de5430a3ad7cae6ff7ee88
Local image was not found.
Removing rapid images for repo sha256
Building image.................
Failed to build Docker Image
NoneType: None
Error: Error building docker image: refusing to create an ambiguous tag using digest algorithm as name

どうやらdeployの時にpush用の同ダイジェスト・別リポジトリのイメージができてしまい、SAMテンプレート上の指定がダイジェスト値なので一意に特定できず失敗しているようです。

 % docker image ls --no-trunc  | grep 051c183de1b050e85624e90b784922f27017dff2a0de5430a3ad7cae6ff7ee88
xxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/samappimageccxxxx/helloworldfunctionxxxxx   HelloWorldFunctionxxxxx-latest   sha256:051c183de1b050e85624e90b784922f27017dff2a0de5430a3ad7cae6ff7ee88   About an hour ago   532MB
sam-local-image                                                                                        latest                                   sha256:051c183de1b050e85624e90b784922f27017dff2a0de5430a3ad7cae6ff7ee88   About an hour ago   532MB

新しい機能なのでこういうこともあります(実装コードから逆算して試してるので自分がやってはいけないパターンを踏んでる可能性もあります)。

終わりに

イメージのビルドと関数のデプロイを分離できるようになったことにより最終的に利用するECRリポジトリへのpushとLambda関数の設定がSAM CLI側で一元的に行えるようになりました。

外部からイメージを持ち込む場合は結局受け渡しのために別ストレージを用意する必要はあるため万能ではない点に注意しましょう。

ちなみにビルド後のテンプレートではダイジェスト値となっていましたが、実際にCloudFormation側で管理されているテンプレートを見るとECRの値となっているためこの辺りはデプロイの際によしなに変換されています。

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      ImageUri: xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/samappimageccxxxxxx/helloworldfunctionxxxxxx
      PackageType: Image
      Architectures:
        - arm64
    Metadata:
      SamResourceId: HelloWorldFunction