[DynamoDB Local] DynamoDB LocalをDockerコンテナサービスとして起動し、別のDockerコンテナから呼び出す方法


2020/03/29

今回は
Amazon DynamoDB Localの公式Dockerイメージを利用したAWS SAM モデルの実践的な設計パターンを、本番環境へ投入前のローカルな開発環境を独自構築してみようと思います。

今回の目的は、
AWS DynamoDB Localの公式Dockerイメージをpullしてきて、ローカルでDynamodb LocalのDockerコンテナをdocker-composeで起動する方法と、そのコンテナをserverless-offlineによって利用する方法をご紹介します。


Dynamodb Localを使う環境の準備

DynamoDBについて

なんだか付け焼き刃程度の私がNoSQLを語ると、専門家の偉い方々からけちょんけちょんにされそうですので、これからガッツリと勉強されたい方はその道の方が書かれた書籍を参考ください。。。

以下はDynamoDBだけはなく、NoSQLを広く網羅しているため、DynamoDBを使おうかどうしようか悩んでおられる方に有用です。

docker-compose.ymlの実装例

まず大前提として、お手元のPC環境にdockerdocker-composeがすでに動作しているものとします。

早速、プロジェクト名
dynamodb_local_dockerを作成し、docker-compose.ymlをプロジェクトに追加してみます。

            
            $ mkdir dynamodb_local_docker && cd dynamodb_local_docker
$ touch docker-compose.yml
        
次にdocker-compose.ymlが作成されたら、このファイルを以下のように編集しておきます。

            
            version: "3"

services:
  dynamodb:
    image: amazon/dynamodb-local
    container_name: dynamodb-local-docker-storage
    volumes:
      - "./data:/home/dynamodblocal/data"
    ports:
      - "5984:5984"
    command: "-jar DynamoDBLocal.jar -port 5984 -dbPath ./data -sharedDb"
        
さて、プロジェクト直下にdataフォルダを作成します。

ここにデータファイルが格納され、ホストとDockerコンテナ上で同期させます。

今回は
-sharedDbを指定しているので、テープルが生成あるたびに、shared-local-instance.dbという名前のデータベースファイルに上書きされて行きます。

            
            $ tree
.
├── data
│   └── shared-local-instance.db #👈この時点ではまだ生成していない
└── docker-compose.yml
        
DynamoDB Localの既定のポートは8000番が開くのですが、ここでは5984番に変えております。(ポート番号はお好みで変更してください)

特に
-sharedDbオプションを付与しなくても動作も問題ないようです。

関連記事(AWS DynamoDB Local 触ったらハマった)の中にも記載されている通り、認証IDとリージョンの設定を読み込んで、[access_key_id]_[region_name].dbのフォーマットで作成される仕組みになっており、-sharedDbなしで使用するには、.aws/credentialsに予め一手間加えることをお忘れなきよう設定しましょう。

参考:
DynamoDBローカルのDockerコンテナにsharedDBオプションを付与する

Dynamodb Localコンテナを起動してみる

それでは、コンテナを起動しましょう。

そのままプロジェクトのルートディレクトリで、

            
            $ docker-compose up
Starting dynamodb-local-docker-storage ... done
Attaching to dynamodb-local-docker-storage
dynamodb-local-docker-storage | Initializing DynamoDB Local with the following configuration:
dynamodb-local-docker-storage | Port: 5984
dynamodb-local-docker-storage | InMemory: false
dynamodb-local-docker-storage | DbPath: ./data
dynamodb-local-docker-storage | SharedDb: true
dynamodb-local-docker-storage | shouldDelayTransientStatuses: false
dynamodb-local-docker-storage | CorsParams: *
dynamodb-local-docker-storage |
        
と、docker-composeから起動することでコンテナが常駐できます。

正常に起動しているかの確認のためにブラウザで
http://localhost:5984/shell/を叩いてみますと、

合同会社タコスキングダム|蛸壺の技術ブログ

のシェル画面に入れていたらOKです。

参考書籍 〜 Nosql・Dynamodbをもっと学びたい方向け

どのデータベースを扱う時にも当てはまるのですが、やはりSQL等々に慣れるためには、一つは実践例を自分の指でポチポチとコーディングしてみることが一番手っ取り早い習得方法かと思います。

DynamoDBの該当パートは少ないめですが、以下の2書を挙げておきます。


