AWS SAM 使って Lambda をデプロイする with TypeScript

2022-06-27
2022-07-26

動機・概要

外部アプリケーションとの連携で使用する

flowchart LR A["API Geteway"]-->B["トークンベース認証"]-->C["AWS Lambda"]

インストール

macOSにインストール

bash
brew install docker brew tap aws/tap brew install aws-sam-cli
bash
sam --version > SAM CLI, version 1.36.0
bash
brew upgrade aws-sam-cli

プロジェクトの作成

bash
sam init

✅ TypeScriptのTemplateがあったので、選択する。(これにてTypeScriptの設定は完了)

bash
...省略 AWS quick start application templates: 1 - Hello World Example 2 - Hello World Example TypeScript 3 - Step Functions Sample App (Stock Trader) 4 - Quick Start: From Scratch 5 - Quick Start: Scheduled Events 6 - Quick Start: S3 7 - Quick Start: SNS 8 - Quick Start: SQS 9 - Quick Start: Web Backend

権限の付与

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-permissions.html

AWSCloudFormationFullAccess IAMFullAccess AWSLambda_FullAccess AmazonAPIGatewayAdministrator AmazonS3FullAccess AmazonEC2ContainerRegistryFullAccess

開発環境で起動する

templateからプロジェクトを作成すると、hello-wordsが作成される

bash
cd hello-words && yarn install # 実行しないと sam build で esbuild でエラーになる sam build

コードの変更

Hot Reloadはなかったので、sam buildを再実行する必要がある。 (sam build --beta-featuresでyesを選択すると、作成したfuntionに対してesbuildを実行してくれるので、yarn compileの実行の必要なさそう)

bash
sam build > You can also enable this beta feature with 'sam build --beta-features' yes Running NodejsNpmEsbuildBuilder:CopySource Running NodejsNpmEsbuildBuilder:NpmInstall Running NodejsNpmEsbuildBuilder:EsbuildBundle
bash
sam build --beta-features

起動

bash
sam local start-api > localhost:3000 で起動する

event.json

localでenvの設定

bash
sam local invoke --env-vars env.json
json
{ "MyFunction1": { "TABLE_NAME": "localtable", "BUCKET_NAME": "testBucket" }, "MyFunction2": { "TABLE_NAME": "localtable", "STAGE": "dev" } }
template.yml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Runtime: nodejs10.x Handler: index.handler Timeout: 10 MemorySize: 1024 Environment: Variables: DYNAMO_ENDPOINT: DUMMY_ENDPOINT Resources: A001: Type: AWS::Serverless::Function Properties: CodeUri: ../build/A001/ Events: HelloWorld: Type: Api Properties: Path: /groups Method: GET Environment: Variables: TABLE_GROUP_WORDS: DUMMY_GROUP_WORDS

invoke: 呼び出しの意味

まとめ

bash
sam build sam local invoke --env-vars env.json sam local start-api
bash
sam local start-api --env-vars env.json

表示されるようになった

ts
const privateKey = process.env.PRIVATE_KEY; console.log('privateKey:', privateKey) > AAAAAAA

template.ymlを変更したら、sam build

ソースコードを変更したら、sam build --beta-features

envを渡したい場合は、--env-vars env.jsonをつける。

Lambda認証

1.トークンベース(通常Bearerトークンが設定されるヘッダーです。) 2.リクエストパラメータベース

認証情報に複数の値は使用しないので、トークンベース認証で実装してみる。

ローカルではサポートされていないので、デプロイして確認する。 (localhost - SAM Local doesn't appear to be running Authorizer functions - Stack Overflowから)

bash
sam deploy --guided # aws credentialsの設定をしていない場合は、 aws configure

Secret Managerを使う

トークン認証用のSECRETを作成する。

bash
openssl rand -hex 32
aws secretsmanager create-secret \ --name 'private-key' \ --secret-binary file://./private-key.pem aws secretsmanager get-secret-value \ --secret-id private-key

aws secretsmanager get-secret-value
--secret-id private-key

Corsの回避

yml
Globals: Function: Timeout: 3 Api: Cors: AllowOrigin: "'*'" AllowMethods: "'OPTIONS,POST'" AllowHeaders: "'Content-Type,X-CSRF-TOKEN'" Resources: ExampleFunction: 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: app/ Handler: app.lambdaHandler Runtime: nodejs14.x Architectures: - x86_64 Environment: Variables: PRIVATE_KEY: PRIVATE_KEY Events: RootEndpoint: Properties: Method: any Path: / Type: Api EverythingElse: Properties: Method: any Path: /{proxy+} Type: Api

responseにも設定してみた

ts
response = { statusCode: 200, body: JSON.stringify({ token: token, }), headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'OPTIONS,POST', }, };

