かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Serverless でも DynamoDB を使いたい!

前回までのあらすじ

TypeScript な Express を aws-serverless-express で Serverless 化して AWS Lambda にデプロイしました。
データベースを使えるように AWS の DynamoDB に接続できるようにしてみたメモ。

serverless.yml に DynamoDB の設定を追加する

serverless.yml に DynamoDB の設定・IAM role の設定を追加しておけば、デプロイした際に自動的に AWS に DynamoDB のテーブルが作成され接続できるようになるようです。

DynamoDB の設定・IAM role の設定をそれぞれ別のファイルで作成して serverless.yml で読み込ませるようにします。

serverless.yml に設定ファイルを読み込ませる設定を追加する

serverless.yml

provider:
  stage: ${opt:stage, 'dev'}
  environment:
    NODE_ENV: ${self:provider.stage}
    # process.env.DYNAMODB_TABLE でテーブル名を取得できるようにする
    DYNAMODB_TABLE: TABLE_NAME-${opt:stage, self:provider.stage}
  # IAM role の設定を読み込む
  iamRoleStatements: ${file(./config/iam.yml)}

resources:
  # DynamoDB の設定を読み込み
  - ${file(./config/dynamodb.yml)}

DynamoDB の設定

./config/dynamodb.yml

Resources:
  MtDynamoDbTable:
    Type: 'AWS::DynamoDB::Table'
    DeletionPolicy: Retain
    Properties:
      AttributeDefinitions:
        -
          AttributeName: id
          AttributeType: S
      KeySchema:
        -
          AttributeName: id
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
      TableName: ${self:provider.environment.DYNAMODB_TABLE}

この設定の基づき DynamoDB が作成される。
データベースのテーブル名は読み込み元の serverless.yml にある environment.DYNAMODB_TABLE から取得される。

IAM role の設定

./config/iam.yml

- Effect: Allow
  Action:
    - dynamodb:Query
    - dynamodb:Scan
    - dynamodb:GetItem
    - dynamodb:PutItem
    - dynamodb:UpdateItem
    - dynamodb:DeleteItem
  Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"

DynamoDB を操作できる IAM role を設定

webpack で DYNAMODB_TABLE をExpress に渡す設定

webpack.EnvironmentPlugin を使うことで serverless.ymlprovider.environment の値を process.env にマージしてプログラムに渡すことが出来る

webpack.config.js

const webpack = require('webpack');
const slsw = require('serverless-webpack');

const enviroment =
  slsw.lib.serverless.service.provider.environment.NODE_ENV || 'dev';

module.exports = {
  entry: slsw.lib.entries,
  // …
  plugins: [
    new webpack.EnvironmentPlugin(
      slsw.lib.serverless.service.provider.environment,
    ),
  ],
  // …
}

cf. TypeScript な Express を aws-seveless-express で Serverless にして AWS Lambda にデプロイするまでの記録 - かもメモ

DynamoDB に接続するサンプルのエンドポイントを作成

aws-sdk をインストール

$ npm install --save aws-sdk

/src/app.ts

import express from 'express';
import * as AWS from 'aws-sdk';
import { v4 as uuid } from 'uuid';

const dynamoDB = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10' });
const TABLE_NAME = process.env.DYNAMODB_TABLE || 'LOCAL_TABLE_NAME';

const app = express();
const router = express.Router();

// ...

router.get(
  '/scan',
  async (req: express.Request, res: express.Response): Promise<void> => {
    try {
      const items = await dynamoDB
        .scan({
          TableName: TABLE_NAME,
        })
        .promise();

      res.send(items);
    } catch (error) {
      res.status(400).send(error);
    }
  },
);

router.get(
    '/test',
  async (req: express.Request, res: express.Response): Promise<void> => {
    try {
      const item = {
        id: uuid(),
        param: 'test',
      };

      await dynamoDB
        .put({
          TableName: TABLE_NAME,
          Item: item,
        })
        .promise();

      res.send(item);
    } catch (error) {
      res.status(400).send(error);
    }
  },
);

// …

AWS に deploy

$ sls deploy --stage prod -v

デプロイが完了したら AWS コンソールで DynamoDB のテーブルが作成されていることを確認。
API Gateway で表示された http://エンドポイントのURL/test で DynamoDB へのデータ作成・ http://エンドポイントのURL/scan で DynamoDB に作成されたデータが表示されていれば OK

