AWS Serverless Application Model (AWS SAM) とは - docs.aws.amazon.com
aws/aws-sam-cli - github.com
- aws 公式の lambda をいい感じに構成・開発・デプロイできる CLI ツール
- ローカル開発や、一部認証系にも対応していてドキュメントも割と豊富
- serverless framework や cdk などが対抗馬、小規模ならこいつで完結できる
- 一度作った stack に function を追加する動線はまだないっぽい
- aws/aws-sam-cli#4498
- いまんとこ手動で追加しないといけないっぽいかな?
- 結構 cfn 分かってないとカスタムできない感じ
- ローカル開発の hot reload みたいなものは正式にサポートしてなさそう
- lambda のアーキテクチャ的にムズいのかな ...
- aws/aws-lambda-nodejs-runtime-interface-client#9
- aws/aws-sam-cli#921
- api gateway を噛ます構成の場合
AWS_PROXY
(プロキシ統合) しかサポートしてないことに注意- 非同期呼び出しとかそういうの、ウチはやってません
- aws/aws-sam-cli#4973
- aws/aws-sam-cli#1003
git bash で使うときはひと工夫いるっぽい。
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
基本的に cfn template をいじったら validate
コマンドでチェックしてから build
する。
$ sam validate -t app-name/template.yml
AWS SAM CLI のインストール > Linux
Introducing AWS SAM Pipelines: Automatically generate deployment pipelines for serverless applications
【小ネタ】AWS SAMを継続的デリバリする際に便利なオプションのご紹介
github actions や code pipeline などに組み込んで sam deploy
叩くやつ。
- sam deploy を叩く ci 用の admin 権限 iam role 作る
- ↑ access key 作って
AWS_ACCESS_KEY_ID
AWS_ACCESS_SECRET_KEY
AWS_DEFAULT_REGION
など parameter store にぶちこむ buildspec.yml
など ci 設定ファイルで上記を環境変数などで読み込む- sam cli を install => sam build => sam deploy の順で叩く
- sam build には local と同じく -t で template を渡す
- sam deploy は
--guided
を外し自動化するため 以下 config toml を食べさせる
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
pipeline で ci 作れるっぽい。が、これ単体で ci 組むケースがまだなくてやってない。
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 -
--use-container
でコンテナデプロイする- ローカルで Lambda をテストする
上記の場合は docker を入れておく必要がある。
# ローカルで Lambda 環境エミュレートしたコンテナを立てて
# プロジェクトを api 経由でテストすることができる
$ sam local start-api
# docker host に向かって request 投げる、みたいな
$ curl http://127.0.0.1:3000/hello
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 '{}'
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
[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 する運用
AWS Lambda 環境変数の使用
SAM deploy doesn't set environment variables #1163
なんか面倒くさそう。
- なんかよくわからん非標準な json で
--env-vars
で読み込めとか言ってる、微妙すぎ- https://zenn.dev/27ma4_ton10/articles/113f8b8bac07c7c9a49f
aws さんさぁ ...そのうち env ファイルもサポートされるかな
- template.yaml に項目セット => parameter-overrides で build, deploy 時に set という枝も
- 処理の内部で使うだけ & sdk kick なら、全部まとめて payload で渡せば十分という説もあり
多分だけど、今んとこ --parameter-overrides
使う以外なさそう。
Parameters:
セクション用意して流し込む parameter の type, default 定義しておく- ↑ を deploy や local invoke, start-api など使うときに
--parameter-overrides
で上書く - sam build で
--parameter-overrides
使えるけど template.yaml には影響しないので意味ない--parameter-overrides
はtemplate.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}",
}
}
AWS SAM が新たに AWS Step Functions をサポート
Simplifying application orchestration with AWS Step Functions and AWS SAM
Step Functions と AWS SAM CLI ローカルのテスト
- step functions の state machine を構成可能
sfn.asl.json
の雛形を作ってもらって変更してく感じぽい
- step functions local (java or docker) と
sam local start-lambda
組み合わせて local test ができるっぽい- cdk の方がパッと見「楽に作れる」ぽいけど、sam なら local test できるってのは大きい気がする
- ちょっとまだ日本語の参考記事が少ない & そこそこ学習大変そうだけど ... やれることの幅はかなり広そう
関連リソースの cfn template こねこねと sfn.asl.json
こねこねを頑張って local build & test => deploy していく感じ。ハードルは高いけど他ツールに比べるとまだ手軽な印象。
試してないけど ecs-cli と組み合わせて esc task の kick とかも local 再現できたら最高だな。
- いまんとこ node@14 runtime を選択しないと esm 使えないと思っていい
- ので commonjs 方式 (=
require()
可能) で import できないライブラリは実行時エラー
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
}
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
非同期呼び出し
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
- api gateway + lambda の
- また後述の lambda function urls もいまんとこ「同期呼び出し」しか対応してないぽい
... よって非同期呼び出しをやりたければ以下 3 択しかなさそう。
- sqs とか sns とか step functions とか aws ピタゴラスイッチで頑張る
- api gateway の非プロキシ統合とかいう古の多機能な設定地獄を頑張る
- sdk 経由で lambda invoke するときに
InvocationType
をEvent
にする- => sdk へ認証を渡す問題 + sdk の local 実行どうしよう問題を頑張る
このクラウド屋さん本当にもうちょいアプリ開発者のこと考えてくれ。 ... ってことで、現状 sam を使って非同期呼び出しするなら step functions や sqs, sns 連携の構成 または 単体 lambda 構成して sdk kick の 2 択と言えそう。構成の面倒くささ考えたら「小規模なら sdk kick 」で「中規模以上なら step functions でステート管理」かな。。。
ちなみに非同期呼び出しの場合はざっくり ↓ のような仕様。
- request 送信側には即座に 202 が返る
- 起動後は内部的に event queuing を行っているらしい
- 実行時失敗は max 2 回 (初回あわせ都合 3 回) まで retry される
- 最近 retry しないよう指定できるようになった
- retry を含めた event 寿命も最大 6 時間まで伸ばせるらしい
- ref. AWS Lambda が非同期呼び出しの最大イベント経過時間と最大再試行回数をサポートするようになりました
単体 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 の非同期呼び出しの際に「成功/失敗時に呼び出す別の lambda 」を指定できるやつ
- 非同期呼び出し前提なので、sam の api gateway 統合 (プロキシ統合) では使えないが、一応 template.yaml (cfn) で構成は可能っぽい
- step functions までいかないけど、単体では構成しづらいバッチ処理とかならありかも
API Gateway で Lambda エラーを処理する
Amazon API Gateway + AWS Lambda でのレスポンス形式
[アップデート]LambdaがHTTPSエンドポイントから実行可能になる、AWS Lambda Function URLsの機能が追加されました!
- ざっくり
statusCode
とbody
返しておくと 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 になって返ってきてややこしい
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\"}"}
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 で構成した例。
- まず zip deploy だとサイズ制限に引っかかるので image deploy を検討
- lambda の docker deploy には通常 lambda 用に最適化された image を使う
public.ecr.aws/lambda/nodejs:16
とか
- ↑ カスタム image から作る場合は awslambdaric, aws-lambda-rie を使い構成
- aws-lambda-rie は local emulate でしか使わないので 公式ブログ の
entry.sh
みたいなスクリプトで if るか、マルチステージビルドでコンテナを使い分けるといいみたい- 【AWS SAM】デプロイとローカル確認用のコンテナイメージを一つのDockerfileで使い分ける
- prod では template.yml の
DockerBuildTarget: prod
でステージ指定 - local 確認
sam local start-api
では--parameter-overrides DockerBuildTarget=local
みたいにできそう
上記方針で色々試行錯誤したけど、結局以下のような構成で実現した。
- 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 が対応してないので、増やしたりとか試せてない
- 実装が誤っているのかもしれないが二重起動 or 長時間実行すると
- よって lambda 機能が安定してきたら ↓ sam local でテストを行うことにした
- nodemon で rie, ric 経由で handler を叩く
- 上記ローカル 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 関数 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 invoke
やsam local start-lambda
の sdk kick はできるが)
... 正直、web api の基本は「即 response していってらっしゃい → 終わったら別動線で教えてね」なので、api gateway の timeout 回避だけで使うなら、「 30 秒以上かかるときはあるけど 10 分以上かかることはない何らかファイルのダウンロード api 」くらいでしか使わない気がした ... 。
とはいえ、料金面で安いので、機械学習の推論 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"}'