シンクロ・フード エンジニアブログ

飲食店.COMを運営する、株式会社シンクロ・フードの技術ブログです

Drone CI で手軽に Docker コンテナで並列にジョブをぶん回す環境を構築した

基盤チームで CI 職人をやっている @fohte です。

今回は、Jenkins と独自ジョブスクリプトを用いたお手製 CI/CD 環境に無限のつらみが発生していたため、OSS の CI/CD ツールである Drone を使った CI/CD 環境に移行した話をします。

Drone とは?

Drone は、Go 言語で書かれた CI/CD 環境を提供する OSS ツールで、以下のような強みがあります。

  • YAML で設定を記述できる
  • 全てのジョブが Docker コンテナ上で動作する
  • master-agent 構成で無限にスケールアウトできる

また、比較的安くイマドキの Docker コンテナを使ったイミュータブルな CI/CD 環境を構築できるという強みもあり、特に CircleCI が大人の事情で使えないような場合に有力な選択肢になりうると思います。

YAML で設定を記述できる

Travis CI や CircleCI など、最近では CI/CD のジョブ設定は静的ファイルで管理することが一般的になっています。 Drone でも、ジョブの設定は YAML で書き、docker-compose の設定をベースとした構造で簡潔に記述することができます。

例として、実際に使っている設定の一部はこんな感じです。

pipeline:
  install-deps:
    image: ruby
    commands:
      - bundle install --jobs=4 --retry=3

  rubocop:
    image: ruby
    commands:
      - bundle exec rubocop

  mysql:
    image: mysql
    detach: true

  wait-mysql:
    image: jwilder/dockerize
    commands:
      - dockerize -wait tcp://mysql:3306 -timeout 30s

  run-tests:
    image: ruby
    commands:
      - bundle exec rspec -fd

  slack:
    image: plugins/slack
    channel: ci
    secrets:
      - slack_webhook
    when:
      status:
        - failure

全てのジョブが Docker コンテナ上で動作する

Drone は、対象の git リポジトリの clone から設定したビルドやテスト、そしてビルド結果の通知までの全てのジョブが Docker コンテナ上で動作します。

CI でも Docker コンテナを用いれば、毎回新規に用意されたクリーンな環境でジョブを実行できますし、ローカルでも同等の環境を使ったテストを実行することが容易になります。

他にもこの特性が活かされている例として、Drone プラグインが挙げられます。

Drone プラグインは、ファイルのキャッシュや Docker イメージの build, push といった汎用的なジョブを提供するもので、その実態はただの Docker イメージです。 これにより、Drone プラグイン自体も単体でテストしやすく、かつ Drone 外の環境でも簡単に利用することができます。

master-agent 構成で無限にスケールアウトできる

Drone は、Web UI を提供したり、実行待ちのジョブを agent に振り分けたりする master と、実際にジョブを実行する agent からなる master-agent 構成になっています。

基本的な流れは以下のようになっています。

f:id:Fohte:20180910183509p:plain

agent は起動時に自動的に master に接続し、master は接続されている agent 複数台に分散してジョブを配置します。
単純に agent の台数を増やせば、master はその分だけ自動的に並列で実行するようにしてくれるので、非常に楽に分散実行する環境を整えられます。

Drone を AWS ECS で構築する

Drone は、master (server), agent 共に Docker イメージが提供されており、それを用いることで簡単に構築できます。

公式のドキュメントでは、docker-compose でそれらの Docker イメージを使って Drone の環境を構築する方法が紹介されており、手元で試してみることも容易になっています。

今回は、1 ヶ月間に数百回の CI ジョブが実行されることを想定しているため、Drone agent を状況に応じてスケールすることを見据え、 docker-compose は用いずに AWS ECS 上に構築することにしました。

インフラ構成

弊社では基本的にインフラを AWS 上に構築していて知見も豊富なため、今回もそれに則り AWS 上に構築しました。

f:id:Fohte:20180906153721p:plain

Drone agent は管理やスケールのしやすさから ECS で立てるようにし、Drone server は (現時点では) スケールする必要がないため、直接 EC2 インスタンス上で Docker コンテナを動かすようにしています。

また、ECR は社内で用いるプライベートな Docker イメージを保持し、それを CI でも用いるために使っており、S3 はキャッシュや成果物の保存先として利用しています。

agent コンテナの配置戦略

今回 Drone を構築する上で最も悩ましかった点が、Drone agent を実行するコンテナの配置戦略でした。

