雑多に技術メモと他色々

主に自分用な技術メモが多くなる気がする。他色々が書かれるかどうかは不明。

DynamoDBをLocalで検証してSDKの違いを確認する

AWS SDKの制御が古いバージョンと新しいバージョンで違うとの話があったので、DynamoDBLocal使いながら検証してみた。

環境

  • 開発時のOSはWindows7
  • 言語はJava
  • mavenでCentralにあるライブラリは取れるものとする
  • 古いバージョンのSDKとしてAWS SDK for Java 1.4.xを利用する
  • 新しいバージョンのSDKとしてAWS SDK for Java 1.11.xを利用する

とりあえずDynamoを用意する

DynamoDBLocalって?

AWSアカウント作ったから本物使ってもいいんだけど、使い方次第で金かかるから不安やん。
という1USDもAWSには金を払いたくない人向けに、現在はローカルで擬似Dynamoを立ち上げることができるのである。便利な世の中になったもんだ。

Dynamoと同一インタフェースで操作できる上にSQLiteでデータ永続化を実現、さらにはJavaScriptSDKを利用してブラウザから操作する機能まで一緒についていると気軽に起動できる割には至れりつくせり。
ローカルだとほぼ発生しないと書いてあるものの結果整合性も擬似ってるらしい。いったいどうやってんだ。
なお、最新のAWS Toolkit for Eclipseにも付属しているとのこと。

注1:あくまで擬似環境なので、同一インタフェースを謳っていても本物を利用した試験を怠ってはいけない。
注2:ちゃんと確認してないものの、リリースされたばかりのような最新機能は未実装もあると思われる。

DynamoDBLocalをダウンロードする

AWS公式から手に入る。いろいろググれば古いバージョンも見つかる。
コンピュータ上の DynamoDB (ダウンロード可能バージョン) - Amazon DynamoDB

自環境では「アジアパシフィック (東京) リージョン」のzipを入手。
ダウンロードページに書いてあるのだが、中身は実行可能jarなのでJava実行環境が必要となる。

コンピュータで DynamoDB を実行するには、Java Runtime Environment (JRE) 6.x 以降のバージョンが必要です。アプリケーションは、以前のバージョンの JRE では動作しません。

さすがにJavaAWS利用のアプリケーション開発という時点でJRE6以降はあると思うけど一応。

DynamoDBLocalを起動する

zipファイルを解凍するとjarが入っている。
jarと同じパスで以下のようにコマンドライン打てば一発で起動できる。めっちゃ簡単。

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

いろいろカスタムする場合は下記に説明書がついている。
DynamoDB 使用に関する注意事項 - Amazon DynamoDB
コマンドラインヘルプを参照するなら下記から。

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

今回の検証範囲なら前述の起動コマンドのままでOK。

デフォルトではポート8000で待ち受けるDynamoDBLocalが起動する。
また、起動後下記にWebブラウザで接続するとブラウザ経由での操作もできるようになっている。
http://localhost:8000/shell/
マネジメントコンソールとはだいぶ違いがあり、JavaScriptSDKでいじれる便利環境が手に入る。

f:id:yamakisso:20181206005452p:plain
ブラウザから開いた時はこんな感じ

DynamoDBLocalにテーブルやデータを準備する

取り急ぎはブラウザ経由のJavaScript操作が手軽だったのでそこからやってみた。

テーブル作成

検証用のテーブルを作成する。
メニューからCreateTableのテンプレートは作ってくれるのでそれベースでちゃちゃっと。
HashKeyとしてそのままhash_key(String)を指定したtest_tableを作成。

var params = {
    TableName: 'test_table',
    KeySchema: [ // The type of of schema.  Must start with a HASH type, with an optional second RANGE.
        { // Required HASH type attribute
            AttributeName: 'hash_key',
            KeyType: 'HASH',
        },
    ],
    AttributeDefinitions: [ // The names and types of all primary and index key attributes only
        {
            AttributeName: 'hash_key',
            AttributeType: 'S', // (S | N | B) for string, number, binary
        },
    ],
    ProvisionedThroughput: { // required provisioned throughput for the table
        ReadCapacityUnits: 1, 
        WriteCapacityUnits: 1, 
    },
};
dynamodb.createTable(params, function(err, data) {
    if (err) ppJson(err); // an error occurred
    else ppJson(data); // successful response

});

テーブル一覧

これはListTablesのテンプレートそのままでOK…と思いきやデフォルトのLimit: 0が許容範囲外である。
optionalと言っているからとりあえずLimitは削除して確認。

