Amazon DynamoDBとDynamoDB LocalのBillingModeに関するAPI仕様差分

既存コードで利用するAWS SDK for Javaのバージョンを適当に最新版にしたときに、DynamoDB Localと本物のAmazon DynamoDBAPIの動作が違っていたせいでバグチケットを起票される羽目になった話。

 

 

[DynamoDB Localとは]

改めて紹介するまでもないくらい有名だが、DynamoDB Local(DynamoDB ローカル)とはAWSクラウドサービスとして提供されているAmazon DynamoDBと互換性のあるアプリケーションである。ローカルの、つまり手元のコンピュータで動作させることが可能なので、AWSアカウントがなくてもDynamoDBのAPIを利用したアプリケーションを開発することができる。 

docs.aws.amazon.com

 

DynamoDB LocalはJavaで実装されて提供されており、利用形態としては主に2つのパターンがある。

  • jarファイルを直接実行する
  • Dockerイメージからコンテナを起動する

 

公式のDockerイメージは確か500MB程度ありファイルサイズがでかいので、Dockerイメージを自作するといいかもしれない。

kdnakt.hatenablog.com

 

と記憶ベースで書いたものの、Docker Hubにある最新版(1.11.477)を確認すると、公式でも200MB程度にスリム化されている。わざわざDockerイメージを自作しなくても、公式のイメージを利用しておく方が面倒が少なそうである。

 

[DynamoDB Localの注意事項]

DynamoDB Localは基本的にはクラウドサービスとして提供されているAmazon DynamoDBと互換性があるものの、いくつかの点で挙動が異なっている、とAWSのドキュメントにも記載されている。

docs.aws.amazon.com

 

公式ドキュメントからいくつか引用すると、DynamoDB Localと本物のAmazon DynamoDBには以下のような違いがある。

 

プロビジョニングされたスループット設定は、ダウンロード可能な DynamoDB で無視されます。

これはまあ分かるような気もする。わざわざこの設定を実装してスループット超過した場合は例外を投げるのは大変そうだから。

 

TransactionConflictExceptions は、トランザクション API に対してはダウンロード可能な DynamoDB によってスローされません。

トランザクションコンフリクトの例外も実装されていないようだ。これもクラウド版に準拠した実装をしようとするとかなり大変に思える。

 

DynamoDB では、結果セットごとに、返されるデータに 1 MB の制限があります。DynamoDB ウェブサービスとダウンロード可能バージョンのいずれにもこの制限が適用されます。ただし、インデックスのクエリを実行しているとき、DynamoDB サービスは、射影されたキーと属性のサイズのみを計算します。一方で、ダウンロード可能バージョンの DynamoDB は、項目全体のサイズを計算します。

先ほどの2つとは異なり、実装されていないわけじゃないけど微妙に動作が異なる、というパターンもあるらしい。 本物のDynamoDBサービスの方が結果セットに含まれる項目の数が多くなるので、そこまで困ったことにはならなそう。

 

[DynamoDB Localのマニュアルに載ってない違い]

先ほどのドキュメントに掲載されていない、DynamoDB Localと本物のDynamoDBで挙動が異なるケースに遭遇したのでメモしておく。

 

以下はJavaで実装されたDynamoDBテーブルを作成するコードの一部である*1。ポイントは以下の通り。

  • このテーブルにはグローバルセカンダリインデックスがある
  • このテーブルはオンデマンドキャパシティモードを利用している
  • 元々はプロビジョニング済みキャパシティモードを利用しており、GlobalSecondaryIndex#withProvisionedThroughputの呼び出しが修正されていない

 

final AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard()
    // DynamoDB Localで実行するときは次の2行のコメントアウトを外す
    //.withEndpointConfiguration(
    //     new EndpointConfiguration("http://localhost:8888", "local"))
    .build();

final List<AttributeDefinition> adList = Arrays.asList(
    new AttributeDefinition("id", ScalarAttributeType.S),
    new AttributeDefinition("status", ScalarAttributeType.S),
    new AttributeDefinition("datetime", ScalarAttributeType.S)
);
final List<KeySchemaElement> kseList = Arrays.asList(
    new KeySchemaElement("id", KeyType.HASH)
);
final List<GlobalSecondaryIndex> gsiList = Arrays.asList(
    new GlobalSecondaryIndex()
        .withIndexName("index_status")
        .withKeySchema(
            new KeySchemaElement("status", KeyType.HASH),
            new KeySchemaElement("datetime", KeyType.RANGE))
        .withProjection(
            new Projection().withProjectionType(ProjectionType.ALL))
        // 次の行をコメントアウトしないと本物のDynamoDBで動作しない
        .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)) 
);   

