serverless.tsで書くAWS Lambda + Node.js + express

2022/05/12に公開

前置き

ServerlessFrameworkの設定ファイルをymlでなく、tsで書いたのでその備忘録です。
AWSに以下のコンポーネントを作成しています。

  • S3
  • IAM
  • Lambda
  • API Gateway
  • Cloud Watch Log Group

環境

  • macOS BigSur 11.6.2
  • npm v8.0.0
  • npx v8.0.0
  • node v14.x
  • Visual Studio Code 1.65.2

準備

AWS Lambda + Node.js + TypeScriptのテンプレート作成コマンドを実行(参考:公式ドキュメント)

npx serverless create --template aws-nodejs-typescript --path serverless-sample

serverless-sampleディレクトリが作成されるので、こちらのディレクトリに移動

cd serverless-template

すでにpackage.jsonが用意されているので、ライブラリをインストールする

npm i

AWSのアクセスキー・シークレットキーを用意し、以下のコマンドを実行

aws configure

以下の入力を求められるので、適当な値を入力

AWS Access Key ID [None]: <取得したアクセスキー>
AWS Secret Access Key [None]: <取得したシークレットアクセスキー>
Default region name [None]: <任意のリージョン>(例:ap-northeast-1)
Default output format [None]: <結果出力形式>(例:json)

試しにテンプレをデプロイする

npx sls deploy -r ap-northeast-1

結果に出力されるエンドポイントに向けてcurlを実行する

curl --location --request POST '{出力されたエンドポイント}' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Frederic"
}'

長めのレスポンスがjsonで返ってくる

{
  "message": "Hello Frederic, welcome to the exciting Serverless world!",
  "event": {
    "resource": "/hello",
    /* 中略 */
    "rawBody": "{\n    \"name\": \"Frederic\"\n}"
  }
}

S3

S3を見るとデプロイ用の素材を収めたバケットが生成されている。

バケット名をカスタマイズしたいので、serverless.tsを編集する

serverless.ts
provider: {
  deploymentBucket: { name: 'serverless-sample-bucket' }
}

上記の記載だけではバケットを作成してくれないので、以下のプラグインを使用する
serverless-deployment-bucket

serverless.ts
plugins: ['serverless-esbuild', 'serverless-deployment-bucket'],

また、以下のようにパッケージをインストールする

npm install serverless-deployment-bucket --save-dev

上記全てを実施した上でデプロイするとdeploymetBucketプロパティで定義した名前のバケットが生成される

IAMロール

デフォルトでは以下のIAMロール・ポリシーが作成されている

  • serverless-sample-dev-ap-northeast-1-lambdaRole
  • serverless-sample-dev-lambda
    • logs:CreateLogStream
    • logs:CreateLogGroup
    • logs:PutLogEvents

上記の内容を参考に自前でlambdaにアタッチするIAMロールを作成する

serverless.ts
resources: {
    Resources: {
      ServerlessSampleLambdaRole: {
        Type: 'AWS::IAM::Role',
        Properties: {
          RoleName: 'serverless-sample-lambda-role',
          AssumeRolePolicyDocument: {
            Version: '2012-10-17',
            Statement: [
              {
                Effect: 'Allow',
                Principal: {
                  Service: ['lambda.amazonaws.com']
                },
                Action: ['sts:AssumeRole']
              }
            ]
          },
          ManagedPolicyArns: [
            'arn:aws:iam::${aws:accountId}:policy/serverless-sample-lambda-managed-policy'
          ]
        },
        DependsOn: ['ServerlessSampleLambdaManagedPolicy']
      },
      ServerlessSampleLambdaManagedPolicy: {
        Type: 'AWS::IAM::ManagedPolicy',
        Properties: {
          ManagedPolicyName: 'serverless-sample-lambda-managed-policy',
          PolicyDocument: {
            Version: '2012-10-17',
            Statement: [
              {
                Sid: 'log',
                Effect: 'Allow',
                Action: ['logs:CreateLogStream', 'logs:CreateLogGroup', 'logs:PutLogEvents'],
                Resource: 'arn:aws:logs:${aws:region}:${aws:accountId}:log-group:/aws/lambda/serverless-sample-${sls:stage}'
              }
            ]
          }
        }
      }
    }
  }

resourcesプロパティ以下にAWSCloudFormationのテンプレート構文を記載して作成していきます。(詳細はこちら
lambdaで使用するiamの定義。roleにはテンプレート構文で定義したプロパティ名を使用可能

serverless.ts
provider: {
    iam: {
      role: 'ServerlessSampleLambdaRole'
    }
}

デプロイするとIAMロールとポリシーが作成され、lambdaに作成したロールがアタッチされている

Lambda + API Gateway

GETリクエストでjsonを返すシンプルなlambdaを作成する。
まず、functionsプロパティで以下を設定する

  • 名前 : nameプロパティで指定
  • ハンドラー : handlerプロパティで指定
  • API Gatewayのパス : eventsプロパティで指定
    • lambda内でパスのハンドリングを行うので、API Gatewayは全てのリクエストを通す設定とする
serverlss.ts
functions: {
  app: {
    name: '${sls:stage}-serveless-sample-lambda',
    handler: 'src/app.handler',
    events: [
      {
        http: {
          method: 'ANY',
          path: '{proxy+}'
        }
      }
    ]
  }
}

続いてhandlerで指定した階層(srcディレクトリ直下)に下記内容のapp.tsファイルを配置する

app.ts
'use strict'

import serverlessExpress from '@vendia/serverless-express'
import express from 'express'

const app = express()
app.get('/test', (_req, res) => {
  res.json({ sample: "serverless" });
  res.end();
})

export const handler = serverlessExpress({ app })

さらに、以下のnpmパッケージをインストールする

以上を実施し、デプロイコマンドを実行
API Gatewayができているので、ステージエディタのGETメソッドを開いたページに表示されているURLへGETリクエストする。({proxy+}の部分はapp.tsで設定したパスtestに変換)

curl https://{ランダム生成された値}.execute-api.ap-northeast-1.amazonaws.com/dev/test

以下のレスポンスが返ってくれば成功!

{"sample":"serverless"}

CloudWatchLogs

現時点で/aws/lambda/dev-serveless-sample-lambdaというロググループが作成されている
自動で生成されたロググループを上書きする場合、serverless.tsresources階層にextensionsプロパティを追加する -> 公式資料はこちら
以下は、ロググループの有効期限を上書きする例

serverless.ts
resources: {
  extensions: {
    AppLogGroup: {
      Properties: {
        RetentionInDays: 1
      }
    }
  }
}

Discussion