DynamoDBのTime to Live機能をローカルで動かしたくてlocalstackにトライしてみた話とか

タイトル長っ。

WEB+DB PRESS vol.102の時とかの方が長かったか……。

 

[2018/04/07修正]

DynamoDBLocalではTimeToLive動いた。

localstackでは結局動かず。無念。

 

[まずは背景の説明から]

ちょっと色々あって、AWS DynamoDBのTime to Live機能(TTL)にお世話になることになった。なることにした。というか、なっている。

 

こちらがAWSの紹介記事。新機能、と書かれているが、ちょうど1年くらい前、2017年3月上旬くらいにオープンになった機能。

aws.amazon.com

 

で、まあちょろっとコード書いて、AWSにデプロイして、テストしてもらって、そこまではよかった。

そこまでは。

 

[DynamoDB Local] 

問題はローカル環境での動作確認だった。

 

話が前後して申し訳ないが、DynamoDBを利用した開発を行うにあたって、伝統的にうちのチームではDynamoDB Localを利用してきた。なにせAWS公式だから、というのが大きい。

docs.aws.amazon.com

 

ところが、である。

いつまで待ってもリンクされているファイルのバージョンが2017-02-16の日付のままなのである。latestと銘打っているくせに。おかしい。TTL機能が付く直前のバージョンで止まっている。

念のため、ファイルをダウンロードして中身を確認すると、同梱されているsdkのバージョンを確認してみる。

dynamodb_local_latest/DynamoDBLocal_lib/aws-java-sdk-dynamodb-1.11.86.jar

古い……2018年3月現在の最新のバージョンは1.11.301だというのに。古すぎる。

 

https://s3-ap-northeast-1.amazonaws.com/dynamodb-local-tokyo

で、リポジトリをよくよく確認すると、ベータ版が置いてある!

というわけで、早速ダウンロードして中身を確認してみる。

dynamodb_local_2017-04-22_beta/DynamoDBLocal_lib/aws-java-sdk-dynamodb-1.11.119.jar

 

jarの中身を確認すると、やはりあった。TTL関連のクラスがある。

com/amazonaws/services/dynamodbv2/model/UpdateTimeToLiveRequest.class

 

[DynamoDB LocalとDynamoDB JavaScript Shell]

古いバージョンのDynamoDB Localを起動してみる。

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

 

JavaSDKのコードを書くのはEclipse起動したりpom.xmlセットアップしたりと諸々だるいのでDynamoDB Localに同梱されているDynamoDB JavaScript Shellを利用して確認をしてみることに。

 

上記コマンドでDynamoDB Localを起動したあと、次のURLを叩くとブラウザでDynamoDB Localを操作できる。

http://localhost:8008/shell/

(ポート番号は起動コマンドで指定したものを利用する)

 

で、チートシートを見てみると……オヤ?

f:id:kidani_a:20180326015544p:plain

 

何かが足りない。

 

AmazonDynamoDBClient (AWS SDK for Java - 1.11.301)

上の公式ドキュメントにあるように、Java版にはAmazonDynamoDBClient#updateTimeToLiveとかのメソッドがあるのに、JavaScriptSDKにはないのか?と思いつつ、JSのドキュメントを見る。

 

Class: AWS.DynamoDB — AWS SDK for JavaScript

日付が、古い。API Version: 2012-08-10て。関連するfunctionもないわなそりゃ。

 

仕方がないので、Java版を使うことに。

せっかくの機会なので、AWS SDK for Java 2.0のDeveloper Previewを使おうとしたが、普段使っているバージョンと違いすぎて、パッとDynamoDBClientを用意することすら難しかったのでここは諦める。いつかリベンジするぞ。

aws.amazon.com

 

public class App {

    private static final String TABLE_NAME = "test-table-1";
    private static final String ATTR_ID = "id";
    private static final String ATTR_VAL = "value";
    private static final String ATTR_TTL = "ttl";