DynamoDB Localの基本的な操作方法

前節まででDynamoDB Localコンテナの起動ができましたが、DynamoDB Localの概要を掴んでいただくために、ちょっとだけ操作法を取り上げようと思います。

まずはDynamoDB LocalのDockerコンテナを使って、
DocumentClientクラスを使ったデータベスの基本操作をみていきましょう。

DocumentClientクラス

当然ながらDynamoDB Localでも、DocumentClientが利用できます。

これによって、
NとかBとか…思い出すのも億劫なDynamoDB標準の型定義を、javascriptのソースコードから自動で、変換してくれる便利クラスです。

公式の変換表からザッとどんな感じに変換されるのかみると、

javascript 型

DynamoDB アトリビュート型

string

S

number

N

boolean

BOOL

null

NULL

Array

L

Object

M

Buffer, File, Blob, ArrayBuffer, DataView, プリミティブ型以外の要素配列

B

この表ような変換をバックグラウンドでやってくれます。

これはもう使わない手はありません。

createTable (テーブルの新規作成)

先ずは新しいテーブルを作ります。

残念ながら(?)、テーブルを新規作成する
createTableメソッドは、AWS.DynamoDBクラスの関数ですので、DocumentClientは利用できません。

今回作成するテーブルは、
string型のchallengerをハッシュアトリビュートに、number型のtimestampをレンジアトリビュートに指定しております。

変数名はなんでも構わないようですが、
テーブル作成時に、`HASH`または`HASH + RANGE`のどちらかの組み合わせを定義しなければならない規則になっているようで、これを怠るとエラーが出ます

ここら辺のDynamoDB特有のキーやインデックスという文言に関する深い話は、
記事(コンセプトから学ぶAmazon DynamoDB【インデックス俯瞰篇】)を参考にされると宜しいかと思います。

            
            // 👇のようにわざわざ新しくAWS.DynamoDBインスタンスをnewしなくても
// DynamoDB JavaScript Shell 起動時に、'dynamodb'という名の
// インスタンスが存在している. (今回は練習も兼ね, 新しいインスタンス'myDynamo'を生成)
const myDynamo = new AWS.DynamoDB({
    endpoint: "http://localhost:5984"
});

const params = {
    TableName: 'hand-game',
    AttributeDefinitions: [
        // 少なくともHASH属性に指定する代表変数が一つ必要.
        // RANGEも追加するなら1項目のみ記述できる.
        {
            AttributeName: 'challenger', // HASH用のフィールド
            AttributeType: 'S' // String型
        },
        {
            AttributeName: 'timestamp', // RANGE用のフィールド
            AttributeType: 'N' // Number型
        }
    ],
    KeySchema: [ // 先程のフィールドにスキーマを指定する.
        {
            AttributeName: 'challenger',
            KeyType: 'HASH'
        },
        {
            AttributeName: 'timestamp',
            KeyType: 'RANGE'
        }
    ],
    ProvisionedThroughput: {
        ReadCapacityUnits: 1,
        WriteCapacityUnits: 1
    }
}

myDynamo.createTable(params, (err, data) => {
    if (err) {
        console.log(err, err.stack);
    }
    else {
        console.log(data);
    }
})
        
このテーブル定義のお品書きの中のProvisionedThroughputに関しては、大抵の場合にはProvisionedキャパシティで利用する方が多いと思いますが、その際には設定をキチンと記述しないといけない約束になっているようです。

なお、もう一つの書き込み・読み込みをオンデマンドにするOn-demandキャパシティでの利用時は、
BillingMode: 'PAY_PER_REQUEST'をつけるようです。

この二つは使用料金も、課金方法も、ユースケースによって微妙に異なるので、
Simple Monthly Calculatorでその辺りを一度御検討ください。

今回のDynamoDB Localのお話では、このデータ量スループット設定は意味がないのですが、控え目に両キャパシティユニット量を
1としておきます。

