以下の内容はhttps://yunkt.hatenablog.com/entry/2026/02/06/235740より取得しました。


SAMでLayerだけ更新したのにAliasが進まない理由と、確実にVersionを切る方法

はじめに

この記事では、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::FunctionAutoPublishAlias を指定すれば、デプロイ時に AWS::Lambda::Version が作られ、Alias が最新の Version を指すように更新されます。 このAliasに対してinvokeするのが、一般的です。

Lambdaで、Blue/Greenデプロイメントをする場合などでは、この運用は、概ね必須になります。

dev.classmethod.jp

ただし注意点があります。 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::Function
  • AWS::Serverless::LayerVersion

は、そのままでは CloudFormation が直接作るリソースではありません。

SAM translator がこれらを

  • AWS::Lambda::Function
  • AWS::Lambda::Version
  • AWS::Lambda::Alias
  • (その他、必要な IAM やログ周り)

といった通常リソースに展開します。

ここが重要で、AutoPublishAlias が「新しい Version を発行するかどうか」を決めるのは、SAM の展開(変換)ロジックの中です。 言い換えると、SAM は「今回のデプロイで新しい AWS::Lambda::Version を作るべきか?」を、Transform 時点での差分検知ロジックで判断します。

intrinsic の展開タイミングが、なぜ関係するのか

LayerVersion ARN を別スタックから受け取り、Function スタックに パラメータとして渡す構成では、Function 側の Layers は多くの場合こうなります。

  • Layers: - !Ref CustomLoggingLayerArn(パラメータ参照)

ここでの !Ref は intrinsic です。

  • CloudFormation の実行では、パラメータ値が確定しているので、$LATESTLayers 更新は反映できます。
  • しかし 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 発行判断は概ねこういう流れです。

  1. Function の Properties を辞書化したものをベースに、ハッシュ(Version の logical id の材料)を作る
  2. Layers が含まれている場合、Layer の参照から“logical id”を取り出せる形(Ref / Fn::GetAtt)のときだけ、その logical id でテンプレート内の Resources を引いて Layer リソース定義(Properties)を取得する
  3. 取得できた 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 が進むケースがあります。

github.com

ここでは、回避策として、 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::LanguageExtensionsAWS::Serverless-2016-10-31(SAM Transform)を併用すると、SAM が生成するリソースの論理IDが 固定値扱い になって変わり得るのに、DependsOn 側ではその論理IDをintrinsicで組み立てられず、依存関係を正しく書けなくなる、という問題が生じます。

github.com

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 に渡されるテンプレートでは、resApiGatewayAWS::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::LanguageExtensionsAWS::Serverless-2016-10-31 の併用は、裏側で生成されるリソースの論理IDが変わり得るのに、DependsOn が動的に書けない、という罠を踏む可能性があります。

終わりに

LambdaとLayerは、あまりにも広く使われており、Functionのバージョンは、本番環境などでは必須と言える機能ですが、意外な罠があるので注意が必要です。




以上の内容はhttps://yunkt.hatenablog.com/entry/2026/02/06/235740より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14