Skip to content

Instantly share code, notes, and snippets.

@yano3nora
Last active March 13, 2025 07:20
Show Gist options
  • Save yano3nora/8588751d73cb3df6c98c638d57d80cd3 to your computer and use it in GitHub Desktop.
Save yano3nora/8588751d73cb3df6c98c638d57d80cd3 to your computer and use it in GitHub Desktop.
[aws: SAM & Lambda] AWS Serverless Application Model (AWS SAM) for Lambda Developing. #aws

Overview

AWS Serverless Application Model (AWS SAM) とは - docs.aws.amazon.com
aws/aws-sam-cli - github.com

  • aws 公式の lambda をいい感じに構成・開発・デプロイできる CLI ツール
  • ローカル開発や、一部認証系にも対応していてドキュメントも割と豊富
  • serverless framework や cdk などが対抗馬、小規模ならこいつで完結できる
  • 一度作った stack に function を追加する動線はまだないっぽい
  • 結構 cfn 分かってないとカスタムできない感じ
  • ローカル開発の hot reload みたいなものは正式にサポートしてなさそう
  • api gateway を噛ます構成の場合 AWS_PROXY (プロキシ統合) しかサポートしてないことに注意

With GitBash

https://bm-ss.net/22120201

git bash で使うときはひと工夫いるっぽい。

API Reference

AWS SAM CLI コマンドリファレンス

Getting Started

AWS SAM CLI のインストール
[Announcement] Installing/Updating AWS SAM CLI through Homebrew #4607

aws-cli が install & setup されている前提。

# for macOS.
$ brew install aws/tap/aws-sam-cli
#
# ↓ はリリースがちょっと遅れるみたいなので ↑ のがいいみたい
#
# $ brew tap aws/tap
# $ brew install aws-sam-cli

# Check version.
$ sam --version
> SAM CLI, version 1.81.0

# Init project.
# 対話で以下プロジェクト設定を選択していく感じ
#
# - template か custom か、などプロジェクトの構成
# - runtime (python, node ...)
# - デプロイを zip または docker image でやる
# - X-Ray header を付与するかどうか
# - CloudWatch のモニタリングを作るかどうか
#
$ sam init

#
# 以降のコマンドは init で生成された directory 配下で行う
# cd するのが面倒なら -t app-name/template.yml のように
# template を指定することもできるっぽい
#

# Build files.
#
# 以降の sam local * や deploy を叩く前に必要
# zip 形式なら関連ファイルをアーカイブしてまとめたり
# image 形式なら docker build をしたりする感じ
#
# ここで動くのはコード類 (zip) とコンテナイメージ (image) だけ
# template.yaml はなにも変更されず .aws-sam に突っ込まれる
# この時点で渡した --parameter-overrides とかはあくまでも
# build 時に参照する CodeUri とかを build 時点で変更したいというとき用
#
$ sam build -t app-name/template.yaml

↑ で build すると .aws-sam ディレクトリができて 以降の local invoke などはプロジェクトルートから叩くことになる。

が、local 以外は作った app ディレクトリ配下で叩く前提っぽくてわかりづらい ... 。一旦全部プロジェクトルートからの叩き方を以下にまとめた。

# Execute for local test.
$ sam local invoke "HelloWorldFunction" -e events/event.json

# Deploy to AWS with guide.
#
# aws account 分けてるなら --profile で
# デプロイ先が build 後の yml に入るようにする
# AWS_PROFILE 環境変数指定して省略してもいい
#
$ sam deploy --guided --profile default

# Monitoring
$ sam logs --stack-name app-name --profile default

# List endpoints
$ sam list endpoints -t app-name/template.yaml --profile default

# Delete CloudFormation Stacks.
$ sam delete --stack-name app-name --profile default

逆に .aws-sam ディレクトリのない場所 (init で作った app-name ディレクトリなど) に入ると local invoke や local start-api は叩けない ので注意。

# .aws-sam がない場所だと error になる
$ cd app-name
$ sam local start-api
>
> Initializing the lambda functions containers.
> Lambda functions containers initialization failed because of Resource ID was not provided
> Error: Lambda functions containers initialization failed 

Sam validate with CFn Template

AWS Lambda External ExtensionsをAWS SAMで管理する

基本的に cfn template をいじったら validate コマンドでチェックしてから build する。

$ sam validate -t app-name/template.yml

Deploy on CI/CD