client.createTable(new CreateTableRequest()
    .withTableName("test_table")
    // オンデマンドキャパシティモードを指定
    .withBillingMode(BillingMode.PAY_PER_REQUEST)
    .withAttributeDefinitions(adList)
    .withKeySchema(kseList)
    .withGlobalSecondaryIndexes(gsiList));

 

このコードをDynamoDB Localの最新版*2に対して実行すると、問題なく動作した。

 

ところが、本物のDynamoDBサービスに対して実行すると、以下のような例外が発生する。BillingMode(キャパシティモード)でPAY_PER_REQUEST(オンデマンド)を指定する場合は、グローバルセカンダリインデックスにProvisionedThroughputを指定するな、と書かれている。どうやらクラウドサービス版DynamoDBとDynamoDB LocalでBillingModeをPAY_PER_REQUESTにした際のリクエストに対するバリデーションの実装に差分があるようだ。

Exception in thread "main"
    com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException:
    One or more parameter values were invalid: 
    ProvisionedThroughput should not be specified for index: 
    index_status when BillingMode is PAY_PER_REQUEST
    (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException; Request ID: XXXXXXXXXXXXXXXXXXXXXXX)
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleErrorResponse(AmazonHttpClient.java:1712)
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1367)
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeHelper(AmazonHttpClient.java:1113)
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.doExecute(AmazonHttpClient.java:770)
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeWithTimer(AmazonHttpClient.java:744)
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.execute(AmazonHttpClient.java:726)
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.access$500(AmazonHttpClient.java:686)
at com.amazonaws.http.AmazonHttpClient$RequestExecutionBuilderImpl.execute(AmazonHttpClient.java:668)
at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:532)
at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:512)
at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.doInvoke(AmazonDynamoDBClient.java:4279)
at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.invoke(AmazonDynamoDBClient.java:4246)
at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.executeCreateTable(AmazonDynamoDBClient.java:1059)
at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.createTable(AmazonDynamoDBClient.java:1025)

 

元々は自分が適当にオンデマンドキャパシティモードの対応を行った所為で、古いソースコードが一部不要になったのを認識できておらず、削除し忘れていたのが問題である。とはいえ、評価担当者がバグチケットを起票する工数や、修正したコードの再デプロイ待ち時間など、無駄な時間を使ってしまったので、是非ともこのあたりの仕様差分は将来のバージョンアップで無くなって欲しい。TransactionConflictExceptionsに対応するよりは、はるかに簡単な実装で済むと思うので。

 

[DynamoDB Localのライセンス]

先ほど引用した過去記事でも触れたが、DynamoDB Localには当然のことながらライセンス契約が存在する。

aws.amazon.com

 

この契約によれば、DynamoDB Localのデコンパイルは禁止されている。 

2. 制限

...(略)...本ソフトウェアのリバースエンジニアリング、逆アセンブリ若しくはデコンパイルを行うこと、又はその他の処理工程若しくは手順を適用することで本ソフトウェアに含まれるソフトウェアのソースコードを引き出すこと、のいずれの行為も行ってはならず...(略)...

 

デコンパイルが禁止されていなければソースコードを読んで、BillingModeのバリデーションが漏れている部分の実装を追加し、クラウドサービス版と同じ動作をするように修正できるのだが。DynamoDB LocalがOSSになって、この辺のプルリクエストを送れるようにならないかなあ……。

 

[まとめ]

  • 開発時には料金節約のためDynamoDB Localが便利
  • DynamoDB Localは本物と違う動きをする部分もある
  • DynamoDB LocalはOSSではないし、デコンパイルしてはいけない

 

*1:aws-java-sdk-dynamodbのバージョンは1.11.557を利用した。前提として、環境変数などでAWSのアクセスキーIDおよびシークレットキーは設定されているものとする。

*2:自分が利用した際は、dynamodb_local_2019-02-07がdynamodb_local_latestとして提供されていた。