▶️ボタンでコンソールに送ると、上手くテーブルが生成されたら、以下のような応答が返ります。

            
            {"TableDescription":{"AttributeDefinitions":[{"AttributeName":"challenger","AttributeType":"S"},{"AttributeName":"timestamp","AttributeType":"N"}],"TableName":"hand-game","KeySchema":[{"AttributeName":"challenger","KeyType":"HASH"},{"AttributeName":"timestamp","KeyType":"RANGE"}],"TableStatus":"ACTIVE","CreationDateTime":"2019-10-02T14:50:32.561Z","ProvisionedThroughput":{"LastIncreaseDateTime":"1970-01-01T00:00:00.000Z","LastDecreaseDateTime":"1970-01-01T00:00:00.000Z","NumberOfDecreasesToday":0,"ReadCapacityUnits":1,"WriteCapacityUnits":1},"TableSizeBytes":0,"ItemCount":0,"TableArn":"arn:aws:dynamodb:ddblocal:000000000000:table/hand-game"}}
        

listTables (テーブル一覧)

先程のテーブルは生成されているのか確認したい場合は、DynamoDBクラスのtableListsメソッドを使います。

            
            const myDynamo = new AWS.DynamoDB({
    endpoint: "http://localhost:5984"
});

myDynamo.listTables({}, (err, data) => {
    if (err) { console.log(err, err.stack); }
    else { console.log(data); }
});
        
このコードスニペットをコンソールに送ると👇のようになります。

合同会社タコスキングダム|蛸壺の技術ブログ

describeTable (テーブル定義の確認)

DynamoDB`DynamoDB`クラスにはdescribe***という感じのメソッド群がありますが、その中でもとりわけテーブル定義の確認時に便利なdescribeTableメソッドを用いて、テーブルの詳細を表示してみます。

            
            const myDynamo = new AWS.DynamoDB({
    endpoint: "http://localhost:5984"
});

myDynamo.describeTable({ TableName: 'hand-game' }, (err, data) => {
    if (err) { console.log(err, err.stack); }
    else { console.log(data); }
});
        
このコードスニペットをコンソールに送ると(👇)テーブルの設定値が確認できます。

合同会社タコスキングダム|蛸壺の技術ブログ

put (要素追加)

ここからDocumentClientクラスのputメソッドでテーブルに要素を一つ書き込んでみます。

            
            const docClient = new AWS.DynamoDB.DocumentClient({
    endpoint: "http://localhost:5984"
});

const date = new Date();
const epochTime = date.getTime();

const params = {
    TableName: 'hand-game',
    Item: {
        challenger: 'taro', // Hash key, mandatory
        timestamp: epochTime, // Range key, required
        challengerSign: '3',
        foeSign: '5',
        issue: 'draw'
    }
};

docClient.put(params, (err, data) => {
    if (err) {
        console.log(err);
    }
    else {
        console.log(data);
    }
});
        
これにひとまず送りますコンソールに送るのですが、{}のようなから応答が帰ってくると思いますが一先ず気にせず先に行きます。

scan (リスト全検索)

さて、先ほどの要素は追加されているのでしょうか?

テーブルをスキャンして、中身をのぞいてみます。

            
            const docClient = new AWS.DynamoDB.DocumentClient({
    endpoint: "http://localhost:5984"
});

docClient.scan({ TableName: 'hand-game' },
    (err, data) => {
        if (err) {
            console.log(err, err.stack);
        }
        else {
            console.log(data);
        }
    }
);
        
これを▶️で送ると、

            
            {"Items":[{"challenger":"taro","challengerSign":"3","issue":"draw","foeSign":"5","timestamp":1570029168460}],"Count":2,"ScannedCount":2}
        
上のような応答があり、書き込みがあったことがわかります。

get (テーブルから先程追加した要素を取得)

テーブル内の要素を個別に取り込むには、DocumentClientクラスのgetメソッドを使います。

            
            const docClient = new AWS.DynamoDB.DocumentClient({
    endpoint: "http://localhost:5984"
});

docClient.get({
        TableName: 'hand-game',
        Key: { // 取得したいHASH値とRANGE値をもつ要素を指定
            challenger: 'taro',
            timestamp: 1570029168460
        }
    }, (err, data) => {
    if (err) { console.log(err); }
    else { console.log(data); }
});
        
このコードスニペットを実行すると、テーブルを捜索して指定の HASH & RANGE に合致した項目を取得してくれます。

合同会社タコスキングダム|蛸壺の技術ブログ

その他の操作

もっと突っ込んで、フィルター処理としてのqueryメソッドの使いこなしもご紹介できたらよかったのですが、本題から逸れるので内容から割愛したいと思います。

もっと応用的なテクニックを知りたい方は、書籍や他の技術ブログもご参考になられてください。


