これは、なにをしたくて書いたもの?
ここまで、何回かAWS SAMを使ってAWS Lambda関数をAmazon API Gatewayのバックエンドにデプロイすることを
試していましたが、すべて単一のAWS Lambda関数でした。
今回は、複数のAWS Lambda関数をデプロイしてみたいと思います。
AWS SAMで、複数のアプリケーションを扱う
ドキュメントを見ていてもそういうサンプルはなさそうでしたが、こちらのissueにヒントがありました。
リソースとしてAWS::Serverless::Functionを定義して、CodeUriをそれぞれ別の場所を指定すれば良い、という
話ですね。
もう一方で、AWS SAMアプリケーション自体をネストする考え方もあるようですが。
ネストされたアプリケーションの使用 - AWS Serverless Application Model
こちらは少し違う気がするので、前者のアプローチを自分でも試してみたいと思います。
環境
今回の環境は、こちら。LocalStack上で確認します。
$ localstack --version 0.13.2.1 $ python3 -V Python 3.8.10 $ awslocal --version aws-cli/2.4.7 Python/3.8.8 Linux/5.4.0-91-generic exe/x86_64.ubuntu.20 prompt/off $ samlocal --version SAM CLI, version 1.36.0
LocalStackの起動。
$ LAMBDA_EXECUTOR=docker-reuse localstack start
ローカルで利用するNode.js。
$ node --version v14.18.2 $ npm --version 6.14.15
2つのAWS Lambda関数を持ったAWS SAMプロジェクトを作成する
まず、initでプロジェクトを作成します。
$ samlocal init
ランタイムはNode.js 14.x、アプリケーション名はsam-multiple-lambdaで作成。
Which template source would you like to use?
1 - AWS Quick Start Templates
2 - Custom Template Location
Choice: 1
What package type would you like to use?
1 - Zip (artifact is a zip uploaded to S3)
2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1
Which runtime would you like to use?
1 - nodejs14.x
2 - python3.9
3 - ruby2.7
4 - go1.x
5 - java11
6 - dotnetcore3.1
7 - nodejs12.x
8 - nodejs10.x
9 - python3.8
10 - python3.7
11 - python3.6
12 - python2.7
13 - ruby2.5
14 - java8.al2
15 - java8
16 - dotnetcore2.1
Runtime: 1
Project name [sam-app]: sam-multiple-lambda
Cloning from https://github.com/aws/aws-sam-cli-app-templates
AWS quick start application templates:
1 - Hello World Example
2 - Step Functions Sample App (Stock Trader)
3 - Quick Start: From Scratch
4 - Quick Start: Scheduled Events
5 - Quick Start: S3
6 - Quick Start: SNS
7 - Quick Start: SQS
8 - Quick Start: Web Backend
Template selection: 1
-----------------------
Generating application:
-----------------------
Name: sam-multiple-lambda
Runtime: nodejs14.x
Architectures: x86_64
Dependency Manager: npm
Application Template: hello-world
Output Directory: .
Next application steps can be found in the README file at ./sam-multiple-lambda/README.md
Commands you can use next
=========================
[*] Create pipeline: cd sam-multiple-lambda && sam pipeline init --bootstrap
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
作成されたディレクトリ内に移動。
$ cd sam-multiple-lambda
今回は「Hello World Example」をテンプレートに選びましたが、使わないので削除します。
$ rm -rf hello-world
作成するAWS Lambda関数は、「Hello World」を返すものと、足し算をする2つのものをTypeScriptで作成します。
$ mkdir hello calc
以下、それぞれ各AWS Lambda関数のディレクトリに対して同じことを行います。
$ cd hello
Node.jsのプロジェクト作成と、TypeScript、型宣言のインストール。
$ npm init -y $ npm i -D typescript $ npm i -D -E prettier $ npm i -D @types/node@v14 @types/aws-lambda $ mkdir src
TypeScriptの設定ファイル。srcディレクトリにソースコードを配置して、ビルド結果はdistディレクトリに出力します。
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "commonjs", "baseUrl": "./src", "outDir": "dist", "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "esModuleInterop": true }, "include": [ "src" ] }
Prettierの設定。
.prettierrc.json
{ "singleQuote": true }
package.jsonのscriptsは、こんな感じにしておきました。npm run buildで、AWS SAMでデプロイするための
準備ができます。
"scripts": { "build": "tsc --project . && cp package*.json dist", "build:watch": "tsc --project . --watch", "format": "prettier --write src" },
依存関係。
"devDependencies": { "@types/aws-lambda": "^8.10.89", "@types/node": "^14.18.4", "prettier": "2.5.1", "typescript": "^4.5.4" }
ソースコードを作成して(後述)、ビルド。
$ npm run build
こういう結果になります。
$ tree dist dist ├── app.js ├── package-lock.json └── package.json 0 directories, 3 files
これを、calcディレクトリに対しても行います。
ソースコードは、それぞれこちら。
hello/src/app.ts
import { APIGatewayProxyEvent, Context } from 'aws-lambda'; export const lambdaHandler = async (event: APIGatewayProxyEvent, context: Context) => { return { statusCode: 200, body: { message: 'Hello World' } } };
calc/src/app.ts
import { APIGatewayProxyEvent, Context } from 'aws-lambda'; export const lambdaHandler = async (event: APIGatewayProxyEvent, context: Context) => { if (event.body) { const request = JSON.parse(event.body); const a = parseInt(request.a, 10); const b = parseInt(request.b, 10); return { statusCode: 200, body: { result: a + b } } } else { return { statusCode: 400, body: { message: 'missing body' } } } };
デプロイする
では、AWS SAM CLIを使ってデプロイします。template.yamlがあるディレクトリに移動。
$ cd ..
テンプレートは、このようにしました。
template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > sam-multiple-lambda Sample SAM Template for sam-multiple-lambda Globals: Function: Timeout: 3 Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello/dist Handler: app.lambdaHandler Runtime: nodejs14.x Architectures: - x86_64 Events: HelloWorld: Type: Api Properties: Path: /hello Method: get CalcFunction: Type: AWS::Serverless::Function Properties: CodeUri: calc/dist Handler: app.lambdaHandler Runtime: nodejs14.x Architectures: - x86_64 Events: Calc: Type: Api Properties: Path: /calc Method: post Outputs: HelloWorldApi: Description: "API Gateway endpoint URL for Prod stage for Hello World function" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" CalcApi: Description: "API Gateway endpoint URL for Prod stage for Calc function" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/calc/"
AWS::Serverless::Functionは2つ定義して、それぞれCodeUriでデプロイ対象のファイルを指すようにしています。
HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello/dist Handler: app.lambdaHandler Runtime: nodejs14.x Architectures: - x86_64 Events: HelloWorld: Type: Api Properties: Path: /hello Method: get CalcFunction: Type: AWS::Serverless::Function Properties: CodeUri: calc/dist Handler: app.lambdaHandler Runtime: nodejs14.x Architectures: - x86_64 Events: Calc: Type: Api Properties: Path: /calc Method: post
ビルドします。
$ samlocal build
それぞれのAWS Lambda関数に対してビルドが行われていることが確認できます。
Building codeuri: /path/to/sam-multiple-lambda/hello/dist runtime: nodejs14.x metadata: {} architecture: x86_64 functions: ['HelloWorldFunction']
Running NodejsNpmBuilder:NpmPack
Running NodejsNpmBuilder:CopyNpmrc
Running NodejsNpmBuilder:CopySource
Running NodejsNpmBuilder:NpmInstall
Running NodejsNpmBuilder:CleanUpNpmrc
Building codeuri: /path/to/sam-multiple-lambda/calc/dist runtime: nodejs14.x metadata: {} architecture: x86_64 functions: ['CalcFunction']
Running NodejsNpmBuilder:NpmPack
Running NodejsNpmBuilder:CopyNpmrc
Running NodejsNpmBuilder:CopySource
Running NodejsNpmBuilder:NpmInstall
Running NodejsNpmBuilder:CleanUpNpmrc
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
[*] Deploy: sam deploy --guided
$ awslocal s3 mb s3://my-bucket
デプロイ。
$ samlocal deploy --stack-name my-stack --region us-east-1 --s3-bucket my-bucket
2つのAWS Lambda関数を含めてデプロイされました。
CloudFormation events from stack operations
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus ResourceType LogicalResourceId ResourceStatusReason
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_COMPLETE AWS::CloudFormation::Stack HelloWorldFunctionRole -
CREATE_COMPLETE AWS::CloudFormation::Stack CalcFunctionRole -
CREATE_COMPLETE AWS::CloudFormation::Stack ServerlessRestApiProdStage -
CREATE_COMPLETE AWS::CloudFormation::Stack ServerlessRestApiDeploymentd73aeb0c06 -
CREATE_COMPLETE AWS::CloudFormation::Stack ServerlessRestApi -
CREATE_COMPLETE AWS::CloudFormation::Stack HelloWorldFunctionHelloWorldPermissionPr -
od
CREATE_COMPLETE AWS::CloudFormation::Stack HelloWorldFunction -
CREATE_COMPLETE AWS::CloudFormation::Stack my-stack -
CREATE_COMPLETE AWS::CloudFormation::Stack CalcFunctionHelloWorldPermissionProd -
CREATE_COMPLETE AWS::CloudFormation::Stack CalcFunction -
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
OutputsでもURLは出力していますが、AWS CLIでREST APIのIDを取得して
$ REST_API_ID=$(awslocal apigateway get-rest-apis --query 'items[0].id' --output text)
動作確認。
$ curl http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello
{"message": "Hello World"}
$ curl -XPOST http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/calc -d '{"a": 5, "b": 3}'
{"result": 8}
2つのAWS Lambda関数が、同じAmazon API GatewayのREST APIのリソースに紐付けられていることが確認できました。
OKですね。
sam build時にnpm run buildを実行する
ここまででやりたいことは確認できましたが、毎回各AWS Lambda関数のディレクトリに移動してnpm run buildするのも
面倒な気がします。
いい方法はないかな?と思ったのですが、sam build時にMakefileを動かせるようです。こちらを試してみましょう。
この部分ですね。
リソースに BuildMethod エントリがある Metadata リソース属性が含まれている場合、sam build は BuildMethod エントリの値に従ってそのリソースを構築します。BuildMethod の有効な値は、1) Lambda ランタイムの識別子の 1 つ、または 2) makefile 識別子です。
makefile 識別子 — リソース用のビルドターゲットのコマンドを実行します。この場合、makefile が Makefile と命名されており、build-resource-logical-id という名前のビルドターゲットが含まれている必要があります。
sam build - AWS Serverless Application Model
Lambdaランタイム識別子はnodejs14.xのような記述のことですが、こちらはデフォルトの挙動な気がするので
ここでは飛ばします。
Metadataについては、こちらに記載があります。
Metadata 属性 - AWS CloudFormation
Makefileの例はないかな?と思ったら、カスタムランタイムのページに書かれていました。
カスタムランタイムの構築 - AWS Serverless Application Model
Makefileは、CodeUriで指定したディレクトリにMakefileという名前で存在する必要があります。
Makefile の場所は、関数リソースの CodeUri プロパティによって指定され、Makefile と命名される必要があります。
また、ビルドターゲットの名前はbuild-[リソース論理ID]である必要があります。このターゲットが見つからない場合、
sam buildコマンドは失敗します。
ここまでの情報をもとに、template.yamlを修正してみます。
Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello Handler: app.lambdaHandler Runtime: nodejs14.x Architectures: - x86_64 Events: HelloWorld: Type: Api Properties: Path: /hello Method: get Metadata: BuildMethod: makefile CalcFunction: Type: AWS::Serverless::Function Properties: CodeUri: calc Handler: app.lambdaHandler Runtime: nodejs14.x Architectures: - x86_64 Events: Calc: Type: Api Properties: Path: /calc Method: post Metadata: BuildMethod: makefile
2つのAWS Lambda関数のCodeUriからdistディレクトリの記述を削除し、MetadataおよびBuildMethod: makefileを
追加しました。
Makefileは、それぞれこのように用意。
hello/Makefile
build-HelloWorldFunction:
rm -rf node_modules
npm i
npm run build
cd dist && \
npm i --production && \
cp -R * ${ARTIFACTS_DIR}
calc/Makefile
build-CalcFunction:
rm -rf node_modules
npm i
npm run build
cd dist && \
npm i --production && \
cp -R * ${ARTIFACTS_DIR}
ARTIFACTS_DIRという環境変数は、後でまた出てきますがビルド結果を配置するディレクトリになります。
cdコマンドの後に&&でつなげて実行していますが、これはMakefileに記述された各行はサブシェルで実行されるため、
次のコマンドには影響しないからですね。
また、このMakefileおよびコマンドは/tmp領域にファイルをコピーしてから実行されるようなのですが(pwdで
見るとCodeUriとは全然違うディレクトリになっています)、最初にnode_modulesをして再インストールしないと
うまく動きませんでした…。
では、ビルドはMakefile側で行うことになるはずなので、過去のビルド結果は削除。
$ rm -rf hello/dist calc/dist $ rm -rf .aws-sam
ビルド。
$ samlocal build
「Building codeuri」というメッセージ以降が「Running CustomMakeBuilder」と変化します。
Building codeuri: /path/to/sam-multiple-lambda/hello runtime: nodejs14.x metadata: {'BuildMethod': 'makefile'} architecture: x86_64 functions: ['HelloWorldFunction']
Running CustomMakeBuilder:CopySource
Running CustomMakeBuilder:MakeBuild
Current Artifacts Directory : /path/to/sam-multiple-lambda/.aws-sam/build/HelloWorldFunction
Building codeuri: /path/to/sam-multiple-lambda/calc runtime: nodejs14.x metadata: {'BuildMethod': 'makefile'} architecture: x86_64 functions: ['CalcFunction']
Running CustomMakeBuilder:CopySource
Running CustomMakeBuilder:MakeBuild
Current Artifacts Directory : /path/to/sam-multiple-lambda/.aws-sam/build/CalcFunction
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
[*] Deploy: sam deploy --guided
この最中にMakefileが実行され、複数のAWS Lambda関数のビルドができるようになります。
また、「Current Artifacts Directory」というのがMakefile内でARTIFACTS_DIRという環境変数で格納されていた値に
なります。
Current Artifacts Directory : /path/to/sam-multiple-lambda/.aws-sam/build/HelloWorldFunction Current Artifacts Directory : /path/to/sam-multiple-lambda/.aws-sam/build/CalcFunction
ビルド結果。
a$ tree .aws-sam .aws-sam ├── build │ ├── CalcFunction │ │ ├── app.js │ │ ├── package-lock.json │ │ └── package.json │ ├── HelloWorldFunction │ │ ├── app.js │ │ ├── package-lock.json │ │ └── package.json │ └── template.yaml └── build.toml 3 directories, 8 files
なお、先ほどの記載しましたが、make自体は別の場所で実行されるため、CodeUriで示した場所にはビルド結果
(今回の場合TypeScriptファイルのビルド結果やnpm installの結果など)は残っていません。
$ ll hello/dist calc/dist ls: 'hello/dist' にアクセスできません: そのようなファイルやディレクトリはありません ls: 'calc/dist' にアクセスできません: そのようなファイルやディレクトリはありません
あとは、この状態のままデプロイが可能です。
$ samlocal deploy --stack-name my-stack --region us-east-1 --s3-bucket my-bucket
複数のAWS Lambda関数を、まとめてビルドできるようになりました、と。
まとめ
AWS SAMを使って、複数のAWS Lambda関数をLocalStack上のAmazon API Gatewayのバックエンドにデプロイして
みました。
これ自体はあっさりいったのですが、数が増えると面倒になるなぁと思ってMakefileの方を調べ始めたらなかなか
大変でした…。
とりあえず、確認したいことはできたので良しとしましょう。