DynamoDB Local を Travis CI で使う

2016.01.14

はじめに

AWSが提供するDynamoDB Localを使うと、Amazon DynamoDBを使ったアプリケーションの結合テストを実際のDynamoDBにアクセスせずに実行することができます。
今回は、Travis CIでDynamoDB Localを使いScalaアプリケーションから結合テストする方法を紹介します。

Travis CIでDynamoDB Localを利用する場合には次の記事もあわせてご参照ください。
Travis CIのDynamoDB Localを最適化する

DynamoDB Localのインストールと起動方法の確認

OSX環境であればHomebrewでDynamoDB Localをインストールできます。

$ brew install dynamodb-local

インストール後は、次のコマンドでDynamoDB Localのサーバーを起動することができます。

$ dynamodb-local -inMemory -sharedDb &

[1] 2697
Initializing DynamoDB Local with the following configuration:
Port:   8000
InMemory:       true
DbPath: null
SharedDb:       true
shouldDelayTransientStatuses:   false

ここで-inMemoryオプションを指定しなければファイルシステムにデータが永続化されますが、今回のようにContinuous Integrationの結合テストで利用する場合は基本的に指定したほうがよいでしょう。
-sharedDbオプションは、リージョン区分のエミュレートをするかどうかを指定できます。-sharedDbオプションを指定する場合はエミュレートせずどのリージョンへのアクセスも単一のDBが対応します。DynamoDBそのものに対するテストを書いている場合を除き、CIの結合テストでは指定しておいたほうがよいでしょう。

上例の3行目を見るとPID(今回は2697ですね)が表示されていますので、サーバー終了にはこの番号を使います。

$ pkill -P 2697

[1]  + 2697 exit 143   dynamodb-local -inMemory -sharedDb

Redisなどはredis-serverの他にクライアントのredis-cliが配布されていますが、DynamoDB Localには専用CLIクライアントがありません。ターミナルから操作する必要がある場合はAWSコマンドラインインターフェースを別途インストールして利用します。
以下のコマンドは利用方法の一例です。DynamoDB Localはポート8000でHTTPの待ち受けをしているのでhttp://localhost:8000/がDynamoDB Localのエンドポイントになります。

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

前提知識はこんなところでしょうか。それでは実際にTravis CIにDynamoDB Localを組み込みます。

Travis CIへDynamoDB Localの組み込み

Travis CIへの組み込み手順はとても簡単です。
今回はScalaアプリをCIする例を挙げますので、まず次の.travis.ymlを用意しました。

.travis.yml

language: scala
scala:
  - 2.11.7
jdk:
  - oraclejdk8

これはTravis CIの ドキュメントに倣って記述しただけです。他言語のCIについてもドキュメントに倣えば問題ないでしょう。

さて、もう一度AWSドキュメントのDynamoDBのページを確認するとtar形式で圧縮されたDynamoDB Localの実行可能jarファイル最新版へのリンクが提供されていることがわかります。また、実行可能jarファイルからDynamoDB Localを立ち上げる方法についてもここに記述されています。

1. 以下のリンクから無料で DynamoDB Local をダウンロードします。

• tar.gz 形式: http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.tar.gz

(中略)

2. アーカイブをコンピュータにダウンロードしたら、内容を抽出し、抽出されたディレクトリを任意の場所にコピーします。

3. DynamoDB Local を開始するには、コマンドプロンプトウィンドウを開き、DynamoDBLocal.jar を抽出したディレクトリに移動し、次のコマンドを入力します。

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

上記を参考に、Travis CIのビルドが走る前に

  • 最新版のDynamoDB Localをダウンロード
  • 適当な場所で.tar.gzを解凍する
  • 実行可能jarファイルでDynamoDB Localを起動する

の手順を行います。設定ファイルは次のようになります。

.travis.yml

language: scala
scala:
  - 2.11.7
jdk:
  - oraclejdk8
install:
  - mkdir /tmp/dynamodb
  - wget -O - http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest | tar xz --directory /tmp/dynamodb
before_script:
  - java -Djava.library.path=/tmp/dynamodb/DynamoDBLocal_lib -jar /tmp/dynamodb/DynamoDBLocal.jar -sharedDb -inMemory &
  - sleep 2

最後にsleep 2しているのは、DynamoDB Localの開始前にテストが実行されないための一般的なおまじないです。
Scalaはコンパイルがクッソ長いのでいらぬ心配ではありますが……

これでTravis CIでDynamoDB Localを利用した結合テストが可能になりした。

テーブル作成、どうする?

さて立ち上がりはしましたが、今はDynamoDB Localの中身は空っぽです。テーブル定義すらありません。一般的なアプリケーションからの結合テストでは、最低限テーブルが定義されている必要があるはずです。
さて、Travis CIでDynamoDB Localを利用した場合のテーブル定義の手段は二通り考えられます。

  1. テーブルを定義した JSON をDynamoDB Localにあらかじめ喰わせておく
  2. テーブルの定義をテストコードで行う(おすすめ)

まず1.から説明します。これはDynamoDBのテーブルを定義したJSONをDynamoDB Localに喰わせておいて、テスト実行前にテーブルを作成しておく方法です。テーブル作成自体は次のコマンドでできます。

$ aws dynamodb create-table --cli-input-json file://{JSONファイルの相対パス} --endpoint-url http://localhost:8000/

上記のコマンドを先ほどの.travis.ymlbefore_scriptの最後に追加すればテーブルを事前に定義できます。しかし、この方法ではテーブル定義JSONのバージョン管理が必要になるのであまりおすすめしないです。どうせバージョン管理する必要があるなら実行可能なコードで定義するほうが良いでしょう。

というわけで2.の説明です。すでにAWS SDK for Javaはインポート済みのアプリ、という前提ならテストコードでテーブル定義を行うのは簡単です。

TableDefinition.scala

import com.amazonaws.services.dynamodbv2.model._
import scala.collection.JavaConversions._

object TableDefinition {
  val Attributes = List(
    new AttributeDefinition("my_hash_key", "S"),
    new AttributeDefinition("my_value_key", "S")
  )
  val KeySchema = new KeySchemaElement("my_hash_key", KeyType.HASH)
  val Throughput = new ProvisionedThroughput(5L, 1L)

  val GsiIndexName = "my-index"
  val GsiKeySchema = new KeySchemaElement("my_value_key", KeyType.HASH)
  val GsiThroughput = new ProvisionedThroughput(5L, 1L)
  val GsiProjection = new Projection().withProjectionType(ProjectionType.KEYS_ONLY)
  
  val MyIndex =
    new GlobalSecondaryIndex()
      .withIndexName(GsiIndexName)
      .withProvisionedThroughput(GsiThroughput)
      .withProjection(GsiProjection)
      .withKeySchema(GsiKeySchema)

  val TableName = "my-table-test"
  val CreateRequest = newCreateRequest(TableName)

  def newCreateRequest(tableName: String) =
    new CreateTableRequest()
      .withTableName(tableName)
      .withAttributeDefinitions(Attributes)
      .withKeySchema(KeySchema)
      .withProvisionedThroughput(Throughput)
      .withGlobalSecondaryIndexes(MyIndex)
}

このような定義ファイルを作って、実際のテストケースの実行前にdynamoDb.createTable(TableDefinition.CreateRequest)のようにしてテーブルを作成してやり、実行後にdynamoDb.deleteTable(TableDefinition.TableName)を書いてテーブルを削除してやりましょう。Spec2ならBeforeAfterトレイトやstepが使えます。
ちなみに、waitForActive()やwaitForDelete()はDynamoDB Localを相手にしている限りは考慮しなくてもOKです。

今回はサンプルなので、特に意味のないGlobal Secondary Indexを追加してみました。GSIの設定まで込みでこの行数なので、個人的には許容範囲です。そしてJSONによる定義に比べると幾分可読性が高く、また型安全でもあります。

この方法にしておくと、ローカル開発環境のDynamoDB Localにテーブルを追加すること(Playアプリをactivator runで実際に動かしてテストする場合など)もScalaから簡単に行えるので結構便利です。

/* import省略 */

object CreateTable extends App {
  def dynamoDb: DynamoDB = {
    val client = new AmazonDynamoDBClient()
    client.setEndpoint("http://localhost:8000")
    new DynamoDB(client)
  }
  
  dynamoDb.createTable(TableDefinition.newCreateRequest("my-table"))
}

注意すべきポイント

AWS SDK for JavaのAmazonDynamoDBClientは短期間に連続で使用するとそこそこのメモリ負荷になります。テストの事前条件として100件のデータをDynamoDB Localに挿入するコードは、そのままTravis CIで実行するとSocketException: Connection resetになってしまいます。このSocketExceptionは一見するとDynamoDB Localとの接続が不正になった事による例外に思えますが、実際はsbtがテストのためにForkしたJVMが死んだことで発生するsbt <-> テスト実行JVM間の例外です。

これを避けるために、先手を打ってbuild.sbtjavaOptionsの設定を追加してください。

build.sbt

javaOptions in test ++= sys.process.javaVmArguments.filter(
  a => Seq("-Xmx","-Xms","-XX").exists(a.startsWith)
)

この問題を解決するにあたっては以下の記事がたいへん参考になりました!!遅ればせながら感謝致します。

まとめ

DynamoDB Localを使ってガンガン結合テストを書きましょう!
Travis CIでも使えるのだから、怖いことなんて何もな……いです。ではまた!

参考