AWS SAM CLI のインストール > Linux
Introducing AWS SAM Pipelines: Automatically generate deployment pipelines for serverless applications
【小ネタ】AWS SAMを継続的デリバリする際に便利なオプションのご紹介

github actions や code pipeline などに組み込んで sam deploy 叩くやつ。

  1. sam deploy を叩く ci 用の admin 権限 iam role 作る
  2. ↑ access key 作って AWS_ACCESS_KEY_ID AWS_ACCESS_SECRET_KEY AWS_DEFAULT_REGION など parameter store にぶちこむ
  3. buildspec.yml など ci 設定ファイルで上記を環境変数などで読み込む
  4. sam cli を install => sam build => sam deploy の順で叩く
    • sam build には local と同じく -t で template を渡す
    • sam deploy は --guided を外し自動化するため 以下 config toml を食べさせる

deploy 用の config toml 作成

sam deploy
sam deploy --guided を使用した設定の書き込み

local で 1 度でも deploy したら config.toml--guided で入れた情報が集約されてるので、そいつをコピって aws_profile とか消して deploy 用ということで git に入れちゃえばいい。

confirm_changeset = false が入っていないと ci でコケるので注意。

# samconfig.prod.toml

version = 0.1
[default.deploy.parameters]
stack_name = "agent-lambda"
resolve_s3 = true
s3_prefix = "agent-lambda"
region = "ap-northeast-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
image_repositories = ["AgentLambdaFunction=001462090553.dkr.ecr.ap-northeast-1.amazonaws.com/agentlambda84ffb623/agentlambdafunction2310f611repo"]
parameter_overrides = "Timeout=\"840\" DockerBuildTarget=\"prod\""
disable_rollback = false

ci では以下のように叩けばよろしい。--no-fail-on-empty-changeset を忘れずに。

# buildspec.yml の例
version: 0.2
env:
  parameter-store:
    AWS_ACCESS_KEY_ID: /path/to/parameter/store/AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY: /path/to/parameter/store/AWS_SECRET_ACCESS_KEY
    AWS_DEFAULT_REGION: /path/to/parameter/store/AWS_DEFAULT_REGION
phases:
  install:
    commands:
      - echo "cd into $CODEBUILD_SRC_DIR"
      - cd $CODEBUILD_SRC_DIR
      # install sam
      - pip install aws-sam-cli
      - sam --version
  build:
    commands:
      - sam build -t sam-app/template.yaml
      - sam deploy --no-fail-on-empty-changeset --config-file samconfig.prod.toml

Sam Pipeline

sam pipeline bootstrap

pipeline で ci 作れるっぽい。が、これ単体で ci 組むケースがまだなくてやってない。

Local invoke

sam local invoke
-e - の example この release note にしかのってない ...

  • api gateway 統合とかしてない単体 lambda ならこいつでテストするのが一番ラク
  • event (payload) は -e event.json みたいに json 渡す
  • または ↓ のように stdin から読み取りも可能
$ echo '{"body": "{\"url\": \"https://localhost:1234\"}"}' | sam local invoke -e -

Local Development with Docker

AWS SAM CLI で Docker を使用するためのインストール方法

  • --use-container でコンテナデプロイする
  • ローカルで Lambda をテストする

上記の場合は docker を入れておく必要がある。

# ローカルで Lambda 環境エミュレートしたコンテナを立てて
# プロジェクトを api 経由でテストすることができる
$ sam local start-api

# docker host に向かって request 投げる、みたいな
$ curl http://127.0.0.1:3000/hello

Integration Test with AWS-SDK / AWS-CLI

自動化されたテストとの統合

local や ci のテストなどで aws-cli や sdk から kick したい場合は sam local start-lambda で local の http://127.0.0.1:3001 に lambda endpoint を作成できるぽい。

$ sam local start-lambda
import boto3
import botocore

lambda_client = boto3.client('lambda',
    region_name="us-west-2",
    endpoint_url="http://127.0.0.1:3001",
    use_ssl=False,
    verify=False,
    config=botocore.client.Config(
        signature_version=botocore.UNSIGNED,
        read_timeout=1,
        retries={'max_attempts': 0},
    )
)

response = lambda_client.invoke(FunctionName="HelloWorldFunction")
# 内部は rie + ric 入った docker 構成っぽいので curl で endpoint 叩いてもいい
$ curl http://localhost:3001/2015-03-31/functions/HelloWorldFunction/invocations -d '{}'

samconfig.toml