    public static void main(String[] args) {
        final AmazonDynamoDB dynamo = AmazonDynamoDBClientBuilder.standard()
                .withCredentials(new SystemPropertiesCredentialsProvider()).withEndpointConfiguration(
                        new EndpointConfiguration("http://localhost:8008", Regions.DEFAULT_REGION.name()))
                .build();

        final CreateTableRequest createTable = new CreateTableRequest().withTableName(TABLE_NAME)
                .withKeySchema(new KeySchemaElement(ATTR_ID, KeyType.HASH))
                .withAttributeDefinitions(new AttributeDefinition(ATTR_ID, ScalarAttributeType.S))
                .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L));
        try {
            dynamo.createTable(createTable);
            sleep();
        } catch (ResourceInUseException ignore) {
        }

        final TimeToLiveSpecification ttlSpec = new TimeToLiveSpecification().withAttributeName(ATTR_TTL)
                .withEnabled(true);
        final UpdateTimeToLiveRequest updateTtl = new UpdateTimeToLiveRequest().withTableName(TABLE_NAME)
                .withTimeToLiveSpecification(ttlSpec);
        try {
            dynamo.updateTimeToLive(updateTtl);
            sleep();
        } catch (AmazonDynamoDBException ignore) {
        }

        Map<String, AttributeValue> item = new HashMap<>();
        item.put(ATTR_ID, new AttributeValue().withS("id-1"));
        item.put(ATTR_VAL, new AttributeValue().withS("val-1"));
        // Wrong usage
// long ttl = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(1);
long ttl = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + 1;
System.out.println(ttl); item.put(ATTR_TTL, new AttributeValue().withN(String.valueOf(ttl))); dynamo.putItem(TABLE_NAME, item); sleep(); item = new HashMap<>(); item.put(ATTR_ID, new AttributeValue().withS("id-1")); GetItemResult res = dynamo.getItem(TABLE_NAME, item); if (res != null) { res.getItem().values().stream().forEach(v -> System.out.println(v.getS() != null ? v.getS() : v.getN())); System.out.println(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); } else { System.out.println("no data"); } sleep(); sleep(); sleep(); res = dynamo.getItem(TABLE_NAME, item); if (res != null) { res.getItem().values().stream().forEach(v -> System.out.println(v.getS() != null ? v.getS() : v.getN())); System.out.println(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); } else { System.out.println("no data"); } } private static void sleep() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }

 

import文は省略。ツッコミどころしかないのは許してほしい。

とりあえず、updateTimeToLiveを呼んでエラーが返ってこないので、一応このベータバージョンのDynamoDB LocalTTLに対応してはいるようだ。

 

実行結果はこんな感じ。

1523089749
id-1
val-1
1523089749
1523089752 id-1 val-1
1523089749
1523089755

 

アレ、消えてない。

docs.aws.amazon.com

公式ドキュメントには、48時間以内には消えます、とある。そう言えばそうだった気もする。DynamoDB Localも同じなんだろうか。

念のためDynamoDB Localを再起動して、getItemを実行して見たが、データが読める。再起動では消えないらしい。

 

[2018/04/07追記]

なぜか公式ドキュメントを無視してTTLにミリ秒を突っ込んでいたのが動作しない原因だったようだ。

修正版のコードだと、3秒ではデータが消えなかったが、10秒以内にはデータが消えて、NullPointerExceptionを吐いていた。

データが消えるとres.getItem()nullを返すのが原因らしい。

 [2018/04/07追記ここまで]

 

[いよいよlocalstackが登場する]

github.com

 

インストールコマンドが書いてある。 

pip install localstack

 

しかし、実行するとエラー。

-bash: pip: command not found

 

あれか、pipってpythonのモジュール管理ツールか……brew pip installとかでいいんだっけ(良くない)、と思いつつ、ググって解決する。

qiita.com

語順が違った。brew install pipか。

 

うまく行ったので、pip install localstackでインストールでき……ない。

f:id:kidani_a:20180328020522p:plain

OSError: [Errno 1] Operation not permitted: '/var/folders/02/w7f9tqb910g1tn5n3vp6qc1c0000gn/T/pip-jr8t0k-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/six-1.4.1-py2.7.egg-info'

 

公式ドキュメントを見ると、MacOS X Sierraの場合にはpermissionsの問題があるので次のコマンドを打てとある。

pip install --user localstack

エラーメッセージが同一かどうか定かではないが、とりあえず試して見る。

 

f:id:kidani_a:20180328020722p:plain

うまく行った。MacOS X High Sierraでも事情は同じらしい。

 

起動のテスト。どっかのパスが通ってないのが悪いのか、公式ドキュメントに載ってるlocalstack startのコマンドは効かなかったので、~/Library/Python/2.7/binに移動して、python localstack startで起動した。

~/Library/Python/2.7/bin akito$ python localstack start
Starting local dev environment. CTRL-C to quit.
2018-03-28T03:30:40:INFO:localstack.services.install: Downloading and installing local Elasticsearch server. This may take some time.
2018-03-28T03:30:40:INFO:localstack.services.install: Downloading and installing local DynamoDB server. This may take some time.
2018-03-28T03:30:40:INFO:localstack.services.install: Downloading and installing local ElasticMQ server. This may take some time.
2018-03-28T03:30:40:INFO:localstack.services.install: Downloading and installing local Kinesis server. This may take some time.
2018-03-28T03:31:17:INFO:localstack.services.install: Downloading and installing LocalStack Java libraries. This may take some time.
Starting mock API Gateway (http port 4567)...
Starting mock DynamoDB (http port 4569)...
Starting mock SES (http port 4579)...
Starting mock Kinesis (http port 4568)...
Starting mock Redshift (http port 4577)...
Starting mock S3 (http port 4572)...
Starting mock CloudWatch (http port 4582)...
Starting mock CloudFormation (http port 4581)...
Starting mock SSM (http port 4583)...
Starting mock SQS (http port 4576)...
Starting local Elasticsearch (http port 4571)...
Starting mock SNS (http port 4575)...
Starting mock DynamoDB Streams service (http port 4570)...
Starting mock Firehose service (http port 4573)...
Starting mock Route53 (http port 4580)...
Starting mock ES service (http port 4578)...
Starting mock Lambda service (http port 4574)...
2018-03-28T03:31:32:WARNING:infra.pyc: Service "elasticsearch" not yet available, retrying...
2018-03-28T03:31:35:WARNING:infra.pyc: Service "elasticsearch" not yet available, retrying...
Ready.

 

今回使いたいのはDynamoDBだけなんだけど、色々動いてくれるらしい。

S3、CloudWatch、Lambda、SQS、SNSとかは普段使ってたりするのでまたの機会に色々試してみたい。 

 

[localstackでDynamoDB TTLを試す]

で、本題。

localstackのDynamoDBが起動している状態で、Javaのコードの接続先URLだけ変更して、コードを実行して見る。

 

実行結果が変わらない。データが消えない。

1522176091787
id-1
val-1
1522176091787
1522176091809
id-1
val-1
1522176091787
1522176094827

 

あれ、と思ってlocalstackのリポジトリをTime to Liveで検索してみると、テストコードが見つかった。

localstack/test_dynamodb.py at master · localstack/localstack · GitHub

TTLのスペック更新リクエストとかは受け付けてくれるようになってるっぽい。

 

確かに自分のコードでも、テーブルをCreateして、UpdateTimeToLiveRequestを投げてもエラーは返ってこなかった。返ってこなかったが、データが消えてくれる訳ではないらしい。

 

果たしてissuesを当たると、予想通り実装されていないようだった。 

github.com

 

 

[2018/04/07追記]

念のためミリ秒問題の修正後のコードで実行してみたが、やはりlocalstackでは動作しなかった。 

[2018/04/07追記ここまで]

 

[まとめ]

  • DynamoDB TTLを使うならやはりAWS上が安定していて良い
  • DynamoDBLocalはTTLを使うとデータが消える [2018/04/07追記]
  • LocalStackはDynamoDB TTLを使ってもデータは消えない
  • とはいえLocalStackは機能が豊富で色々便利そう
  • AWS SDK for Java 2.0への移行は変更点多くて難しそう

 

DynamoDB LocalとかLocalStackでの対応状況がよろしくないところを見ると、あんまり使うべき機能ではないのかしら。あるいは、使用目的が適切ではないのか。

改めて、実現したいことを考え直して見たほうがいいのかもしれない。