Amplify Rest APIで利用してるLambdaをProvisioned Concurrencyモードで利用するまでの勉強スレッド
Amplify Rest APIのLambdaでNest.jsを使っているが、Lambdaが動いてないときにアクセスすると4-7秒ほどレスポンスに時間がかかる。(コールドスタート)
[1]LambdaをProvisioned Concurrencyモードで起動させて、
ApiGatewayからは上記のLambdaに接続することで改善を試みる
そもそもCloudformationの知識が必要だから勉強
Cloudformationのパラメータどこで見ればいいんだっていつもなってたけどここか!
リソースタイプによって利用できるプロパティは異なるので、公式ドキュメントとにらめっこしながら、指定していきます。
https://dev.classmethod.jp/articles/cloudformation-beginner01/
こりゃ確かににらめっこしながら設定してくやつだ(笑)
ChatGPTも間違いがよくあるからAWSでインフラやるなら自分で覚えるのは必須だな
組み込み関数は要チェック
テンプレートスニペット
Cloudformation外からの変更を検知できるのか
チームでAWS使ってる場合は便利かも?
そもそも権限でカバーすべきなきもするけど、インフラもやってるエンジニアが間違って検証のために変更して戻り忘れるケースはありそう...
よし、いったん基礎を詰め込むのはここまでで
ApiGatewayとLambdaでAPIを構築するcloudformationを作成してみよう
普通のlambdaを構築するテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda function Hello World.
Resources:
primer:
Type: AWS::Lambda::Function
Properties:
Runtime: nodejs18.x
Role: arn:aws:iam::<accountID>:role/TestLambdaBasicRole // Lambdaに紐づけるロールLambda実行に必要な権限のみ与えてる。このテンプレートではCloudwatchLogsのみ権限を与えてる
Handler: index.handler
Code:
ZipFile: |
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello World'),
};
return response;
};
Description: Hello World Lambda Function
TracingConfig:
Mode: Active
Lambdaにversionをつけて、Provisioned Concurrency設定を追加
AWSTemplateFormatVersion: '2010-09-09'
Description: Lambda function Hello World.
Resources:
MyHelloWorldFunction:
Type: AWS::Lambda::Function
Properties:
Runtime: nodejs18.x
Role: arn:aws:iam::<accountId>:role/TestLambdaBasicRole
Handler: index.handler
Code:
ZipFile: |
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello World'),
};
return response;
};
Description: Hello World Lambda Function
TracingConfig:
Mode: Active
# Provisioned:
# Type: AWS::Lambda::Alias
# Properties:
# FunctionName: MyHelloWorldFunction
# # or
# # Function ARN
# FunctionVersion: String
# Name: String
# ProvisionedConcurrencyConfig:
# ProvisionedConcurrencyConfiguration
# RoutingConfig:
# AliasRoutingConfiguration
MyHelloWorldFunctionVersion:
Type: AWS::Lambda::Version
Properties:
FunctionName: !Ref MyHelloWorldFunction
Description: v1
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 3
一旦メモ
overrideでAPIGatewayを上書きする方法
RestAPIの各単語についての基礎をChatGPTに教わる
とりあえず、完成まで少しのところまで来た気がする
まずは、Lambdaの同時実行(Provisioned Concurrency)を設定するコード
amplify/backend/function/${function-name}/${function-name}-cloudformation-template.json
のResources
セクションにに以下のコードを追加する
"HogeFunctionVersionName": { // ←自分で名前を決める。Cloudformationの一意な論理名。好きに決めて良い。分かりやすいのが好ましい。
"Type": "AWS::Lambda::Version",
"Properties": {
"FunctionName": {
"Fn::If": [
"ShouldNotCreateEnvResources",
"hogeFunction", // ←function名を入力。やりたいことはFunctionNameに${function-name}-${env}とすること。このあたりの書き方は同じファイルのどこかにあると思う。
{
"Fn::Join": [
"",
[
"hogeFunction", // ←function名を入力。
"-",
{
"Ref": "env"
}
]
]
}
]
}
}
},
"HogeFunctionAlias": { // ←自分で名前を決める。Cloudformationの一意な論理名。好きに決めて良い。分かりやすいのが好ましい。
"Type": "AWS::Lambda::Alias",
"Properties": {
"FunctionName": {
"Fn::If": [
"ShouldNotCreateEnvResources",
"hogeFunction", // ←function名を入力。
{
"Fn::Join": [
"",
[
"hogeFunction", // ←function名を入力。
"-",
{
"Ref": "env"
}
]
]
}
]
},
"FunctionVersion": {
"Fn::GetAtt": [
"HogeFunctionVersionName", // ←上で設定したLambdaバージョンの論理名を入力。
"Version"
]
},
"Name": "provisioned", // ← 自分で好きに設定する。この項目名がLambdaのAlias名になる。
"ProvisionedConcurrencyConfig": {
"ProvisionedConcurrentExecutions": 3
}
}
},
これでLambdaFunctionにAliasを設定し、ProvisionedConcurrentExecutions
で同時実行数を3に設定している。3はプロジェクトによって適時変えればよい。
次にAPIGatewayの統合リクエストで紐づけるLambdaを通常のLambda($latestのLambda)ではなく、同時実行設定のあるAlias付きのLambdaに接続するための手順
amplify override api
を実行する。
amplify/backend/api/${functionName}/override.ts
が生成されるはず。
以下のように編集する
// This file is used to override the REST API resources configuration
import { AmplifyApiRestResourceStackTemplate } from '@aws-amplify/cli-extensibility-helper'
export function override(resources: AmplifyApiRestResourceStackTemplate) {
const accountId = 'xxx' // AWSアカウントID
const functionName = 'hogeFunction' // 関数名
const env = 'devb' // TODO: 動的に取得する方法を探す
const region = 'ap-northeast-1' // リージョンを指定
const alias = 'provisioned' // amplify/backend/function/${function-name}/${function-name}-cloudformation-template.json で指定したエイリアス名
const lambdaArn = `arn:aws:lambda:ap-northeast-1:${accountId}:function:${functionName}-${env}:${alias}`
const integrationUri = `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations`
// REST APIの各メソッドをループして、新しいLambda関数に紐づける
for (const path in resources.restApi.body.paths) {
resources.restApi.body.paths[path]['x-amazon-apigateway-any-method'] = {
...resources.restApi.body.paths[path]['x-amazon-apigateway-any-method'],
'x-amazon-apigateway-integration': {
...resources.restApi.body.paths[path]['x-amazon-apigateway-any-method'][
'x-amazon-apigateway-integration'
],
uri: integrationUri,
},
}
}
}
これでamplify pushを実行した際にAPIGatewayから呼び出されるLambdaがAlias付きのLambdaになる。
これでamplify push
すると自動的にAPIGatewayに同時実行設定のあるLambdaAliasを接続することができたわけだが、実際の運用ではamplify pushするたびにLambdaが最新の状態になってほしい。
それを実現するのが以下のファイル。
説明は省く。
#!/bin/bash
# Lambda 関数名
FUNCTION_NAME="hogeFunction-env"
# 最新バージョンを発行
NEW_VERSION=$(aws lambda publish-version --function-name $FUNCTION_NAME --query 'Version' --output text)
# エイリアス 'provisioned' に新バージョンを紐づけ
aws lambda update-alias --function-name $FUNCTION_NAME --name provisioned --function-version $NEW_VERSION
echo "New version $NEW_VERSION published and aliased to 'provisioned'"
実行後すぐに新しいバージョンでLambdaが動くわけではなく同時実行設定が終わってから新しいバージョンに処理を裁くようにAliasがうまいことやってるみたい。イメージはECSのローリングアップデート?
そして、amplify push するときに権限が必要になるかも。
以下のみの設定で一発でうまくいくか分からないが、自分は以下の設定で最終的にはうまくいった。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:PutFunctionConcurrency",
"lambda:DeleteFunctionConcurrency",
"lambda:ListVersionsByFunction",
"lambda:CreateAlias",
"lambda:DeleteAlias",
"lambda:UpdateAlias"
],
"Resource": [
"arn:aws:lambda:ap-northeast-1:${accountId}:function:${functionName}-${env}"
]
},
{
"Effect": "Allow",
"Action": [
"lambda:PutProvisionedConcurrencyConfig",
"lambda:GetProvisionedConcurrencyConfig"
],
"Resource": [
"arn:aws:lambda:ap-northeast-1:${accountId}:function:${functionName}-${env}:provisioned" // エイリアスを指定する
]
}
]
}
後はoverrideやpost-pushで動的に環境変数を取得する方法の調査
それが終わったら一度新しい環境を生成して検証する
override.tsでenvを取る方法はこれか?
const projectInfo = getProjectInfo()
を入れるとamplify push時にoverrideファイルが検知されない🤔
- No override file found
- To override cofamiRestApi run "amplify override api"
どんだけはまりポイントあんねん
// This file is used to override the REST API resources configuration
import {
AmplifyApiRestResourceStackTemplate,
AmplifyProjectInfo,
} from '@aws-amplify/cli-extensibility-helper'
export function override(
resources: AmplifyApiRestResourceStackTemplate,
amplifyProjectInfo: AmplifyProjectInfo
) {
console.log(amplifyProjectInfo) // undefinedになる
amplify --version
11.0.0
upgradeしてみる。
root@e929bc0bdd4f:/usr/src/app# amplify upgrade
Downloading latest Amplify CLI
100% [========================================] 0.0 seconds left
Successfully upgraded to Amplify CLI version 12.8.2!
はっ!!12まで上がってる!大丈夫か?
再度amplify push実行
⠙ Fetching updates to backend environment: devb from the cloud.hello from override.ts
{ envName: 'xxx', projectName: 'xxxxx' }
おー!!取れたー!!
でもなんか以下のエラーも出るようになったから見なきゃだな
⠼ Generating UI components...🛑 Codegen job status is failed
No UI components to generate
✖ Failed to sync UI components
post-push.shは以下のコードでenvを参照することにする
CURRENT_AMPLIFY_ENV=$(amplify status | grep 'Current Environment:' | awk '{print $4}')
一旦新しい環境を生成してみる
これで最初から同時実行が設定されてたらうまく構築できてそう。
上手くできてたら本番環境にのみ同時実行設定をする方法の調査をする。
新しい環境生成
amplify env addで環境を追加して
amplify push -y
でインフラ構築
policyで新しい環境を使うから設定を更新する必要があることに注意が必要
lambdaの件でcloudformationの修正が必要そう
Reason: Resource handler returned message: "Function not found: arn:aws:lambda:ap-northeast-1::function:-devc (Service: Lambda, Status Code: 404, Request ID: c699cdc9-0ee7-4007-b7dd-475341897dd7)" (RequestToken: 32e0032d-c779-4195-eb58-62975fd53364, HandlerErrorCode: NotFound)
🛑 Resource is not in the state stackUpdateComplete
Name: HogeFunctionVersion (AWS::Lambda::Version), Event Type: create, Reason: Resource handler returned message: "Function not found: arn:aws:lambda:ap-northeast-1::function:-devc (Service: Lambda, Status Code: 404, Request ID: c699cdc9-0ee7-4007-b7dd-475341897dd7)" (RequestToken: 32e0032d-c779-4195-eb58-62975fd53364, HandlerErrorCode: NotFound), IsCustomResource: false
cloudformationのlambda versionとalias 設定のところにdependsOnを設定する必要があるみたい
amplify push はバックエンドだけか
amplify publish でバックエンドとフロントの両方をデプロイするから最初はamplify publishを使うべ加茂な。
う~ん、amplify pushとpublishをした後に、cloudfrontからAPI呼び出すときに{"message": "Internal server error"}
になる。なぜだ。
調査用にAPIGatewayのCloudwatchLogsをオンにしてみる
ログとトレースを編集
ステージ > ログとトレースを編集
でいろいろオンにする
その前に権限設定が必要だった
以下の手順でAPIGatewayがCloudwatchLogsに書き込めるロールを作成して、それをAPIGatewayにアタッチする。そしてログをオンにすればできる。
APIごとに設定するのではなく、Apigatewayに対して設定するのか🤔
→ロールの名前はそれっぽいのがわかるようにつけた方がよさそうだな
NG: ${appName}-apigateway-role
Good: apigateway-role
再びcloudfrontのURLにアクセスしてAPIを実行してみるとログが出力された
ログのグループ名はAPI-Gateway-Execution-Logs_${hoge}/${hoge}
となっていた
Apigatewayにリンクないのか・・・?
Lambdaのリソースベースのポリシーを以下のように修正したらAPIGateway -> Lambda 呼び出しは成功するようになった
{
"ArnLike": {
- "AWS:SourceArn": "arn:aws:execute-api:ap-northeast-1:${accountId}:${apiGatewayId}/*/*/hogeFunction-${stage}"
+ "AWS:SourceArn": "arn:aws:execute-api:ap-northeast-1:${accountId}:${apiGatewayId}/*/*/*"
}
}
がしかし、今度はpost-push.shでエラーが起きている...
----- 🪝 post-push execution start -----
🛑 node:internal/modules/cjs/loader:1078
throw err;
^
Error: Cannot find module '/usr/src/app/status'
at Module._resolveFilename (node:internal/modules/cjs/loader:1075:15)
at Function._resolveFilename (pkg/prelude/bootstrap.js:1951:46)
at Module._load (node:internal/modules/cjs/loader:920:27)
at Function.runMain (pkg/prelude/bootstrap.js:1979:12)
at node:internal/main/run_main_module:23:47 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
Node.js v18.15.0
🛑 post-push hook script exited with exit code 1
🛑 exiting Amplify process...
post-push.shに書いているCURRENT_AMPLIFY_ENV=$(amplify status | grep 'Current Environment:' | awk '{print $4}')
のamplify statusでエラーになってるみたい。
amplify コマンド打てないの・・・?
とりあえずこの際node.jsに置き換えようかと思う
雑なメモ
post-pushは以下のようにすればうまくいくことを確認
#!/usr/bin/env node
console.log('post-push script is running...')
const runCommandFile = (args) => {
const { spawnSync } = require('child_process')
// .sh スクリプトファイルのパス
console.log(`\nexecuting ${args[0]}...`)
// spawnSync を使用してスクリプトを同期的に実行
const result = spawnSync('bash', args, { encoding: 'utf8' })
// 標準出力と標準エラー出力を出力
console.log(`\nstdout: \n${result.stdout}`)
console.error(`\nstderr: \n${result.stderr}`)
// プロセス終了コードをログ出力
console.log(`\nchild process exited with code ${result.status}`)
}
/**
* @param data { { amplify: { environment: { envName: string, projectPath: string, defaultEditor: string }, command: string, subCommand: string, argv: string[] } } }
* @param error { { message: string, stack: string } }
*/
const hookHandler = async (data, error) => {
// 渡したい引数
const envName = data.amplify.environment.envName
// .sh スクリプトファイルのパス
const scriptPath = 'amplify/hooks/post-push-commands.sh'
runCommandFile([scriptPath, envName])
}
const getParameters = async () => {
const fs = require('fs')
return JSON.parse(fs.readFileSync(0, { encoding: 'utf8' }))
}
getParameters()
.then((event) => {
console.log('event', JSON.stringify(event))
return hookHandler(event.data, event.error)
})
.catch((err) => {
console.error(err)
process.exitCode = 1
})
#!/bin/bash
# エラー発生時にスクリプトを終了する
set -e
echo "Received argument: $1"
# Amplify CLI を使用して現在の環境を取得
# CURRENT_AMPLIFY_ENV=$(amplify env list | grep '*' | awk '{print $2}')
# CURRENT_AMPLIFY_ENV=$(amplify status | grep 'Current Environment:' | awk '{print $4}')
# amplify status
# エラーになるのかちゃんと取れずにただの空文字が格納される
# CURRENT_AMPLIFY_ENV=$(amplify status | grep 'Current Environment:' | awk '{print $4}' | tr -d ' ')
# echo "Current Amplify environment from amplify status: $CURRENT_AMPLIFY_ENV"
CURRENT_AMPLIFY_ENV=$1
echo "Current Amplify environment: $CURRENT_AMPLIFY_ENV"
# Lambda 関数名
FUNCTION_NAME="hogeFunction-"$CURRENT_AMPLIFY_ENV
echo "FUNCTION_NAME: $FUNCTION_NAME"
# 最新バージョンを発行
NEW_VERSION=$(aws lambda publish-version --function-name $FUNCTION_NAME --query 'Version' --output text)
# エイリアス 'provisioned' に新バージョンを紐づけ
aws lambda update-alias --function-name $FUNCTION_NAME --name provisioned --function-version $NEW_VERSION
echo "New version $NEW_VERSION published and aliased to 'provisioned'"
bashとjsで分ける必要もないと思うけど、いったんこれでOK
amplify/backend/function/hogeFunction/hogeFunction-cloudformation-template.json
に以下を追加してamplify pushすればOK
"HogeFunctionAliasResourceBasedPolicy": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"FunctionName": {
"Fn::Sub": [
"${LambdaFunctionArn}:${AliasName}",
{
"LambdaFunctionArn": {
"Fn::GetAtt": [
"LambdaFunction",
"Arn"
]
},
"AliasName": "provisioned" // TODO: パラメータで管理する
}
]
},
"Action": "lambda:InvokeFunction",
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": [
// 本当は以下のようにApigatewayのIDをつけたいが妥協案。Apigatewayが別の管理のされ方をしているのでFn::GetAttなどで取ってくるのが難しい。変数を使うならシステムマネージャとかありだけど、そこまで管理しなくても良いと思った。自分のアカウントからしかアクセスできないことは担保できてるから大丈夫なはず。
// "arn:aws:execute-api:${region}:${account}:hogeHugaId/*/*/*",
"arn:aws:execute-api:${region}:${account}:*/*/*/*",
{
"region": {
"Ref": "AWS::Region"
},
"account": {
"Ref": "AWS::AccountId"
}
}
]
}
},
"DependsOn": "HogeFunctionAlias"
},
再度新しい環境を追加して環境構築挑戦する
amplify add env # devd を追加
root@e929bc0bdd4f:/usr/src/app# amplify add env # devd を追加
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the environment devd
Using default provider awscloudformation
Adding backend environment devd to AWS Amplify app:
role の権限に新しい環境分の権限を追加する
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:PutFunctionConcurrency",
"lambda:DeleteFunctionConcurrency",
"lambda:ListVersionsByFunction",
"lambda:CreateAlias",
"lambda:DeleteAlias",
"lambda:UpdateAlias"
],
"Resource": [
+ "arn:aws:lambda:ap-northeast-1:${accountId}:function:hogeFunction-devd"
]
},
{
"Effect": "Allow",
"Action": [
"lambda:PutProvisionedConcurrencyConfig",
"lambda:GetProvisionedConcurrencyConfig"
],
"Resource": [
+ "arn:aws:lambda:ap-northeast-1:${accountId}:function:hogeFunction-devd:provisioned"
]
}
]
}
amplify push でバックエンドをデプロイ
amplify push
lambda エイリアスのトリガーの apigateway で以下のエラーが出てるのが気になる...
API Gateway:
arn:aws:execute-api:${region}:${accountId}:*/*/*/*
The API with ID * could not be found in ${region}.
詳細
Service principal: apigateway.amazonaws.com
Statement ID: amplify-cofami-devd-52706-functioncofamiRes-HogeFunctionAliasResourceBased-XYfxGGPLgSEs
あーリソースベースのポリシーでAPI IDを*にしていて、そんなID見つかりませんてことか。
じゃあ、どうやって自動化すればいいんじゃい!?
→環境ごとにAPIIDを手動でcloudformationに入力か!?
amplify publish でフロントエンドをデプロイ
amplify push で cors 対応済みのバックエンドをデプロイ
cloudfront の URL がでるから、cors に追加する(自分は restapi の node.js に書いてるからそこの処理を更新する)
この時点でフロントエンドにアクセスしたが、エラーが出る
cors のエラー?まだデプロイ中なのか?
Access to fetch at 'https://xxx.execute-api.ap-northeast-1.amazonaws.com/devd/me' from origin 'https://xxx.cloudfront.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
→ デプロイして最新バージョンへの反映が完全に終わってなかった!
数秒後にアクセスしたら無事に動くことを確認した!!!ついに、、、!!
ということで本番に反映する
同時実行は本番だけで適用させたいから、本番のみで同時実行するように設定等を見直す。