AWS SAM CLI の設定ファイル

  • sam init sam build sam deploy などのコマンドで samconfig.toml ファイルが生成される
  • 中に deploy 先の region や command で指定した option など sam 関連の設定が入る感じ
  • 現状、いまいち使いづらくて改善 discuss やってるみたい
  • 実行端末のファイルパスや aws の profile など載るので .gitignore したほうがよさそうかな
$ echo '.aws-sam/' >> .gitignore
$ echo 'samconfig.toml' >> .gitignore

Config Environment

設定ファイルのルール
AWS SAMのsamconfigを使って環境ごとにビルド・デプロイしてみる

  • [environment.command_subcommand.parameters] の規則で command の parameter (--parameter--overrides とか) を設定して command 実行時に省略することができる
  • environment の部分は --config-env で指定できる、未指定だと default 扱い
  • なので、しっかり作り込めば sam command --config-env ${DEV|PROD} みたいに操作できる

とはいえ .gitignore したいファイルなので、以下 2 択。自分は toml 管理は投げた。

  • samconfig.example.toml みたいなの作って初期セットアップで貼り付ける運用
  • こいつの制御完全にあきらめて、パラメータを環境変数に仕込んで実行時 load する運用

Environment Variables

AWS Lambda 環境変数の使用
SAM deploy doesn't set environment variables #1163

なんか面倒くさそう。

template.yaml で環境変数使いたい

多分だけど、今んとこ --parameter-overrides 使う以外なさそう。

  • Parameters: セクション用意して流し込む parameter の type, default 定義しておく
  • ↑ を deploy や local invoke, start-api など使うときに --parameter-overrides で上書く
  • sam build で --parameter-overrides 使えるけど template.yaml には影響しないので意味ない
    • --parameter-overridestemplate.yaml 解釈時に override するもの
    • sam build は zip 化や image build しかしてない (= template.yaml の update とかはしない)
    • よって code zip, image build に関する CodeUri みたいなセクションしか作用しない

以下はメモリサイズを環境変数から流し込む例。

# .envrc (.envrc.example とかで適当にチーム共有)

export SAM_LOCAL_OPTION="--parameter-overrides MemorySize=64"
export SAM_PROD_OPTION="--parameter-overrides MemorySize=512"
# template.yaml に --parameter-overrides 用の parameter を用意
Parameters:
  MemorySize:
    Type: Number # これ間違えるとちゃんと解釈されない
    Default: 128

Globals:
  Function:
    MemorySize: !Ref MemorySize
# build 時は影響しない項目なので特に指定しない
$ sam build -t sam-app/template.yaml

# local 開発用の環境変数を流して、local では低いメモリで実行するとか
$ sam local start-lambda ${SAM_LOCAL_OPTION}

自分は npm project なら package.json > scripts に書いておくと楽。

{
  "scripts": {
    "sam:build": "sam build -t sam-app/template.yaml",
    "sam:local:invoke": "sam local invoke MyFunctionName ${SAM_LOCAL_OPTION} -e -",
    "sam:local:start": "sam local start-lambda ${SAM_LOCAL_OPTION}",
    "sam:deploy": "sam deploy --guided ${SAM_PROD_OPTION}",
  }
}

Step Functions with Local Test

AWS SAM が新たに AWS Step Functions をサポート
Simplifying application orchestration with AWS Step Functions and AWS SAM
Step Functions と AWS SAM CLI ローカルのテスト

関連リソースの cfn template こねこねと sfn.asl.json こねこねを頑張って local build & test => deploy していく感じ。ハードルは高いけど他ツールに比べるとまだ手軽な印象。

試してないけど ecs-cli と組み合わせて esc task の kick とかも local 再現できたら最高だな。


Lambda Basic Knowledges

Using Pure ESM Package

https://aws.amazon.com/jp/about-aws/whats-new/2022/01/aws-lambda-es-modules-top-level-await-node-js-14/

  • いまんとこ node@14 runtime を選択しないと esm 使えないと思っていい
  • ので commonjs 方式 (= require() 可能) で import できないライブラリは実行時エラー

Invoke by AWS SDK for Node.js

aws/aws-sdk-js-v3 - github.com
Getting started in Node.js - docs.aws.amazon.com
Lambda examples using SDK for JavaScript (v3) - docs.aws.amazon.com
AWS SDK for JavaScriptはV2とV3の2種類あるぞ!
[@aws-sdk/client-lambda] - Payload data in uint8array format - how to resolve data to expected output? #2252