AddDefaultAuthorizerToCorsPreflight: falseと下記の設定を行う。

yaml
Api: Cors: AllowOrigin: "'*'" AllowMethods: "'OPTIONS,POST'" AllowHeaders: "'Origin, X-Requested-With, Content-Type, Accept,

express - Request header field Access-Control-Allow-Headers is not allowed by itself in preflight response - Stack Overflow

responseにもくっつける

ts
const headersOption = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization', 'Access-Control-Allow-Methods': 'POST,OPTIONS', };

Warningの回避

Your template contains a resource with logical ID "ServerlessRestApi", which is a reserved logical ID in AWS SAM. It could result in unexpected behaviors and is not recommended.
botocore.exceptions.NoCredentialsError: Unable to locate credentials
bash
aws configure

でconfigurationを設定する

samの検証

sam validate --profile=${PROFILE}

Preflight Request

Preflight request (プリフライトリクエスト)に関しては、SAMでAPI Gateway認証をする場合は、設定をfalseにするのが良さそう。

Corsについては下記記事がすごく分かりやすかった

なんとなく CORS がわかる...はもう終わりにする。 - Qiita

Templateを指定してデプロイ

templateとSecret Managerを使用して、環境別のSecret類を管理するのが楽だと感じた。

bash
sam deploy -t ./template.yaml sam deploy -t template.production.yaml

envを渡す場合

下記のように設定すると、sam deploy時にenvを渡すことができる。

yaml
Globals: ... Parameters: # 👈 SecretMangerId: Type: String Description: Secret Manager ID Default: staging/my-secret Origin: Type: String Description: Origin Default: http://localhost:3000 Resources: AuthorizerFunction: Properties: Environment: # 👈 Variables: SECRET_MANAGER_ID: !Ref SecretMangerId ORIGIN: !Ref Origin

sam deployの具体的なコマンドはこちら

bash
SECRET_MANAGER_ID="staging/my-secret" ORIGIN="http://localhost:3000" sam deploy --force-upload --parameter-overrides SecretMangerId=$SECRET_MANAGER_ID Origin=$ORIGIN

Lambda内での取得。dotenvなどはinstallする必要はなかった。

ts
const origin = process.env.ORIGIN; const secretManagerId = process.env.SECRET_MANAGER_ID; if (!origin || typeof origin !== 'string') { throw new Error('ORIGIN is not defined'); } if (!secretManagerId || typeof secretManagerId !== 'string') { throw new Error('SECRET_MANAGER_ID is not defined'); }

aws-sdkでsecretManagerIdを引数に渡すときになぜかエラーが発生したので、toString()を使用した。

ts
import * as AWS from 'aws-sdk'; export const secretsManagerClient = async (secretManagerId: string) => { try { const secretsManager = new AWS.SecretsManager({ region: 'us-east-1', }); const response = await secretsManager .getSecretValue({ // NOTE: toString()をつけないと { message: 'Invalid name. Must be a valid name containing alphanumeric characters, or any of the following: -/_+=.@!' } が返ってくる SecretId: secretManagerId.toString(), // 👈 ここ }) .promise(); if ('SecretString' in response) { return response.SecretString; } else { throw new Error('Decrypting secrets failed'); } } catch (err) { return JSON.stringify({ err }, null, 2); } };

Option系

yaml
# Cache Time to live in seconds # https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-property-api-lambdatokenauthorizationidentity.html Identity: ReauthorizeEvery: 0

Corsの設定

CorsConfiguration - AWS Serverless Application Model

ドキュメント

メモ

  • stacknameが同じ場合、上書きされるので注意

https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html#w4076aac38c11c12c25