RSpecでDynamoDB?コレ、どうしろって言うんだ?

Pocket
LINEで送る

こんにちは、孤独のグルメ好きエンジニアの奥野です。

弊社では、サービスにDynamoDBを利用しています。
今回は、Rails製プロダクトからこちらを参照する必要があり、RSpecでのテストの下準備をしました。
あまり無いケースかと思いますが、何かの役に立ちましたら幸いです。

※Rails製プロダクトからは、 dynamodb-api を利用して DynamoDB を操作しています。

1. Dynamodb Containerを起動させよう!

RSpecでは DynamoDBLocal を利用する事にしました。
内部的にはsqliteのようですが、お金を気にせず各種操作を試すことが出来ます。
どんなものかCUIから軽く突いてみたい場合にもおすすめです。

1-1) 個人環境

手元でのRSpec実行用に、docker-compose.yml へコンテナを追加します。
javaのメモリー系のオプションは好みかと思いますが、RSpec以外でも利用する為に多めにリソースを割り当てています。

  • sharedDB は、rubyプロセス以外からもデータの中身を参照する為
  • inMemory は、少しでも早く・・・の願いを込めて設定しています(永続性も不要の為)

docker-compose.yml

  dynamodb:
    image: amazon/dynamodb-local
    expose:
      - 8000
    ports:
      - 8000:8000
    environment:
      DEFAULT_REGION: ap-northeast-1
    entrypoint: java
    command: " -Xms1024M -Xmx1024M -Xss8M -jar DynamoDBLocal.jar -sharedDb -inMemory"

1-2) circle ci環境

.circleci/config.yml に、コンテナを追加します。
こちらは dynamodb-local.jar をダウンロードしてバックグラウンドで動作させます。

    - run:
        name: Setup Dynamodb Container
        command: |
          curl -k -L -o dynamodb-local.tgz http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.tar.gz
          tar -xzf dynamodb-local.tgz
          java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb -port 8000 -sharedDb -inMemory
        background: true
    - run:
        name: Setup Dynamodb Table
        command: |
          aws configure set region ap-northeast-1

1-3) test時の接続先をコンテナに変更する

RSpec(test)の時だけ、接続先を dynamodb-local に変更しますので
config/initializers/dynamodb_api.rb で設定を切り替えます。
(configのgemを使って、環境毎の設定を変えています。)

Dynamodb::Api.config do |config|
  config.access_key_id = ENV['AWS_ACCESS_KEY_ID']
  config.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
  config.region = ENV['AWS_REGION']
  config.endpoint = Settings.dig(:aws, :dynamodb, :endpoint)
  config.table_name_prefix = Settings.dig(:aws, :dynamodb, :table_name_prefix)
  config.index_name_prefix = Settings.dig(:aws, :dynamodb, :index_name_prefix)
end

config/settings/test.yml

aws:
  dynamodb:
    endpoint: http://localhost:8000

2. テーブルを作成しよう!

サービスは稼働させる事が出来ましたので、aws上のテーブルを dynamodb-local にも作成します。
まずはテーブル作成の処理を準備します。
以降は、何度も実行しますのでrake task として定義します。

2-1) 既存のテーブルを移行する(ダンプコマンド作成)

aws上のdynamodbから定義ファイルを作成します。
aws dynamodb describe-table で、簡単に内容をjsonで取得できるのですが
awsでは必要なのに、dynamodb-local には不要な情報がありますので、これを除去します。
こちらのタスクを実行すると db/dynamodb/ 配下に定義ファイルが作成されます。(aws_cliとjqは、インストール済みの想定です。)

ハンズラボさんの技術ブログを参考にしました。

  desc 'Describe dynamodb table'
  task :describe_table, [:table_name] => :environment do |_task, args|
    command = <<~"SHELL"
      aws dynamodb describe-table #{endpoint_url} --table-name #{args[:table_name]} |
        jq '.Table' |
        jq 'del(.TableArn)' |
        jq 'del(.GlobalSecondaryIndexes[]?.ItemCount)' |
        jq 'del(.GlobalSecondaryIndexes[]?.IndexStatus)' |
        jq 'del(.GlobalSecondaryIndexes[]?.IndexArn)' |
        jq 'del(.GlobalSecondaryIndexes[]?.IndexSizeBytes)' |
        jq 'del(.GlobalSecondaryIndexes[]?.ProvisionedThroughput.NumberOfDecreasesToday)' |
        jq 'del(.GlobalSecondaryIndexes[]?.ProvisionedThroughput.LastIncreaseDateTime)' |
        jq 'del(.GlobalSecondaryIndexes[]?.ProvisionedThroughput.LastDecreaseDateTime)' |
        jq 'del(.LocalSecondaryIndexes[]?.IndexStatus)' |
        jq 'del(.LocalSecondaryIndexes[]?.IndexArn)' |
        jq 'del(.LocalSecondaryIndexes[]?.ItemCount)' |
        jq 'del(.LocalSecondaryIndexes[]?.IndexSizeBytes)' |
        jq 'del(.LocalSecondaryIndexes[]?.ProvisionedThroughput.NumberOfDecreasesToday)' |
        jq 'del(.LocalSecondaryIndexes[]?.ProvisionedThroughput.LastIncreaseDateTime)' |
        jq 'del(.LocalSecondaryIndexes[]?.ProvisionedThroughput.LastDecreaseDateTime)' |
        jq 'del(.ProvisionedThroughput.NumberOfDecreasesToday)' |
        jq 'del(.ProvisionedThroughput.LastIncreaseDateTime)' |
        jq 'del(.ProvisionedThroughput.LastDecreaseDateTime)' |
        jq 'del(.TableSizeBytes)' |
        jq 'del(.TableStatus)' |
        jq 'del(.TableId)' |
        jq 'del(.ItemCount)' |
        jq 'del(.IndexArn)' |
        jq 'del(.CreationDateTime)' > db/dynamodb/#{args[:table_name]}.json
    SHELL
    execute(command)
  end
  
  def execute(command)
    puts command
    stdout_str, stderr_str, _status = Open3.capture3(command)
    puts stdout_str, stderr_str
  end

  def endpoint_url
    if Dynamodb::Api.config.endpoint.present?
      "--endpoint-url #{Settings.aws.dynamodb.endpoint} "
    else
      ''
    end
  end

2-2) よく使うaws cliコマンドもrake taskにします

テーブルの作成と削除をコマンドに定義しました。

  desc 'Create dynamodb table'
  task :create_table, [:path_to_schema] => :environment do |_task, args|
    command = "aws dynamodb create-table #{endpoint_url} --cli-input-json file://#{args[:path_to_schema]}"
    execute(command)
  end

  desc 'Delete dynamodb table'
  task :delete_table, [:table_name] => :environment do |_task, args|
    command = "aws dynamodb delete-table #{endpoint_url} --table-name #{args[:table_name]}"
    execute(command)
  end

3. database_cleaner的な動きをやろう!

3-1) DynamodbReset.all を実装する

RSpecでは、投入したデータのお掃除が必要になります。
要所で DynamodbReset.all と呼び出すことで、テーブルをお掃除するヘルパーを用意します。
(実際にはテーブルを削除して、再作成を行います)

feedforceさんの技術ブログを参考にしました。

module DynamodbReset
  def self.all
    require 'rake'
    Rails.application.load_tasks

    Rake::Task['dynamodb:delete_table'].execute(Rake::TaskArguments.new([:table_name], ['dynamo_table']))
    Rake::Task['dynamodb:delete_table'].clear

    Rake::Task['dynamodb:create_table'].execute(Rake::TaskArguments.new([:path_to_schema], ['db/dynamodb/dynamo_table.json']))
    Rake::Task['dynamodb:create_table'].clear
  end
end

4. テストを充実させる準備ができたかな?

以上で、RSpecの下準備は出来ました。あとは心意気次第です。
好みによってVCRなどのWebmockを使ってても良いですが、今回は動きを理解するためにも、dynamodb-localを試してみます。

Pocket
LINEで送る