# 使いたい resource ごとに /client-s3 とか
# 分かれてる感じっぽくて lambda なら client-lambda を入れる
#
$ npm i @aws-sdk/client-lambda

実行権限 policy は ↓ みたいにして sdk 用 iam user 作って割り当てて access key / secret access key 作って ... みたいな感じ。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowInvoke",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": "ここにARN入れる"
        }
    ]
}
// 開発時には aws-lambda-rie 構成の lambda container を kick する想定
// もちろん sam local start-lambda でも endpoint をかえれば同様にテストできる

import { LambdaClient, InvokeCommand, InvocationType } from '@aws-sdk/client-lambda'

const isDev = process.env.NODE_ENV === 'development'

const lambdaClient = new LambdaClient({
  region: isDev
    ? 'us-east-1' // local では dummy でいい
    : process.env.AGENT_LAMBDA_AWS_REGION,
  endpoint: isDev
    ? 'http://lambda-container-service-name:8080'
    : process.env.AGENT_LAMBDA_ENDPOINT,
  credentials: isDev
    ? {
        // local なら dummy でいい
        accessKeyId: 'local-dummy-key',
        secretAccessKey: 'local-dummy-key',
      }
    : {
        accessKeyId: `${process.env.AGENT_LAMBDA_AWS_ACCESS_KEY}`,
        secretAccessKey: `${process.env.AGENT_LAMBDA_AWS_SECRET_KEY}`,
      },
})

// payload として渡す event object
// (lambda handler の例ではだいたい event って書く)
//
const event = {
  // aws cli で叩くときと同様に blob 送信なため
  // この object がそのまま event に格納される
  // api gateway 経由のときと違って lambda の
  // handler 側であらためて JSON.parse など不要
}

try {
  const {
    Payload,
    LogResult,
    StatusCode,
    FunctionError,
  } = await lambdaClient.send(new InvokeCommand({
    // 同期呼び出し、非同期なら Event で、DryRun とかもあるぽい
    InvocationType: InvocationType.RequestResponse,
    Payload: Buffer.from(JSON.stringify(event)),
    FunctionName: isDev
      ? 'function' // local なら名前だけで ARN 全部指定はいらない rie 構成ならこうのはず
      : process.env.AGENT_LAMBDA_ARN,
  }))


  /**
   * payload はざっくり 2 パターン
   *
   * - lambda ランタイム異常ケース => string (Timeout とか)
   * - lambda ランタイム正常ケース => unit8array (lambda 内の return)
   */
  const response: AgentLambdaResponse = (() => {
    try {
      return (
        Payload &&
        Buffer.from(Payload).length &&
        JSON.parse(Buffer.from(Payload).toString())
      )
    } catch (error) {
      return Buffer.from(Payload || '').toString()
    }
  })()

  const results = {
    response,
    logs: LogResult,
    status: StatusCode,
    error: FunctionError,
    meta: $metadata,
  }

  // lambda api kick が 200 でも中の lambda logic から
  // statusCode とか返却してたら当然そっちも見てあげる必要ある
  // もちろん return してなければ見る必要ないが、普通は ↓ とか参考に組むと思う
  // https://docs.aws.amazon.com/apigateway/latest/developerguide/handle-errors-in-lambda-integration.html
  //
  if (
    typeof response === 'string' ||
    Number(response?.statusCode) >= 400
  ) {
    throw results
  }

  return results
} catch (e: any) {
  /**
   * send (api kick) そのものに失敗した場合はここに入る
   *
   * send の失敗時に throw される error がなぜか
   * 意味不明な JSON Syntax Error になっている ...
   * 一時的な回避策として設けられた $response からデバッグ情報をたどれる
   * 秘匿情報とかも入るからあえて隠してるとか?あんまりちゃんと追ってない
   *
   * https://github.com/aws/aws-sdk-js-v3/issues/4576
   */
  console.error(e?.$response)

  throw e
}

Invoke by AWS-CLI

AWS Lambdaをコマンドラインから実行してログを標準出力する
Python の AWS Lambda 関数ログ作成

いつも思うけどこんなの人類に使いこなせないよ ... この場合 payload がそのまま event に JSON.parse された状態で格納されることに注意。

$ aws lambda invoke \
  --profile ${AWS_PROFILE} \
  --function-name ${LAMBDA_NAME} \
  --invocation-type RequestResponse \
  --payload $(echo '{"some":"event"}' | base64) \
  /dev/stdout --log-type Tail

Asynchronous Invocation

