API Gateway、Lambda(Node.js)、DynamoDB を使ったAPIの作り方

API Gateway、Lambda、DynamoDB を使ったAPIの作り方を備忘録も兼ねて記載します。
プログラム言語にはNode.jsを利用します。

# システム構成
システム構成は以下のようになります。
![AWS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/d172c951-87f7-ef41-4bb4-cf660f65d99b.png)

# システム概要
今回はサンプルとして、DynamoDBに格納されているイベント情報をPOSTリクエストで取得して、Jsonを出力するAPIを作ります。

# 1. DynamoDBにテーブルを作成
最初にイベント情報を格納するテーブルを作成します。
![dynamodb1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/c813f1e4-6c70-8b35-a53a-57a532ede10a.png)

テーブル名は **event**、パーティションキーは **eid** とします。
入力し終わったらページ下部の「テーブルの作成」をクリックします。
![dynamodb2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/fed74d7b-0057-ee5c-d07a-bd30a2543e3a.png)

下図のようになれば、テーブル完成です。
![dynamodb3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/b947b102-ed24-a4f3-7f6b-b34d1c6f16ec.png)

# 2. DynamoDBにサンプルデータを登録
作成した eventテーブルにサンプルデータを登録します。
作成した event をクリックしてください。
![dynamodb4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/ffeb5aba-b434-4e37-39d4-f1c5f8f4b847.png)

「項目を作成」をクリックします。
![dynamodb5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/62a84ff6-24c1-1a6c-74f9-ca2d11d19819.png)

フォームでも良いのですが、今回はJSONを選択します。
![dynamodb6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/eb60ee2c-bf42-3a4a-e612-3397ca1da2ad.png)

以下のようにJsonデータを入力して、「項目の作成」をクリックしてください。
![dynamodb7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/76081597-754a-7942-2a36-2e49508ab849.png)

「項目の探索」から以下のようにスキャンを実行して、データが表示できれば登録は成功です。
![dynamodb8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/124effc8-1ea1-a609-f3e5-31c82f93b596.png)

# 3. IAMロールの作成
LambdaからDynamoDBにアクセスする為のIAMロールを作成します。

IAMの画面に遷移し、「ロールを作成」をクリック
![iam1a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/e9e9ae3b-0f38-aeab-a8c7-867bffdef052.png)

信頼されたエンティティタイプに「AWSのサービス」を選び、ユースケースに「Lambda」を選択して 次へをクリック
![iam2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/6559b7ae-78c6-f3a3-0573-db24eb4896d2.png)

検索フォームに「dynamodb」と入力して表示を絞り込み、ポリシーから **AmazonDynamoDBFullAccess** と **AmazonLambdaDynamoDBExecutionRole** を選択して次へクリック
![iam3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/25b7c67d-e846-d1ed-6152-580372e545c0.png)

任意の名称（ここでは「LambdaAccess」とする）を入力して、ページ下部のロールを作成をクリック
![iam4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/23337b3f-24b3-1504-bc18-36790602d13e.png)

ロール一覧画面で、作成したロールが表示されていればOKです。
![iam5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/cd7acd2b-0d19-b4d4-7ff1-8a7d092c643b.png)

