初めに
先日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