非同期呼び出し
How can I invoke a Lambda function asynchronously from my Amazon API Gateway API?
AWS Lambda関数を非同期で呼ぶ場合の動きを改めて確める

  • lambda には「同期呼び出し」「非同期呼び出し」の 2 種類ある
    • レスポンスの返却まで待つやつと、いってらっしゃいするやつ
    • 単体 lambda を sdk で叩く場合はどっちも対応できる
  • 「非同期呼び出し」は api gateway + lambda の プロキシ統合 (AWS_PROXY) では利用できない
    • api gateway + lambda の 非プロキシ統合 (AWS) 構成だけ「非同期呼び出し」に対応してる
    • が、リクエストペイロードのマッピング?とか色々構成面倒らしい
    • で、sam は プロキシ統合 (AWS_PROXY) の api gateway + lambda 構成しか現状サポートしてない
    • aws/aws-sam-cli#1003
  • また後述の lambda function urls もいまんとこ「同期呼び出し」しか対応してないぽい

... よって非同期呼び出しをやりたければ以下 3 択しかなさそう。

  • sqs とか sns とか step functions とか aws ピタゴラスイッチで頑張る
  • api gateway の非プロキシ統合とかいう古の多機能な設定地獄を頑張る
  • sdk 経由で lambda invoke するときに InvocationTypeEvent にする
    • => sdk へ認証を渡す問題 + sdk の local 実行どうしよう問題を頑張る

このクラウド屋さん本当にもうちょいアプリ開発者のこと考えてくれ。 ... ってことで、現状 sam を使って非同期呼び出しするなら step functions や sqs, sns 連携の構成 または 単体 lambda 構成して sdk kick の 2 択と言えそう。構成の面倒くささ考えたら「小規模なら sdk kick 」で「中規模以上なら step functions でステート管理」かな。。。

ちなみに非同期呼び出しの場合はざっくり ↓ のような仕様。

Simple Lambda x Asynchronous Invocation Example

単体 lambda を deploy して sdk から async kick するならこんなか。

こいつを api gateway + lambda (or function urls) から kick するとか。普通にサーバサイドの何かから kick するとかすれば、web api からの非同期呼び出しができそう。...とはいえある程度複雑なら諦めて step functions 組むのがよさそう。

あと local では InvocationType.Event は sam も rie も未対応なので、関数の動作チェック単体なら rie で、非同期実行含めた確認なら本番でって感じ。

Resources:
  MyLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      Runtime: nodejs14.x
      CodeUri: .

  # sdk や cli からの invoke 設定
  MyLambdaFunctionEventInvokeConfig:
    Type: AWS::Lambda::EventInvokeConfig
    Properties:
      FunctionName: !Ref MyLambdaFunction
      Qualifier: $LATEST
      MaximumRetryAttempts: 1 # retry 数指定とかはここで
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda'

const client = new LambdaClient(/* ... */)

(async () => {
  try {
    const response = await client.send(new InvokeCommand({
      FunctionName: 'MyLambdaFunction',
      InvocationType: 'Event', // TODO
    }))

    console.log(response)
  } catch (e) {
    console.error(e)
  }
})()

Lambda Destination

Introducing AWS Lambda Destinations

  • lambda の非同期呼び出しの際に「成功/失敗時に呼び出す別の lambda 」を指定できるやつ
  • 非同期呼び出し前提なので、sam の api gateway 統合 (プロキシ統合) では使えないが、一応 template.yaml (cfn) で構成は可能っぽい
  • step functions までいかないけど、単体では構成しづらいバッチ処理とかならありかも

Error handling

API Gateway で Lambda エラーを処理する
Amazon API Gateway + AWS Lambda でのレスポンス形式
[アップデート]LambdaがHTTPSエンドポイントから実行可能になる、AWS Lambda Function URLsの機能が追加されました!

  • ざっくり statusCodebody 返しておくと return を wrap してくれる
  • lambda 起動時・ランタイムエラーと ↑ のような「期待される終了」だと当然ふるまいは違う
    • ランタイムエラーなら、lambda が「異常終了」したとみなされ stack trace 含めて log に残る
    • ユーザコードで return は statusCode によらず「正常終了」扱いで開始/終了 log だけ
  • api からの返却も結構バリエーション豊富でハンドリングしっかりしないとだるい
    • 正常終了 x web api kick なら payload (return 値) が json で返ってくる
    • 正常終了 x sdk kick なら payload (return 値) が blob で返却される
    • 異常終了 x web api kick なら普通にエラーを status code + string message で返してくる
    • 異常終了 x sdk kick だと timeout みたいなケースでは payload が string で、その他異常だと空 blob になって返ってきてややこしい

