はじめに
この記事では、Lambda と Layer を組み合わせた運用でハマりやすいポイントを 2 つ紹介します。
1つ目は、$LATEST を直接 invoke すると、更新中に「設定とコードの反映タイミングのズレ」を踏み得ること。
2つ目は、SAM の AutoPublishAlias を使っていても、Layer の更新だけでは alias が進まず、$LATEST と alias の実行結果がズレ得ることです。
最後に、再現例とともに、確実に回避するための設定(例: AutoPublishCodeSha256)もまとめます。
$LATEST は「一貫性のあるデプロイ状態」を保証しない
Lambda を使う際、バージョンやエイリアス(Qualifier)を指定せずに invoke すると、暗黙に $LATEST を実行することになります。
$LATEST は「未発行の最新版」であり、更新中も含めて内容が変わり得るため、アプリケーションとして一貫した状態(更新前か更新後か)を常に保証してくれるわけではありません。
特に Layer を利用している場合、Layer の設定変更(Layers の付け替え)と Function のコード変更(デプロイパッケージ更新)が、スタック更新の中で別タイミングに反映されることで、 「コードは古いのに Layer だけ変わった(またはその逆)」瞬間を踏む可能性があります。
例えば、Layerが以下のコードだとします。
def info(msg: str) -> None: print(f"[INFO] {msg}")
そして、そのLayerに依存するFunctionのコードが以下だとします。
from custom_logging import info def lambda_handler(event, context): info("hello from layer") return {"ok": True}
Layer は以下のテンプレートで、Layer 用の CloudFormation スタックがあるとします。
AWSTemplateFormatVersion: "2010-09-09" Description: Custom logging layer stack Parameters: LayerS3Bucket: Type: String LayerS3Key: Type: String Resources: CustomLoggingLayer: Type: AWS::Lambda::LayerVersion UpdateReplacePolicy: Retain DeletionPolicy: Retain Properties: LayerName: custom-logging Content: S3Bucket: !Ref LayerS3Bucket S3Key: !Ref LayerS3Key CompatibleRuntimes: - python3.12 Outputs: CustomLoggingLayerArn: Value: !Ref CustomLoggingLayer
そして Function は、以下の SAM 用のテンプレートでデプロイされるものとします。
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: Demo function that depends on a layer Parameters: CustomLoggingLayerArn: Type: String Resources: DemoFunction: Type: AWS::Serverless::Function Properties: FunctionName: demo-latest-layer-pitfall Runtime: python3.12 Handler: app.lambda_handler CodeUri: app/ Timeout: 10 Layers: - !Ref CustomLoggingLayerArn Policies: - AWSLambdaBasicExecutionRole
この状態でFunctionのテンプレートからLayerの指定を取り除きます。
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: Demo function that used to depend on a layer Parameters: CustomLoggingLayerArn: Type: String Resources: DemoFunction: Type: AWS::Serverless::Function Properties: FunctionName: demo-latest-layer-pitfall Runtime: python3.12 Handler: app.lambda_handler CodeUri: app/ Timeout: 10 # Layers: ← removed Policies: - AWSLambdaBasicExecutionRole
Functionのコードを以下のようにして、Layerに依存しないコードにしたとします。
def lambda_handler(event, context): # no longer depends on custom_logging layer print("hello without layer") return {"ok": True}
このスタックを更新している最中に、タイミング悪く $LATEST への invoke が発生すると、以下のエラーが出る場合があります。
[ERROR] Runtime.ImportModuleError: Unable to import module 'app': No module named 'custom_logging' Traceback (most recent call last): ... INIT_REPORT Init Duration: xxx ms Phase: init Status: error Error Type: Runtime.ImportModuleError START RequestId: ... Version: $LATEST
これが発生する理由は、$LATEST が「常にその時点の最新版」である一方で、スタック更新中は Lambda の設定更新やコード更新が段階的に進み得るためです。
補足: 公開済み Version(1, 2, ...)は immutable(不変)なので、本番トラフィックは Version + Alias に寄せるのが基本の防御策になります。
SAM の AutoPublishAlias は「Layer の更新」だけでは新しい Version を発行しないことがある
Lambdaでは、関数にVersionを発行し、VersionにAliasを切って運用するのが定石です。
SAM でも AWS::Serverless::Function に AutoPublishAlias を指定すれば、デプロイ時に AWS::Lambda::Version が作られ、Alias が最新の Version を指すように更新されます。
このAliasに対してinvokeするのが、一般的です。
Lambdaで、Blue/Greenデプロイメントをする場合などでは、この運用は、概ね必須になります。
ただし注意点があります。
Function のコードは一切変えずに、Layer だけ更新して Layers に新しい LayerVersion ARN を設定しても、AutoPublishAlias だけでは新しい Function Version が発行されないことがあります。
言い換えると、デプロイ後に
$LATESTは新しい Layer を見ている(設定更新は反映される)- しかし Alias(例:
:live)は古い Version を指したまま(新しい Version が作られないと alias は進めない)
という「ズレ」が起きます。
よって、Layerを変更してデプロイしたら、そのLayerを使った処理をしてくれると思いきや、そうなっていない、ということが普通に起きえます。 個人的には、AWSのLambda(というかSAM)の最大の罠な気がします。
具体例を書くと、
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: Demo function that depends on a layer (AutoPublishAlias only) Parameters: CustomLoggingLayerArn: Type: String Description: LayerVersion ARN from the layer stack Outputs Resources: DemoFunction: Type: AWS::Serverless::Function Properties: FunctionName: demo-sam-layer-versioning Runtime: python3.12 Handler: app.lambda_handler CodeUri: app/ Timeout: 10 Layers: - !Ref CustomLoggingLayerArn Policies: - AWSLambdaBasicExecutionRole AutoPublishAlias: live Outputs: FunctionName: Value: !Ref DemoFunction LiveAliasArn: Value: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${DemoFunction}:live"
この Function のテンプレートに対して、パラメータに最新の LayerVersion ARN を指定して sam deploy を行っても、
新しい Function Version が発行されず、alias 経由の実行には Layer の更新が反映されないことがあります($LATEST には反映されても、alias が進まない)。
なぜこんなことが起きるのか
ポイントは、SAM の AutoPublishAlias はデフォルトでは「コード変更(主に CodeUri)をトリガーにして新しい Version を発行する」という点です。
そのため「LayerVersion ARN だけが変わった」状況では、新しい AWS::Lambda::Version が作られず、alias も進まないことがあります。
ただ、Layerとはいえ、コードの変更ではあるわけです。
では、SAMが、Versionを発行するべきと判断するのは何なのか、もう少し深掘りしていきます。
intrinsicとは
CloudFormation テンプレートには、値を動的に組み立てるための組み込み関数(式)があります。 これが一般に intrinsic functions(組み込み関数) と呼ばれるものです。代表例は次の通りです。
Ref(!Ref):パラメータ値、疑似パラメータ、リソース参照などを取り出すFn::Sub(!Sub):文字列の中で${...}を展開するFn::GetAtt(!GetAtt):リソースの属性値(ARN 等)を取り出すFn::Join/Fn::If/Fn::Equalsなど:条件分岐や文字列結合
ここで重要なポイントは、SAM は Transform(テンプレート展開)の段階で「Version を作るべきか」を判断している点です。 その判断は、デプロイ実行中に確定する「実 ARN」そのものではなく、Transform 時点で見えているテンプレート上の情報(プロパティの形)に依存します。
SAM を使わない(純 CloudFormation)場合
SAM を使わない場合(=純粋な CloudFormation テンプレートだけの場合)、役割分担は基本的にこうなります。
- aws cloudformationコマンドを実行するローカル: テンプレートを用意し、パラメータを指定して CloudFormation に渡す
- CloudFormation(AWS サービス): スタックの作成/更新の実行中に intrinsic を評価・展開する
もう少しだけ具体化すると、
Parametersはスタック更新開始時点で値が確定し、!Refはそれに置き換えられる!Ref/!GetAttが リソース参照の場合、その物理 ID や ARN はリソース作成後に確定するため、「実行の途中で」値が確定して使えるようになる
つまり CloudFormation は、スタック操作の中で intrinsic を評価しつつ、リソースを作成・更新します。
SAM を使う場合
SAM テンプレートは Transform: AWS::Serverless-2016-10-31 を使います。
これは、CloudFormation がスタックを作る前に、SAM がテンプレートを “通常の CloudFormation リソース” に展開する工程が入る、ということです。
たとえば SAM の
AWS::Serverless::FunctionAWS::Serverless::LayerVersion
は、そのままでは CloudFormation が直接作るリソースではありません。
SAM translator がこれらを
AWS::Lambda::FunctionAWS::Lambda::VersionAWS::Lambda::Alias- (その他、必要な IAM やログ周り)
といった通常リソースに展開します。
ここが重要で、AutoPublishAlias が「新しい Version を発行するかどうか」を決めるのは、SAM の展開(変換)ロジックの中です。
言い換えると、SAM は「今回のデプロイで新しい AWS::Lambda::Version を作るべきか?」を、Transform 時点での差分検知ロジックで判断します。
intrinsic の展開タイミングが、なぜ関係するのか
LayerVersion ARN を別スタックから受け取り、Function スタックに パラメータとして渡す構成では、Function 側の Layers は多くの場合こうなります。
Layers: - !Ref CustomLoggingLayerArn(パラメータ参照)
ここでの !Ref は intrinsic です。
- CloudFormation の実行では、パラメータ値が確定しているので、
$LATESTのLayers更新は反映できます。 - しかし SAM の
AutoPublishAliasの Version 発行判断は、Transform(変換)段階の差分判定ロジックに依存します。
そして実務的には、今回の現象を決めている要因は次の設計です。
AutoPublishAliasはデフォルトでは「コード差分(主にCodeUri)」を Version 発行の主トリガーとしている- 「LayerVersion ARN だけの差し替え」は、(少なくともデフォルト設定では)Version を必ず増やす変更として扱われない
その結果、
$LATESTは新しい Layer を参照できる(=Function の設定更新が走る)- でも新しい発行済み Version は作られない(=alias が進まない)
- alias 経由の実行は古い Version のまま(=古い Layer のまま)
という “乖離” が起きます。
AutoPublishAliasAllProperties を使えばいいのでは?
答えは、「万能ではない」です。
AutoPublishAliasAllProperties: true は「Function のプロパティ差分も含めて」Version 発行判断に使いますが、
Layers については参照の形や解決可否に制約があり、今議論しているような、 別スタックから LayerVersion ARN をパラメータで受け取る構成 では、期待どおりに差分検知できないケースがあります。
直感的には「LayerVersion ARN の“実値”まで見てほしい」と思うのですが、Transform 時点で見える情報だけで判断している以上、取りこぼしが起き得ます。
もう少し踏み込むと、ここで言っている「差分」とは “デプロイで渡したパラメータ値の差分” ではなく、Transform 時点で見えるテンプレート(YAML/JSON)の構造上の差分です。
たとえば、Function 側のテンプレートが次のようになっているとします。
Layers: - !Ref CustomLoggingLayerArn
このとき、sam deploy --parameter-overrides CustomLoggingLayerArn=...:1 を ...:2 に変えても、SAM translator から見える Layers の見た目は常に「Ref でパラメータを参照している」という同じ構造のままです。
(SAM は Transform 中に「CustomLoggingLayerArn の実値が何か」までは確定できない/扱わない)
実装の観点では、AutoPublishAliasAllProperties の Version 発行判断は概ねこういう流れです。
- Function の
Propertiesを辞書化したものをベースに、ハッシュ(Version の logical id の材料)を作る Layersが含まれている場合、Layer の参照から“logical id”を取り出せる形(Ref/Fn::GetAtt)のときだけ、その logical id でテンプレート内のResourcesを引いて Layer リソース定義(Properties)を取得する- 取得できた Layer 定義の
Propertiesを、Function 側のハッシュ入力に混ぜる(=Layer の更新も差分として扱える)
ここで別スタック構成だと、!Ref CustomLoggingLayerArn の logical id は「パラメータ名」になってしまい、テンプレート内 Resources から Layer 定義を引けません。
その結果、SAM は「Layer の中身(LayerVersion ARN の実値)まで辿ってハッシュに混ぜる」ことができず、Function の Version 発行判断に Layer 更新が入りません。
逆に言うと、同一テンプレート内に AWS::Lambda::LayerVersion が定義されていて、Function の Layers がそれを !Ref/!GetAtt で参照できる形になっている場合は、Layer 側 Properties をハッシュ入力に混ぜられるため、AutoPublishAliasAllProperties でも期待どおりに Version が進むケースがあります。
ここでは、回避策として、 AutoPublishCodeSha256 でLayerも含めたハッシュを渡して強制的にバージョンを発行させる、という手が提案されています。
ではどうするの?
上で提案されているように、 AutoPublishCodeSha256 を渡すのが、最も確実な方法と言えます。
テンプレートを以下のように記載します。
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: Demo function that depends on a layer (force publish with AutoPublishCodeSha256) Parameters: CustomLoggingLayerArn: Type: String Description: LayerVersion ARN from the layer stack Outputs AutoPublishCodeSha256Override: Type: String Description: >- Any string. When this value changes, SAM will publish a new Lambda Version for the AutoPublishAlias even if the function code didn't change. Recommended: sha256 of the referenced LayerVersion ARN(s). Resources: DemoFunction: Type: AWS::Serverless::Function Properties: FunctionName: demo-sam-layer-versioning Runtime: python3.12 Handler: app.lambda_handler CodeUri: app/ Timeout: 10 Layers: - !Ref CustomLoggingLayerArn Policies: - AWSLambdaBasicExecutionRole AutoPublishAlias: live AutoPublishCodeSha256: !Ref AutoPublishCodeSha256Override Outputs: FunctionName: Value: !Ref DemoFunction LiveAliasArn: Value: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${DemoFunction}:live"
そして、これをデプロイするスクリプトでは、このように記載します。
#!/usr/bin/env bash
set -euo pipefail
# Deploys the function stack via SAM.
# Usage:
# ./scripts/deploy_function.sh <stack-name> <layer-version-arn> <template-basename> [region] [auto-publish-code-sha256]
# Example:
# ./scripts/deploy_function.sh demo-fn-stack arn:aws:lambda:...:layer:custom-logging:1 template.yaml ap-northeast-1
# ./scripts/deploy_function.sh demo-fn-stack arn:aws:lambda:...:layer:custom-logging:2 template_all_properties.yaml ap-northeast-1
# ./scripts/deploy_function.sh demo-fn-stack arn:aws:lambda:...:layer:custom-logging:2 template_code_sha256.yaml ap-northeast-1
STACK_NAME=${1:?stack-name required}
LAYER_ARN=${2:?layer arn required}
TEMPLATE_BASENAME=${3:-template.yaml}
REGION=${4:-}
CODE_SHA256_OVERRIDE=${5:-}
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEMPLATE="$ROOT_DIR/function-stack/${TEMPLATE_BASENAME}"
if [[ ! -f "$TEMPLATE" ]]; then
echo "Template not found: $TEMPLATE" >&2
exit 1
fi
SAM_REGION_ARGS=()
if [[ -n "$REGION" ]]; then
SAM_REGION_ARGS+=(--region "$REGION")
fi
PARAM_OVERRIDES=("CustomLoggingLayerArn=$LAYER_ARN")
if [[ "$TEMPLATE_BASENAME" == "template_code_sha256.yaml" ]]; then
if [[ -z "$CODE_SHA256_OVERRIDE" ]]; then
CODE_SHA256_OVERRIDE="$(printf '%s' "$LAYER_ARN" | shasum -a 256 | awk '{print $1}')"
fi
PARAM_OVERRIDES+=("AutoPublishCodeSha256Override=$CODE_SHA256_OVERRIDE")
fi
sam deploy \
--stack-name "$STACK_NAME" \
--template-file "$TEMPLATE" \
--resolve-s3 \
--capabilities CAPABILITY_IAM \
--parameter-overrides "${PARAM_OVERRIDES[@]}" \
--no-confirm-changeset \
--no-fail-on-empty-changeset \
"${SAM_REGION_ARGS[@]}"
要するに、LayerのバージョンのARNのハッシュ値を与えてあげる、という感じです。
その他の方法(実験的)
AWS::LanguageExtensions を併用し、LayerVersion ARN(テンプレート上の参照)を Function の環境変数へダミーで埋め込んで、
「Function のプロパティ差分」として見せることで Version 発行を促す、という発想があります。
ただし、これは “LayerVersion ARN の実値が必ず事前に分かる” という意味ではありません(別スタック参照や ImportValue/SSM 等の解決タイミング次第では、狙いどおりに差分にならないことがあります)。
以下のようにFunctionのテンプレートを記載するイメージです。
AWSTemplateFormatVersion: "2010-09-09" Transform: - AWS::LanguageExtensions - AWS::Serverless-2016-10-31 Description: Demo function that depends on a layer (LanguageExtensions + dummy env var) Parameters: CustomLoggingLayerArn: Type: String Description: LayerVersion ARN from the layer stack Outputs (or any resolved ARN string) Resources: DemoFunction: Type: AWS::Serverless::Function Properties: FunctionName: demo-sam-layer-versioning Runtime: python3.12 Handler: app.lambda_handler CodeUri: app/ Timeout: 10 Layers: - !Ref CustomLoggingLayerArn Environment: Variables: # Dummy variable to try to make the layer version change visible as a Function property change. # Note: whether this triggers a new published Version depends on how/when the value is resolved. LAYER_VERSION_ARN_DUMMY: !Ref CustomLoggingLayerArn # Example of LanguageExtensions usage (canonical JSON string) LAYER_INFO_JSON: Fn::ToJsonString: LayerArn: !Ref CustomLoggingLayerArn Policies: - AWSLambdaBasicExecutionRole AutoPublishAlias: live AutoPublishAliasAllProperties: true Outputs: FunctionName: Value: !Ref DemoFunction LiveAliasArn: Value: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:${DemoFunction}:live"
しかし、この方法には注意点があります。
AWS::LanguageExtensions と AWS::Serverless-2016-10-31(SAM Transform)を併用すると、SAM が生成するリソースの論理IDが 固定値扱い になって変わり得るのに、DependsOn 側ではその論理IDをintrinsicで組み立てられず、依存関係を正しく書けなくなる、という問題が生じます。
SAM は、テンプレートに書いていないリソースを内部で自動生成することがあります(典型例が AWS::Serverless::Api から生成される Stage など)。
一方で DependsOn では !Join / !Sub などの intrinsic が使えません。つまり「生成される論理IDに合わせて DependsOn を動的に組み立てる」ことができません。
例えば、テンプレートで、
- resApiGateway :
AWS::Serverless::Api - resUsagePlan :
AWS::ApiGateway::UsagePlan(UsagePlanはSAMではなく素のCFN)
を書いたとすると、SAMでは、自動的に、 AWS::Serverless::Api から勝手に AWS::ApiGateway::Stage を作ります。
そうすると、CloudFormation に渡されるテンプレートでは、resApiGateway が AWS::ApiGateway::RestApi 等に展開され、さらに Stage(AWS::ApiGateway::Stage)も生成されます。
AWS::ApiGateway::Stage は、元々のテンプレに書かれていないので、論理IDはSAMが決める必要があります。
そして SAM はその論理IDを決めるとき、StageName の値を材料にする実装になっています。
LanguageExtensions 無しの場合、StageName: !Ref paramEnvironment は intrinsic のまま SAM に渡るため、SAM は StageName を固定文字列として扱えず、生成される論理IDが「固定パターン」になります。
LanguageExtensions ありの場合、テンプレートが SAM に渡る前に前処理が入り、!Ref paramEnvironment が(Issue の例では)local のような固定文字列に見える形になってしまい、
生成される論理IDが別名(...local... を含む名前)になることがあります。
結果として Stage の論理IDが resApiGatewaylocalStage になってしまいます。
その結果、テンプレートに DependsOn: resApiGatewayStage と書いていた場合に、生成後のテンプレート上では resApiGatewayStage という論理IDが存在せず、依存関係が壊れます。
このように、AWS::LanguageExtensions と AWS::Serverless-2016-10-31 の併用は、裏側で生成されるリソースの論理IDが変わり得るのに、DependsOn が動的に書けない、という罠を踏む可能性があります。
終わりに
LambdaとLayerは、あまりにも広く使われており、Functionのバージョンは、本番環境などでは必須と言える機能ですが、意外な罠があるので注意が必要です。