API GatewayとLambdaとDynamoDBでサーバーレス環境を構築する

API Gateway + Lambda + DynamoDB = Serverless

AWSでサーバーレスを構築するアーキテクチャを考えた時に、もっとも基本的でわかりやすい構成は、API Gateway + Lambda + DynamoDB(ないしはS3)の組み合わせだと思います。

このエントリーで触れること

  • Lambda関数の作り方
  • API Gatewayでのリソースの作り方
  • API GatewayとLambda関数の紐づけ方
  • Lambda関数でのDynamoDBのいじり方

AWS SAMには触れないです。

Lambdaで関数を作る

Lambdaで関数を作ってデプロイするのは非常に簡単な作業です。

ランタイムはnode.jsを使います。

関数を作成する方法としては、 1. AWSコンソール上で関数を作成する 2. ローカルで関数を作成してzip化し、Lambdaにアップロードする 3. ローカルで関数を作成してzip化し、S3にアップロードした後に、Lambdaからそれを呼ぶ 4. ローカルで関数を作成してzip化し、cliでデプロイする の4種類の方法があり、もっとも簡便に作るのであれば1で事足ります。

しかし、もっと込み入った内容のロジックが必要なのであれば、2,3,4の方法を選びます。

たとえば、node_modulesを入れてyarnでインストールしてきたライブラリを使いたいとか、lambda上ではできないけどローカルでは用意できるようなものを設定したい時には、2,3,4の方法でデプロイした方が良さそうです。

ここでは1と2,3,4の関数作成方法に分けて簡単に関数を作ってみます。

AWSコンソール上で関数を作成

AWS Lambdaのページからサイドペインの「関数」を選択すると、関数が一覧で表示されます。 この一覧のヘッダー部分に以下のように関数の作成ボタンがあります。

Screenshot 0031-11-05 at 10.42.09 PM.png

ボタンクリック後の設定では、

  • 一から作成(選択)
  • 関数名(入力)
  • ランタイム(ここではnode.js10.x) (選択)
  • 基本的なLambdaアクセス宣言で新しいロールを作成(選択)

「関数の作成」からLambda関数を作成します。

すると、作成した関数の詳細ページへ飛び、関数のコードを編集することができます。

Screenshot 0031-11-12 at 9.37.29 PM.png

デフォルトのコードでは、'Hello from Lambda!'の文字列がstatusCode200で返ってくるようになっています。 ここでrequestを受け取って処理するなどを行うためには、この後説明するAPI Gatewayの設定を行う必要があります。

ローカルで関数を作成してLambdaにアップロード

$ mkdir myFunction | cd myFunction
$ yarn
$ yarn add moment
$ vim index.js

index.jsの中身は適当にこんな感じで:point_down_tone2:

Screenshot 0031-11-12 at 9.34.11 PM.png

コードが書けたらnode_modulesと同じレベルのディレクトリ内でzipコマンドを実行します。

$ tree
.
├── index.js
├── node_modules // 以下省略
├── package.json
└── yarn.lock
$ zip myFunction . -r
$ ls
index.js       myFunction.zip node_modules   package.json   yarn.lock

解凍した時に上記の内容を含んだmyFunctionディレクトリがトップレベルになる必要があるのでzipコマンドには-rオプション(recurse into directory)をつけています。

zipができたらLambdaのコンソールに移動して、以下のコードエントリタイプのドロップダウンから「.zipファイルをアップロード」を選択します。

Screenshot 0031-11-05 at 11.14.08 PM.png

アップロードボタンが出てくるのでそこから上で作成したzipをアップロードしてみましょう。 保存すると、関数コード欄のソースコードがローカルで作成したコードに書き換わります。

インストールしておいたmomentも使えるようになり、エディターのサイドペインにnode_modulesが追加されていると思います。 コンソール上だけだとnode_modulesを利用することができませんが、このようにローカルで作成してからアップロードするとコンソール上でもnode_modulesを扱えるようになります。