Examples

Python official image x Hot Reload

Lambda コンテナイメージをローカルでテストする
awslambdaric - pypi.org

official に標準で入ってる rie と pip で入れる ric を watchmedo で再起動させる構成。

# https://hub.docker.com/r/amazon/aws-lambda-python
FROM public.ecr.aws/lambda/python:3.10 as base
COPY app.py requirements.txt ./

FROM base as docker-compose
RUN python -m pip install awslambdaric
RUN python -m pip install "watchdog[watchmedo]"
ENTRYPOINT ["watchmedo", "auto-restart", "-d", ".", "-p", "*.py", "--", "aws-lambda-rie", "python", "-m", "awslambdaric"]
CMD ["app.lambda_handler"]

FROM base as main
RUN python -m pip install -r requirements.txt -t .
CMD ["app.lambda_handler"]
version: "2.4"
services:
  sam-lambda:
    ports:
      - "8080:8080"
    build:
      context: ./sam-lambda/src
      dockerfile: Dockerfile
      target: docker-compose
    volumes:
      - ./sam-lambda/src:/var/task # /var/task を mount して hot reload 起動

docker compose での起動後はこんな感じで叩ける & hot reload 効くはず。

$ curl http://localhost:8080/2015-03-31/functions/function/invocations -d '{}'
> {"statusCode": 200, "body": "{\"message\": \"hello world\"}"}

Lambda x Puppeteer

Dockder - pptr.dev
Lambda コンテナイメージの作成
AWS Lambda をコンテナイメージで動かした話
AWS Lambda カスタムイメージと Runtime Interface Clients
Running headful Chrome with extensions in a Lambda function
Lambda コンテナイメージで [email protected] を使ってみた

lambda x puppeteer を puppeteer 公式 image x puppeteer-core library 使いつつ sam で構成した例。

上記方針で色々試行錯誤したけど、結局以下のような構成で実現した。

  • puppeteer は「 chromium binary を puppeteer 公式 image に pre-install されているものを利用 」して、handler からは puppeteer-core で制御
  • Dockerfile は用途別にマルチステージビルドで構成
    • base image として ric を build した状態のコンテナを作る
    • puppeteer 公式 image に base image から build 済み ric を copy
    • ↑ を app base として 3 stage に分ける
      • docker-compose でのローカル開発 (hot reload 対応) build
      • sam local command でのローカルテスト (hot reload 未対応) build
      • prod 用の rie 省略版 build
  • lambda の初期開発時はローカル開発 build で docker-compose で開発
    • nodemon で rie, ric 経由で handler を叩く entrypoint.js みたいなやつをファイル変更を起点に再起動かける感じ
    • 但し、この方法では「単発 x 短時間」の実行しかできない (二重実行・メモリ割当ができない)
      • 実装が誤っているのかもしれないが二重起動 or 長時間実行すると SIGSEGV でおちる
      • AWS_LAMBDA_FUNCTION_MEMORY_SIZE に rie が対応してないので、増やしたりとか試せてない
    • よって lambda 機能が安定してきたら ↓ sam local でテストを行うことにした
  • 上記ローカル build を sam build => sam local command でテスト
    • aws の案内通り rie, ric 経由で handler を叩くだけの構成
    • この構成だと hot reload 効かない (毎回 build しないといけない) のであくまでも動作確認的に利用
    • 但し、この構成ならメモリ割り当て、二重起動に対応しておりある程度の処理をローカルでもこなせる
  • ok なら prod build で deploy
    • rie の構成を skip した ric 経由での handler kick 構成
  • 最初は api-gateway 連携で web api kick で起動を考えてたけど、どうも現状 web api kick で非同期起動は sam じゃ構成できないっぽいので sdk kick 前提で api 消した
#
# aws-lambda-ric を build して後続にわたすための image
#
FROM node:16-buster as aws-lambda-ric-build-image

# この辺は公式の案内通り
RUN apt-get update && \
  apt-get install -y \
  g++ \
  make \
  cmake \
  unzip \
  libcurl4-openssl-dev

RUN mkdir -p /ric
WORKDIR /ric
RUN npm install aws-lambda-ric

#
# puppeteer の公式 image
# chromium とかが元から入ってて PUPPETEER_CACHE_DIR で
# chromium バイナリの位置を指定できる
#
FROM ghcr.io/puppeteer/puppeteer:19.11.1 as puppeteer-base

