RailsアプリケーションのCIにDynamoDB Localを導入した話

こんにちは、事業開発部 サーバーサイドエンジニアの堀江(kentarohorie)です。

今回はRailsアプリケーションのCIにDynamoDB Localを導入した事例をご紹介します。

広告入稿システムとCI

クックパッドでは自社製の広告入稿システム・配信サーバーを運用しています。また広告の一部はDynamoDBを利用したアーキテクチャで入稿・配信されています。詳細は以前の記事「広告配信サーバーにおける DynamoDB Accelerator (DAX) 活用事例の紹介」で紹介されています。この入稿・配信のうち、広告入稿システムのCIに対してDynamoDB Localの導入を行いました。

広告入稿システムのCIではブランチへのpush、またはmasterへの変更をトリガーにCIサーバー上でスクリプトが実行されていました。CIサーバーにはMySQLやPostgreSQLの環境が用意されており、スクリプトが実行されるとサーバー上のDBを初期化してrspecが実行されていました。多くのテストでそれらのDBを利用したテストが実行されていましたが、DynamoDBに関しては実際のDBを使用できていませんでした。

そのため、DynamoDBを利用している箇所ではAWS SDK DynamoDBClientのput_itemやdelete_itemなどのメソッドを一つ一つstubしたテストが書かれていました。これは例えばDynamoDBを利用したコードが増えたり、その箇所を間接的に利用する必要があるコードが生まれた場合に、DynamoDBの利用を気にしながら必要に応じて都度stubするといった作業が必要になるということです。
例えば以下のようなstubがit句毎に書かれていました。

it "..." do
  expect(dynamodb_client).to receive(:delete_item).with(
    hash_including(
      table_name: "table_name",
      key: { pk: "product_key" },
    )
  )

  expect { subject }.to change { ... }.to(false)
end

こうした状況の中でDynamoDBを利用している箇所で、stubせずともテストを書けるようにしようというモチベーションがありました。

DynamoDB Local導入に必要な環境を整備

DynamoDB Localの導入にあたっては執筆時点で3つの方法がAWSで紹介されています。

  • Apache Mavenリポジトリとして利用
  • Java環境を用意して実行
  • Dockerイメージを利用

これら方法のうち、Dockerイメージを利用してDynamoDB Localを導入しました。理由は全社的にCodeBuildの利用が推進されており、CodeBuild上でDockerを利用してCIを回すという事例が社内に既に多く存在していたためです。CodeBuildはAWSが提供するCI/CD用ビルドサービスであり、Androidアプリ CIをCodeBuildに切り替えた事例などクックパッドでは広く活用されています。

上記検討の後、まずは既存のビルド部分をCodeBuildに置き換え、Codebuild上のDockerでテストを実行できる環境を用意しました。ビルド部分の置き換えはJenkinsのCodeBuildプラグインを利用しました。次に社内で用意されているCodeBuild用Dockerイメージをベースに広告入稿システムのDockerイメージを作り、MySQLやPostgreSQLを利用する処理はスクリプトを用意してdocker-compose up時に実行されるようにしました。具体的にはDBの初期化やrspecの実行などです。

f:id:kentarohorie:20200721123459p:plain
Before

f:id:kentarohorie:20200721123513p:plain
After

この置き換え作業では、既存のCIと比べた場合に可能な限りCI時間が長くならないことを意識して進めました。CodeBuildに置き換える場合これまでになかったDockerイメージのビルドや立ち上げといった工程が増えるためにCI時間が長くならざるをえません。しかしCI時間は短ければ短いほうが良いので、許容できる程度までCodeBuildでのCI時間を縮める必要がありました。

具体的には以下の工夫を行いました。

  1. CodeBuild上でのDockerイメージビルドはキャッシュを利用する
  2. docker-composeでマウントするファイルを可能な限り減らす

広告入稿システムはRailsで動いており、ビルド時間でネックになっていたのはnode_modulesとgemのインストール工程でした。当初はCodeBuildのS3キャッシュを利用してnode_modulesとgemをキャッシュする方針で作業を行っていました。しかしその方法ではnode_modulesとgemファイル群をCodeBuildサーバー(コンテナの外)に持つ必要があり、docker-composeでマウントする必要のあるファイルが多くなり結果コマンド実行時間が遅くなるという問題が発生しました。

次にDocker Layer Cacheを利用する方法を試しました。はじめはCodeBuildで用意されている「ローカルキャッシュ」のDocker Layer Cacheモードを利用していましたが、ライフスパンが30分程度と短いため、CIの稼働頻度が30分に一度回るほどは高くない広告入稿システムではあまり恩恵を受けれませんでした。

そこで最終的に、ECRを利用してDocker Layer Cacheすることになりました。具体的にはCodeBuildのPOST_BUILDフェーズでECRへDockerイメージをpushし、次のBuild時にそのイメージをキャッシュとして利用する、というようにしました。

phases:
  pre_build:
    commands:
      - ....
      - docker pull "${REPO}:latest" || true
      - ...
  build:
    commands:
      - ...
      - docker build --tag "rspec" --tag "${REPO}:latest" -- cache-from "${REPO}:latest" -f Dockerfile .
      - ...
  post_build:
    commands:
      - ...
      - docker push "${REPO}:latest"
      - ...

DynamoDB Localをテストへ導入

CodeBuildへの置き換えが完了した後はdocker-compose.ymlにAmazonが公式に配布しているDynamoDB Localイメージを組み込み、テスト時にそれを読み込むように設定しました。具体的にはAWSのconfigをアップデートする処理をテスト実行前に読み込むようにしました。広告入稿システムのテストでは他にAWSリソースを使用していなかったため、DynamoDBリソースに絞った設定はしませんでした。

次にテスト実行時にDBが初期化されるようにしました。 広告システム関連で使われているDynamoDBにはdynaというgemを利用したDB初期化の仕組みがあります。 dynaはDynamoDBをDSLで管理できるものです。したがって、テスト実行時のDB初期化はdocker-compose up時に走らせるscript内にDB初期化を行うdynaコマンドを実行することで達成しました。

最後に、広告入稿システムのテストでDynamoDBに関するstubを外していく作業を行いました。これでDynamoDB LocalのCI導入が完了しました。

導入結果

DynamoDB LocalをCIに導入することで以下を達成できました。

  • DynamoDBに関する処理のstubを考えずにテストが書けるようになった
  • DynamoDBに関するテストコードを、各人の環境で実行できるようになった
  • DynamoDBを利用したコードの保守性を向上させることができた
  • DynamoDBやClientの仕様変更に耐えやすいテストになった

導入後、DynamoDBに関する最初の作業としてDynamoDBのクライアントgem aws-sdk-dynamodbのアップデート作業を行いました。specではstubせずにDynamoDB Localにアクセスしているのでテストが通った結果に安心感を持つことができ、導入によるメリットを実感しました。

最後に

以上、広告入稿システムのCIにDynamoDB Localを導入した事例をご紹介しました。

クックパッドにはユーザーが触る画面を改善しているサービス開発領域や、収益を支えている広告領域など、様々な領域でエンジニアが活躍しています。そしてそれらの領域ではエンジニアを随時募集しています。興味を持っていただいた方のご応募をお待ちしております。

新卒採用: https://info.cookpad.com/careers/new-graduates/

キャリア採用: https://info.cookpad.com/careers/jobs/

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/