実践編: DynamoDB Localをローカルで利用するためのSAMモデル

前節までで、DynamoBD LocalのGUI操作の雰囲気程度を味わっていただけたかと思います。

ここからは話題変わって、DynamoDB LocalのDockerコンテナを扱う側の部分をローカルでどう構築するかの内容です。

まず本来の
本番で運用したとして、SAM(Serverless Application Model)の模式図は下のような感じです。

合同会社タコスキングダム|蛸壺の技術ブログ

それで、前回まではDynamoDBをローカルでどう取り扱うのかの内容を載せておりました。

今回記事の残り半分の
Api Gateway + Lambdaをどうしましょうか、という内容にフォーカスします。

結論から言いますと、
Serverless OfflineというServerless Frameworkのプラグインエミュレータを使えば、ローカルでもApi Gateway + Lambdaの擬似サービスを簡単に実現することが可能です。

したがって、模式図で示すと以下のようになります。

合同会社タコスキングダム|蛸壺の技術ブログ

それでは
Serverless OfflineのDockerコンテナサービスを構築してみましょう。

ローカルSAMプロジェクトの構造

まずはプロジェクトの作成を行います。

適当なプロジェクトのフォルダを作成し、そこのルートから
treeすると、

            
            $ tree -I "node_modules"
.
├── Dockerfile
├── docker-compose.yml
└── package.json
        
とりあえず上記の3つの空ファイルを先に作ります。

余談ですが、npmでのパッケージインストール後に
node_modulesが出来た状態で、treeをそのまま叩くとコンソールが悲惨な状態になります。

余程の理由がない場合には、オプションに
-I "node_modules"を入れておかれると幸せになれます。

Serverless Offlineコンテナ作成

それでは、以下Serverless OfflineコンテナをビルドするためのDockerfiledocker-compose.ymlの設定ファイルを記載します。

コピペするなりしてご利用ください。

Dockerfile

まずはDockerfileから実装していきます。

            
            FROM node:10-alpine

#Install dependent packages
RUN apk update && apk upgrade && apk add --no-cache \
    bash git openssh \
    python \
    curl \
    groff \
    jq

#awscli on dokcer alpine
ARG pip_installer="https://bootstrap.pypa.io/get-pip.py"
ARG awscli_version="1.16.248"

#Install awscli
RUN curl ${pip_installer} | python && \
    pip install awscli==${awscli_version}

#Completing input
RUN bash -c 'echo complete -C '/usr/bin/aws_completer' aws >> $HOME/.bashrc'

#Setting envirnment for development
WORKDIR /usr/src/app
ENV PS1="[\u@\h:\w]$"

#Installing npm package manager
RUN npm i -g yarn
        
今回はnode:10-alpineをベースイメージとして拡張しました。

nodejsが正常に動作していれば、node:10-alpineにこだわる理由もないのですが、コンテナの容量が小さいので今回などのような用途ではおすすめです。

なお、本番用のAWSへのデプロイで利用する
aws cliは本内容では用いないので必要ではないのですが、そのまま本番のサービスへ移行したい場合には直ぐにでもデプロイ出来ます。

オプション程度にご紹介だけしておきます。

docker-compose.yml

次にdocker-compose.ymlも用意しておきます。

もちろん必須ではないのですが、今回のようなカスタムDockerコンテナで、dockerコマンドで以下の今回のコンテナ全てのオプションを引数で与えるのは尋常ではないため、
docker-composeを活用しましょう。

            
            version: '3'

services:
    app: # appというサービス名で構築
        image: my/sls-offline-test # コンテナー名
        build: .
        environment:
            NODE_ENV: development
            AWS_ACCESS_KEY_ID: "ABCDEFGHIJKLMNOPQRST"
            AWS_SECRET_ACCESS_KEY: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
            AWS_DEFAULT_REGION: "us-east-1"
            LOCAL_SLS_SERVICE_NAME: "sls-offline-test" # Serverless Offlineのアプリ名
        ports:
            - "5985:5985" # Serverless Offlineのサービス用に5985番を指定
            # - "3000:3000" # デフォルトのServerless Offileのサービスポート
        volumes:
            - ./:/usr/src/app
        tty: true
        
コンテナを立ち上げて常駐サービスとする際に、http://localhost:5985でアクセスできるようにデフォルトの3000番から変更しております(番号はお好みです)。