# COPY --from で build 済 ric を前段 image から copy してる
USER root
RUN mkdir -p /ric
COPY --from=aws-lambda-ric-build-image /ric /ric

# https://uncaughtexception.hatenablog.com/entry/2022/12/09/191520
ARG FUNCTION_DIR="/home/pptruser"
ENV PUPPETEER_CACHE_DIR ${FUNCTION_DIR}/.cache/puppeteer
WORKDIR ${FUNCTION_DIR}

# handler (アプリ本体) もこの辺にいれちゃう
COPY app.js package*.json ./
RUN npm ci

#
# aws-lambda-rie を追加する local 動作用の base image
#
FROM puppeteer-base as local-base
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /ric/aws-lambda-rie
RUN chmod +x /ric/aws-lambda-rie

#
# docker compose の volume mount と
# nodemon + rie, ric 経由で handler kick する
# js script で hot reload 開発可能な local image
# --signal SIGTERM でプロセスを kill => restart させてる
#
FROM local-base as docker-compose
COPY entrypoint.js /ric/entryoint.js
RUN npm i -g nodemon
ENTRYPOINT [ "nodemon", "--signal", "SIGTERM", "/ric/entryoint.js" ]
CMD [ "app.handler" ]

#
# sam local * command 用の rie => ric => handler 構成
# 公式の案内通りの構成
#
FROM local-base as sam-local
ENTRYPOINT [ "/ric/aws-lambda-rie" ]
CMD [ "/ric/node_modules/.bin/aws-lambda-ric", "app.handler" ]

#
# production image として
# aws-lambda-rie の構成を skip した image
#
FROM puppeteer-base as prod
CMD [ "/ric/node_modules/.bin/aws-lambda-ric", "app.handler" ]
// entrypoint.js
// dockerfile の構成上別に if る必要はないんだけど ...
const { execSync } = require('child_process')
const arg = process.argv[2]

if (!process.env.AWS_LAMBDA_RUNTIME_API) {
  execSync(
    '/ric/aws-lambda-rie /ric/node_modules/.bin/aws-lambda-ric ' + arg,
    { stdio: 'inherit' },
  )
} else {
  execSync(
    '/ric/node_modules/.bin/aws-lambda-ric ' + arg,
    { stdio: 'inherit' },
  )
}
{
  "private": true,
  "version": "0.0.0",
  "dependencies": {
    "puppeteer-core": "19.11.1"
  },
}
// app.js
'use strict'
const puppeteer = require('puppeteer-core')

// docker compose exec で入って調べた binary path (もっと正しい指定方法ありそう)
const executablePath = `
  ${process.env.PUPPETEER_CACHE_DIR}/chrome/linux-1108766/chrome-linux/chrome
`.trim()

module.exports.handler = async (event) => {
  try {
    const body = JSON.parse(event.body)
    const url = body.url

    const browser = await puppeteer.launch({
      executablePath,
      headless: true,
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-gpu',
        '--no-first-run',
        '--no-zygote',
        '--single-process',
      ],
    })

    const page = await browser.newPage()
    await page.goto(url, { waitUntil: 'load', timeout: 0 })

    const content = await page.evaluate(() => document.body.innerHTML)
    await browser.close()

    return {
      statusCode: 200,
      body: JSON.stringify(content),
    }
  } catch (e) {
    return {
      statusCode: 500,
      body: e.message,
    }
  }
}
# .envrc とか .env とかで docker, sam 共用の値を定義

export AWS_LAMBDA_FUNCTION_TIMEOUT=600
export SAM_LOCAL_OPTION="--parameter-overrides DockerBuildTarget=sam-local Timeout=${AWS_LAMBDA_FUNCTION_TIMEOUT}"
export SAM_PROD_OPTION="--parameter-overrides DockerBuildTarget=prod Timeout=${AWS_LAMBDA_FUNCTION_TIMEOUT}"
# template.yaml より抜粋
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sammapp

# 環境ごとの override 用 params を設定して、動的な項目を環境変数で制御
Parameters:
  Timeout:
    Type: Number
    Default: 3
  DockerBuildTarget:
    Type: String
    Default: prod # ここ、存在する stage じゃないと build が正常に走らなかった?

Globals:
  Function:
    Timeout: !Ref Timeout
    MemorySize: 1024

