selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

SAM Localを利用してLocalで動かしているAWS Lambda からdynamodb-localにアクセスする方法

この記事に書かれていること

  • SAM CLIの環境構築方法
  • SAM CLIを使ってLocalでLambdaを起動する方法
  • SAM CLIを使ってLocalで起動しているLambdaから、Localで用意したDynamoDB containerにアクセスする方法
  • これらの処理を僕が趣味で作っているAWS Lambdaを例に説明します。

この記事に書かれていないこと

  • SAM CLIとは何か?
  • Lambdaを利用する際のwebpackの設定

利用環境

  • nodejs8.10
  • TypeScript 3.4.5
  • SAM CLI 0.15.0
  • python 3.7.2

事前準備

aws-sam-cliのinstall

Installing the AWS SAM CLI on macOS というAWS公式の手順に則ってinstallします。

aws-sam-cliは、pythonのバージョン 2.7、3.6、3.7 に対応しています。もし手元の環境がそれらのバージョンに一致していないのであれば、対応しているバージョンのpythonをinstallしましょう。なお2.7は2020年の1月にはメンテナンスが終了されますので、今から入れるのであれば 3以上にすると良いでしょう。

$ brew install pyenv
$ brew install pyenv-virtualenv
$ pyenv install 3.7.2
$ pyenv local 3.7.2
$ brew tap aws/tap
$ brew install aws-sam-cli
$ sam --version
SAM CLI, version 0.15.0

dynamodb-localのdocker imageをpull

こちらもamazon公式のdocker imageを利用します。下記のコマンドを実行してdocker imageをpullしましょう

docker pull amazon/dynamodb-local

SAM Localテスト用データ作成

aws-sam-cliを使ってLocalからLambdaを起動するためのデータを作成します。今回は、シンプルにAPI Gatewayから起動することにします。

sam local generate-event \
  apigateway aws-proxy \
  --path datadog_report \
  --method GET > events/event_apigateway.json

このコマンドによって作成されたjsonこちらになります。

実装

docker-composeの設定

# docker-compose.yml
version: "3"

services:
  dynamodb-local:
    container_name: dynamodb
    image: amazon/dynamodb-local
    build: ./
    ports:
      - 8000:8000
    command: -jar DynamoDBLocal.jar -dbPath /data -sharedDb
    volumes:
      - ./data:/data
    networks:
      - lambda-local
networks:
  lambda-local:
    external: true

この設定において重要な点は3点あります。

1点目は、DynamoDB localのコマンドオプションに -dbPath /data を指定している点です。-dbPathでdockerがマウントしているvolumeに書き出すことによって、指定したディレクトリにデータを吐き出させるようにしています。こうすることで、データを永続化しています。-inMemoryオプションを使ってしまうと、毎回データが削除されてしまうので、開発時にそのオプションを利用するのは少し手間が掛かってしまうでしょう。(テストのときはあると良さそうです)

2点目は、DynamoDB localのコマンドオプションに、 -sharedDbオプションを指定しているところです。-sharedDbオプションを指定しない場合、データはmyaccesskeyid_region.db というフォーマットで格納されます。これはこれで、毎回起動するときにそのあたりのパラメータをちゃんと設定できていればよいのですが、今回は簡単のため-sharedDbオプションを指定しています。

3点目は、networksを指定しているところです。aws-sam-localによってlocalで実行されるLambdaは、起動時にdockerのnetworkを指定することができます。ここで指定したnetworksを aws-sam-localの起動時にも利用することによって、localで起動しているLambdaから、このdocker containerにアクセスすることができるようになります。

これらDynamoDB localのオプション内容については、公式ドキュメントに記載があるので参照してください。ということで設定ができたので、下記コマンドを実行してDynamoDB Localの環境を構築しましょう。

docker network create lambda-local
docker-compose up

typescript

ぼくが趣味で作っている、AWS Lambdaのコードから取ってきたやつです。 https://github.com/selmertsx/datadog_slack_report

今思えばちょっと設計に改善の余地ありですな...。この後新しい機能を追加予定なので、そのときにでもリファクタリングしようと思います。一旦必要そうなもののみ引っ張ってきました。