また、環境変数で設定している
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGIONは、aws-cliが本番用のデプロイの際に必須となるIAMロールのキー値です。

本番用にAWSへアクセスされる際には正しく設定しましょう。

コンテナのビルドとインタラクティブモードへの移行

早速コンテナをビルドしてみましょう。(ビルドは初回のみ)

            
            $ docker-compose build
        
コンテナがうまくビルド出来たでしょうか。

その後は、このコンテナをバックグラウンドで起動し、インタラクティブへ入って、コンテナの
bashに切り替えてプロジェクトの開発の続きを行います。

            
            $ docker-compose up -d
Creating your_awesome_project_app_1 ... done

$ docker-compose exec app bash
[root@xxxxxxxxxx:/usr/src/app]$
        

Serverlessアプリケーションの作成 (前準備)

ここから暫くは、インタラクティブモードでコンテナに潜って作業します。

まずプロジェクトのルートディレクトリの
package.jsonに最低限の項目を作成します。

            
            {
    "name": "sls-offline-test",
    "version": "0.0.1",
    "scripts": {}
}
        
とこんな感じです。

次に
serverlessdevDependanciesでインストールします。

            
            $ yarn add serverless -D
        
すると、package.jsonの開発パッケージ依存性に最新版がインストールされます。

その後、
appというフォルダへserverlessアプリ本体を置きます。

            
            $ sls create --template aws-nodejs-ecma-script \
    --name $LOCAL_SLS_SERVICE_NAME \
    --path app
        
ここでは、serverless側で用意されているテンプレートでaws-nodejs-ecma-scriptを指定しています。

aws-nodejsとしても構いませんがこのテンプレートはpackage.jsonwebpack.config.jsが自動で生成されませんのでご留意ください。

sls createserverlessアプリの雛形一式wを作成するコマンドで、以下のようにappフォルダー以下にファイルが追加されているのを確認してください。

            
            $ tree -I 'node_modules'
.
├── Dockerfile
├── docker-compose.yml
├── package.json
├── app
│   ├── first.js
│   ├── package.json
│   ├── second.js
│   ├── serverless.yml
│   └── webpack.config.js
└── yarn.lock
        
初回はserverlessアプリに必要なパッケージをインストールする必要がありますので、

            
            cd ./app && yarn install
        
を実行してください。

このまま
appフォルダのディレクトリで、aws-sdkserverless-offlineプラグインをここでインストールしましょう。

            
            #'app'フォルダー内で実行
$ yarn add aws-sdk serverless-offline -S
        
これでserverlessアプリを作成するための下ごしらえが出来ました。

ちなみに、
appフォルダに自動で生成されたfirst.jssecond.jsはいらないので削除し、handler.jsという名前の空のソースコードファイルを作成しておきます。

そうするとこの時点で、今回のプロジェクトは、

            
            $ tree -I 'node_modules'
.
├── Dockerfile
├── docker-compose.yml
├── package.json
├── app
│   ├── handler.js
│   ├── package.json
│   ├── serverless.yml
│   └── webpack.config.js
└── yarn.lock
        
こんな感じになっていると思います。

ちょうど区切りもいいので、具体的なLambdaのハンドラの実装とコンテナの動作確認は次節に回します。

書籍紹介〜Dockerをもっと学びたい方向け

ここまででDockerコンテナのServerless Offlineプラグインの導入までをご紹介しました。

どちらかと言えば、
docker-composeの使い所的なお話が中心でしたが、ローカル上のDockerコンテナ同士のネットワーキングでは、この使い方を理解できることが重要です。

ぜひこの機会にdockerの知識を深めましょう。


実践編: Serverless-OfflineコンテナからDynamodb Localサービスを利用する方法

作業もいよいよ大詰めです。

それではServerlessアプリの設計図にあたる
serverless.ymlをデザインしましょう。

serverless.ymlを実装

今回のサーバレスapiの設計は以下のようにします。

            
            service:
  name: sls-offline-test

plugins:
  - serverless-webpack
  - serverless-offline # ここにServerless Offlineプラグインを追加

custom:
  serverless-offline:
    host: 0.0.0.0 # 重要: 既定値のlocalhostではdocker内部から呼び出せません
    port: 5985 # サービスポートを5985に変更

provider:
  name: aws
  runtime: nodejs8.10

functions:
  play:
    handler: handler.playGame # handler.js内の playGame 関数を呼び出す
    events:
      - http:
          path: handgame
          method: post
  list:
    handler: handler.showHistory # handler.js内の showHistory 関数を呼び出す
    events:
      - http:
          path: handgame
          method: get
        
ポイントだけを軽く解説しますと、今回のサーバレスRestアプリ(sls-offline-test)は、playというPOSTメソッドでDynamoDBに要素を追加し、listというGETメソッドでDyanmoDBの中身をリストさせるようにしている設計を記述しております。

前者
play関数の中身を、後述しますhandler.jsplayGame関数に、後者listshowHistory関数に対応し、各々実装を適切に実装すればOKです。

最終的には、以下のようなコマンドをDynamoDB Localのサービスコンテナへ投げて反応をみることが今回の目的です。

            
            #👇POSTメソッドのイメージ
curl 'http://localhost:5985/handgame?{何かのクエリ文字列}' -X POST

#👇GETメソッドのイメージ
curl 'http://localhost:5985/handgame'
        

ハンドラ関数を定義

それでは、上で予告していたAWS Lambdaのhandler.jsの実装です。

今回、ただDynamoDBのアクセスするだけでは物足りなさを感じましたので、
ジャンケンよりちょっびり複雑な弊社地元の拳遊び...のルールに乗っ取り、DynamoDBのテーブルに書き込むたびに勝敗(issue)を付けてくれるPOSTメソッドの実装に余分な物を付け足しておりますが、'ここを弄れば応用が聞くんだな'程度に捨て置きください。

            
            "use strict";

// オフライン(isOffline)を判定し、ローカル利用なら DynamoDB Localに、
// そうでないなら通常のAWSへサービスのターゲットを割り振ります。
const aws = require("aws-sdk");
const getDynamoClient = (event) => {
    let dynamodb = null;
    if ("isOffline" in event && event.isOffline) {
        dynamodb = new aws.DynamoDB.DocumentClient({
            region: "localhost",
            // Dockerコンテナの環境変数 OUTER_DYNAMODB_IP には別のyamlファイルを用意する (後述)
            endpoint: `http://${process.env.OUTER_DYNAMODB_IP}:5984`
        });
    } else {
        dynamodb = new aws.DynamoDB.DocumentClient();
    }
    return dynamodb;
}

// とあるローカルな遊びのルール...
const judgeGame = (challenger, foe) => {
    const c = Math.abs(challenger - foe);
    const c_sgn = Math.sign(challenger - foe);
    if (c === 1 && c_sgn > 0) {
        return "won";
    } else if (c === 1 && c_sgn < 0) {
        return "lose";
    } else {
        return "draw";
    }
}

// コンピュータと対戦させて、試合結果のインスタンスを吐き出す。
const createGameInstance = (challenger, challengerHand) => {
    const handRule = ["zero", "one", "two", "three", "four", "five"];
    const date = new Date();
    const foeHandIndex = Math.floor( Math.random() * 6);
    return {
        timestamp: date.getTime(),
        challenger: challenger,
        challengerSign: challengerHand,
        foeSign: handRule[foeHandIndex],
        issue: judgeGame(handRule.indexOf(challengerHand), foeHandIndex)
    };
}

// POSTメソッドの実体関数
module.exports.playGame = (event, context, callback) => {
    const gameResult = createGameInstance(
        event.queryStringParameters.name,
        event.queryStringParameters.hand
    );
    const params = {
        TableName: "hand-game", // 今回は'hand-game'というテーブル名を使います。
        Item: gameResult
    };

    const docClient = getDynamoClient(event);
    docClient.put(params, (error) => {
        const response = {statusCode: null, body: null};
        if (error) {
            console.log(error);
            response.statusCode = 500;
            response.body = {
                code: 500,
                message: "Doesn't show the list due to internal error!"
            };
        } else {
            response.statusCode = 200;
            response.body = JSON.stringify(gameResult);
        }
        callback(null, response);
    });
};

// GETメソッドの実体関数
module.exports.showHistory = (event, context, callback) => {
    const docClient = getDynamoClient(event);
    docClient.scan({
        TableName: "hand-game" // 今回は'hand-game'というテーブル名を使います。
    }, (error, data) => {
        const response = {
            statusCode: null,
            body: null
        };
        if (error) {
            console.log(error);
            response.statusCode = 500;
            response.body = {
                code: 500,
                message: "Doesn't show the list due to internal error!"
            };
        } else if ("Items" in data) {
            response.statusCode = 200;
            response.body = JSON.stringify({handGame: data["Items"]});
        }
        callback(null, response);
    });
};
        
...ということでここまでで、実装は終わりです。

あとはServerless Offlineをコンテナ起動してみれば良いわけですが、手元の
kubernetesが相変わらず復旧できず残念な感じでしたので、dockerだけで頑張ってみます。

package.jsonへスクリプトの追加

npmのスクリプトでserverless offlineが立ち上がるようにscriptsタブに追加しておきます。

            
            {
    "name": "sls-offline-test",
    "version": "0.0.1",
    "scripts": {
        "start": "cd ./app && sls offline" // 👈 追加
    },
    "devDependencies": {
        "serverless": "^1.53.0"
    }
}
        

複数のdocker-compose.ymlの合わせ技

もし現在のdockerコンテナのインタラクティブモードに入っていたら、exitコマンドで抜けて、docker-compose downでコンテナを落としておきましょう。

さて、
docker-composeでは、-fオプションを使って、複数の設定ファイルの合成が可能です。

プロジェクトのルートディレクトリに
env-local.ymlを追加し、下のような感じにしておきます。

            
            $ tree -I 'node_modules'
.
├── Dockerfile
├── app
│   ├── handler.js
│   ├── package.json
│   ├── serverless.yml
│   ├── webpack.config.js
│   └── yarn.lock
├── docker-compose.yml
├── env-local.yml
├── package.json
└── yarn.lock
        
ここで比較するため、docker-composeの設定を確認するコマンドのdocker-compose configを使ってみます。

            
            $ docker-compose config
services:
  app:
    build:
      context: /{現在のプロジェクトのフォルダまでのパス}
    environment:
      AWS_ACCESS_KEY_ID: ABCDEFGHIJKLMNOPQRST
      AWS_DEFAULT_REGION: us-east-1
      AWS_SECRET_ACCESS_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      LOCAL_SLS_SERVICE_NAME: sls-offline-test
      NODE_ENV: development
    image: my/sls-offline-test
    ports:
    - 5985:5985/tcp
    tty: true
    volumes:
    - /{現在のプロジェクトのフォルダまでのパス}:/usr/src/app:rw
version: '3.0'
        
と出力され、現在のdocker-compose.ymlの内容が反映されるのが確認できます。

env-local.ymlに以下の内容を記述します。

            
            version: '3'

services:
  app:
    entrypoint: yarn start
    environment:
      OUTER_DYNAMODB_IP: "172.0.0.1" # 適当なIP値を与えておきます
        
早速、このdocker-composeの設定ファイルの二つを合成してみます。

            
            $ docker-compose -f docker-compose.yml -f env-local.yml config
services:
  app:
    build:
      context: /{現在のプロジェクトのフォルダまでのパス}
    entrypoint: yarn start # 👈この項目が追加
    environment:
      AWS_ACCESS_KEY_ID: ABCDEFGHIJKLMNOPQRST
      AWS_DEFAULT_REGION: us-east-1
      AWS_SECRET_ACCESS_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      LOCAL_SLS_SERVICE_NAME: sls-offline-test
      NODE_ENV: development
      OUTER_DYNAMODB_IP: 172.0.0.1 # 👈この項目が追加
    image: my/sls-offline-test
    ports:
    - 5985:5985/tcp
    tty: true
    volumes:
    - /{現在のプロジェクトのフォルダまでのパス}:/usr/src/app:rw
version: '3.0'
        

DynamoDB LocalのDockerコンテナのIPの取得

以前の記事(Verdaccio on DockerでコンテナのローカルIPを調べる方法)で記載しているため、詳細はそちらをご覧ください。

上記で作成していた
DynamoDB LocalのDockerコンテナを立ち上げます。

そして別のコンソールからIPを以下のように調べます。

            
            #👇DynamoDB Local のコンテナIDをチェック
$ docker ps -a
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS                      PORTS                              NAMES
b7209695b7e1        amazon/dynamodb-local   "java -jar DynamoDBL…"   6 hours ago         Up 6 hours                  0.0.0.0:5984->5984/tcp, 8000/tcp   dynamodb-local-docker-storage