Resources:
  AgentLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Image
      Architectures:
        - x86_64
    Metadata:
      DockerContext: ./src
      Dockerfile: Dockerfile
      DockerBuildTarget: !Ref DockerBuildTarget
# local 開発用の docker-compose.yml
version: "2.4"
services:
  samapp:
    ports:
      - "9000:8080"
    build:
      context: ./path/to/samapp
      dockerfile: Dockerfile
      # build stage を指定
      target: docker-compose
    environment:
      # docker rie の起動時間も環境変数から制御
      - AWS_LAMBDA_FUNCTION_TIMEOUT=AWS_LAMBDA_FUNCTION_TIMEOUT
    volumes:
      #
      # nodemon を発火させるため volume mount
      #
      - ./path/to/samapp:/home/pptruser
      #
      # ↑ だけだと puppeteer image 内の chromium binary と
      # node_modules が host os 側の mount で消えちゃうから
      # volume mount 対象から除外して guest os 側に残るようにする
      #
      - /home/pptruser/.cache
      - /home/pptruser/node_modules

想定する動作ケースは以下。

# build samapp for local dev
$ docker compose build

# launch samapp service on local
$ docker compose up

# test samapp service on local
#
# 2015-03-31 とかは rie ちゃんの方言っぽい
# sam local command と違って param (event) の渡し方が違う
# https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-test.html
#
# 以降は nodemon がファイル変更を検知していい感じに rie を restart してくれるはず
#
$ curl http://localhost:9000/2015-03-31/functions/function/invocations -d '{"body": "{\"url\": \"https://example.com\"}"}'
> "\n<div>\n    <h1>Example Domain</h1>\n ..."
# build samapp for local test
$ sam build -t path/to/samapp/template.yaml

# test lambda api on local
$ sam local start-lambda ${SAM_LOCAL_OPTION}
$ curl http://localhost:9000/2015-03-31/functions/AgentLambdaFunction/invocations -d '{"body": "{\"url\": \"https://example.com\"}"}'
> "\n<div>\n    <h1>Example Domain</h1>\n ..."
# build for deploy
$ sam build -t path/to/samapp/template.yaml

# deploy
$ sam deploy ${SAM_PROD_OPTION} --guided --profile your-profile

# aws lambda invoke や sdk や aws console から起動チェック

Lambda Function Urls

Lambda 関数 URL
リクエストとレスポンスのペイロード
AWS SAM で AWS Lambda の 関数 URL を利用してみました
SAMを利用したLambda(Function URLs)デプロイ

  • template.yaml の編集 → build → deploy だけで ok ぽい
  • api gateway 機能が使えない代わりに timeout 制限 (29 sec) などもなくなる
  • payload の渡し方が少し異なる (content-type header が要るとか) ので注意
  • sam local start-api は現状 api gateway 統合しかサポートしてない
    • ので lambda function urls の endpoint は deploy テストするしかない
    • (もちろん単体 lambda として sam local invokesam local start-lambda の sdk kick はできるが)

... 正直、web api の基本は「即 response していってらっしゃい → 終わったら別動線で教えてね」なので、api gateway の timeout 回避だけで使うなら、「 30 秒以上かかるときはあるけど 10 分以上かかることはない何らかファイルのダウンロード api 」くらいでしか使わない気がした ... 。

AWS Lambdaの関数URL(Function URLs)のユースケースを真面目に考える

とはいえ、料金面で安いので、機械学習の推論 api みたいな、そこそこリクエストありつつ 29 sec を超える (が、数分かかるようなものではない) ところでは使えるのかな。

# template.yaml
Resources:
  MySamFunction:
    Type: AWS::Serverless::Function
    Properties:
      Architectures:
        - x86_64
      # Events:
      # この Events が api gateway らしいので
      # 不要なら消しちゃう
      #
      # ↓ に function urls 用設定を追加する感じ
      FunctionUrlConfig:
        AuthType: NONE

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  # MySamApi:
  # こいつも api gateway 用なので消しちゃう
  #
  # ...
  #
  # ↓ function urls 用の設定を追加
  # 項目名の末尾に `Url` が必要なことに注意
  MySamFunctionUrl:
    Description: "Function URLs endpoint"
    Value: !GetAtt MySamFunctionUrl.FunctionUrl
# build, deploy 後はこんな感じで確認
# -H content-type で json 投げる旨伝えないと行けないので注意
#
$ curl https://xxx.lambda-url.ap-northeast-1.on.aws/ \
  -H 'content-type: application/json' \
  -d '{"hello":"world"}'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment