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


※ 当ページには【広告/PR】を含む場合があります。
2020/03/29
【Serveless Framework V.3対応】serverless.ymlのpackageを利用してzipにまとめるファイルを選択する方法
【AWS/Serverless Framework】 S3rver & Serverless Offineで構築するローカルのS3 bucketライクなストレージ環境の導入する方法




今回は
Amazon DynamoDB Localの公式Dockerイメージ の実践的な設計パターンを、本番環境へ投入前のローカルな開発環境を独自構築してみようと思います。
今回の目的は、
AWS DynamoDB Localの公式Dockerイメージ をpullしてきて、ローカルで Dynamodb Local のDockerコンテナを docker-compose で起動する方法と、そのコンテナを serverless-offline によって利用する方法をご紹介します。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

Dynamodb Localを使う環境の準備

DynamoDBについて



なんだか付け焼き刃程度の私がNoSQLを語ると、専門家の偉い方々からけちょんけちょんにされそうです。
これからNoSQLをガッツリと勉強されたい方はその道の方が書かれた書籍を参考ください。。。
書籍紹介として、「
RDB技術者のための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の該当パートは少ないめですが、「
Amazon Web Servicesサーバーレスレシピ (技術の泉シリーズ)
」と「
Amazon Web Services 業務システム設計・移行ガイド
」の2書を挙げておきます。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

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 クラスには 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メソッドの使いこなし もご紹介できたらよかったのですが、本題から逸れるので内容から割愛したいと思います。
もっと応用的なテクニックを知りたい方は、書籍や他の技術ブログもご参考になられてください。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

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



前節までで、DynamoBD LocalのGUI操作の雰囲気程度を味わっていただけたかと思います。
ここからは話題変わって、DynamoDB LocalのDockerコンテナを扱う側の部分をローカルでどう構築するかの内容です。
まず本来の
本番 で運用したとして、 SAM(Serverless Application Model) の模式図は下のような感じです。

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


それで、前回まではDynamoDBをローカルでどう取り扱うのかの内容を載せておりました。
今回記事の残り半分の
Api Gateway + Lambda をどうしましょうか、という内容にフォーカスします。
結論から言いますと、
Serverless Offline のプラグインエミュレータを使えば、ローカルでも 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をこれから学びたい人のためのオススメ書籍&教材特集

実践編: 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のアクセスするだけでは物足りなさを感じましたので、
ジャンケンよりちょっびり複雑な弊社地元の拳遊び... を付けてくれる 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
    }
  ]
}

        

とデータベースにバッチリと記憶されております。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集

まとめ



ちょっと長い記事になりましたが
DynamoDB Local をローカル環境だけで利用するお話をダイジェストでやってみました。
このやり方のメリットは、AWSのアカウントが無くともDynamoDBが試せるため、これからAWSをはじめられる初学者や、複雑なデータベース構造でもきちんと動作するかの試験をやったりと、何かと便利に活用できるかと思います。
機会があれば、またいつか何か応用的な一例を挙げてサンプルを特集するかもしれませんので、興味があれば今後ともブログを覗いてみてください。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

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

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法】Dockerをこれから学びたい人のためのオススメ書籍&教材特集