var params = {
    ExclusiveStartTableName: 'table_name', // optional (for pagination, returned as LastEvaluatedTableName)
};
dynamodb.listTables(params, function(err, data) {
    if (err) ppJson(err); // an error occurred
    else ppJson(data); // successful response
});

テーブルに適当にデータ突っ込む

PutItemのテンプレートがっつり削った。なんか「登録されたよ」的な出力はないけど成功はしているみたい。

var params = {
    TableName: 'test_table',
    Item: { // a map of attribute name to AttributeValue
        'hash_key' : 'testKey',
        'unique_value' : 'testValue1',
    },
};
docClient.put(params, function(err, data) {
    if (err) ppJson(err); // an error occurred
    else ppJson(data); // successful response
});

テーブルの中身確認

Scanのテンプレートをがっつり削って実行。

var params = {
    TableName: 'test_table',
};
dynamodb.scan(params, function(err, data) {
    if (err) ppJson(err); // an error occurred
    else ppJson(data); // successful response
});

旧バージョンSDKのv1APIで検証 → できなかった

強制的にSessionTokenAPI叩いてしまうの突破するためにめんどくさいコード組んでみたのだけど、結果的にそこ突破してもDynamoDBLocalはv1APIで叩けないことが判明。
DynamoDBLocalから下記のようにエラーメッセージが返却される。

DynamoDB Local does not support v1 API

ちなみにv2APIすら実装されていない1.3.xなどのSDKを使うと上記メッセージすら出ずに認証エラーを示すレスポンスが返却されてハマる。
その際のメッセージは下記の通り。どちらにしろLocal検証することはできない。

AWS Error Code: MissingAuthenticationToken, AWS Error Message: Request must contain either a valid (registered) AWS access key ID or X.509 certificate

DEBUGログで確認してみたけど、どうもv2APIが実装された段階のSDKで、v1APIで送るパラメータも少し変わっていたようである。

旧バージョンSDKのv2APIで検証

とりあえずmavenで旧バージョンSDKを参照

mavenaws-java-sdkの1.4.3を引き込む。

そして旧バージョンSDK用のJavaコード

こんな感じで@DynamoDBVersionAttributeをStringのAttributeで使った場合にどうなるかが検証対象。
※ 本来AWS側が意図している@DynamoDBVersionAttributeはNumberかByteの属性に利用が限定されている。実際に組む際にString属性への付与はやってはいけない。これは間違ってやってしまったときの動作を検証する遊びです。

package com.sample.dynamotest;

import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.ConsistentReads;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBVersionAttribute;

/**
 * Java SDK 1.4.x検証用のコードです
 */
public class OldDynamoTest {

	@DynamoDBTable(tableName = "test_table")
	public static class TestTable {

		private String hashKey;
		private String uniqueValue;

		@DynamoDBHashKey(attributeName = "hash_key")
		public String getHashKey() {
			return hashKey;
		}

		public void setHashKey(String hashKey) {
			this.hashKey = hashKey;
		}

		@DynamoDBVersionAttribute
		@DynamoDBAttribute(attributeName = "unique_value")
		public String getUniqueValue() {
			return uniqueValue;
		}

		public void setUniqueValue(String uniqueValue) {
			this.uniqueValue = uniqueValue;
		}
	}

	public static void main(String[] args) {
		// 適当なAccessKey/SecretKeyを指定した上で、DynamoDBLocalの起動している先にエンドポイントを向ける
		AmazonDynamoDBClient client = new AmazonDynamoDBClient(new BasicAWSCredentials("fakeAKey", "fakeSKey"));
		client.setEndpoint("http://localhost:8000");
		DynamoDBMapper mapper = new DynamoDBMapper(client);

		// 検証用データを準備
		TestTable data = new TestTable();
		data.setHashKey("testKey");
		data.setUniqueValue("testValue1");

		// 投げてみて検証!
		// loadなら…
		// TestTable loaded = mapper.load(data, new DynamoDBMapperConfig(ConsistentReads.CONSISTENT));

		// saveなら…
		// mapper.save(data, new DynamoDBMapperConfig(ConsistentReads.CONSISTENT));

		// deleteなら…
		mapper.delete(data, new DynamoDBMapperConfig(ConsistentReads.CONSISTENT));
	}
}

旧バージョンSDKの検証結果

save/delete/loadの挙動を確認してみた。

使用API(+パターン) 挙動
save リクエスト前にエラー(DynamoDBMappingException)
delete(HashKey存在せず) リクエストして整合性チェックエラー(ConditionalCheckFailedException)
delete(HashKeyに紐付くuniqueValueが一致) 成功
delete(HashKeyに紐付くuniqueValueが不一致) リクエストして整合性チェックエラー(ConditionalCheckFailedException)
load 成功

新バージョンSDKで検証

とりあえずmavenで新バージョンSDKを参照

mavenaws-java-sdk-dynamodbの1.11.466を引き込む。

そして新バージョンSDK用のJavaコード

deprecated警告が出るけど、前述の旧バージョン検証のコードそのままでもビルドは通る。

新バージョンSDKの検証結果

全部動作しなくなる。内部の挙動が変わって事前にBeanのアノテーション制御を総なめ+チェックが厳密化されたのかな。
内部的に@DynamoDBVersionAttributeが動作しないはずのloadでもエラーが発生するようになっている。

使用API 挙動
save リクエスト前にエラー(DynamoDBMappingException)
delete リクエスト前にエラー(DynamoDBMappingException)
load リクエスト前にエラー(DynamoDBMappingException)

新バージョンSDKの動きを旧バージョンに合わせてみる

Stringに無理やり付与した@DynamoDBVersionAttributeが特定の効き方をしていたのはdelete限定なので、そこの動きが合うように組み直してみる。
アノテーション貼っているとチェックエラーでどうしようもないので、外しつつ自前でExpectするのが妥当なのだろう。

組み直し版のJavaコード

アノテーション外して、自前Expect加える。本当はdeprecated消したかったけど、AccessKey/SecretKeyベタ書き方式から切り替えないとダメっぽかったので一旦諦め。

package com.sample.dynamotest;

import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBDeleteExpression;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.ConsistentReads;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue;

/**
 * Java SDK 1.11.x検証用のコードです
 */
public class NewDynamoTest {

	@DynamoDBTable(tableName = "test_table")
	public static class TestTable {

		private String hashKey;
		private String uniqueValue;

		@DynamoDBHashKey(attributeName = "hash_key")
		public String getHashKey() {
			return hashKey;
		}

		public void setHashKey(String hashKey) {
			this.hashKey = hashKey;
		}

		// @DynamoDBVersionAttributeを削除
		@DynamoDBAttribute(attributeName = "unique_value")
		public String getUniqueValue() {
			return uniqueValue;
		}

		public void setUniqueValue(String uniqueValue) {
			this.uniqueValue = uniqueValue;
		}
	}

	public static void main(String[] args) {
		// 適当なAccessKey/SecretKeyを指定した上で、DynamoDBLocalの起動している先にエンドポイントを向ける
		AmazonDynamoDBClient client = new AmazonDynamoDBClient(new BasicAWSCredentials("fakeAKey", "fakeSKey"));
		client.setEndpoint("http://localhost:8000");
		DynamoDBMapper mapper = new DynamoDBMapper(client);

		// 検証用データを準備
		TestTable data = new TestTable();
		data.setHashKey("testKey");
		data.setUniqueValue("testValue2");

		// 投げてみて検証!
		// loadなら…
		// TestTable loaded = mapper.load(data, new DynamoDBMapperConfig(ConsistentReads.CONSISTENT));

		// saveなら…
		// mapper.save(data, new DynamoDBMapperConfig(ConsistentReads.CONSISTENT));

		// DynamoDBDeleteExpressionで削除用の条件を追加する
		DynamoDBDeleteExpression delExpression = new DynamoDBDeleteExpression();
		delExpression
				.withExpectedEntry("unique_value", new ExpectedAttributeValue(new AttributeValue(data.uniqueValue)));
		mapper.delete(data, delExpression, new DynamoDBMapperConfig(ConsistentReads.CONSISTENT));
	}
}

ExpectedAttributeValueカラム名がベタ書きなのが微妙に気に入らない…

組み直し後の検証結果

DEBUGログのリクエスト内容見ると違いは見られるものの、外部仕様レベルで旧SDKと大体等価にはできた。
付随してsaveが正常動作するようにもなるが、さすがに動かなかったものが動くようになる点は許容とする。

使用API(+パターン) 挙動
save 成功(元々動いてなかった部分なので差分は気にしない)
delete(HashKey存在せず) リクエストして整合性チェックエラー(ConditionalCheckFailedException)
delete(HashKeyに紐付くuniqueValueが一致) 成功
delete(HashKeyに紐付くuniqueValueが不一致) リクエストして整合性チェックエラー(ConditionalCheckFailedException)
load 成功

まとめ

  • DynamoDBLocal便利(でもめっちゃ古いAPIは検証できないよ)
  • SDKバージョンによって動作の違いはあった(間違った使い方が前提になってるけど…)
  • SDKバージョンの差分を埋める組み換えもなんとかできそう