#👇コンテナIDからIPを取得
$ docker inspect --format='{{range .NetworkSettings.Networks}}{{.Gateway}}{{end}}' b72
172.19.0.1
        
よって現在のターゲットのIPは172.19.0.1であることがわかります。

この値を
env-local.ymlに反映します。

            
            version: '3'

services:
  app:
    entrypoint: yarn start
    environment:
      OUTER_DYNAMODB_IP: "172.19.0.1" # DynamoDB Localコンテナの(デフォルトゲートウェイ)IP
        
ちなみに、このIP値はDockerコンテナを再起動させる度に変わることもあります。

その場合、IP値が変わった場合その都度値を手動で更新する必要があります。

Serverless Offlineコンテナの起動

ここまで長い道のりでしたが、ようやく起動できます...コンテナの起動は以下のコマンドです。

            
            $ docker-compose -f docker-compose.yml -f env-local.yml up
$ cd ./app && sls offline
app_1  | Serverless: Bundling with Webpack...
app_1  | Time: 6313ms
app_1  | Built at: 10/06/2019 9:47:16 AM
app_1  |      Asset     Size   Chunks             Chunk Names
app_1  | handler.js  6.4 MiB  handler  [emitted]  handler
app_1  | Entrypoint handler = handler.js
#...中略
app_1  | Serverless: Starting Offline: dev/us-east-1.
app_1  |
app_1  | Serverless: Routes for play:
app_1  | Serverless: POST /handgame
app_1  | Serverless: POST /{apiVersion}/functions/sls-offline-test-dev-play/invocations
app_1  |
app_1  | Serverless: Routes for list:
app_1  | Serverless: GET /handgame
app_1  | Serverless: POST /{apiVersion}/functions/sls-offline-test-dev-list/invocations
app_1  |
app_1  | Serverless: Offline [HTTP] listening on http://0.0.0.0:5985
app_1  | Serverless: Enter "rp" to replay the last request
        
これでサーバレスAPIが、無事に立ち上がりました。

動作確認〜じゃんけんゲームをさせてみる

では試しに、別のコンソールから適当な要素をローカルのDynamoDBにPOSTしてみます。

            
            #👇チャレンジャー名=tarao が 零の手=zero を出して勝負
$ curl 'http://localhost:5985/handgame?hand=zero&name=tarao' -X POST | jq '.'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   104  100   104    0     0    298      0 --:--:-- --:--:-- --:--:--   298
{
  "timestamp": 1570355702256,
  "challenger": "tarao",
  "challengerSign": "zero",
  "foeSign": "four",
  "issue": "draw"
}

#👇チャレンジャー名=ikura が 四の手=four を出して勝負
$ curl 'http://localhost:5985/handgame?hand=four&name=ikura' -X POST | jq '.'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   104  100   104    0     0    515      0 --:--:-- --:--:-- --:--:--   517
{
  "timestamp": 1570355971259,
  "challenger": "ikura",
  "challengerSign": "four",
  "foeSign": "five",
  "issue": "lose"
}
        
とまぁ、2試合投げてみました。ここでDynamoDBのリストを覗いてみると、

            
            $ curl 'http://localhost:5985/handgame' | jq '.'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1360  100  1360    0     0   8545      0 --:--:-- --:--:-- --:--:--  8553
{
  "handGame": [
    {
      "challenger": "ikura",
      "challengerSign": "four",
      "issue": "lose",
      "foeSign": "five",
      "timestamp": 1570355971259
    },
    {
      "challenger": "tarao",
      "challengerSign": "zero",
      "issue": "draw",
      "foeSign": "four",
      "timestamp": 1570355702256
    }
  ]
}
        
とデータベースにバッチリと記憶されております。


まとめ

ここまで読んで頂いてありがとうございました。

ちょっと長い記事になりましたが
DynamoDB Localをローカル環境だけで利用するお話をダイジェストでやってみました。

このやり方のメリットは、AWSのアカウントが無くともDynamoDBが試せるため、これからAWSをはじめられる初学者や、複雑なデータベース構造でもきちんと動作するかの試験をやったりと、何かと便利に活用できるかと思います。

機会があれば、またいつか何か応用的な一例を挙げてサンプルを特集するかもしれませんので、興味があれば今後ともブログを覗いてみてください。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。