Screenshot 0031-11-05 at 11.23.16 PM.png

ローカルで関数を作成してS3にアップロード

Lambdaに直接zipをアップロードすることもできますが、S3にアップロードしたzipを参照することもできます。 Lambda上に表示される注意書きにもありますが、大きなサイズのzipをアップロードする際にはS3経由でLambdaにデプロイするのが望ましいようです(具体的には10MB以上はS3の利用を検討した方が良いらしいです)。

上記で作成したmyFunction.zipを使いまわします。

$ zip myFunction . -r

でmyFunction.zipを作成したのち、S3コンソールを開きます。

適当な名前のバケットを作成して、その中にmyFunction.zipをアップロードします。

Screenshot 0031-11-12 at 9.43.06 PM.png

次にLambdaコンソールを開きます。 関数コードの「AmazonS3からのファイルのアップロード」を選択して、S3にアップロードしたzipのオブジェクトURLを入力します。

Screenshot 0031-11-12 at 9.46.42 PM.png

あとは関数を保存するだけです。 テストを実行すると、レスポンスが返ってきます。

Screenshot 0031-11-12 at 9.48.54 PM.png

ローカルで関数を作成してCLIでデプロイ

CLIで関数を作成する場合は、create-functionを利用します。 https://docs.aws.amazon.com/cli/latest/reference/lambda/create-function.html

  create-function
--function-name <value>
--runtime <value>
--role <value>
--handler <value>
[--code <value>]
[--description <value>]
[--timeout <value>]
[--memory-size <value>]
[--publish | --no-publish]
[--vpc-config <value>]
[--dead-letter-config <value>]
[--environment <value>]
[--kms-key-arn <value>]
[--tracing-config <value>]
[--tags <value>]
[--layers <value>]
[--zip-file <value>]
[--cli-input-json <value>]
[--generate-cli-skeleton <value>]

また、関数を更新する場合には、update-function-codeかupdate-function-configurationを利用します。 https://docs.aws.amazon.com/cli/latest/reference/lambda/update-function-code.html https://docs.aws.amazon.com/cli/latest/reference/lambda/update-function-configuration.html

update-function-codeはlambda関数のコードを更新するコマンドで、 update-function-configurationはlambda関数の設定(ランタイムや環境変数)を更新するコマンドです。

他のコマンドについてはCLI Command Referenceを参照してください。

さて、デプロイした後はそれだけでは関数は使えませんので、API Gatewayを介して呼び出せるように設定します。

API GatewayからLambda関数を利用する

API GatewayとLambdaの関係性は、

  • API Gateway クライアントからの通信を受け付けてLambda関数にリクエストを渡す。 また、そこから受け取ったレスポンスをクライアントに返却する。 (API GatewayはLambda以外にも既存のHTTPエンドポイントやSQSやSNSなどのAWSリソースと連携させることもできます)

  • Lambda API Gatewayから受け取ったリクエストを基に処理を実行し、レスポンスを返却する。

Screenshot 0031-10-27 at 7.58.53 PM.png

① メソッドリクエス

クエリーのパラメータ等を設定する。

② 統合リクエス

API Gatewayのリソースが呼び出す対象を設定する(ここではlambda関数となる)。 lambdaへ送るbodyパラメーターのテンプレートを設定する。

③ 統合レスポンス

lambdaから返ってきたレスポンスをAPI Gatewayのレスポンス設定にマッピングする。

④ メソッドレスポンス

API Gatewayがクライアントへ返すステータスコードごとのレスポンスヘッダーやボディを設定する。

API Gatewayでエンドポイントを作る

API Gatewayの「APIの作成」からAPIを作っていきます。 今回はプロトコルをRESTとし、名前はmyAPIとします。

Screenshot 0031-11-12 at 10.05.19 PM.png

作成されたmyAPIのリソース画面で、「アクション」のドロップダウンをクリックするとアクション一覧が表示されます。 ここでリソースを作成します。