今回は、AWS ECS を利用し、EC2 インスタンス上で複数の agent コンテナを動かしています。 最近東京リージョンでも Fargate がサービス開始しましたが、訳あって EC2 バックエンドを用いています。

Fargate か EC2 バックエンドか

今回は EC2 バックエンドを用いましたが、Fargate も非常に魅力的で、どちらを採用するかはなかなか決めあぐねていました。

Fargate と EC2 バックエンドは、それぞれ以下のような長所・短所があります。

Fargate EC2
データ永続性
料金の安さ
スケールの容易さ
運用コストの低さ

Fargate は、運用コストは最小限に無限にスケールできることが魅力的ではありますが、EC2 バックエンドに比べると、データの永続化が Fargate 単体では行えないことや、同等の CPU・メモリ構成でも 2 倍弱の料金がかかってしまうことが懸念点でした。

記事執筆時点では、コンテナごと *1 にストレージは用意されているものの、EBS のような外部ストレージがアタッチできず、停止するとデータが失われてしまいます。
この制約により、CI ジョブ内で用いる Docker イメージは agent コンテナを終了・起動するたびに新規に build や pull されることになってしまい、CI ジョブが遅くなることが Fargate を用いる上での一番の懸念点でした。

アプリケーションの依存ライブラリなどの依存ファイルに関しては、S3 にキャッシュとして置いておき、毎回キャッシュをダウンロードするという戦略が取れますが、Fargate の仕組み的に、Docker イメージに関してはそのような戦略を取ることが難しいように考えています。

運用し始めてから発覚したこと

EBS の I/O が多すぎて I/O クレジットが足りなくなる

現状の構成では、ECS のコンテナインスタンス (= EC2 インスタンス) はオートスケーリングせずに 1 台のみを用意しています。*2

その EC2 インスタンスのルートボリュームとして 10 GiB にも満たないサイズの EBS (gp2) をアタッチしていますが、CI ジョブが走っている間は EBS への I/O が 1,500-2,000 IOPS ほどあり、想定よりも遥かに多くなっていました。 アタッチしている EBS はベースラインパフォーマンスが 100 IOPS であるため、常にこれを超えてバーストされており、多くの I/O クレジットを消費していました。

f:id:Fohte:20180918172221p:plain
頻繁に CI ジョブが走っているときのコンテナインスタンスに紐づけている EBS の I/O クレジット (急激に回復しているところは EBS の容量を増やしています)

これは、CI ジョブでテストのために並列数分だけ立てているデータベースや、依存パッケージのダウンロードによって発生する頻繁な I/O 操作が原因でした。

解決策としては、コンテナインスタンスにインスタンスストアが用意されているインスタンスタイプを選択し、頻繁に行われる I/O 操作の先をインスタンスストアに向けることで、頻繁な I/O 操作に耐えられるようにしました。
具体的には、この I/O 操作はすべて Docker のデータボリューム内で行われたものだったため、/var/lib/docker/volumes をインスタンスストア内の任意のディレクトリに bind-mount することで解決しています。*3

今回はもともと C5 インスタンスを用いていたため C5d インスタンスに変更しましたが、大幅にコストが上がることはなく、スムースに解消することができました。
結論として、CI の worker (Drone の場合は agent) のホストにはインスタンスストアが用意されているインスタンスタイプを選択することがおすすめです。

Drone の所感

インフラ面で考えることは少なくありませんでしたが、小規模な CI サーバーを立てる場合は Docker コンテナを起動するだけで良く、敷居は比較的低いように感じました。

Docker コンテナでできることであればどのような用途でも柔軟に使うことができますし、Web UI も単純明快で扱いやすく、そして CI/CD としての役割を十分に果たしてくれるため、開発者の方々にも好評価でした。

まとめ

今回は、Drone の紹介と、それを構築した話から、構築・運用する上で課題となった点、そしてそれらをどのように解決したかという話をしました。

これからも継続的に今回構築した環境を改善していき、ガンガン CI/CD を回していける環境を整えていきます。


シンクロ・フードではエンジニアを募集しています。ご興味のある方は以下よりご連絡ください!

www.synchro-food.co.jp

*1:厳密にはタスクごと

*2:運用が安定し、CI の需要や負荷を計測した後に、必要であればオートスケーリングする予定です。

*3:初めは bind-mount ではなくインスタンスストア内に用意したディレクトリにシンボリックリンクを貼っていたのですが、シンボリックリンクでは Docker がデータボリュームを削除できないという問題があったため、bind-mount で回避しています。この問題に関しては issue が上がっている (moby/moby#33800) ので、そちらを参考にしてください。