// https://github.com/selmertsx/datadog_slack_report/blob/c4e59fdb60b2e190bd58f7e823268d8b697e3dfb/src/index.ts
import { APIGatewayEvent, Callback, Context } from "aws-lambda";
import moment from "moment-timezone";
import "source-map-support/register";
import { Billing } from "./Billing";
import { SlackClient } from "./SlackClient";

export async function datadog_handler(event: APIGatewayEvent, context: Context, callback: Callback) {
  const fromTime = moment({ hour: 0, minute: 0, second: 0 })
    .tz("Asia/Tokyo")
    .subtract(1, "days")
    .format("X");

  const toTime = moment({ hour: 23, minute: 59, second: 59 })
    .tz("Asia/Tokyo")
    .subtract(1, "days")
    .format("X");

  try {
    const billing = new Billing();
    const report = await billing.calculate(fromTime, toTime);
    const slackClient = new SlackClient();
    await slackClient.post(report.slackMessageDetail());

    callback(null, {
      statusCode: 200,
      headers: {
        "Content-Type": "application/json;charset=UTF-8",
      },
      body: JSON.stringify({ status: 200, message: "OK" }),
    });
  } catch (err) {
    throw new Error(err);
  }
}
// https://github.com/selmertsx/datadog_slack_report/blob/e078d2427806f3f9b402a3af1fbe79c98b0e2a5a/src/DynamoDBClient.ts
import { DynamoDB } from "aws-sdk";
import { ReservedPlan } from "./typings/datadog";

export class DynamoDBClient {
  private client = new DynamoDB.DocumentClient({
    endpoint: "http://dynamodb:8000", // ここが重要!!!!!
    region: "ap-north-east1",
  });

  public getReservedPlans(): Promise<ReservedPlan[]> {
    return new Promise<any>((resolve: any, rejects: any) => {
      this.client.scan({ TableName: "DatadogPlan" }, (error, data) => {
        if (error) {
          rejects(error);
        } else if (data.Items == undefined) {
          resolve([]);
        } else {
          const results: ReservedPlan[] = [];
          data.Items.forEach(item => {
            results.push({ productName: item.Product, plannedHostCount: item.PlannedHostCount });
          });
          resolve(results);
        }
      });
    });
  }
}

さて、長々とコードが書いてあるのであれなのですが、重要なのは1点だけです。DynamoDBのendpointについて http://${dynamodb-localのcontainer名}:8000 としていることです。これによってSAM Localで起動したAWS Lambdaから、LocalのDynamoDBにアクセスすることができます。

  private client = new DynamoDB.DocumentClient({
    endpoint: "http://dynamodb:8000", // ここが重要!!!!!
    region: "ap-north-east1",
  });

起動方法

ということで、ここまでやったら後は起動するだけ。起動する際は、 sam local invoke コマンドの --docker-network オプションに、先程 docker-compose.yml で指定した network名を設定してみましょう。具体的には下記のコマンドになります。

$ npx webpack --config webpack.prod.js
$ sam local invoke --docker-network lambda-local -e events/event_apigateway.json --env-vars .env.json DatadogReport

2019-04-25 10:30:30 Found credentials in environment variables.
2019-04-25 10:30:30 Invoking index.datadog_handler (nodejs8.10)

Fetching lambci/lambda:nodejs8.10 Docker container image......
2019-04-25 10:30:33 Mounting /Users/shuhei.morioka/project/speee/datadog_slack_report as /var/task:ro,delegated inside runtime container
START RequestId: dbefc77e-42dc-1d21-a444-0abc44875df5 Version: $LATEST
END RequestId: dbefc77e-42dc-1d21-a444-0abc44875df5
REPORT RequestId: dbefc77e-42dc-1d21-a444-0abc44875df5  Duration: 4299.35 ms    Billed Duration: 4300 ms        Memory Size: 256 MB     Max Memory Used: 121 MB

ということで、Localで動いているAWS LambdaからDynamoDB Localにアクセスすることができました〜。