SAM Local Invoke は Lambda の環境変数に設定された Ref を認識できないため env.json を自動生成して samcofig.yaml で参照してみた

SAM Local Invoke は Lambda の環境変数に設定された Ref を認識できないため env.json を自動生成して samcofig.yaml で参照してみた

Clock Icon2025.02.08

こんにちは!クラウド事業本部コンサルティング部のたかくに(@takakuni_)です。

最近、AWS SAM を使用する機会が増えてきました。 sam local invoke とっても便利ですよね。

ある日、いつものように意気揚々と sam local invoke を実行していると、次のエラーに遭遇しました。※DEBUG モードの結果です。

{"timestamp": "2025-02-08T10:59:52Z", "level": "DEBUG", "message": "Certificate path: /var/task/botocore/cacert.pem", "logger": "botocore.httpsession", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:52Z", "level": "DEBUG", "message": "Starting new HTTPS connection (1): s3.ap-northeast-1.amazonaws.com:443", "logger": "urllib3.connectionpool", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "https://s3.ap-northeast-1.amazonaws.com:443 \"GET /HogeOutputBucket?list-type=2&max-keys=1&encoding-type=url HTTP/1.1\" 404 None", "logger": "urllib3.connectionpool", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Event before-parse.s3.ListObjectsV2: calling handler <function _handle_200_error at 0x2aaaae697920>", "logger": "botocore.hooks", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Event before-parse.s3.ListObjectsV2: calling handler <function handle_expires_header at 0x2aaaae697740>", "logger": "botocore.hooks", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Response headers: {'x-amz-request-id': '9VQD11G2TC9QQP43', 'x-amz-id-2': 'm1T94G5F/n9nK060r4cGXcM8krLecA0WMa9BLVN50paMI9fR+6lq4nIMMCdtEgAy3vj0IlWs7aw=', 'Content-Type': 'application/xml', 'Transfer-Encoding': 'chunked', 'Date': 'Sat, 08 Feb 2025 10:59:53 GMT', 'Server': 'AmazonS3'}", "logger": "botocore.parsers", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Response body:\nb'<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message><BucketName>HogeOutputBucket</BucketName><RequestId>9VQD11G2TC9QQP43</RequestId><HostId>m1T94G5F/n9nK060r4cGXcM8krLecA0WMa9BLVN50paMI9fR+6lq4nIMMCdtEgAy3vj0IlWs7aw=</HostId></Error>'", "logger": "botocore.parsers", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Response headers: {'x-amz-request-id': '9VQD11G2TC9QQP43', 'x-amz-id-2': 'm1T94G5F/n9nK060r4cGXcM8krLecA0WMa9BLVN50paMI9fR+6lq4nIMMCdtEgAy3vj0IlWs7aw=', 'Content-Type': 'application/xml', 'Transfer-Encoding': 'chunked', 'Date': 'Sat, 08 Feb 2025 10:59:53 GMT', 'Server': 'AmazonS3'}", "logger": "botocore.parsers", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Response body:\nb'<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message><BucketName>HogeOutputBucket</BucketName><RequestId>9VQD11G2TC9QQP43</RequestId><HostId>m1T94G5F/n9nK060r4cGXcM8krLecA0WMa9BLVN50paMI9fR+6lq4nIMMCdtEgAy3vj0IlWs7aw=</HostId></Error>'", "logger": "botocore.parsers", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Event needs-retry.s3.ListObjectsV2: calling handler <function _update_status_code at 0x2aaaae697a60>", "logger": "botocore.hooks", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Event needs-retry.s3.ListObjectsV2: calling handler <botocore.retryhandler.RetryHandler object at 0x2aaaaf931400>", "logger": "botocore.hooks", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "No retry needed.", "logger": "botocore.retryhandler", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Event needs-retry.s3.ListObjectsV2: calling handler <bound method S3RegionRedirectorv2.redirect_from_error of <botocore.utils.S3RegionRedirectorv2 object at 0x2aaaaf853da0>>", "logger": "botocore.hooks", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}
{"timestamp": "2025-02-08T10:59:53Z", "level": "DEBUG", "message": "Event after-call.s3.ListObjectsV2: calling handler <function decode_list_object_v2 at 0x2aaaae696840>", "logger": "botocore.hooks", "requestId": "05fc7e67-b045-40d1-b55d-355f39546b5b"}

NoSuchBucket...? HogeOutputBucket には変換されたバケット名が入るはずなのにな...?

HogeOutputBucket には心当たりがあり、以下のように template.yaml にて定義し、環境変数 OUTPUT_BUCKET に Ref 関数を指定して、動的にマッピングしています。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: hogehoge

Globals:
  Function:
    Tracing: Active
  Api:
    TracingEnabled: true

Resources:
  HogeOutputBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub hoge-output-${AWS::AccountId}

  HogeOutputFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/hoge_report/
      Handler: app.lambda_handler
      Runtime: python3.12
      Architectures:
        - x86_64
      Timeout: 30
      LoggingConfig:
        LogFormat: JSON
      MemorySize: 128
      Environment:
        Variables:
          OUTPUT_BUCKET: !Ref HogeOutputBucket
      Policies:
        - S3WritePolicy:
            BucketName: !Ref HogeOutputBucket
        - Statement:
            - Effect: Allow
              Action:
                - s3:*
              Resource: !Sub arn:aws:s3:::${HogeOutputBucket}/*

アプリケーションのコードも非常にシンプルで、OUTPUT_BUCKET は、環境変数から取得する仕組みを採用していました。

app.py
import json
import boto3
import logging
from datetime import datetime, timezone, timedelta
import os
from botocore.exceptions import ClientError

logger = logging.getLogger()
logger.setLevel("DEBUG")

s3 = boto3.client('s3')

OUTPUT_BUCKET = os.environ['OUTPUT_BUCKET']

def lambda_handler(event, context):
    """
    Lambda handler to convert the input data to HTML format
    """
    try:
        response = s3.list_objects_v2(Bucket=OUTPUT_BUCKET, MaxKeys=1)

    except ClientError as e:
        logger.error(f"S3バケットのアクセス権限がありません: {e}")
    return {
        'statusCode': 200,
        'body': {
            'message': 'レポートの生成が完了しました',
            's3_bucket': OUTPUT_BUCKET,
            'response': response
        }
    }

状況証拠から、HogeOutputFunction の OUTPUT_BUCKET には、解決されなかった HogeOutputBucket が渡されています。一体どういうことなのでしょうか。

local invoke では Ref 関数は機能しない

以下の Issue にある通り、local invoke では Ref 関数が機能しません。

https://github.com/aws/aws-sam-cli/issues/2588

そのため、次のような方法で Ref 部分を上書きする必要があります。

  1. .env を利用する
  2. template.yaml で Ref を使わずベタ書きする
  3. --env-vars を利用する
    1. sam local invoke HogeOutputFunction --env-vars env.json
  4. --parameter-overrides を利用する
    1. sam local invoke HogeOutputFunction --parameter-overrides HogeOutputBucket=hoge-output-123456789012

参考

https://qiita.com/c3drive/items/9c1ed0686dc3aec88935

今回、私は 3 番を利用して、env.json を自動作成するようなワークフローを書いてみました。

やってみた

最終的に次のようなワークフローを作成しました。ステップバイステップで解説します。

gen_environment_local_invoke.yaml
name: Generate-Environment-Variables-Config

on:
  workflow_dispatch:

env:
  STACK_NAME: hoge

jobs:
  generate:
    runs-on: ubuntu-22.04
    permissions:
      id-token: write
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
          aws-region: ap-northeast-1
          role-session-name: GenerateEnvironmentVariables

      - name: Generate env.json
        id: generate
        run: |
          # CloudFormationスタックの情報を取得
          STACK_INFO=$(aws cloudformation describe-stacks --stack-name hoge)

          # 環境変数マッピングの定義
          # OutputKey の "FunctionEnv" を含む部分で分割し、関数名と環境変数名を抽出
          ENV_JSON=$(echo $STACK_INFO | jq -r '
            .Stacks[0].Outputs
            | map(select(.OutputKey | contains("FunctionEnv")))
            | reduce .[] as $item ({};
              ($item.OutputKey | capture("(?<func>.+)FunctionEnv(?<env>.+)$")) as $parts
              | .[$parts.func] = (.[$parts.func] // {})
              | .[$parts.func][$parts.env | gsub("(?<=[a-z])(?=[A-Z])";"_") | ascii_upcase] = $item.OutputValue
            )' > events/env.json)
      - name: Create Pull Request if changes detected
        uses: peter-evans/create-pull-request@v7
        with:
          branch: feature/auto-env-config
          title: Update Environment Variables configuration
          commit-message: Update Environment Variables configuration
          base: develop

CloudFormation の出力

パワープレイ目ですが、 Lambda の環境変数としてどの変数が利用されているのか識別するために、template.yaml関数名FunctionEnv環境変数名 の名前で、Output を定義しました。今回だと HogeOutputFunction の OUTPUT_BUCKET のため、HogeOutputFunctionEnvOutputBucket です。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: hogehoge

Globals:
  Function:
    Tracing: Active
  Api:
    TracingEnabled: true

Resources:
  HogeOutputBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub hoge-output-${AWS::AccountId}

  HogeOutputFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/hoge_report/
      Handler: app.lambda_handler
      Runtime: python3.12
      Architectures:
        - x86_64
      Timeout: 30
      LoggingConfig:
        LogFormat: JSON
      MemorySize: 128
      Environment:
        Variables:
          OUTPUT_BUCKET: !Ref HogeOutputBucket
      Policies:
        - S3WritePolicy:
            BucketName: !Ref HogeOutputBucket
        - Statement:
            - Effect: Allow
              Action:
                - s3:*
              Resource: !Sub arn:aws:s3:::${HogeOutputBucket}/*

Outputs:
  ####################################
  # 自動生成用出力
  # 関数名FunctionEnv環境変数名
  # 例:HogeOutputFunctionEnvOutputBucket
  ####################################
  HogeOutputFunctionEnvOutputBucket:
    Description: Report Output Bucket Name
    Value: !Ref HogeOutputBucket

GitHub Actions では先ほどの 関数名FunctionEnv環境変数名 を引っ張り、 events/env.json に登録します。

gen_environment_local_invoke.yaml
name: Generate-Environment-Variables-Config

on:
  workflow_dispatch:

env:
  STACK_NAME: hoge

jobs:
  generate:
    runs-on: ubuntu-22.04
    permissions:
      id-token: write
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
          aws-region: ap-northeast-1
          role-session-name: GenerateEnvironmentVariables

      - name: Generate env.json
        id: generate
        run: |
+          # CloudFormationスタックの情報を取得
+          STACK_INFO=$(aws cloudformation describe-stacks --stack-name hoge)

          # 環境変数マッピングの定義
+          # OutputKey の "FunctionEnv" を含む部分で分割し、関数名と環境変数名を抽出
+          ENV_JSON=$(echo $STACK_INFO | jq -r '
+            .Stacks[0].Outputs
+            | map(select(.OutputKey | contains("FunctionEnv")))
+            | reduce .[] as $item ({};
+              ($item.OutputKey | capture("(?<func>.+)FunctionEnv(?<env>.+)$")) as $parts
+              | .[$parts.func] = (.[$parts.func] // {})
+              | .[$parts.func][$parts.env | gsub("(?<=[a-z])(?=[A-Z])";"_") | ascii_upcase] = $item.OutputValue
+            )' > events/env.json)
      - name: Create Pull Request if changes detected
        uses: peter-evans/create-pull-request@v7
        with:
          branch: feature/auto-env-config
          title: Update Environment Variables configuration
          commit-message: Update Environment Variables configuration
          base: develop

PR の作成

作成された events/env.json を develop ブランチにマージするための PR を自動作成します。feature ブランチの変更内容によっては、PR 作成は不要のケースもあるため、今回は workflow_dispatch で任意のタイミングで利用可能としました。

gen_environment_local_invoke.yaml
name: Generate-Environment-Variables-Config

+ on:
+   workflow_dispatch:

env:
  STACK_NAME: hoge

jobs:
  generate:
    runs-on: ubuntu-22.04
    permissions:
      id-token: write
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
          aws-region: ap-northeast-1
          role-session-name: GenerateEnvironmentVariables

      - name: Generate env.json
        id: generate
        run: |
          # CloudFormationスタックの情報を取得
          STACK_INFO=$(aws cloudformation describe-stacks --stack-name hoge)

          # 環境変数マッピングの定義
          # OutputKey の "FunctionEnv" を含む部分で分割し、関数名と環境変数名を抽出
          ENV_JSON=$(echo $STACK_INFO | jq -r '
            .Stacks[0].Outputs
            | map(select(.OutputKey | contains("FunctionEnv")))
            | reduce .[] as $item ({};
              ($item.OutputKey | capture("(?<func>.+)Env(?<env>.+)$")) as $parts
              | .[$parts.func] = (.[$parts.func] // {})
              | .[$parts.func][$parts.env | gsub("(?<=[a-z])(?=[A-Z])";"_") | ascii_upcase] = $item.OutputValue
            )' > events/env.json)
+      - name: Create Pull Request if changes detected
+        uses: peter-evans/create-pull-request@v7
+        with:
+          branch: feature/auto-env-config
+          title: Update Environment Variables configuration
+          commit-message: Update Environment Variables configuration
+          base: develop

env.json の生成

それでは env.json を生成してみましょう。

うまく生成できているようです。

samconfig.yaml の修正

sam local invoke の時は env.json を見るようにします。samconfig.yaml を編集します。

samconfig.yaml
version: 0.1
default:
  global:
    parameters:
      stack_name: 'hoge'
      region: 'ap-northeast-1'
  build:
    parameters:
      cached: true
      parallel: true
  validate:
    parameters:
      lint: true
  deploy:
    parameters:
      capabilities: 'CAPABILITY_IAM'
      confirm_changeset: true
      resolve_s3: true
  package:
    parameters:
      resolve_s3: true
  sync:
    parameters:
      watch: true
  local_start_api:
    parameters:
      warm_containers: 'EAGER'
      profile: 'hoge'
  local_start_lambda:
    parameters:
      warm_containers: 'EAGER'
      profile: 'hoge'
  local_invoke:
    parameters:
      warm_containers: 'EAGER'
      profile: 'hoge'
+      env_vars: ./events/env.json

うまく動きましたね。めでたしめでたし。

takakuni@ hoge % sam local invoke HogeOutputFunction
Invoking app.lambda_handler (python3.12)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.12-rapid-x86_64.
Mounting /Users/takakuni/Documents/hoge/.aws-sam/build/HogeOutputFunction as /var/task:ro,delegated, inside runtime container
{"timestamp": "2025-02-08T13:18:13Z", "level": "DEBUG", "message": {"timestamp": "MASKED_TIMESTAMP", "level": "DEBUG", "message": "Signature:\nMASKED_SIGNATURE", "logger": "botocore.auth", "requestId": "MASKED_REQUEST_ID"}}
{"timestamp": "MASKED_TIMESTAMP", "level": "DEBUG", "message": "Event request-created.s3.ListObjectsV2: calling handler <function add_retry_headers at MASKED_MEMORY_ADDRESS>", "logger": "botocore.hooks", "requestId": "MASKED_REQUEST_ID"}
{"timestamp": "MASKED_TIMESTAMP", "level": "DEBUG", "message": "Sending http request: <AWSPreparedRequest stream_output=False, method=GET, url=https://hoge-output-123456789012.s3.ap-northeast-1.amazonaws.com/?list-type=2&max-keys=1&encoding-type=url, headers={'User-Agent': b'Boto3/1.36.16 md/Botocore#1.36.16 ua/2.0 os/linux#6.6.65-0-virt md/arch#x86_64 lang/python#3.12.7 md/pyimpl#CPython exec-env/AWS_Lambda_python3.12 cfg/retry-mode#legacy Botocore/1.36.16', 'X-Amz-Date': b'MASKED_DATE', 'X-Amz-Security-Token': b'MASKED_SECURITY_TOKEN', 'X-Amz-Content-SHA256': b'MASKED_SHA256', 'Authorization': b'AWS4-HMAC-SHA256 Credential=MASKED_CREDENTIAL/MASKED_DATE/ap-northeast-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=MASKED_SIGNATURE', 'amz-sdk-invocation-id': b'MASKED_INVOCATION_ID', 'amz-sdk-request': b'attempt=1'}>", "logger": "botocore.endpoint", "requestId": "MASKED_REQUEST_ID"}
{"timestamp": "MASKED_TIMESTAMP", "level": "DEBUG", "message": "Certificate path: /var/task/botocore/cacert.pem", "logger": "botocore.httpsession", "requestId": "MASKED_REQUEST_ID"}
{"timestamp": "MASKED_TIMESTAMP", "level": "DEBUG", "message": "Starting new HTTPS connection (1): hoge-output-123456789012.s3.ap-northeast-1.amazonaws.com:443", "logger": "urllib3.connectionpool", "requestId": "MASKED_REQUEST_ID"}
{"timestamp": "MASKED_TIMESTAMP", "level": "DEBUG", "message": "https://hoge-output-123456789012.s3.ap-northeast-1.amazonaws.com:443 \"GET /?list-type=2&max-keys=1&encoding-type=url HTTP/1.1\" 200 None", "logger": "urllib3.connectionpool", "requestId": "MASKED_REQUEST_ID"}
{"timestamp": "MASKED_TIMESTAMP", "level": "DEBUG", "message": "Event before-parse.s3.ListObjectsV2: calling handler <function _handle_200_error at MASKED_MEMORY_ADDRESS>", "logger": "botocore.hooks", "requestId": "MASKED_REQUEST_ID"}
{"timestamp": "MASKED_TIMESTAMP", "level": "DEBUG", "message": "Event before-parse.s3.ListObjectsV2: calling handler <function handle_expires_header at MASKED_MEMORY_ADDRESS>", "logger": "botocore.hooks", "requestId": "MASKED_REQUEST_ID"}
{"timestamp": "MASKED_TIMESTAMP", "level": "DEBUG", "message": "Response headers: {'x-amz-id-2': 'MASKED_ID', 'x-amz-request-id': 'MASKED_REQUEST_ID', 'Date': 'MASKED_DATE', 'x-amz-bucket-region': 'ap-northeast-1', 'Content-Type': 'application/xml', 'Transfer-Encoding': 'chunked', 'Server': 'AmazonS3'}", "logger": "botocore.parsers", "requestId": "MASKED_REQUEST_ID"}

まとめ

以上、「SAM Local Invoke は Lambda の環境変数に設定された Ref を認識できないため env.json を自動生成して samcofig.yaml で参照してみた」でした。

sam local の開発体験と、CloudFormation の良さを保つにはどうしたらいいだろうと思い、少しパワー目ですがやってみました。

他にもやり方あると思ってて、 AWS::Serverless::Function で指定した Lambda 関数分、実態から環境変数を取得して作成も良さそうな気がしました。

このブログがどなたかの参考になれば幸いです。

クラウド事業本部コンサルティング部のたかくに(@takakuni_)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.