# 4. Node.js をインストール
今回はNode.js で Lambda関数を作っていきます。その為にローカル環境に Node.js をインストールします。
環境は **Windows 10 home** です。
こちらの記事が分かりやすかったので、こちらを見ながらNode.jsをインストールしましょう。
- [Node.jsをインストールする](https://qiita.com/sefoo0104/items/0653c935ea4a4db9dc2b)

# 5. ローカルのNode.jsでLambda用の関数を作成
ローカルのNode.jsでLambdaにアップロードする関数を作っていきます。
関数の仕様としては、先につくっておいたDynamoDB（eventテーブル）をよみこんでJsonレスポンスを返却するという単純なものです。
まずは任意のディレクトリ（ここではCドライブ直下のlambda\apiディレクトリ）に移動しましょう
```
cd C:\lambda\api  ### ディレクトリを移動
```
ここで npm の初期化と必要なモジュールをインストールします。
```
C:\lambda\api> npm init  ### 初期化します（ package.jsonファイルが作成される）

npm WARN config global `--global`, `--local` are deprecated. Use `--location=global` instead.
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (api)  ### そのままEnter
version: (1.0.0)  ### そのままEnter
description:  ### そのままEnter
entry point: (index.js)  ### そのままEnter
test command:  ### そのままEnter
git repository:  ### そのままEnter
keywords:  ### そのままEnter
author:  ### そのままEnter
license: (ISC) MIT  ### MITライセンス
About to write to C:\lambda\api\package.json:

{
  "name": "api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT"
}

Is this OK? (yes)  ### そのままEnter
npm notice
npm notice New minor version of npm available! 8.11.0 -> 8.13.2
npm notice Changelog: https://github.com/npm/cli/releases/tag/v8.13.2
npm notice Run npm install -g npm@8.13.2 to update!
npm notice
```
必要なライブラリをインストール
```
cd C:\lambda\api> npm install moment-timezone  ### 時刻関連のライブラリをインストール
```
すると、ディレクトリが以下のようになります。
```
├─package.json
├─package-lock.json
└─node_modules
    ├─moment  ### 自動でインストールされるライブラリ
    └─moment-timezone  ### インストールしたライブラリ
```
DynamoDBの「eventテーブル」を操作するロジックである **event.js** を **modules** ディレクトリに置きます。
package.jsonがあるディレクトリに移動して以下コマンドを実行してください。
```
mkdir modules
cd modules
vi event.js   ### エディタは任意のものを使ってください（ここではviエディタを利用）
```
event.js には以下を記述してください。
```
const aws = require("aws-sdk");
const dynamoDB = new aws.DynamoDB.DocumentClient({ region: "ap-northeast-1" });

/**
 * --------------------------------------------------------
 * テーブルの定義（将来用に環境毎にテーブルを分ける）
 * --------------------------------------------------------
 */
const DB_Event = {
    prd:'event',    //今回作ったDynamoDBテーブルはこれ
    stg:'stg_event',
    dev:'dev_event'
};

/**
 * --------------------------------------------------------
 * イベント情報の読み込み
 * --------------------------------------------------------
 */
module.exports.evLoad = async(posts) => {
    return new Promise(function (resolve){
        
        let params = {
            TableName: DB_Event[process.env.STAGE], //環境変数でテーブルを選択する（※1）
            Key: {
                'eid':posts.eid
            }
        };
        dynamoDB.get(params, function(err, data){
            if(err){
                console.log("error: " + err)
                resolve({'status': err.message});
            }
            else{
                if(Object.keys(data).length != 0){
                    resolve({'status':'success', 'ev':data.Item});
                }
                else{
                    resolve({'status':'data length error'});
                }
            }
        });
    });
};
```
次にメインの処理である **index.js** を作成します。
package.jsonがあるディレクトリに移動して以下コマンドを実行してください。
```
vi index.js   ### エディタは任意のものを使ってください（ここではviエディタを利用）
```
index.js には以下を記述してください。
```
const mods_event = require("./modules/event");
const moment = require("moment-timezone");
moment.tz.setDefault("Asia/Tokyo");

exports.handler = async (event, context, callback) => {

    if (event.action === undefined) {
        console.log('action は必須です');
        callback(null, { 'result': 'no action' });
        return;
    }
    let posts = Object.assign({}, event);
    console.log("posts: " + JSON.stringify(posts));

    //action毎に処理を分岐
    switch(posts.action){
        case 'overview': //イベント概要ページ
            await overview(callback, posts);
            break;
        default:
            break;   
    }
};

/**
 * --------------------------------------------------------
 * イベント概要ページの処理
 * --------------------------------------------------------
 */
 async function overview(callback, posts) {

    //イベント情報の読み込み
    let res = await mods_event.evLoad(posts);
    let ev;
    if (res.status == 'success') {
        ev = Object.assign({}, res.ev);
    }
    else {
        await errorReturn(res, callback);
        return;
    }

    successReturn(callback, ev);
}

/**
 * --------------------------------------------------------
 * OKレスポンス
 * --------------------------------------------------------
 */
 function successReturn(callback, ev) {

    let data = {
        'status': 'success',
        'item': ev
    };

    callback(null, {
        'statusCode': 200,
        "headers": {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*' 
        },
        'body': data
    });
}

/**
 * --------------------------------------------------------
 * NGレスポンス
 * --------------------------------------------------------
 */
async function errorReturn(message, callback) {

    callback(null, {
        'statusCode': 200,
        "headers": {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*' 
        },
        'body': {
            'status': 'error',
            'errorMessage': message
        }
    });
}
```
これでLambda用の関数（Node.js）に必要なファイルが揃いました。
ディレクトリは以下のようになります。
```
├─modules
│   └─event.js   ### viで作成したファイル
├─node_modules
│   ├─moment
│   └─moment-timezone
├─index.js   ### viで作成したファイル
├─package.json
└─package-lock.json
```
続いて、Lambaへアップロードする為に、作成したファイル一式を選択して、圧縮します。
ここでは圧縮したzipファイルを **eventFunction.zip** とします。
![zip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/1bc5de60-60b0-9512-7b56-29731c5bf192.png)

# 6. Lambdaの設定（関数のアップロード）
Lambdaの画面に遷移して、「関数の作成」をクリック。
![Lambda1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/894dffaf-25fa-388f-169b-623a908ebbe9.png)

今回は「一から作成」を選んで、任意の関数名（ここでは**eventFunction**）を入力します。
ランタイムは「Node.js」を選択し、実行ロールには、先に作っておいた **LambdaAccess** を選択します（このロールが無いとDynamoDBアクセス時に access denied エラーになります）
その後にページ下部の「関数の作成」をクリックします。
![Lambda2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/eb76cd65-abd9-7a48-6914-b9abdc27a170.png)

すると、以下のようなレイアウトの画面が表示されますので、アップロード元 の「.zipファイル」をクリックします。
![Lambda3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/65777e70-66a1-e81b-bd70-6bc94298fb9e.png)

以下のダイアログが表示されるので、上記で作成した **eventFunction.zip** を選択してアップロードします。 
![Lambda5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/e6acbc02-2248-3ab0-3d4e-bdd243d665c7.png)

# 7. Lambdaの設定（環境変数）
event.js ソースコードにあった **(※1)** で利用する環境変数を登録します。
今回は本番運用用のLambda関数として登録するので、**prd** という文字列を登録していきます。
Lambdaの画面から「設定」タブを選択して「編集」をクリックしてください。
![Lambda6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/fec6e2a9-5521-049f-914c-2ecad8818ede.png)
「環境変数の追加」をクリックしてください。
![Lambda7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/58647f18-cf9b-8f20-0a3f-001c5df70a99.png)
キーに **STAGE**、値に **prd** を入力して「保存」をクリックしてください。
これで環境変数の設定は完了です。
![Lambda8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/b618b26d-6066-68ec-2954-76bb5e277f4f.png)

# 8. LambdaからDynamoDBに接続してみる
ここまででLambdaとDynamoDBの設定が完了しました。
試しにテストコンソールで Lambdaにアクセスして DynamoDBからデータを取ってきましょう。

「Configure test event」をクリックします。
![test1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/bf8d1513-acfd-44e0-bdeb-639df5387a42.png)

すると以下のようなダイアログが表示されるので、イベント名（ここではeventFunction）とイベントJSON（これがリクエストJSONになる）を入力します。
そして、わかりずらいですが、ダイアログを下にスクロールすると「保存」ボタンがあるので、それをクリックします。
![test2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/b79d4898-c2b0-225b-cc1e-e5b8599e8d6d.png)

その後、作成したイベント名（eventFunction）を選択しておきます。
![test3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/b9a568ed-e68e-8d1b-b6be-acfb645d9d67.png)

最後に「Test」ボタンをクリックします。以下のようなレスポンスJSONが返ってくれば成功です（DynamoDBからイベント情報を読み取り成功です）
![test4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/7c1853f0-9ef3-e5c3-9b3e-330c27cd1d3e.png)

# 9. API Gatewayの設定
API Gateway の画面に移って、「REST API」の「構築」をクリックします。
![apig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/a3945012-759f-8d73-8dc3-12c673e7198d.png)

「新しいAPI」をチェックし、任意のAPI名（ここではeventとします）を入力します。
エンドポイントタイプには「リージョン」を選択して、「APIの作成」をクリックします。
![apig2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/2af412ce-97ee-ef43-e445-267900ef5933.png)

「アクション」メニューから「メソッドの作成」を選択します。
![apig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/7440f772-924e-d1b1-bc23-19c605de5b67.png)

プルダウンメニューから「POST」リクエストを選択します。
![apig4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/47d81aa9-19a2-353c-57ff-9ba8307386e5.png)

POSTの隣の「レ」マークをクリックして反映します
![apig5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/fbed52cf-af3a-8f3b-cf47-81df5af34f5e.png)

Lambda関数の欄に、先に作成したLambda関数である「eventFunction」を入力して「保存」をクリックします。
![apig6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/6b8db536-81a0-c07c-6cf5-4d0c32d5897f.png)

Lambda関数を呼び出す権限を聞かれるので「OK」をクリックします。
![apig7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/804f2d42-55ef-0e83-ab91-739ef7b95fdf.png)

リクエストとレスポンスの間に入力したLambda関数である「eventFunction」が表示されます。
![apig8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/c7c6cae8-b8c4-c4e2-d6a1-264170eb552e.png)

Lambdaの画面に移っても、eventFunction のフロントに API Gateway が設定されていることが分かります。
![apig9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/ba6df7c8-d308-387e-6f80-0587abeaa0f9.png)

実際に疎通させるには、API Gateway をデプロイする必要があります。
アクションメニューから「APIのデプロイ」を選択します。
![apig10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/7bafbca4-1571-adff-70ad-cd5c420c06e9.png)

ステージ名に任意の名称を入力します（今回は本番用のデプロイなので「prd」と入力します）
※prdはURLにも反映されます。
![apig11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/c60217f9-7a4f-f8a5-529c-28bf4291732c.png)

デプロイが完了しました。
「URLの呼び出し」というところに、このAPI Gateway のエンドポイントが表示されます。
このURLを使って API Gateway → Lambda → DynamoDB にアクセスすることが出来るようになりました。
![apig12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/04fa969a-a33f-999f-200e-fdc1a0e8b7a4.png)

試しにpostmanをつかって実際にAPIにアクセスしてみましょう。
postmanの設定を以下ように
　メソッド：post
　URL:API Gateway のエンドポイント
　リクエストボディ：下記のJSON
を入力して「Send」ボタンをクリックしましょう。
レスポンスにDynamoDBのeventテーブルで登録したイベント情報が返ってきたら成功です。
![apig13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2348676/655b1574-1d9c-fe5b-f24c-f416a1a9630b.png)

なお、独自ドメインでアクセスしたい場合には以下の記事を参考にしてみてください。
- [API Gatewayに独自ドメインでアクセスする方法（エッジ最適化編）](https://qiita.com/chinone0711/items/4677014a74a80df9ac75)
- [API Gatewayに独自ドメインでアクセスする方法（リージョン編）](https://qiita.com/chinone0711/items/64f12aa6739baeebb3af)

また、GETでパラメータを取得したい場合には以下の記事が参考になりました。
- [API Gateway から Lambda にパラメータを渡す](https://qiita.com/minsu/items/c9e983f109b1cf5a516e)

以上

# 参考資料
- [Node.jsをインストールする](https://qiita.com/sefoo0104/items/0653c935ea4a4db9dc2b)