これは、なにをしたくて書いたもの?
ここまで、何回か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
の方を調べ始めたらなかなか
大変でした…。
とりあえず、確認したいことはできたので良しとしましょう。