ローカルで TypeScript + AWS SAM (Lambda/DynamoDB) の開発環境を構築する

## はじめに

ナビタイムジャパン/ACTS(研究開発部門)アプリグループの木村です。

私からは、来る12/15に行われる [Developer Boost](https://event.shoeisha.jp/devboost/20181215) にて発表させて頂く、「サーバーレス+SPAでつくる！ユーザーご意見管理システム」のセッションから、
AWS SAMを用いたサーバーレスAPIの開発環境を構築する方法について紹介させていただきます。

普段サーバーサイドを開発している方でも、「サーバーレスアプリってどうやって作るの？」と疑問を持たれる方は少なくないと思います。(実際私もそうでした。)

この記事を読んで、開発のイメージを掴んでいただけるとうれしいです。

## AWS SAMとは？

[公式サイト](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/serverless_app.html)には、以下のように記載されています。

> AWS サーバーレスアプリケーションモデル (AWS SAM) はサーバーレスアプリケーションを定義するモデルです。

…そのまんまですね。

つまるところ、サーバーレスアプリケーションを構成するために必要な**各要素の関係・定義をまとめた**ものです。

例えば、AWS上でサーバーレスのAPIを構築する場合には、以下のようなサービスを用います。

* API Gateway
* Lambda
* DynamoDB

これらのサービスを連携するためには、通常はマネジメントコンソール上で必要なものを都度作成し、連携する設定をポチポチ進めていくことになります。

しかし、アプリケーションがスケールしていくと、これらの設定を都度していくのは非常に運用コストがかかります。
特に、別環境に同じようなアプリケーションを構築する必要が出た場合、同じ作業をひたすら繰り返して…となると、つらみのある作業になりますよね。

AWS SAMでは、上に挙げたサービスがそれぞれどういった定義で、どういった関係を持っているのかをYAMLで定義することができ、CloudFormationを利用して一括でデプロイできる環境を提供してくれます。

なお、AWSだけでなくGCPやAzureでも利用できる、同じようなフレームワークとして、[Serverless](https://serverless.com/)と呼ばれるものも有名です。

## 実際に作ってみる

前置きはこのあたりにして、実際にローカルでサーバーレスアプリケーションの開発環境を構築してみましょう。

今回は、よくあるTodo管理のAPIを題材として進めたいと思います。

流れは以下のような感じです。

1. プロジェクトのベースをつくる
2. APIの本体となるLambdaの実装をつくる
3. SAMでAPI定義を作成し、APIを立ち上げる
4. ローカルでDynamoDBを立ち上げ、連携する

### 必要な環境

事前に、開発するPCで下記のセットアップを済ませておきます。
※なお、今回の環境についてはMacで動作確認済です。(Windowsでは未確認です。スミマセン)

* Node.js
  * バージョンはとりあえずv.8系以降ならOK
* Docker
* [aws-cli](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/installing.html)
  * Configureについては、サンプルのものをそのまま適用しておけばOKです。
* [aws-sam-cli](https://github.com/awslabs/aws-sam-cli/blob/develop/docs/installation.rst)


### (1)プロジェクトのベースをつくる

今回メインで実装するLambdaの実行環境は、Node.jsのv8.10を採用しました。


生のJavaScriptをゴリゴリ書いていくのもよいですが、最近はTypeScriptを用いて型のある幸せな世界で開発できる環境が整っていますので、

* 開発はTypeScript
* 実行時はJavaScriptにトランスパイルしたものを利用

という状態にします。

TypeScriptの開発環境を整えるには、Nodeのプロジェクトでwebpackをモジュールバンドラとして利用するのが世間一般的にもしっくり来るかと思いますので、そのように進めます。

まずは初期化。

```shell
$ mkdir aws-sam-local-api
$ cd aws-sam-local-api
$ npm init -y
```

続いて、必要なモジュールのインストールを行います。

```shell
$ npm i -D webpack webpack-cli typescript ts-node awesome-typescript-loader aws-sdk glob
$ npm i -D @types/node @types/webpack @types/aws-lambda @types/glob
```

※tslint, prettierの設定はお好みでどうぞ。

続いて、 `webpack.config` の設定です。  
今回は、APIの1エンドポイントにつき、1つのLambda関数を割り当てるような構成にしていきます。

通常ですと、webpackを用いるときは1つのentryに集約されることがおおいですが、今回は複数entryを扱えるような構成で設定を記述していきます。

```typescript:webpack.config.ts
import * as Webpack from 'webpack';
import {resolve} from 'path';
import {sync} from 'glob';

/** ビルド対象ルートディレクトリ */
const SRC_PATH = resolve(__dirname, './src/functions/');
/** entryとなるファイル名 */
const ENTRY_NAME = 'index.ts';
/** ビルド結果出力先 */
const BUILT_PATH = resolve(__dirname, './built');
/** ビルド種別 */
const BUILD_VARIANT = process.env.NODE_ENV;

/**
 * ビルド対象のentryを解決する
 * @returns {Webpack.Entry} entry
 */
const resolveEntry = (): Webpack.Entry => {
  const entries: {[key: string]: string} = {};
  const targets: string[] = sync(`${SRC_PATH}/**/${ENTRY_NAME}`);
  const pathRegex = new RegExp(`${SRC_PATH}/(.+?)/(.+?)/${ENTRY_NAME}`);
  targets.forEach((value: string) => {
    let key: string;
    switch (BUILD_VARIANT) {
      case 'production':
        key = value.replace(pathRegex, 'prd_$1_$2/index');
        break;
      case 'development':
        key = value.replace(pathRegex, 'dev_$1_$2/index');
        break;
    }
    entries[key] = value;
  });

  return entries;
};

const config: Webpack.Configuration = {
  target: 'node',
  mode: BUILD_VARIANT === 'production' ? 'production' : 'development',
  resolve: {
    extensions: ['.ts', '.js']
  },
  entry: resolveEntry(),
  output: {
    filename: '[name].js',
    path: BUILT_PATH,
    library: '[name]',
    libraryTarget: 'commonjs2'
  },
  module: {
    rules: [
      {
        test: /\.ts?$/,
        loader: 'awesome-typescript-loader'
      }
    ]
  }
};

export default config;
```

(2019/07/25追記: `pathRegex` の正規表現が間違っていたため修正しました)

`src` 配下のディレクトリ構成を、 `src/functions/(関数名)/index.ts` となるように作成しておき、 `index.ts` 内にLambdaの実装をしていくことで、
最終的にバンドルされた結果(の各Lambda関数)が `built/(関数名)/index.js` として出力されます。

ここの(関数名)の部分のディレクトリ構成をもう一段階掘って、
`src/functions/(分類)/(関数名)index.ts` → `built/(分類)_(関数名)/index.js`
として出力するなど調整できます。
ディレクトリ構成が実行するLambda関数の命名になるので、各プロジェクトでどのように開発していくか次第でentryの解決方法を調整することをおすすめします。


次に、TypeScriptの設定を `tsconfig.json` に追加していきます。
こちらも、 `noImplictAny` などのオプションはお好みでどうぞ。

```json:tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "outDir": "./built",
    "allowJs": true
  },
  "include": [
      "./src/**/*"
  ]
}
```

最後に `package.json` に、実装したTSファイルを保存に自動でビルドするようにwatchタスクを追加しておきましょう。

```json:package.json
  "scripts": {
    "watch": "NODE_ENV=development webpack --watch"
  }
```

ここまでで、プロジェクトの基本的なセットアップは完了です。

現段階での構成は↓のような感じ。

```txt
.
├── package-lock.json
├── package.json
├── src
├── tsconfig.json
├── tslint.json
└── webpack.config.ts
```

※ `package-lock.json` は自動で生成されているはずです。

### (2)APIの本体となるLambdaの実装をつくる

さて、ここからメインの実装に入ってきます。

まず最初は簡単に、 `GET /ping` のAPIの実装を作ってみましょう。
`src/functions/get_ping/` 配下に、 `index.ts` を作成します。

```typescript:src/functions/get_ping/index.ts
import {APIGatewayEvent, Context, Callback} from 'aws-lambda';

const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const result = {
    status: 200,
    message: 'OK!!!!!'
  };

  callback(null, {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json;charset=UTF-8'
    },
    body: JSON.stringify(result)
  });
};

export {handler};
```

`handler` として実装している部分が、実際にLambda上で動作する関数になります。

この関数は、引数に

* API Gatewayからのリクエストに関するイベント
* 実行環境のContext情報
* Lambda実行結果のCallback

をとります。リクエストパラメーターやヘッダーは `event` から受け取ることが可能です。

今回はサンプルなので、パラメーターなどは特に意識せず、固定のJSONを返すようにします。

`callback()` の第2引数が実際のAPIレスポンスに関する値になりますが、ここで返すべき値は「ステータスコード」「レスポンスヘッダー」「レスポンスボディ」です。(詳細は[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format)も参照してください。)

### (3)SAMでAPI定義を作成し、APIを立ち上げる

続いて、これを実際にAPIリクエストを受け取ったときに動作するよう、SAMの設定を追加していきます。
新たに、 `sam/dev/template.yaml` を作成し、以下のような設定を記載します。

```YAML:sam/dev/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
  Function:
    Timeout: 30
Resources:
  devPing:
    Type: 'AWS::Serverless::Function'
    Properties:
      Runtime: nodejs8.10
      CodeUri: ../../built/dev_get_ping
      Handler: index.handler
      Events:
        Vote:
          Type: Api
          Properties:
            Path: /ping
            Method: get
```

これがSAMのテンプレートと呼ばれるものになります。
Resources配下に実行されるアプリケーションの処理のかたまりを定義していくイメージです。

今回の `GET /ping` へのアクセス時に実行される処理を、 `devPing` という名前で定義しておき、

* `GET /ping` へのリクエストが来たとき
  * Properties -> Events -> Vote 配下の記述
* 指定したLambda関数を実行する
  * Properties -> Runtime の実行環境で CodeUri, Handler で指定したものを実行

というような設定を記述しています。

(2)で実装したLambda関数は、webpackのバンドルが完了すると `built/dev_get_ping/index.js` として出力されており、その中で `handler` がexportされているので、 `index.handler` でその実装が指定されている、というわけです！

ここまでで、SAMの設定は完了しているので、実際にAPIを動かしてみましょう。
事前にインストール済みの aws-sam-cli を介して、上記templateをもとにサーバーレスアプリケーションをAPIとして立ち上げます。

```shell
$ sam local start-api -p 3001 -t ./sam/dev/template.yaml 
```

これで、`http://localhost:3001/ping` にリクエストしてみましょう。

```shell
$ curl -X GET http://127.0.0.1:3001/ping

{"status":200,"message":"OK!!!!!"}
```

index.tsのresultで指定したJSONが返却されていますね！

ここまでで、プロジェクトの構成は以下のようになりました。

```txt
.
├── package-lock.json
├── package.json
├── sam
│   └── dev
│       └── template.yaml
├── src
│   └── functions
│       └── main
│           └── get_ping
│               └── index.ts
├── tsconfig.json
├── tslint.json
└── webpack.config.ts
```

### (4) ローカルでDynamoDBを立ち上げ、連携する

いよいよAPI実装らしい部分に突入します。

ローカルで擬似的にDynamoDBを立て、その中にあるテーブルに対してレコード取得・追加の操作を、APIを介して行ってみましょう。

#### ローカルでのDynamoDB環境について

ローカルでは、DynamoDBの仮想環境をDockerイメージを立ち上げることで実現します。  
このDockerイメージはAWSが[公式で配布](https://hub.docker.com/r/amazon/dynamodb-local/)してくれています。
※ちなみに、公式がDockerImageを提供する以前は `tray/dynamodb-local` というものがあり、そちらを利用していました。

これを用いて、Todoリストのアイテムを管理するテーブルをDynamoDBに作成してみます。

Todoのテーブル定義は、

| カラム名(*は必須) | 型 | 内容 |
|:------------|:-------|:------------------|
| id *        | string | ID |
| title *     | string | Todoのタイトル |
| description | string | 内容 |
| done *      | number | 完了:１, 未完了:0 |
| priority    | number | 優先度 |

と簡単なものにしておきます。idをHashKeyとして定義し、doneとpriorityをRangeKeyとしたGSIも設定しておきます。
(doneの値がnumberになっているのは、BOOLだとGSIが設定できないためです。)

Dockerで立てたDynamoDBに対しては、AWS CLIを介してテーブル作成とダミーデータ入力を行います。

CLIで流し込むデータは、以下のようなJSONを用意しておきます。

```json:sam/dev/dynamo/tbl_todo.json
{
  "TableName": "tbl_todo",
  "AttributeDefinitions": [{
      "AttributeName": "id",
      "AttributeType": "S"
    },
    {
      "AttributeName": "done",
      "AttributeType": "N"
    },
    {
      "AttributeName": "priority",
      "AttributeType": "N"
    }
  ],
  "KeySchema": [{
    "AttributeName": "id",
    "KeyType": "HASH"
  }],
  "GlobalSecondaryIndexes": [{
      "IndexName": "PriorityIndex",
      "KeySchema": [{
          "AttributeName": "id",
          "KeyType": "HASH"
        },
        {
          "AttributeName": "priority",
          "KeyType": "RANGE"
        }
      ],
      "Projection": {
        "ProjectionType": "ALL"
      },
      "ProvisionedThroughput": {
        "WriteCapacityUnits": 1,
        "ReadCapacityUnits": 1
      }
    },
    {
      "IndexName": "DoneIndex",
      "KeySchema": [{
          "AttributeName": "id",
          "KeyType": "HASH"
        },
        {
          "AttributeName": "done",
          "KeyType": "RANGE"
        }
      ],
      "Projection": {
        "ProjectionType": "ALL"
      },
      "ProvisionedThroughput": {
        "WriteCapacityUnits": 1,
        "ReadCapacityUnits": 1
      }
    }
  ],
  "ProvisionedThroughput": {
    "WriteCapacityUnits": 1,
    "ReadCapacityUnits": 1
  }
}
```

```json:sam/dev/dynamo/put_items.json
{
  "tbl_todo": [{
      "PutRequest": {
        "Item": {
          "id": {
            "S": "todo0001"
          },
          "title": {
            "S": "Todo No.1"
          },
          "description": {
            "S": "Todo Item 01"
          },
          "done": {
            "N": "1"
          }
        }
      }
    },
    {
      "PutRequest": {
        "Item": {
          "id": {
            "S": "todo0002"
          },
          "title": {
            "S": "Todo No.2"
          },
          "description": {
            "S": "Todo Item 02"
          },
          "done": {
            "N": "0"
          },
          "priority": {
            "N": "3"
          }
        }
      }
    }
  ]
}
```

これらを使ってDynamoを立ち上げるまでの処理は、シェルにまとめておくと楽です。

```sh:sam/dev/dynamo/setup.sh
# !/bin/bash

set -e

# 前回のコンテナが残っていた場合は一度破棄する
REF=$(docker ps -q --filter ancestor=amazon/dynamodb-local)
if [ -n "$REF" ]; then
  docker rm -f $REF
fi
docker run -d -p 8000:8000 amazon/dynamodb-local

# create table
aws dynamodb create-table \
  --endpoint-url http://localhost:8000 \
  --cli-input-json file://$(pwd)/sam/dev/dynamo/tbl_todo.json

# insert dummy item
aws dynamodb batch-write-item \
   --endpoint-url http://localhost:8000 \
   --request-items file://$(pwd)/sam/dev/dynamo/put_items.json
```

このシェルを `setup.sh` とし、 `sam/dev/dynamo` 配下にJSONと一緒にまとめておいて、npmのスクリプト経由で実行できるようにしておくのがおすすめです。

```json:package.json
  "scripts": {
    "setup": "sh ./sam/dev/dynamo/setup.sh",
    "watch": "NODE_ENV=development webpack --watch"
  }
```

プロジェクトの構成は以下のようになっているはずです。

```txt
.
├── package-lock.json
├── package.json
├── sam
│   └── dev
│       ├── dynamo
│       │   ├── put_items.json
│       │   ├── setup.sh
│       │   └── tbl_todo.json
│       ├── env.json
│       └── template.yaml
├── src
│   └── functions
│       └── get_ping
│           └── index.ts
├── tsconfig.json
├── tslint.json
└── webpack.config.ts
```

それでは、ここからDynamoDBへの操作をAPIを介して行ってみましょう。

APIに、以下の2つのエンドポイントを作成します。

* `GET /todo/{id}`
  * 指定したIDのTodoアイテムを取得する
* `POST /todo`
  * 新たにTodoアイテムを登録する

まずは、SAMのテンプレートにこれらの定義を追記します。

```yaml:sam/dev/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
  Function:
    Timeout: 30
Resources:
  # ...中略

  getTodo:
    Type: 'AWS::Serverless::Function'
    Properties:
      Runtime: nodejs8.10
      CodeUri: ../../built/dev_get_todo
      Handler: index.handler
      Policies: AmazonDynamoDBFullAccess
      Environment:
        Variables:
          LOCAL_URL: $LOCAL_URL
          REGION: $REGION
      Events:
        Vote:
          Type: Api
          Properties:
            Path: /todo/{id}
            Method: get
  postTodo:
    Type: 'AWS::Serverless::Function'
    Properties:
      Runtime: nodejs8.10
      CodeUri: ../../built/dev_post_todo
      Handler: index.handler
      Policies: AmazonDynamoDBFullAccess
      Environment:
        Variables:
          LOCAL_URL: $LOCAL_URL
          REGION: $REGION
      Events:
        Vote:
          Type: Api
          Properties:
            Path: /todo
            Method: post

```

注意点としては、`Policies` としてDynamoDBへのアクセスを追加していることと、`Environment` にいくつか設定を追記している部分です。

このうち、 `Environment` の部分については、ローカルで立てたDynamoDBのDockerイメージへアクセスするために必要なものです。
これらの値がどのように効いてくるかは、このあとの説明で出てきます。

続いては、まずGETのAPIに相当するLambda関数を実装しましょう。

```typescript:src/functions/get_todo/index.ts
import {APIGatewayEvent, Context, Callback} from 'aws-lambda';

import {TodoItem, APIResponse, StatusCode, ContentType} from '../../types/todo_api';
import localConfigure from '../localConfigure';

import {getTodo} from '../../db/todo';

localConfigure();

const handler = async (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const todoId = event.pathParameters.id;

  const todo = await getTodo(todoId);

  const items: TodoItem[] = [];

  if (todo) {
    items.push({
      id: todo.id,
      title: todo.title,
      description: todo.description,
      done: todo.done === 1,
      priority: todo.priority
    });
  }

  const result: APIResponse<TodoItem> = {items};

  callback(null, {
    statusCode: StatusCode.OK,
    headers: {
      'Content-Type': ContentType.APPLICATION_JSON
    },
    body: JSON.stringify(result)
  });
};

export {handler};
```

テーブルのレコードやAPIのレスポンスについては、ある程度型定義があったほうが良いので、これらは事前に別ファイルに定義しています。

```typescript:src/types/todo_api.ts
/** APIレスポンスのベース */
export interface APIResponse<T> {
  items: T[];
}

/** (共通)priority定義 */
export type TodoPriority = 1 | 2 | 3 | 4 | 5;

export namespace Tables {
  /** Todoリストのレコード */
  export interface TodoRecord {
    id: string;
    title: string;
    description?: string;
    done: number;
    priority?: TodoPriority;
  }
}

/** Todoリストのアイテム */
export interface TodoItem {
  id: string;
  title: string;
  description?: string;
  priority?: TodoPriority;
  done: boolean;
}

export namespace StatusCode {
  export const OK = 200;
  export const BAD_REQUEST = 400;
  export const INTERNAL_SERVER_ERROR = 500;
}

export namespace ContentType {
  export const APPLICATION_JSON = 'application/json;charset=UTF-8';
}
```

さて、API側の実装説明にもどります。

`event.pathParameters` から、パスパラメーターを取得することができます。
これは、SAMの設定で `Path: /todo/{id}` とした部分の {} 内がパスパラメーター扱いになります。
もし、クエリパラメーターを取得したい場合は、 `event.queryStringParameters` から取得することができます。

ここで取得したTodoのIDを用いて、DynamoDBからレコードをGetします。

AWSのJavaScriptSDKで用意されている、DynamoDBへのアクセスを行う`DocumentClient`は、DBアクセスの非同期処理をPromiseで返してくれるよう実装されていますので、async/awaitを利用した実装ができます。

DBへのアクセス処理そのものは、これまたDAOを作る感覚で別ファイルへ切り出しています。

```typescript:src/db/todo.ts
import * as AWS from 'aws-sdk';
import {DocumentClient} from 'aws-sdk/lib/dynamodb/document_client';

import {Tables} from '../types/todo_api';

const TABLE_NAME = 'tbl_todo';

export const getTodo = async (id: string): Promise<Tables.TodoRecord> => {
  const dynamoClient: DocumentClient = new AWS.DynamoDB.DocumentClient();
  const param: DocumentClient.GetItemInput = {
    TableName: TABLE_NAME,
    Key: {id}
  };

  const record: DocumentClient.GetItemOutput = await dynamoClient.get(param).promise();
  return record.Item as Tables.TodoRecord;
};
```

(※無理やりキャストしたりしていますが、今回はサンプルなのでお許しください。)

さて、ここで思い出してほしいのは、SAMに記載した`Environment` の設定です。

これがどこに効いてくるかというと、 functionsの実装にある `localConfigure()` の処理で効いてきます。  
その実装は以下のとおり。

```typescript:src/functions/localConfigure.ts
import * as AWS from 'aws-sdk';

/**
 * SAMローカル実行時の設定
 */
export default function localConfigure(): void {
  if (process.env.AWS_SAM_LOCAL) {
    AWS.config.update(
      {
        region: process.env.REGION,
        endpoint: process.env.LOCAL_URL
      },
      true
    );
  }
}

```

AWS SAM CLIを用いてこれらの関数を実行する場合には、 `process.env.AWS_SAM_LOCAL = true` となっています。

このとき、configとしてendpointを指定してあげないと、AWSのSDKはどの場所のDynamoDBを見ればよいかわからず、DynamoDBへアクセスできない状態になります。
そのため、ローカルで実行する場合のみ、このconfig更新をしてあげる必要があります。
※実際にAWS環境で動かす場合は、同じアカウントの同じリージョンの情報を参照するので、これらの設定は不要です。

ここで指定した `REGION` と `LOCAL_URL` は、SAMの `Environment` に指定した値が有効となります。  
では、SAMのEnvoronment側の値はどのように行うかというと、AWS SAM CLIでの実行時引数としてパラメーターを与えることができます。

このパラメーターは、以下のようなJSONとして定義しておきます。

```json:sam/dev/env.json
{
  "getTodo": {
    "LOCAL_URL": "http://${YOUR_IP_ADDRESS}:8000",
    "REGION": "ap-northeast-1"
  }
}
```

これを、SAM CLIの実行時に、以下のように指定します。

```shell
$ sam local start-api -p 3001 -t ./sam/dev/template.yaml --env-vars ./sam/dev/env.json
```

すると、

* `env.json` の "getTodo" の設定が、SAMのテンプレート内で"getTodo"と命名したブロック内の `Environment` に適用される
* SAMのテンプレート内の `Environment` が、Lambda実行時の`process.env`に反映される

という流れになります。

なお、env.jsonで指定するローカルのアドレスは、`localhost`指定ではなく、自身のマシンのネットワーク上のIPアドレスである必要があります。
(SAMで実行しているLambdaもDockerコンテナ上で実行されているので、localhostで見る場所が変わってしまうためだと思われます。)

さて、それではTodoのアイテムを取得してみましょう。

```shell
$ curl -X GET http://127.0.0.1:3001/todo/todo0001

{"items":[{"id":"todo0001","title":"Todo No.1","description":"Todo Item 01","done":true}]}
```

事前にDBに入れた値がAPIのレスポンスとして取得できているでしょうか？

同じ容量で、`POST /todo` も作っていきます。

```typescript:src/functions/post_todo/index.ts
import {APIGatewayEvent, Context, Callback} from 'aws-lambda';

import {Tables} from '../../types/todo_api';
import localConfigure from '../localConfigure';

import {insertTodo} from '../../db/todo';

localConfigure();

const handler = async (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const todoId = new Date().getTime(); // 実際のIDはTimestampでやることはないですが…

  const reqBody = JSON.parse(event.body);

  const insertRecord: Tables.TodoRecord = {
    id: `${todoId}`,
    title: reqBody.title,
    done: 0
  };
  if (reqBody.description) {
    insertRecord.description = reqBody.description;
  }
  if (reqBody.priority) {
    insertRecord.priority = reqBody.priority;
  }

  const success = await insertTodo(insertRecord);
  if (success) {
    callback(null, {
      statusCode: 200,
      body: JSON.stringify(insertRecord)
    });
  } else {
    callback(null, {
      statusCode: 500,
      body: JSON.stringify({
        message: 'internal server error'
      })
    });
  }
};

export {handler};

```

```typescript:src/db/todo.ts
export const insertTodo = async (record: Tables.TodoRecord): Promise<boolean> => {
  const dynamoClient: DocumentClient = new AWS.DynamoDB.DocumentClient();
  const param: DocumentClient.PutItemInput = {
    TableName: TABLE_NAME,
    Item: record
  };

  console.log(param);

  let ret = true;
  try {
    await dynamoClient.put(param).promise();
  } catch (e) {
    console.log(e);
    ret = false;
  }

  return ret;
};
```

`env.json` の修正もわすれずに。

```json:sam/dev/env.json
{
 "getTodo": {
    "LOCAL_URL": "http://${YOUR_IP_ADDRESS}:8000",
    "REGION": "ap-northeast-1"
  },
  "postTodo": {
    "LOCAL_URL": "http://${YOUR_IP_ADDRESS}:8000",
    "REGION": "ap-northeast-1"
  }
}
```

実行してみます。

```shell
$ curl -X POST http://127.0.0.1:3001/todo \
  -d '{
    "title": "from API",
    "description": "any message?",
    "priority": 5
  }'

{"id":"1542444222979","title":"from API","done":0,"description":"any message?","priority":5}

# GETできるか確認
$ curl -X GET http://127.0.0.1:3001/todo/1542444222979

{"items":[{"id":"1542444222979","title":"from API","description":"any message?","done":false,"priority":5}]}
```

追加したレコードを取得するまで行うことができました！

### 最終的なプロジェクト構成

以下のようになりました。

```txt
.
├── package-lock.json
├── package.json
├── sam
│   └── dev
│       ├── dynamo
│       │   ├── put_items.json
│       │   ├── setup.sh
│       │   └── tbl_todo.json
│       ├── env.json
│       └── template.yaml
├── src
│   ├── db
│   │   └── todo.ts
│   ├── functions
│   │   ├── localConfigure.ts
│   │   └── main
│   │       ├── get_ping
│   │       │   └── index.ts
│   │       ├── get_todo
│   │       │   └── index.ts
│   │       └── post_todo
│   │           └── index.ts
│   └── types
│       └── todo_api.ts
├── tsconfig.json
├── tslint.json
└── webpack.config.ts
```

## まとめ

いかがでしたでしょうか？

サーバーレスアプリケーションの開発では、とくにLambdaやDynamoDBが絡む部分が、マネジメントコンソール上だけでは非常に開発がしづらい部分かと思います。

これらをローカルで手軽に開発・実行できる環境が整うだけで、サーバーレスアプリケーションの開発のハードルが下がるのではないかと思っています。

ここで開発したものを実際にAWS環境へデプロイするためには、もうひと手間加える必要があります。

* API Gateway側の定義をつくる
  * Swaggerを用いてAPI仕様を記述し、各エンドポイントに対して名前をつけておく
* SAMのテンプレートで、対応するAPIのエンドポイントの設定にSwaggerでつけた名前を反映
* CloudFormationを用いて、パッケージ化+デプロイ

という流れになります。

ですが、ローカルで構築したものをほぼ流用できるので、Swaggerの定義をつくること以外はほとんど手間にならないと思います。

環境ごとにSAMのテンプレートファイルを分割しなければならないので、そこだけがイケてない部分かなぁと思います…
ここについては、未だに良い方法が思い浮かんでないので、なにかアイデアがある方は共有いただけると嬉しいです！

EC2インスタンスを立ち上げてAPIを作るよりも、サーバーレスのほうが低コストで気軽に試すことができるのが大きなメリットになります。
なにか小さなシステムで利用するには最適ですので、ぜひお試しください！