Local 環境での DynamoDB との共存

local環境は Docker で作成していました。
--stage prod の時は AWS の DynamoDB に接続して、--stage dev の時は Docker 上の DynamoDB に接続するようにします。

Docker 上の DynamoDB に接続するには設定が必要なので process.env.NODE_DEVprod で無ければ接続先を Docker 上の DynamoDB に向ける設定をすれば OK

/src/app.ts

import express from 'express';
import * as AWS from 'aws-sdk';
import { v4 as uuid } from 'uuid';

if (process.env.NODE_ENV !== 'prod') {
  const config = {
    endpoint: 'http://localhost:XXXX', // docker 上の DynamoDB のエンドポイントURL
    region: 'northeast-1',
  };
  AWS.config.update(config);
}

const dynamoDB = new AWS.DynamoDB.DocumentClient({ apiVersion: '2012-08-10' });

これで local 動作させる時は Docker の DynamoDB を使用できるようになりました!
₍ ᐢ. ̫ .ᐢ ₎👌 ヤッタネ

やっとアプリを開発できる下準備が出来ました…
後は画像を使いたい場合に S3 にアップロードする方法とかが必要になってきそうですが、それは追々…


[参考]

真夜中のダイナモ

真夜中のダイナモ

Serverless-layers で node_modules を Lambda にデプロイするパッケージから除く

前回までのあらすじ

TypeScript な Express アプリを aws-severless-express で Serverless 化して AWS にデプロイする所までできました。
しかしデプロイしたパッケージ (zip) には node_modules のファイルが含まれており、サイズが巨大なものになっていました。

serverless-layers で node_modules を Lambda にデプロイするパッケージと分離する

Lambda Layers

Lambda Layersとは、複数のLambda関数で外部ライブラリやビジネスロジックを共有できる仕組みです。
使用するライブラリや共通のビジネスロジックをZIPアーカイブし、Layerに追加することができます。 デプロイパッケージの容量を少なくすることができるためLambdaのソースコードの管理が楽になります。 また依存関係をインストールしてパッケージ化する際に発生するエラーを回避することや、ライブラリのビルドの手順を省くこともできます。
cf. AWS Lambda Layersでライブラリを共通化 - Qiita

serverless-layers の導入方法

  1. Lambda Layers をデプロイする S3 バケットを作成
  2. serverless-layers パッケージのインストール
  3. serverless.yml に設定を追加
  4. デプロイ

1. Layers をデプロイする S3 バケットを作成

AWS コンソールにアクセスして S3 に適当なバケットを作成
作成したバケット名を控えておく。 アクセス権限などはデフォルトのままでOK。

2. serverless-layers のインストール

$ npm install --save-dev serverless-layers

cf. Serverless Layers

3. serverless.yml に設定を追加

serverless.yml

plugins:
  - serverless-webpack
  - serverless-offline
+  - serverless-layers

custom:
  webpack:
    webpackConfig: 'webpack.serverless.config.js'
-    includeModules: true
+  serverless-layers: 
+    layersDeploymentBucket: S3_BUCKET_NAME

serverless-layers が node_modules を分離するので、webpack で externals: [nodeExternals()] する為にモジュールを組み込むようにしていた includeModules: true の指定は削除。(この指定があると Layers に node_modules がアップロードされた上で、node_modules が含まれたパッケージが Lambda にデプロイされてしまいました)

S3 のバケット名を git 管理しないようにする

serverless.yml に直接 S3 のバケット名を書いてしまうと git 管理しているとバケット名がそのまま GitHub に載ってしまったりするので、gitignore したファイルに設定を書いて、${file()} を使って serverless.yml で読み込むようにします。

.gitignore

config/**/*.yml

config/serverless-settings.yml

layersDeploymentBucket: S3_BUCKET_NAME

serverless.yml

custom:
  serverless-layers:
    layersDeploymentBucket: ${file(./config/serverless-settings.yml):layersDeploymentBucket}

4.デプロイ

ここまでの設定ができたら実際に AWS にデプロイしてみます

$ sls deploy --stage prod -v

デプロイが完了したら AWS コンソールから S3 にアクセスすると、Layers 用のバケットに node_modules が zip 化されたファイルがアップロードされ、Lambda 関数で呼び出されるバケットには node_modules が除外されたアプリの zip ファイルがアップロードされていることが確認できます。
そして、API Gateway で表示されるエンドポイントにアクセスすると、express が問題なく動作していることが確認できました。

所感

設定をして deploy すれば自動的に Layers 用のバケットが作られるものだと思っていたので、AWS コンソールから先に S3 バケットを作成しなければならならない部分が解るまでに少し時間がかかってしまいましたが、それ以外は簡単に巨大になりがちな node_modules を分離することが出来てよかったです!

Next Step
  • DynamoDB との接続

[参考]

AWSによるサーバーレスアーキテクチャ

AWSによるサーバーレスアーキテクチャ

  • 作者:Peter Sbarski
  • 発売日: 2018/03/14
  • メディア: 単行本(ソフトカバー)

TypeScript な Express を aws-seveless-express で Serverless にして AWS Lambda にデプロイするまでの記録

TL;DR

AWS も Lambda も自分で調べて〜なレベル感なのでなんも分かってません。
とりあえずガチャガチャやってみて、動いてるんじゃね?となったので、そこまでもプロセスをまとめただけのエントリーになります。なので、たぶん超長い記事になってると思います。 :pray:

Serverless 化する TypeScript な Express の構成

👇 前回までのあらすじ

構成はこんな感じ

/api
  |- /env
  |    |- .env.dev
  |    |- .env.prod
  |- /src
  |    |- index.ts // Express server root
  |- /webpack // このディレクトリ下に webpack の設定ファイル
  |    |- base.config.js
  |    |- // … 略
  |- package.json
  |- tsconfig.json

AWS Cli でユーザーを設定する

AWS cli をインストール

homebrew でインストールできるっぽい

$ brew install awscli

Docker 上で動かすこともできるみたいだけど、ちょっと面倒そうなので

ユーザーの設定

対話式で default ユーザーの設定ができる

$ aws configure
AWS Access Key ID:
AWS Secret Access Key:
Default region name: 
Default output format: 

AWS 上に作成した IAM ユーザーの AWS Access Key ID, AWS Secret Access Key を指定。
Default region は ap-northeast-1, Default output format は json を指定しました。

~/.aws/credentialsAccess Key ID, Secret Access Key の設定が、~/.aws/config に region と output format の設定のファイルが作成される

~/.aws/credentials

[default]
aws_access_key_id = DEFAULT_USER_ACCESS_KEY_ID
aws_secret_access_key = DEFAULT_USER_SECRET_ACCESS_KEY

~/.aws/config

[default]
region = ap-northeast-1
output = json

このファイルにある [default] が以降で使われる serverless で使われるユーザーの profile 名になる


Serverless パッケージのインストール

$ npm install -g serverless
$ sls -v

ドキュメントにあるように global に serverless コマンド (sls) が使えるようにインストール。
あまり global に色々入れたくないので、local にインストールしても大丈夫なのかな?

aws-serverless-express で Serverless 化する

1つのエンドポイントで全てのパスを受け止めて Lambda 関数にパスして内部で動作している Express でそれぞれのURLに合う処理を行うようにするっぽい cf. AWS LambdaとNuxt.jsでServer Side Renderingする(2020年版) - Sweet Escape

パッケージのインストール

$ npm install --save aws-lambda aws-serverless-express
$ npm install --save-dev @types/aws-lambda @types/aws-serverless-express

serverless.yml

severless コマンドやデプロイする際に使われる設定っぽい。(正確に理解できているわけではない)

service: my-serverless-app

provider:
  name: aws
  runtime: nodejs12.x
  # デフォルトが us-east-1 なのでTOKYOリージョンを指定 
  region: ap-northeast-1
  # --stage オプションの値をセット、オプションがない場合は dev を使用
  stage: ${opt:stage, 'dev'}
  # .aws/credentials の [default] ユーザーを使用
  profile: default

# Lambda の設定
functions:
  app:
    # `./src/handler.ts` の handler 関数をエンドポイントにする
    handler: src/handler.handler
    timeout: 30
    # 全ての URL を受け止めて Lambda に渡す
    events:
      - http: ANY /
      - http: 'ANY {proxy+}'

# デプロイパッケージの設定
package:
  exclude: # デプロイパッケージから除外
    - '**'
  include: #  デプロイパッケージに含める
    - dist/**

TypeScript な Express を webpack でビルドしていたので、package 設定で、serverless.yml のあるディレクトリを全て除外した後に webpack でビルドされる dist ディレクトリ配下のみが対象となる設定にしました。

Lambda 用のエントリーポイントを作成する

API Gateway で受け止めた URL を Lmabda が ./src/hander.tshandler 関数に渡し、この関数を通じて Express アプリにアクセスします。

Express のファイルを分割する

local で作成していた際は app.listen(PORT)localhost を立ち上げてアプリを作成していましたが、Serverless にした際にこの app.listen(PORT)localhost が動作する処理が残っているとエラーが発生して上手く動作しなかったので、local での開発時だけ app.listen(PORT) できるように ./src/index.ts を分割します。

./src/app.ts … local, Serverless 共通で使用する Express を export する

import express from 'express';
const app = express();
const router = express.Router();

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', (req: express.Request, res: express.Response) => {
  const { method, path, query } = req;

  return res.send(
    JSON.stringify({
      message: 'Hello World Serverless!',
      method,
      path,
      query,
    }),
  );
});

// Routing
app.use('/', router);

export default app;

./src/index.ts … local 開発時のエントリーポイント

import app from './app';
const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`listening on port http://localhost:${PORT}`);
});

app.ts の Express を app.listen() で動かすだけ

Lambda から呼び出される hander 関数を作成

./src/handler.tsAPI Gateway から渡されるイベント URL を hander 関数で受け止めて app.ts の Express に渡す

import { APIGatewayEvent, Context, Handler } from 'aws-lambda';
import * as awsServerlessExpress from 'aws-serverless-express';
import app from './app';
const server = awsServerlessExpress.createServer(app);

export const handler: Handler = (event: APIGatewayEvent, context: Context) => {
  awsServerlessExpress.proxy(server, event, context);
};

serverless-offline で Local でも Serverless で実行できるようにする

パッケージのインストール

$ npm install --save-dev serverless-offline

serverless.yml に下記を追加

plugins:
  - serverless-offline

offline での実行

$ sls offline
…
offline: [HTTP] server ready: http://localhost:3000 🚀

デフォルトの --stage dev の場合は http://localhost:3000/dev が Serverless のエンドポイントになっっている


Lambda で関数が呼べるように webpack する

TypeScript で書いていたので Serverless としてデプロイするにもビルドする必要があるだろうと思い Lambda 用に作成した handler.ts をエントリーポイントにした webpack.config.js を作成してビルドしてみたのですが、通常の webpack でビルドすると出力されるプログラムは !function(){} な無記名関数の中にカプセル化されてしまうようで、serverless-offline で試してみようとしたら Serverless の handler になる関数にアクセスできず 404 が返ってくるだけになってしまいました…

serverless-webpack を使う

Serverless で使えるようにいい感じに webpack してくれるっぽい

パッケージのインストール

$ npm install --save-dev serverless-webpack

serverless.yml を編集

service: my-serverless-app

plugins:
  - serverless-offline
+  - serverless-webpack

+ custom:
+   webpack:
+     webpackConfig: 'webpack.serverless.config.js'
+     includeModules: true

provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  stage: ${opt:stage, 'dev'}
  profile: default

# Lambda の設定
functions:
  app:
    # `./src/handler.ts` の handler 関数をエンドポイントにする
    handler: src/handler.handler
    timeout: 30
    # 全ての URL を受け止めて Lambda に渡す
    events:
      - http: ANY /
      - http: 'ANY {proxy+}'

- # デプロイパッケージの設定
- package:
-   exclude: # デプロイパッケージから除外
-     - '**'
-   include: #  デプロイパッケージに含める
-     - dist/**
  • custom.webpack.webpackConfig … serverless-webpack で使用する webpack の設定ファイル。指定がない場合は webpack.config.js が使用される
  • custom.webpack.includeModules … webpack 側で webpackl-node-externals を使用して node_modules を除外する場合は、includeModulestrue にしておく必要があるみたいです。cf. Serverless Webpack の使い方まとめ - Qiita

serverless-webpack でビルドされたものが sls offline, sls deploy の対象になるようだったので、package の設定は不要なので (たぶん) 削除しました。

severless 用の webpack config ファイルの作成

custom.webpack.webpackConfig で指定した設定ファイル webpack.serverless.config.js を作成します

webpack.serverless.config.js

const webpack = require('webpack');
const slsw = require('serverless-webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const Dotenv = require('dotenv-webpack');

const enviroment = process.env.NODE_ENV || 'dev';

module.exports = {
  mode: 'production',
  // serverless.yml で handler に指定したファイルが自動的にエントリーポイントに出来るっぽい
  entry: slsw.lib.entries,
  target: 'node',
  externals: [nodeExternals()],
  plugins: [
    new Dotenv({
      path: path.resolve(__dirname, `../env/.env.${enviroment}`),
    }),
  ],
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
        options: {
          configFile: 'tsconfig.json',
        },
      },
    ],
  },
  resolve: {
    extensions: ['.ts', '.js', '.json'],
    alias: {
      '@': path.join(__dirname, '/src/'),
    },
  },
};

serverless-webpack でビルド

# stage prod でビルド
$ sls webpack --stage prod
# stage dev でビルド
$ sls webpack --stage dev

.webpack ディレクトリにビルドされたファイルが、.serverless ディレクトリに zip 化されたファイル (serverless にデプロイされるファイル) が生成されます。


serverless-webpack でビルドする際にも process.env に値を渡したい

$ NODE_ENV=dev sls webpack --stage dev

上記のように NODE_ENV= を先頭につければ sls webpack でも process.env に値を渡すことができましたが、折角 stage オプションがあり二重っぽいので、serverless-webpack でビルドする際は stage オプションを NODE_ENV としてプログラムに渡せるようにしたいと思います。

serverless.yml の値を webpack.config で取得してプログラムに渡す

slsw.lib.serverless.service で webpack の設定で serverless.yml のプロパティにアクセスできるので、serverless.yml に NODE_ENV プロパティを作成して、webpack で process.env にしてプログラムに渡せる方針にします。

serverless.yml stage の値を NODE_ENV プロパティに設定する
provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  stage: ${opt:stage, 'dev'}
  profile: default
+  environment:
+     NODE_ENV: ${self:provider.stage}

--stage オプションで渡された値が provider.environment.NODE_ENV にセットされる

webpack.EnvironmentPlugin を使って provider.environment の値を process.env にマージしてプログラムに渡す

webpack.serverless.config.js

const webpack = require('webpack');
const slsw = require('serverless-webpack');
// …

const enviroment =
  slsw.lib.serverless.service.provider.environment.NODE_ENV ||
  'dev';

module.exports = {
  // …
  plugins: [
    new Dotenv({
      path: path.resolve(__dirname, `../env/.env.${enviroment}`),
    }),
    // serverless.yml の provider.environment の値をオブジェクトとして process.env にマージしてプログラムに渡す
    new webpack.EnvironmentPlugin(
      slsw.lib.serverless.service.provider.environment,
    ),
  ],
  //...
};

これで severless でビルドする際も /src 内のプログラムで local で開発しているときと同様に process.env.NODE_ENV が扱えるようになりました。


Deploy

$ sls deploy --stage prod -v

sls deploy コマンドを実行すると sls webpack が実行された後にビルドされた zip が AWS にデプロイされる。
-v オプションを付けると途中経過がターミナル上で確認できる。

サービスの削除

デプロイしたサービスを削除する場合は sls remove コマンドを実行すれば OK

$ sls remove -v

コマンドでは削除しきれないものもあるらしいので、最終的には AWS のコンソールで確認するのが良さそうです。(理解が浅いがゆえに謎課金が発生するの精神的気に結構辛い〜)


所感

検索 + タブ開きまくりでなんとか TypeScript な Express を Serverless 化して AWS にデプロイ・ローカルでも serverless-offline での実行が出来る状態にすることができました。 AWS なんもわからん + Serverless 初挑戦で理解が足りないことが二重・三重になってしまっていたので、この開発とデプロイができる環境を作ること自体にすごく時間がかかってしまって、肝心のサービスの中身はまだ何も手がつけられていないという状態… 取り敢えずサービスをリリースしたいという目的が優先なら知見のある技術選定でまずはサクッと作ってしまうのが大事だと感じました。

Next Step

  1. ビルドされた zip が node_modules を含んでいて巨大なので serverless-layers を使えば node_modules を s3 に分離してアップロードできそうなので試してみる
  2. local / デプロイした状態で DynamoDB が使えるようにする

[参考]

AWS Lambda実践ガイド (impress top gear)

AWS Lambda実践ガイド (impress top gear)

  • 作者:大澤文孝
  • 発売日: 2017/10/16
  • メディア: 単行本(ソフトカバー)