Screenshot 0031-11-12 at 10.05.53 PM.png

リソース名を入れて「リソースの作成」を行います。

Screenshot 0031-11-12 at 10.18.59 PM.png

そして、作成したリソースにメソッドを追加していきます。 「アクション」ドロップダウンからメソッドの作成を選択し、メソッドの種類を選択します。 ここではGETを選択してチェックします。

Screenshot 0031-11-12 at 10.19.48 PM.png

GETメソッドの統合タイプにLambda関数を選択し、 Lambda関数に上記で作成したmyFunction関数を選択します。

Screenshot 0031-11-12 at 10.21.50 PM.png

保存すると以下のような画面が表示されます。 ここで前述したメソッドリクエスト、統合リクエスト、統合レスポンス、メソッドレスポンスを設定します。

Screenshot 0031-11-12 at 10.23.11 PM.png

今回冗長となるのでこれらの細かい説明は省きます。

あとはAPIをデプロイして、作成されるエンドポイント/myresourceにアクセスしてみましょう。 デプロイ時のステージなどは好きなように設定してみてください。

Screenshot 0031-11-12 at 10.28.12 PM.png

すると、先ほど作成したLambda関数のレスポンスがブラウザに表示されるはずです。

Screenshot 0031-11-12 at 10.29.14 PM.png

LambdaからDynamoDBに接続する

次はLambdaからDynamoDBの値を取得して、その値をクライアントへ返せるようにします。

DynamoDBにデータを作る

DynamoDBのダッシュボードから「テーブルの作成」をクリックします。

Screenshot 0031-11-12 at 10.35.39 PM.png

パーティションキー等の設計についてはDynamoDBのベストプラクティスを参考にしてみてください。

作成したテーブルに項目を追加します。 「項目の作成」からパーティションキーの値の他に任意の項目を追加します。

Screenshot 0031-11-12 at 10.40.10 PM.png

これでデータが用意できたので、このテーブルにLambdaから接続して値を取得できるようにします。

以下のようなjsファイルを用意し、zip化します。

const AWS = require('aws-sdk')
const moment = require('moment')
const dynamo = new AWS.DynamoDB.DocumentClient({region: 'ap-northeast-1'})

exports.handler = async (event, context) => {
  const params = {
    TableName: "testTable",
    Key:{
      uuid: "7dd1b5f4-bb05-4a87-b464-5ddf88254837"
    }
  }
  await dynamo.get(params, (err, data) => {
    if (err) context.fail(err)
    context.succeed({
      statusCode: 200,
      body: data,
      time: moment().format('YYYY/MM/DD')
    })
  }).promise()
}
$ tree
.
├── index.js
├── node_modules // 以下省略
├── package.json
└── yarn.lock
$ zip myFunction . -r
$ ls
index.js       myFunction.zip node_modules   package.json   yarn.lock

直接LambdaにアップロードでもS3にアップロードしてそれを参照するのでも良いです。 アップロード後に保存してテストを実行してみてください。

Screenshot 0031-11-13 at 12.27.32 AM.png

こんな感じでレスポンスが返ってきたら成功です。 この関数を紐づけたAPI Gatewayのエンドポイントに接続してみてください。

Screenshot 0031-11-13 at 12.28.57 AM.png

DynamoDBから取得したデータがブラウザに表示されると思います。

もっと応用的に、リクエストを受け取って任意のデータを取得したいような場合には、統合リクエストのマッピングテンプレート等を設定して、Lambdaに渡るeventの中身を定義する必要がありますが、ここでは省きます。

ここまでで以下の項目について触れてきました。

  • Lambda関数の作り方
  • API Gatewayでのリソースの作り方
  • API GatewayとLambda関数の紐づけ方
  • Lambda関数でのDynamoDBのいじり方

上でやったようにコンソールで色々と操作しましたが、AWS SAMを使えばコマンドラインでサーバーレスを構築する構築することもできます。 ローカルでテストができるのは気が楽ですね:blush: