開発時のDynamoDB環境としてDynamoDB Localを用いた際に行なったノウハウを公開します。

はじめに 好物はインフラとフロントエンドのかじわらゆたかです。 現在私が構築を行っているサービスでは、DynamoDBをデータストアとして用いています。 開発時に開発者各々に開発用のDynamoDBのテーブルを作成すると […] 続きを読む

DynamoDB ローカルでチュートリアル

DynamoDBをローカルで動かすことができたので、メモ。
あとはチュートリアルを試しながらって感じで。

1. ローカル実行用にDynamoDBをダウンロード

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.html

2.解凍し当該フォルダで実行

java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb

3.テーブルにアクセスしてみる

terminal$ aws dynamodb list-tables --endpoint-url http://localhost:8000

実行結果

{
    "TableNames": []
}

ブラウザでチュートリアル

下記URLにアクセスするとチュートリアルを試せる
http://localhost:8000/shell/

スクリーンショット 2017-12-30 9.15.07.png

右側のコンソール上で、 tutorial.start() と入力するとチュートリアルが開始される。

チュートリアルを進めていくと、最初にImageテーブルが作成される。
KeySchema が、primary key となり、ユニークなものとなる。

// This CreateTable request will create the Image table.
// With DynamoDB Local, tables are created right away. If you are calling
// a real DynamoDB endpoint, you will need to wait for the table to become
// ACTIVE before you can use it. See also dynamodb.waitFor().
var params = {
    TableName: 'Image',
    KeySchema: [
        {
            AttributeName: 'Id',
            KeyType: 'HASH'
        }
    ],
    AttributeDefinitions: [
        {
            AttributeName: 'Id',
            AttributeType: 'S'
        }
    ],
    ProvisionedThroughput:  {
        ReadCapacityUnits: 1,
        WriteCapacityUnits: 1
    }
};
console.log("Creating the Image table");
dynamodb.createTable(params, function(err, data) {
    if (err) ppJson(err); // an error occurred
    else ppJson(data); // successful response
});

スクリーンショット 2017-12-30 9.25.31.png

ターミナルでもImageテーブルが作成されていることが確認できる。

terminal$ aws dynamodb list-tables --endpoint-url http://localhost:8000
{
"TableNames": [
"Image"
]
}

続きを読む

AWSでServerlessの環境をCIするための選択肢を調べたメモ

利用するサービスはAWSに限定するとした場合に開発・テスト・デプロイを継続するための選択肢をいくつか調べてみました。

開発したいものは、「API Gateway – Lambda – DynamoDB」で構成されるWebサービスとします。

正直なところ、対象像を少し絞らないと選択肢が多すぎて好みの問題になりそうなので・・・

注意

調査用のメモです。

実際に全ての選択肢で開発をしてみたわけではなく、入門記事やドキュメントを少しみた程度で判断しています。

そのため正確性に欠ける内容となっている可能性が高いことをご了承ください。

共通点

開発ツールで対応していない部分についてのデプロイについては、AWS CLIで対応していくことになるでしょう。

LocalStack

モックサービスの対応数にまず驚いた。

https://github.com/localstack/localstack

LocalStack はローカルでAWSのサービスのモックを動かしてしまうというツールです。
DockerコンテナでAWS各種サービスを一気に立ち上げることができるので、ローカル環境で開発を完結させることが出来ます。

これ自体にはAWSを管理する機能はなく、Lambdaをローカル環境で開発してテストするときに、ローカルで同時にAPI Gateway + DynamoDBを動かしたいという場合に必要となりそうです。
DynamoDB自身はAmazonからDynamoDB Localが提供されているので、どちらを使うかは検証が必要でしょう。

起動コマンドも簡単で、一発で全て立ち上げることが出来ます。

docker-compose up

macの場合は、$TMPDIRにシンボリックリンクがある場合、少しコマンドを変える必要があるようです。

TMPDIR=/private$TMPDIR docker-compose up

DynamoDB Local

ローカル開発用のDynamoDB。
ほとんどのLambda開発ツールがAPI Gatewayもサポートしていることから、DynamoDBを接続するローカル環境ではLocalStackよりはこちらを使用することになるのではないかと。公式提供でもあるし。

ダウンロードとインストールガイドはこちら

aws-sam-local

SAM は Serverless Application Modelのことで、aws-sam-localはAmazon公式がサポートしているローカル開発環境です。今はまだbetaですが、近いうちに良くなりそうです。

https://github.com/awslabs/aws-sam-local

AWS Blogで利用例が紹介されていて、DynamoDB Localを使う場合についても少し触れられています。
https://aws.amazon.com/jp/blogs/news/new-aws-sam-local-beta-build-and-test-serverless-applications-locally/

作成したLambda FunctionをローカルDockerコンテナで実行したり、現時点ではPythonとNode.jsはデバッグ出来るようです。(自分で試したところ、上手くいかなかった)

また、Lambda関数を単純に実行するだけではなく、ローカルのAPI Gatewayを通して実行を確認できます。

PostManで簡単にAPIの動作検証が行えたり、実際のHTTPアクセスの形でLambda関数の検証がローカルで行えます。
また、実行時間やメモリ消費量が表示されるため、AWSにデプロイする前に関数の効率化が出来ます。

Serverless Framework

現状で一番正解に近いと思います。

https://github.com/serverless/serverless

こちらのQitta記事こっちのQiita記事が参考になりそうです。
DynamoDB Localと連携するためのプラグインが存在し、serverless.ymlファイルにAWS上での構成を記載していき、そのままAWSにデプロイ出来るようです。

Apex

Go言語でLambdaが書ける!!!

純粋にLambdaの開発だけで見ればとても良さそうです。
ただし、ローカル実行はサポートしておらず、AWSリソースの操作もLambdaに限定されています。
その辺りは、Terraformで補う考えのようです。

公式

https://github.com/apex/apex

chalice

Python用のマイクロサービスフレームワークです。

https://github.com/aws/chalice

次の様なコードで、APIを定義できます。

chalice
@app.route('/resource/{value}', methods=['PUT'])
def put_test(value):
    return {"value": value}

デプロイでAPI Gateway, Lambdaに反映されるため、コードの見通しが良くなりそうです。
IAM Rolesなどは自動生成される様なので、とにかく簡単にコードを書いて、すぐデプロイということが出来そうです。

インストールからコード記述、デプロイまでの操作がHelloworldならこんなに簡単に出来るようです。

$ pip install chalice
$ chalice new-project helloworld && cd helloworld
$ cat app.py

from chalice import Chalice

app = Chalice(app_name="helloworld")

@app.route("/")
def index():
    return {"hello": "world"}

$ chalice deploy
...
https://endpoint/dev

$ curl https://endpoint/api
{"hello": "world"}

Zappa

こちらも、とにかく簡単にPythonで作成した関数をAWSにデプロイ出来ることがウリのようです。

https://github.com/Miserlou/Zappa

gifアニメーションで実演が付いています。(README.mdから引用しました)
README.md

Zappa Demo Gif

REST APIを作成する以外にも、AWS Eventsに対応するものや、Asynchronous Task Executionといって、別のLamdba関数を非同期に呼び出すことが出来る機能を持っているようです。
というか、chaliceに比べると非常に多彩。

また、ZappaはWSGI(Web Server Gateway Interface)をサポートしているので、他のアプリケーションサーバーにデプロイすることも可能です。

chalice vs Zappa

こちらに比較記事がありました。

続きを読む

DynamoDB LocalとCognitoを併用する場合の注意点

先に結論

DynamoDB LocalとCognitoを併用する場合は、必ず別々のendpointを定義する。
特にCognito側のendpointを書く事例はほとんど見かけないので忘れがち。

utils/auth.js
import AWS from 'aws-sdk'

const cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider({
  apiVersion: '2016-04-18',
  region: 'ap-northeast-1',
  endpoint: `https://cognito-idp.ap-northeast-1.amazonaws.com/`,
})

export default function auth(AccessToken) {
  return new Promise((resolve, reject)=>{
    cognitoIdentityServiceProvider.getUser({AccessToken}, (err, data)=>{
      if (err) return reject(err)
      return resolve(data)
    })
  })
}
utils/dynamoose.js
import dynamoose from 'dynamoose'

dynamoose.AWS.config.update({region:'ap-northeast-1'})
if ( process.env.NODE_ENV === 'dev' ) {
  dynamoose.AWS.config.endpoint = new dynamoose.AWS.Endpoint('http://localhost:8000')
}
export default dynamoose

前提

  • 今回のケースではNode.jsのdynamooseというORMを使っているが、その他のケースでも発生する可能性があるので念のためメモ。
  • ServerlessFrameworkを使用しているが、それは今回の件とは関係ないはず?

経緯

  • ServerlessFrameworkのプラグイン serverless-dynamodb-local を使って、DynamoDB Localを起動し、Cognitoでユーザー管理をしようとした
  • アプリケーションを実行すると、1度目は成功するが2度目で必ず下記エラーが出ることに気づいた
MissingAuthenticationToken: Request must contain either a valid (registered) AWS access key ID or X.509 certificate.
  • AccessTokenが間違えてるのかなと思ったが合ってた
  • リージョン指定が間違えてるのかなと思ったが合ってた
  • DynamoDBの向き先をローカルではなく本番に向けたらエラーが出なかった
  • 向き先をローカルに戻して、CognitoのSDK側にendpointを指定したらエラーが出なかった

推察

  • 恐らくCognito User Poolは内部でDynamoDBを使っていて、DynamoDBのendpointを変えるとCognitoのSDK側にも影響を及ぼしてしまうのではないか?
  • このあたり詳しい人、もしくは中の人、今回の件について何かあれば教えてもらえたら嬉しいです

続きを読む

aws-sam-localだって!?これは試さざるを得ない!

2017-08-11にaws-sam-localのベータ版がリリースされました。
単に「早速試したで!」と言う記事を書いても良かったのですが、少し趣向を変えてサーバレス界隈の開発環境のこれまでの推移を語った上で、aws-sam-localの使ってみた感想もお話しようかと思います。

サーバレス開発環境の今昔

サーバレス自体がかなり最近になって生まれた風潮なので昔とか言うのも問題はあるかと思いますが、とにかくサーバレスなるものの開発環境について私にわかる範囲でお話しようと思います。誤りや不正確な点については編集リクエストやコメントを頂けると幸いです。

なお、サーバレスという言葉は一般的な名詞ですが、私がAWS上でしかサーバレスに触れていないため、AzureやGCPなどには触れず、もっぱらLambdaの話になってしまうことをあらかじめご了承ください。

Lambdaのデプロイは辛かった

Lambdaの基本的なデプロイ方法はZIPで固めてアップロードです。
直接ZIPをLambdaに送るか、あらかじめS3に置いておいてLambdaにはそのURLを教えるかといった選択肢はありましたが、手動でZIPに固めてAWSに送るという手順は不可避でした。なんかもうすでに辛い。

さらに言うとLambdaに送りつけられるのはLambdaで実行するコードだけ。性質上Lambdaは単体で使われることはほとんどなく、他のサービスと連携することがほとんどなのにその辺は自分で管理するしかありませんでした。辛い。

CloudFormationで管理することは可能でしたが、CloudFormationテンプレートを書くのがかなりダルいことと、CloudFormationの更新とZIPのアップロードを別途行う必要があって手順が煩雑化しやすいため、「もうええわ」と手動管理してることが多かったと思われます。

また、ローカル環境で実行するには一工夫必要でした。

颯爽登場!Serverlessフレームワーク

そんな時に颯爽と現れたのがServerlessフレームワークでした。
ServerlessフレームワークにおいてはLambdaファンクション及び関連するリソースを独自のyamlファイルで管理します。結局は一度CloudFormationテンプレートに変換されるのですが、CloudFormationテンプレートよりも単純な形式で記述できたのが流行った一因かと思います。また、sls deployコマンドでLambdaのコードのアップロードおCloudFormationスタックの更新を一括で行ってくれたため、デプロイの手順は従来よりもはるかに簡略化されたかと思われます。

Lambdaテストしづらい問題

デプロイに関する問題はServerlessフレームワークや、ほぼ同時期に現れたSAMによって改善されましたが、開発プロセスにおいて大きな課題がありました。

テストし辛ぇ…

上記の通りLambdaは性質上他のサービスと連携することが多いため、その辺をローカル環境でどうテストするかに多くの開発者が頭を抱えました。対策として

  1. モッククラスを作って、実際のサービスのような振る舞いをさせる
  2. プロダクションとは別のリージョンに環境を再現して、そこで実行する

といった方法がありましたが、それぞれ

  1. モッククラスの実装がすこぶるダルい 下手したらロジック本体より時間かかる
  2. クラウドにデプロイしないとテストできないため、時間がかかる

といったデメリットがありました。

LocalStackとaws-sam-local

サーバレス開発者の嘆きを聞いたAtlassianがローカル環境でAWSのサービスのエンドポイントを再現するなんとも素敵なツールを作り上げました。それがLocalStackです。
再現されているサービスの数が物足りなく感じられたり、サードパーティ製であることに一抹の不安を覚えたりする人もいるかと思いますが、これ以上を求めるなら自分で作るぐらいしかないのが現状かと思います。

そしてaws-sam-local。こちらはLocalStackと少し趣が異なります。LocalStackが連携するサービスのエンドポイントを再現して提供するのに対して、aws-sam-localは実行環境の提供という意味合いが強いです。そして重要なことはAWSの公式がサポートしているということです。公式がサポートしているということです。大事なことなので(ry
「実行するのはローカルでNode.jsなりPythonなりで動かせばええやん」と思いがちですが、ランタイムのバージョンなどを本番環境と確実に揃えられるのは大きな利点です。
まだベータ版が出たばっかなので今後に期待といったところでしょう

aws-sam-local触ってみた

それでは実際に触ってみましょう。
ちなみに当方環境は

  • OS: macOS Sierra 10.12.6
  • Docker for Mac: 17.06.0-ce-mac19

です。

事前準備とインストール

公式のInstallationの項目を読み進めますが、事前にDockerを使えるようにしておく必要があります。

Macだったら普通にDocker For Macをインストールしておけば問題ありません。
一方Windowsだと

スクリーンショット 2017-08-16 14.48.18.png

まさかのDocker Toolbox推奨。 Docker For Windowsェ…
そしてaws-sam-localのインストールですが、私は-gオプション排斥論者なのでローカルインストールします

npm install aws-sam-local

実装

今回はこちらを参考にAPIゲートウェイから呼び出すLambdaを実装します。
ほぼ丸パクリですが一部アレンジしてますのでソースものっけます。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM Local test
Resources:
  HelloWorld:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      Runtime: nodejs6.10
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: put

ランタイムをnodejs6.10に変更してます。
新しく作る場合にわざわざ古いバージョンを使う必要もありませんので。

余談ですが、WebStormのCloudFormation用のプラグインは今の所SAMには対応してないのか、Type: AWS::Serverless::Functionのところにめっちゃ赤線を引かれます。

index.js
/**
 * Created by yuhomiki on 2017/08/16.
 */

"use strict";

const os = require("os");
console.log("Loading function");


const handler = (event, context, callback) => {
  return callback(null, {
    statusCode: 200,
    headers: { "x-custom-header" : "my custom header value" },
    body: "Hello " + event.body + os.EOL
  });
};

exports.handler = handler;

完全に書き方の趣味の問題です。
内容は参考ページのものと全く同じです。

package.json
{
  "name": "sam_test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "invoke-local": "sam local invoke HelloWorld -e event.json",
    "validate": "sam validate",
    "api-local": "sam local start-api"
  },
  "author": "Mic-U",
  "license": "MIT",
  "dependencies": {
    "aws-sam-local": "^0.1.0"
  }
}

aws-sam-localをローカルインストールしているので、package.jsonのscriptsに追記しています。

実行

それでは実行してみましょう

上記のpackage.jsonに記載した

  • invoke-local
  • validate
  • api-local

を実行していきます。

invoke-local

Lambdaファンクションをローカル環境で実行します。
Lambdaファンクションに渡すevent変数はワンライナーで定義することも可能ですが、あらかじめJSONファイルを作っといた方が取り回しがいいです。

json.event.json
{
  "body": "MIC"
}

実行結果はこんな感じ

スクリーンショット 2017-08-16 15.25.42.png

まず最初にdocker pullしてランタイムに応じたDockerイメージをダウンロードします。
その後はコンテナ内でLambdaファンクションを実行し、最後にcallbackに与えた引数を出力といった流れです。
ログの形式がすごくLambdaですね。あとタイムゾーンもUTCになっていますね。
メモリの使用量をローカルで確認できるのは嬉しいですね。

-dオプションをつけることでデバッグもできるようです。
公式のgithubにはご丁寧にVSCodeでデバッグしてる様子がgifで上げられてます。

validate

テンプレートファイルのチェックをします。
デフォルトではカレントディレクトリのtemplate.yamlファイルをチェックしますが、-tオプションで変更することが可能です。

失敗するとこんな感じに怒られます。

スクリーンショット 2017-08-16 15.33.47.png

成功した時は「Valid!」とだけ言ってきます。きっと必要以上に他人に関わりたくないタイプなのでしょう。

api-local

sam local start-apiコマンドはローカル環境にAPIサーバを立ち上げます。
ホットリロード機能がついてるので、立ち上げっぱなしでもソースを修正したら自動で反映されます。いい感じですね。

スクリーンショット 2017-08-16 15.40.58.png

立ち上がるとこんなメッセージが出るので、あとはCURLなりPostManなりで煮るなり焼くなり好きにしましょう。

CURLの結果はこんな感じ
スクリーンショット 2017-08-16 15.51.39.png

所感

Lambdaのローカル実行環境を公式が用意したことに大きな意義があるかと思います。
Dockerさえあればすぐに使えることと、SAMテンプレートを書かざるをえないのでInfrastructure as Codeが自然と根付いていくのも個人的には好感を持てます。

ただし、まだベータ版なこともあって機能的にもの足りない部分があるのも事実です。
具体的にはやはりDynamoDBとかもテンプレートから読み取ってDockerコンテナで用意してくれたらなーと思います。LocalStackやDynamoDB Localでできないこともないでしょうが、DynamoDB Localに関してはテンプレートからテーブル作ったりするの多分無理なのでマイグレーション用のコードを書くことになりますし、LocalStackに関しては実はあまり真面目に使ったことないのでわかりませんが環境構築に一手間かかりそう。ていうかできれば一つのツールで完結させたい。

SAMしかりaws-sam-localしかり、AWS側としてもより開発がしやすくなるような環境づくりをしていくような姿勢を見せているので、今後のアップデートに期待したいところですね。

続きを読む

DynamoDB Local Viewerに機能追加(スキーマ情報取得、ソート・フィルタ、データ削除)

以前作成したDynamoDBのLocal Viewerですが、現状の機能だと色々と不足してきたため更新をかけました。以前の記事はこちら。

DynamoDB Local用のViewerをSpring Bootベースで作ってみた

当時はScanメソッドでとりあえずデータを取ってましたが、今回は以下に対応させました。

  • テーブルの詳細情報確認
  • 指定したテーブルの中身の削除(制限あり)
  • 指定したテーブルの削除
  • テーブルデータのソートと絞り込み

結果はこちらに登録してあります。

https://github.com/kojiisd/dynamodb-local-view

テーブルの詳細情報確認

テーブル名をクリックした際にスキーマ情報を取得できるようにしました。

スクリーンショット 2017-04-21 7.10.54.png

こんな感じのダイアログを出すようにしています。

スクリーンショット 2017-04-21 7.11.21.png

指定したテーブルの中身の削除(制限あり)

個人的には一番欲しかった機能です。ローカルでDynamoDBを使った動作確認をしている際に、いちいちテーブルをドロップしてから作り直すのをスクリプトを組んで実施するのが面倒でした。なので「Clear」をクリックすれば中身をからにしてくれる機能を作りました。

スクリーンショット 2017-04-21 7.12.01.png

以下は削除後のスキーマ情報です。tableSizeBytesやitemCountが0になっているのがわかります。
ただいくつか制限があり、以下の実装のようにいくつかテーブル情報を引き継いでいません。この辺りうまい方法を知っている人がいればぜひ知りたいです。

CreateTableRequest createRequest = new CreateTableRequest().withTableName(tableName)
        .withAttributeDefinitions(describeResult.getTable().getAttributeDefinitions())
        .withKeySchema(describeResult.getTable().getKeySchema())
        .withProvisionedThroughput(new ProvisionedThroughput()
                .withReadCapacityUnits(describeResult.getTable().getProvisionedThroughput().getReadCapacityUnits())
                .withWriteCapacityUnits(describeResult.getTable().getProvisionedThroughput().getWriteCapacityUnits()));

スクリーンショット 2017-04-21 7.11.49.png

どんな仕組みにしたかはこちらに書きました。

Local DynamoDBのテーブルデータを削除したいときの実装(テーブル削除→作成)

指定したテーブルの削除

スクリーンショット 2017-04-21 7.13.16.png

スクリーンショット 2017-04-21 7.13.40.png

テーブルデータのソートと絞り込み

Scanのページには、各ヘッダで並び替えができるように、また検索結果にフィルタをかけられるように簡易のフィルタ機能をつけました。

スクリーンショット 2017-04-21 9.06.23.png

まとめ

そろそろQuery機能もつけて、実際のAWS ConsoleでのDynamoDBのユーザビリティを再現したいと思います。

続きを読む

DynamoDB Local用のViewerをSpring Bootベースで作ってみた

DynamoDB Localって便利ですよね。実際にAmazon DynamoDBにつなぎに行かなくてもCRUD操作をローカル環境で試せるので重宝しています。

java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb

このコマンドでDynamoDBがシミユレートできてしまうのだから、とても便利。

が、、、投入したデータの確認、となると一手間かかってしまいます。localhost:8000/shell(デフォルトの画面URL)で見れるこの画面、ありがたいのですが、今ひとつ物足りないですよね。

スクリーンショット 2017-01-04 19.54.49.png

やっぱり本家のこういう画面が欲しいなあ、、、と。こういうテーブル一覧とか、、、

スクリーンショット 2017-01-04 19.45.06.png

各テーブルの詳細画面とか。

スクリーンショット 2017-01-04 19.56.45.png

とりあえず目下自分が困っていたのは投入したデータの確認が容易にできないことだったので、DynamoDB Localにつなぎに行ってデータの中身を可視化してくれるViewerを作成しました。

結果

こちらに登録してあります。ご自由にお使いください。起動方法などはREADME.mdをご覧ください。

https://github.com/kojiisd/dynamodb-local-view

かなり力技で実装している部分があります。ソートやフィルタは実装していません。Pull Requestしてくれる方、大歓迎です。またデータ増えた時にパフォーマンス大丈夫かとかその辺は、知らんがな(´・ω・`)。今回の要件ではそんなにローカルでデータ保持しない想定だからダイジョーブ。

Mavenでのビルドから、Javaコマンドで実行できます。

$ mvn clean package
$ java -jar target/dynamodb-view-0.0.1-SNAPSHOT.jar

前提

  • 2016-05-17_1.0バージョンのDynamoDB Local jarファイルを利用しました。
  • Spring Boot + AngularJSで開発しました。

機能と画面

listTablesscanしかしていません。画面も2画面しか作っていません。

1. テーブル一覧画面

DynamoDB Localを起動した状態でアプリケーションから接続に行きます。 localhost:8080 でページが見れます。この画面は現在DynamoDB Localに保存されているテーブルの一覧になります。

スクリーンショット 2017-01-22 16.35.32.png

各テーブルにはリンクを付与しており、クリックするとそのテーブルのデータ一覧が見れます。

2. テーブル詳細画面

こんな感じになっています。DynamoDBは列指向ですので、動的に列が増えたとしても、対応していないレコードは空で表示するような仕組みにしています。

スクリーンショット 2017-01-22 16.35.22.png

この例ではsensor_idカラムとtimestampカラムをそれぞれ「HASH」、「RANGE」にしてテーブル作成しています。

テーブル作成のサンプルデータ

今回のテーブル(sample_table3)を表示するためのテーブル作成定義とデータサンプルは以下になります。

とりあえずこのままDynamoDB Localの管理画面(localhost:8000/shellでアクセスできるアレ)に投入すれば、テーブルの作成はできます。

CreateTable用データ定義
var params = {
    TableName: "sample_table3",
    KeySchema: [
        {
            AttributeName: "sensor_id",
            KeyType: "HASH"
        },
        {
            AttributeName: "timestamp",
            KeyType: "RANGE"
        }
    ],
    AttributeDefinitions: [
        {
            AttributeName: "sensor_id",
            AttributeType: "S"
        },
        {
            AttributeName: "timestamp",
            AttributeType: "S"
        }
    ],
    ProvisionedThroughput: {
        ReadCapacityUnits: 1, 
        WriteCapacityUnits: 1
    }
};

dynamodb.createTable(params, function(err, data) {
    if (err) ppJson(err); // an error occurred
    else ppJson(data); // successful response
});

データ作成はこんな感じでできます。

サンプルデータ用定義
var params = {
    TableName: "sample_table3",
    Item: { 
        sensor_id: "acceleration-sensor01",
        accelerationX: 1.254,
        accelerationY: 0.001,
        accelerationZ: 0.178,
        timestamp: "2017-01-22T10:01:24"
    },
    ReturnValues: "NONE",
    ReturnConsumedCapacity: "NONE",
    ReturnItemCollectionMetrics: "NONE"
};
docClient.put(params, function(err, data) {
    if (err) ppJson(err); // an error occurred
    else ppJson(data); // successful response
});

最後に

今回のツールのおかげで、個人的にDynamoDB Localに保持したデータの確認がかなり容易になりました。このツールを使ってバシバシデータの確認をしてもらえたら、と思います。

おまけ(少し困ったところ)

今回列指向のデータベースに対するアクセスでしたので、各レコードが持っている異なるカラムを、どのようにJava側でデータ保持して、画面側に渡し、表示するのがいいのか、については少し苦戦しました。

結果として以下のようにしました。

  1. [サーバ] 一旦値が空のキーのみのMapを作成する → カラムの過不足をなくす
  2. [サーバ] データが存在する部分だけ値を入れる → データが存在しない箇所は空文字になる
  3. [クライアント] ヘッダ作成部とデータ表示部でテーブルの作り方に一工夫

[サーバ] 一旦値が空のキーのみのMapを作成する

この辺りの実装ですね。少し力技です。画面側の表示を簡単にするためにサーバ側に処理を多めに入れています。

private Map<String, String> createEmptyColumnMap(ScanResult scanResult) {
    Map<String, String> columnMap = new LinkedHashMap<String, String>();
    for (Map<String, AttributeValue> valueMap : scanResult.getItems()) {
        for (Map.Entry<String, AttributeValue> valueMapEntry : valueMap.entrySet()) {
            columnMap.put(valueMapEntry.getKey(), StringUtils.EMPTY);
        }
    }
    return columnMap;
}

[サーバ] データが存在する部分だけ値を入れる

ここも力技。今回いくつかの型には(全部ではない)対応しましたが、これってDynamoDBのAPIを利用してもっとうまくできないですかね?

private String extractColumnValue(AttributeValue value) {
    String result = StringUtils.EMPTY;

    if (value == null) {
        return result;
    }

    if (value.getBOOL() != null) {
        result = value.getBOOL().toString();
    } else if (value.getB() != null) {
        result = value.getB().toString();
    } else if (value.getN() != null) {
        result = value.getN();
    } else if (value.getS() != null) {
        result = value.getS();
    } else if (value.getM() != null) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            result = mapper.writeValueAsString(value.getM());
        } catch (JsonProcessingException ex) {
            // TODO Exception handling
            ex.printStackTrace();
            result = StringUtils.EMPTY;
        }
    } else if (value.getSS() != null) {
        result = String.join(VALUE_SEPARATOR, value.getSS().toArray(new String[0]));
    } else if (value.getNS() != null) {
        result = String.join(VALUE_SEPARATOR, value.getNS().toArray(new String[0]));
    } else if (value.getBS() != null) {
        result = String.join(VALUE_SEPARATOR, value.getBS().toArray(new String[0]));
    }
    return result;
}

[クライアント] ヘッダ作成部とデータ表示部でテーブルの作り方に一工夫

ng-repeatを使ってAngularJS側でパッとテーブル表示したかったのですが、カラム名が動的に変わるのでこんな感じの実装になりました。limitTo:を使ってほぼ無理矢理です。これもよりスマートなやり方があればぜひ知りたいです。

<table class="table table-condensed table-bordered table-striped">
    <thead>
        <tr ng-repeat="data in datas | limitTo:1">
            <th>No</th>
            <th ng-repeat="(key, val) in data">{{ key }}</th>
        </tr>
    </thead>
    <tbody>
      <tr ng-repeat="data in datas">
          <td>{{ $index + 1 }}</td>
          <td ng-repeat="(key, val) in data">{{ val }}</td>
      </tr>
    </tbody>
</table>

続きを読む

Serverless アプリケーションをローカルで開発する

AWSに代表されるServerless Architectureはクラウド上での動作が前提ですが、Serverless Frameworkのプラグインを用いることにより、ローカル環境でも動作させることが可能になるのでご紹介します。AWSにデプロイすることなく開発が可能になるので、より素早く開発ができます。また、AWSのアカウントを持っていない方もServerlessの世界を体験できるかと思います。ここではじゃんけんを行うAPIの開発を通して、ローカルでの開発方法を説明します。完成版のソースコードは以下にあります

構成

API Gateway、Lambda、DynamoDBを用いたアーキテクチャをここでは想定します。ローカル開発環境ではそれぞれ serverless-offline、javascriptファイル、DynamoDB Local が対応します。

Screen Shot 2016-12-23 at 14.55.06.png

環境

  • macOS sierra
  • Node.js v4.6.2
  • Serverless Framework v1.4

プロジェクトの作成

Serverless Frameworkを用いて開発するのでインストールを行い、新しいサービスを作成します。

$ npm install -g serverless
$ mkdir serverless-janken
$ sls create -t aws-nodejs -n serverless-janken

関連するパッケージのインストール

API Gatewayの代用として利用する serverless-offline プラグイン、Serverless FrameworkからDynamoDB Localを操作できるようにする serverless-dynamodb-local プラグインをインストールします。

$ npm install aws-sdk
$ npm install --save-dev serverless-offline
$ npm install --save-dev serverless-dynamodb-local

インストール後、Serverless Frameworkからプラグインとして利用できるように設定に記入します。

$ vi serverless.yml
# service: serverless-janken の下に以下を追記
plugins: 
 - serverless-dynamodb-local
 - serverless-offline

DynamoDB Local のインストール

serverless-dynamodb-local プラグインを利用してDynamoDB Localをインストールします。

$ sls dynamodb install

DynamoDB Local テーブルの定義

DynamoDB Localで利用するテーブルを定義します。サンプルとして、プレイヤー名とUnixtimeをキーとするテーブルを作成しました。

$ mkdir migrations
$ vi migrations/jankens.json
# 下記内容で保存する
{
    "Table": {
        "TableName": "jankens",
        "KeySchema": [{
            "AttributeName": "player",
            "KeyType": "HASH"
        }, {
            "AttributeName": "unixtime",
            "KeyType": "RANGE"
        }],
        "AttributeDefinitions": [{
            "AttributeName": "player",
            "AttributeType": "S"
        }, {
            "AttributeName": "unixtime",
            "AttributeType": "N"
        }],
        "ProvisionedThroughput": {
            "ReadCapacityUnits": 1,
            "WriteCapacityUnits": 1
        }
    },
    "Seeds": [{
        "player": "user1",
        "unixtime": 1482418800,
        "player_hand": "rock",
        "computer_hand": "paper",
        "judge": "lose"
    }]
}

DynamoDB Local の起動

DynamoDB Local起動時にテーブルの作成とシードデータの挿入を行うため、 serverless.yml に設定を入れます。

serverless.yml
# service: serverless-janken の下に以下を追記
custom:
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migration: true
    migration:
      dir: ./migrations

DynamoDB Localを起動します。

$ sls dynamodb start
Dynamodb Local Started, Visit: http://localhost:8000/shell

Table creation completed for table: jankens
Seed running complete for table: jankens

ブラウザで http://localhost:8000/shell にアクセスし、テーブルの中身を確認します。左側のエディタに下記を記入し、再生ボタンを押します。

var params = {
    TableName: 'jankens',
};
dynamodb.scan(params, function(err, data) {
    if (err) ppJson(err);
    else ppJson(data);
});

図のように右側にシードデータの内容が確認できれば、テーブルの作成&シードの挿入が完了しています。

Screen Shot 2016-12-23 at 15.08.59.png

Lambdaの開発

データベースの設定ができたので、ロジック部分を書いていきます。handler.js を開いて以下の内容で保存します。じゃんけんを行うAPI playJanken とじゃんけん結果を参照するAPI listJankens のためのロジックを書いています。このコードはAWSでもローカルでも動作が可能なように、 event.isOffline を見て接続するDynamoDBを切り替えています。

hander.js
"use strict";

var AWS = require("aws-sdk");

var judgeJanken = function (a, b) {
    var c = (a - b + 3) % 3;
    if (c === 0) return "draw";
    if (c === 2) return "win";
    return "lose";
}

var getDynamoClient = function (event) {
    var dynamodb = null;
    if ("isOffline" in event && event.isOffline) {
        dynamodb = new AWS.DynamoDB.DocumentClient({
            region: "localhost",
            endpoint: "http://localhost:8000"
        });
    } else { 
        dynamodb = new AWS.DynamoDB.DocumentClient();
    }
    return dynamodb;
}

module.exports.playJanken = function (event, context, callback) {
    console.log("Received event:", JSON.stringify(event, null, 2));
    console.log("Received context:", JSON.stringify(context, null, 2));

    var dynamodb    = getDynamoClient(event);
    var date        = new Date();
    var unixtime    = Math.floor(date.getTime() /1000);

    var hand        = ["rock", "scissors", "paper"];
    var player_name = event.queryStringParameters.name;
    var player_hand = event.queryStringParameters.hand;
    var player      = hand.indexOf(player_hand);
    var computer    = Math.floor( Math.random() * 3) ;
    var judge       = judgeJanken(player, computer);

    var params = {
        TableName: "jankens",
        Item: {
            player: player_name,
            unixtime: unixtime,
            player_hand: player_hand,
            computer_hand: hand[computer],
            judge: judge
        }
    };

    dynamodb.put(params, function(err) {
        var response = {statusCode: null, body: null};
        if (err) {
            console.log(err);
            response.statusCode = 500;
            response.body = {code: 500, message: "PutItem Error"};
        } else {
            response.statusCode = 200;
            response.body = JSON.stringify({
                player: player_hand,
                computer: hand[computer],
                unixtime: unixtime,
                judge: judge
            });
        }
        callback(null, response);
    });
};

module.exports.listJankens = function (event, context, callback) {
    console.log("Received event:", JSON.stringify(event, null, 2));
    console.log("Received context:", JSON.stringify(context, null, 2));

    var dynamodb = getDynamoClient(event);
    var params   = { TableName: "jankens" };

    dynamodb.scan(params, function(err, data) {
        var response = {statusCode: null, body: null};
        if (err) {
            console.log(err);
            response.statusCode = 500;
            response.body = {code: 500, message: "ScanItem Error"};
        } else if ("Items" in data) {
            response.statusCode = 200;
            response.body = JSON.stringify({jankens: data["Items"]});
        }
        callback(null, response);
    });
};

API Gatewayの設定

最後に前項で作成したロジックを呼ぶエンドポイントを作成するために serverless.yml に設定を入れます。

  • GET /jankens… じゃんけん結果の参照
  • POST /jankens… じゃんけんを行い結果をDynamoDB Localに保存
serverless.yml
service: serverless-janken

custom:
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migration: true
    migration:
        dir: ./migrations

plugins:
  - serverless-dynamodb-local
  - serverless-offline

provider:
  name: aws
  runtime: nodejs4.3

functions:
  playJanken:
    handler: handler.playJanken
    events:
      - http:
          path: jankens
          method: post
  listJankens:
    handler: handler.listJankens
    events:
      - http:
          path: jankens
          method: get

テスト

以上で、データベース、ロジック、エンドポイントがそろったのでローカルで起動させて利用してみます。

$ sls offline

別のシェルでcurlでAPIを叩いて利用してみます。うまくいかない場合は sls dynamodb start でDynamoDB Localが起動していることを確認してください。

$ curl 'http://localhost:3000/jankens?hand=rock&name=test' -X POST
{"player":"rock","computer":"scissors","unixtime":1482469235,"judge":"win"}
$ curl 'http://localhost:3000/jankens'
{"jankens":[{"unixtime":1482469235,"player_hand":"rock","judge":"win","player":"test","computer_hand":"scissors"},{"unixtime":1482418800,"player_hand":"rock","judge":"lose","player":"user1","computer_hand":"paper"}]}

AWSにデプロイ

ローカルで開発ができたら、AWS上にデプロイします。AWSで動かすには、DynamoDBのテーブル定義と、IAMロールの設定が必要でしたので、serverless.yml に追記して下記のようにします。

serverless.yml
service: serverless-janken

custom:
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migration: true
    migration:
        dir: ./migrations

plugins:
  - serverless-dynamodb-local
  - serverless-offline

package:
  exclude:
    - node_modules/**
    - migrations/**
    - .git/**

provider:
  name: aws
  runtime: nodejs4.3
  # DynamoDBの利用の許可
  iamRoleStatements:
    -  Effect: 'Allow'
       Action:
         - 'dynamodb:PutItem'
         - 'dynamodb:Scan'
       Resource: '*'

functions:
  playJanken:
    handler: handler.playJanken
    events:
      - http:
          path: jankens
          method: post
  listJankens:
    handler: handler.listJankens
    events:
      - http:
          path: jankens
          method: get

# DynamoDB Tableの作成
resources:
  Resources:
    JankensTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: jankens
        KeySchema:
          - AttributeName: player
            KeyType: HASH
          - AttributeName: unixtime
            KeyType: RANGE
        AttributeDefinitions:
          - AttributeName: player
            AttributeType: S
          - AttributeName: unixtime
            AttributeType: N
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

最後にServerless Commandでデプロイします。

$ sls deploy
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (1.81 KB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
......................
Serverless: Stack update finished...
Service Information
service: serverless-janken
stage: dev
region: ***
api keys:
  None
endpoints:
  POST - https://***.amazonaws.com/dev/jankens
  GET - https://***.amazonaws.com/dev/jankens
functions:
  serverless-janken-dev-playJanken: arn:aws:lambda:***:***:function:serverless-janken-dev-playJanken
  serverless-janken-dev-listJankens: arn:aws:lambda:***:***:function:serverless-janken-dev-listJankens

以上でローカルで開発したServerless アプリケーションをAWSにデプロイができました。

続きを読む