これは、なにをしたくて書いたもの?
AWS SAMを使って、TypeScriptで書いたアプリケーションを試してみたいなということで。
やり方
TypeScriptのビルド結果+package.json
を配置したディレクトリに、AWS SAMのテンプレート(template.yaml
)に
書かれているCodeUri
を合わせてあげれば良さそうです。
環境
今回の環境は、こちら。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
AWS SAMプロジェクト作成
まずは、AWS SAMのプロジェクトを作成します。
$ samlocal init 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-typescript-example 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-typescript-example 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-typescript-example/README.md Commands you can use next ========================= [*] Create pipeline: cd sam-typescript-example && sam pipeline init --bootstrap [*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
ランタイムはNode.js 14.xです。
作成されたディレクトリ、ファイル。
$ tree sam-typescript-example sam-typescript-example ├── README.md ├── events │ └── event.json ├── hello-world │ ├── app.js │ ├── package.json │ └── tests │ └── unit │ └── test-handler.js └── template.yaml 4 directories, 6 files
ディレクトリ内に移動して
$ cd sam-typescript-example
少し中身を見てみます。
template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > sam-typescript-example Sample SAM Template for sam-typescript-example # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 3 Resources: HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: hello-world/ Handler: app.lambdaHandler Runtime: nodejs14.x Architectures: - x86_64 Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: get 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 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/" HelloWorldFunction: Description: "Hello World Lambda Function ARN" Value: !GetAtt HelloWorldFunction.Arn HelloWorldFunctionIamRole: Description: "Implicit IAM Role created for Hello World function" Value: !GetAtt HelloWorldFunctionRole.Arn
hello-world/package.json
{ "name": "hello_world", "version": "1.0.0", "description": "hello world sample for NodeJS", "main": "app.js", "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs", "author": "SAM CLI", "license": "MIT", "dependencies": { "axios": "^0.21.1" }, "scripts": { "test": "mocha tests/unit/" }, "devDependencies": { "chai": "^4.2.0", "mocha": "^8.2.1" } }
hello-world/app.js
// const axios = require('axios') // const url = 'http://checkip.amazonaws.com/'; let response; /** * * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format * @param {Object} event - API Gateway Lambda Proxy Input Format * * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html * @param {Object} context * * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html * @returns {Object} object - API Gateway Lambda Proxy Output Format * */ exports.lambdaHandler = async (event, context) => { try { // const ret = await axios(url); response = { 'statusCode': 200, 'body': JSON.stringify({ message: 'hello world', // location: ret.data.trim() }) } } catch (err) { console.log(err); return err; } return response };
README.md
には、使い方が書かれています。
生成されたAWS Lambda関数を、TypeScriptで書き直す
この時点で、hello-world
ディレクトリ内にはNode.js向けのAWS Lambda関数があるわけですが、こちらをTypeScriptで
書き直していきます。
まずはディレクトリ内へ。
$ cd hello-world
tests
はいったん削除。
$ rm -rf tests
package.json
のdevDependencies
は、空にしておきます。dependencies
に書かれているaxiosも使わないのですが、
今回はこのまま残しておくことにします。
"dependencies": { "axios": "^0.21.1" }, "scripts": { "test": "mocha tests/unit/" }, "devDependencies": { }
TypeScriptとPrettierをインストール。
$ npm i -D typescript $ npm i -D -E prettier
$ mkdir src
TypeScriptの設定。
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" ] }
TypeScriptファイルのビルド結果は、dist
ディレクトリに出力するようにします。
Prettierの設定。
.prettierrc.json
{ "singleQuote": true }
ビルドとフォーマットをscripts
に定義。この内容は、また後で変更します。
"scripts": { "build": "tsc --project .", "format": "prettier --write src" },
Node.jsとAWS Lambdaの型宣言のインストール。
$ npm i -D @types/node@v14 $ npm i -D @types/aws-lambda
調べていて、aws-lambda
の型宣言の意味がずっとわからなかったのですが、README.md
に書かれていました。
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/README.md
Types helpful for implementing handlers in the AWS Lambda NodeJS runtimes, the handler interface and types for AWS-defined trigger sources.
aws-lambdaパッケージとは関係がなく、あくまで型宣言のみのようです。
scripts
も書き直して、この時点でこうなりました。
"dependencies": { "axios": "^0.21.1" }, "scripts": { "build": "tsc --project .", "format": "prettier --write src" }, "devDependencies": { "@types/aws-lambda": "^8.10.89", "@types/node": "^14.18.4", "prettier": "2.5.1", "typescript": "^4.5.4" }
TypeScriptで書き直したソースコード。元にしたJavaScriptファイルから、QueryStringを見るように少し変更しています。
src/app.ts
import { APIGatewayProxyEvent, Context } from 'aws-lambda'; export const lambdaHandler = async ( event: APIGatewayProxyEvent, context: Context ) => { try { let message; if (event.queryStringParameters && event.queryStringParameters['message']) { message = event.queryStringParameters['message']; } else { message = 'world'; } return { statusCode: 200, body: JSON.stringify({ message: `hello ${message}` }), }; } catch (err) { console.log(err); return err; } };
TypeScriptでのソースコードは書き終えたので、生成されていたJavaScriptファイルは削除します。
$ rm app.js
最後に、scripts
の内容をもう1度変更します。build
で、TypeScriptのビルド後にpackage.json
およびpackage-lock.json
も
dist
ディレクトリにコピーするように変更しておきます。
※ただ、sam build
の時はpackage-lock.json
を見ていないかも…
"scripts": { "build": "tsc --project . && cp package*.json dist", "format": "prettier --write src" },
ビルド。
$ npm run build
出力結果。
$ tree dist dist ├── app.js ├── package-lock.json └── package.json 0 directories, 3 files
これで、AWS Lambda関数側の準備は終わりました。
LocalStackにデプロイする
続いて、作成したAWS Lambda関数をLocalStackにデプロイします。
ひとつ上のディレクトリに移動して
$ cd ..
現時点でこのような構成になっています。
$ tree -L 3 . ├── README.md ├── events │ └── event.json ├── hello-world │ ├── dist │ │ ├── app.js │ │ ├── package-lock.json │ │ └── package.json │ ├── node_modules │ │ ├── @types │ │ ├── prettier │ │ └── typescript │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── app.ts │ └── tsconfig.json └── template.yaml 8 directories, 10 files
ここでtemplate.yaml
のCodeUri
を、hello-world
からhello-world/dist
に変更します。
Resources: HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: hello-world/dist Handler: app.lambdaHandler Runtime: nodejs14.x
これで、sam build
時にこの場所を見てくれるようです。
では、ビルド(LocalStackのAWS SAM CLIを使っていますが)。
$ samlocal build
このような表示になり
Building codeuri: /path/to/sam-typescript-example/hello-world/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 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
dist
ディレクトリに配置した内容が.aws-sam/build/HelloWorldFunction
に配置されているとともに、package.json
の
dependencies
の内容(今回はaxios
)がインストールされていることもわかります。
$ tree .aws-sam -L 4 .aws-sam ├── build │ ├── HelloWorldFunction │ │ ├── app.js │ │ ├── node_modules │ │ │ ├── axios │ │ │ └── follow-redirects │ │ └── package.json │ └── template.yaml └── build.toml 5 directories, 4 files
devDependendies
は含まれていませんね。
そしてここで気づいたのですが、package-lock.json
がありませんね…。package.json
のバージョン指定を注意して
おかないと、ここでモジュールのバージョンがずれるかもしれません…。
あとは、デプロイするだけです。Amazon S3バケットを作成して
$ awslocal s3 mb s3://my-bucket
デプロイ。
$ samlocal deploy --stack-name my-stack --region us-east-1 --s3-bucket my-bucket
Amazon API Gatewayの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 http://localhost:4566/restapis/$REST_API_ID/Prod/_user_request_/hello?message=lambda {"message":"hello lambda"}
OKですね。
まとめ
AWS SAMを使って、TypeScriptで書いたNode.jsアプリケーションをLocalStackにAWS Lambda関数としてデプロイして
みました。
package.json
の扱いと、template.yaml
のCodeUri
を調整すればなんとかなりそうなので、これで覚えておきましょう。