![SAM Local Invoke は Lambda の環境変数に設定された Ref を認識できないため env.json を自動生成して samcofig.yaml で参照してみた](https://images.ctfassets.net/ct0aopd36mqt/wp-thumbnail-07eaebd66be96825f09534c76d05964b/7c8c45cd92c42f6391edbb4b7133429b/aws-sam.png)
SAM Local Invoke は Lambda の環境変数に設定された Ref を認識できないため env.json を自動生成して samcofig.yaml で参照してみた
こんにちは!クラウド事業本部コンサルティング部のたかくに(@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 関数を指定して、動的にマッピングしています。
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
は、環境変数から取得する仕組みを採用していました。
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 関数が機能しません。
そのため、次のような方法で Ref 部分を上書きする必要があります。
.env
を利用するtemplate.yaml
で Ref を使わずベタ書きする--env-vars
を利用するsam local invoke HogeOutputFunction --env-vars env.json
--parameter-overrides
を利用するsam local invoke HogeOutputFunction --parameter-overrides HogeOutputBucket=hoge-output-123456789012
参考
今回、私は 3 番を利用して、env.json
を自動作成するようなワークフローを書いてみました。
やってみた
最終的に次のようなワークフローを作成しました。ステップバイステップで解説します。
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
に登録します。
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 で任意のタイミングで利用可能としました。
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
を編集します。
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_)でした!