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

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


はじめに

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

私からは、来る12/15に行われる Developer Boost にて発表させて頂く、「サーバーレス+SPAでつくる!ユーザーご意見管理システム」のセッションから、

AWS SAMを用いたサーバーレスAPIの開発環境を構築する方法について紹介させていただきます。

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

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


AWS SAMとは?

公式サイトには、以下のように記載されています。

AWS サーバーレスアプリケーションモデル (AWS SAM) はサーバーレスアプリケーションを定義するモデルです。
…そのまんまですね。

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

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

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

しかし、アプリケーションがスケールしていくと、これらの設定を都度していくのは非常に運用コストがかかります。

特に、別環境に同じようなアプリケーションを構築する必要が出た場合、同じ作業をひたすら繰り返して…となると、つらみのある作業になりますよね。

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

なお、AWSだけでなくGCPやAzureでも利用できる、同じようなフレームワークとして、Serverlessと呼ばれるものも有名です。


実際に作ってみる

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

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

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

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


必要な環境

事前に、開発するPCで下記のセットアップを済ませておきます。

※なお、今回の環境についてはMacで動作確認済です。(Windowsでは未確認です。スミマセン)

  • Node.js

    • バージョンはとりあえずv.8系以降ならOK
  • Docker

  • aws-cli

    • Configureについては、サンプルのものをそのまま適用しておけばOKです。
  • aws-sam-cli


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

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

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

  • 開発はTypeScript
  • 実行時はJavaScriptにトランスパイルしたものを利用
という状態にします。

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

まずは初期化。

$ mkdir aws-sam-local-api 
$ cd aws-sam-local-api 
$ npm init -y 
続いて、必要なモジュールのインストールを行います。

$ 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を扱えるような構成で設定を記述していきます。

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; 
src 配下のディレクトリ構成を、 src/functions/(関数名)/index.ts となるように作成しておき、 index.ts 内にLambdaの実装をしていくことで、

最終的にバンドルされた結果(の各Lambda関数)が built/(関数名)/index.js として出力されます。

ここの(関数名)の部分のディレクトリ構成をもう一段階掘って、
src/functions/(分類)/(関数名)index.tsbuilt/(分類)_(関数名)/index.js

として出力するなど調整できます。

ディレクトリ構成が実行するLambda関数の命名になるので、各プロジェクトでどのように開発していくか次第でentryの解決方法を調整することをおすすめします。

次に、TypeScriptの設定を tsconfig.json に追加していきます。

こちらも、 noImplictAny などのオプションはお好みでどうぞ。

tsconfig.json
{ 
  "compilerOptions": { 
    "module": "commonjs", 
    "target": "es6", 
    "noImplicitAny": true, 
    "noImplicitReturns": true, 
    "noFallthroughCasesInSwitch": true, 
    "outDir": "./built", 
    "allowJs": true 
  }, 
  "include": [ 
      "./src/**/*" 
  ] 
} 
最後に package.json に、実装したTSファイルを保存に自動でビルドするようにwatchタスクを追加しておきましょう。

package.json
"scripts": { 
    "watch": "NODE_ENV=development webpack --watch" 
  } 
ここまでで、プロジェクトの基本的なセットアップは完了です。

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

. 
├── 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 を作成します。

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レスポンスに関する値になりますが、ここで返すべき値は「ステータスコード」「レスポンスヘッダー」「レスポンスボディ」です。(詳細は公式ドキュメントも参照してください。)


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

続いて、これを実際にAPIリクエストを受け取ったときに動作するよう、SAMの設定を追加していきます。

新たに、 sam/dev/template.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として立ち上げます。

$ sam local start-api -p 3001 -t ./sam/dev/template.yaml  
これで、http://localhost:3001/ping にリクエストしてみましょう。

$ curl -X GET http://127.0.0.1:3001/ping 
 
{"status":200,"message":"OK!!!!!"} 
index.tsのresultで指定したJSONが返却されていますね!

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

. 
├── 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が公式で配布してくれています。

※ちなみに、公式がDockerImageを提供する以前は tray/dynamodb-local というものがあり、そちらを利用していました。

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

Todoのテーブル定義は、

カラム名(*は必須) 内容
id * string ID
title * string Todoのタイトル
description string 内容
done * number 完了:1, 未完了:0
priority number 優先度
と簡単なものにしておきます。idをHashKeyとして定義し、doneとpriorityをRangeKeyとしたGSIも設定しておきます。

(doneの値がnumberになっているのは、BOOLだとGSIが設定できないためです。)

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

CLIで流し込むデータは、以下のような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 
  } 
} 
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を立ち上げるまでの処理は、シェルにまとめておくと楽です。

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のスクリプト経由で実行できるようにしておくのがおすすめです。

package.json
"scripts": { 
    "setup": "sh ./sam/dev/dynamo/setup.sh", 
    "watch": "NODE_ENV=development webpack --watch" 
  } 
プロジェクトの構成は以下のようになっているはずです。

. 
├── 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のテンプレートにこれらの定義を追記します。

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関数を実装しましょう。

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のレスポンスについては、ある程度型定義があったほうが良いので、これらは事前に別ファイルに定義しています。

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を作る感覚で別ファイルへ切り出しています。

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() の処理で効いてきます。


その実装は以下のとおり。

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環境で動かす場合は、同じアカウントの同じリージョンの情報を参照するので、これらの設定は不要です。

ここで指定した REGIONLOCAL_URL は、SAMの Environment に指定した値が有効となります。


では、SAMのEnvoronment側の値はどのように行うかというと、AWS SAM CLIでの実行時引数としてパラメーターを与えることができます。

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

sam/dev/env.json
{ 
  "getTodo": { 
    "LOCAL_URL": "http://${YOUR_IP_ADDRESS}:8000", 
    "REGION": "ap-northeast-1" 
  } 
} 
これを、SAM CLIの実行時に、以下のように指定します。

$ 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のアイテムを取得してみましょう。

$ 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 も作っていきます。

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}; 
 
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 の修正もわすれずに。

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" 
  } 
} 
実行してみます。

$ 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}]} 
追加したレコードを取得するまで行うことができました!


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

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

. 
├── 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を作るよりも、サーバーレスのほうが低コストで気軽に試すことができるのが大きなメリットになります。

なにか小さなシステムで利用するには最適ですので、ぜひお試しください!

コメント

このブログの人気の投稿

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2020-12-01 09:41:49 RSSフィード2020-12-01 09:00 分まとめ(69件)