CircleCIの新料金体系のためにDocker Executorに移行した

CircleCI の Performance Pricing Plan を利用するにあたって、開発用に利用する Docker 環境をどう管理していくかについて考える機会があったため、この記事で整理しておこうと考えました。

Performance Pricing Plan

CircleCI の通常の料金体系では、同時に利用可能なコンテナ数に応じて課金されるようになっています。しかし2019年1月9日現在、実験的に従量課金形式の新しい料金体系が提供されています。この新しい料金体系を、この記事では Performance Pricing Plan と呼ぶことにします。

この新しい料金体系の中で、自分が注目している特徴は以下の点です。

  • コンテナの並列数に上限が無い
  • 使わなければ課金されない
  • 実行環境の性能を変更できる(但し Docker Executor でしか変更できない)

コンテナの並列数に上限が無ければ、もし同じ組織内でジョブを実行したいタイミングが被っても、待ち時間無く実行できます。従量課金であるため、これによって課金される費用が変わることもありません。

また実行環境の性能(仮想コア数やメモリ容量)を変更でき、この性能に応じて課金額が変更されるようになります。そのため、そこまで性能を必要としないジョブについてはこれまでより費用を抑えることができたり、ジョブの実行時間短縮のために費用をかけられたり、これまで実行環境の性能不足で実現できなかったことが実現できるようになったりというメリットがあります。

Docker Executor

Performance Pricing Plan を選択することで実行環境の性能を変更できるようになるのですが、Docker Executor を利用している場合にしか変更できないという制約があります。CircleCI では、各ジョブを動かす環境を、以下の3つの内から選択できます。

  • Docker Executor
  • Machine Executor
  • MacOS Executor

MacOS Executor は iOS や MacOS アプリを開発している場合に利用するための macOS の VM 環境で、Machine Executor は Linux の VM 環境です。一方 Docker Executor は少し特殊な環境で、指定したイメージの Docker コンテナで処理を実行します。

CircleCI の Docker Executor では Remote Docker という機能が提供されており、TCP 経由で外部の環境と通信することで、Docker コンテナの中からでも擬似的に docker コマンドや docker-compose コマンドが利用できるようになっています。但し、ファイルのマウントやネットワークといった低レベルな機能は Remote Docker 経由では利用できないという制約があります。

Machine Executor を利用すれば、上述した Docker 関係の制約はありません。しかし Docker Executor でなければ実行環境の性能は変更できず、コストを最適化できません。そのため、最適化するにはこの制約下でジョブを実現する必要があります。

Docker Executor の制約に引っかかる箇所

開発環境では MySQL や Redis などのサービスをそれぞれ Docker コンテナとして起動しておきつつ、それらと通信するアプリケーションサーバやテストプロセスを必要に応じて Docker コンテナとして起動するような使い方をよくしています。これらの依存関係やネットワークの管理には docker-compose を利用していますが、default ネットワークで動かしている限りは Docker Executor でも適切に動作するようなので、これは問題にはなりませんでした。もしカスタムのネットワークを利用している場合は問題になる場合がありそうです。

他に、ホスト側でファイルを書き換えたときにコンテナ側で更新が反映されるようにしたり、npm などのパッケージ管理システムでインストールしたファイルのキャッシュを保持できるようにするために、docker-compose でボリュームのマウントの設定をしています。これまでは Machine Executor を利用していたので、ローカル環境でテストを実行するのと同じ方法で、CircleCI でも docker-compose を利用してテストを実行できていました。しかし Docker Executor に移行すると、このマウントありきで動くような設定にしていると問題がありそうです。

対策

最初から Docker イメージ内にアプリケーションの実行に必要なファイルを含めておき、ボリュームのマウントを行わなくても動くような Docker イメージにしておくことで、マウントを利用できない Docker Executor の環境でも動かせるようにしました。Dockerfile の末尾に COPY . /app のように書いて全ファイルコピーしつつ、.dockerignore で必要なファイル以外はコピーされないように取り計らっています。開発環境では、ホスト側でファイルを変更したらコンテナ側にそれが反映されてほしいので、既にファイルの含まれているディレクトリに対して更にホスト側のカレントディレクトリをマウントして動かしています。

また docker-compose がデフォルトで docker-compose.yml と docker-compose.override.yml を設定ファイルとして読み込むことを利用して、ローカル環境(非 CircleCI 環境)でのみ適用したいボリュームやネットワークの設定を docker-compose.override.yml にまとめ、CircleCI 上で docker-compose を動かすときは docker-compose.override.yml を利用しないようにしました。

これらの対策を加えることで、docker-compose を利用する形を保ったままジョブの実行環境を Machine Executor から Docker Executor に移行し、実行環境の性能を調整できるようになりました。

余談

一部のプロジェクトでは、CircleCI から Capistrano で SSH を利用してコードをデプロイしています。CircleCI のホストが提供しているソケットファイルを Docker コンテナにマウントすることで、Docker コンテナ側から SSH を利用できているのですが、この方法は Docker Executor だと利用できないため、ここだけ Machine Executor を利用しています。

他に困ったこととして、マウントできない環境で docker-compose でテストを実行するとテスト結果をまとめたファイルが実行側で受け取れないのではという問題がありましたが、docker cp でコンテナ内からファイルを転送することで単純に解決できました。CircleCI ではテストのメタデータを保存しておくことで統計データやテストのサマリーを表示してくれる機能があるので、このために利用しています。