CircleCIアンチパターン 2015春

今日はCircleCIで気持ちよくCIを回すために、抑えておいた方が良いアンチパターンについて書きます。わりと基本的な話なので、心当たりがあれば見直してみると良いと思います。

Fat Repository Anti Pattern(巨大なリポジトリ

CircleCIのコンテナは使い捨てですが、対象のリポジトリを毎回cloneするのではなくて、2回目移行は前回のCI時にキャッシュしておいたリポジトリを利用することで差分取得を実現しています。

checkoutフェーズのRestore source cacheのことですね。

f:id:a-yamada:20150331140744p:plain

この手法によって最新取得のコストを大幅に低減することができますが、コンテナ初期化時にS3からリポジトリのアーカイブを取得するという特性上、あまりにも巨大な数GB超えのリポジトリとなるとアーカイブの取得+展開に数十秒〜数分もかかってしまうことになります。当然、リポジトリのアーカイブ+保存にも時間がかかってしまいます。

ちなみに弊社で運用してる超レガシープロジェクトで、何の対策もせずにCIを回すと90秒くらいかかりました。これではまったく良さを活かせません。

解決策

Frequent Push Anti Pattern(頻繁なPush)

CircleCIはPush、またはPullRequestをトリガーとしてCIを開始します。つまり、開発が活発で頻繁にPush/PullRequestが行われているようなプロジェクトだと、前のCIが完了する前に次のCIが始まってしまうということもザラにあります。CircleCIは1コンテナであれば無料ですが、それ以上コンテナを追加する場合は1コンテナあたり$50かかります。1コンテナしかなければビルドはどんどん詰まります。

しかし、最近ではPullRequestを自動でCIするような開発スタイルは避けられないのである程度の規模の開発であれば、課金するのが吉でしょう。Jenkinsの運用コストと天秤にかけてといったところでしょうか。

細切れcommit/pushをするようなプロジェクトであれば、コミットログに「[ci skip]」を含めることで軽微な修正でCI回さなくて良いケースでCIを省略できますね。

このアンチパターンは「頻繁にPushするのはダメだ!」というよりも、積極的なPullRequestベースの開発でビルドが詰まって開発を落とさないような運用を考え、適切な課金量を見定めるべきであるといった意味合いが強いです。

ちなみに弊社ではCircleCI enterpriseの導入を進めていて、専用インスタンス富豪的なコンテナ数で利用しているので特に詰まったりするようなことはありません。enterprise最高!

解決策
  • Push/PullRequestの運用の戦略的見直し(細切れなpushを控える等)
  • 課金してコンテナを追加する
  • コミットログに[ci skip]を含めてpushして、不要なCIを回避する

Overconfident Cache Anti Pattern(キャッシュの過信)

Frequent Pushの項目でも述べた通り、CircleCIではいかにしてキャッシュ取得・保存のコストを減らすかがカギになります。cache_directoriesは定期的に見直し、クリアすることが必要です。

また、キャッシュ機構はライブラリをキャッシュして再度ダウンロードさせないようにする面で有用ですが、キャッシュはされていても再度ダウンロードしにいってしまうような罠もあります。例えば、Mavenのpom.xmlで依存ライブラリのバージョンを固定せず、以下のように最新のライブラリを利用するようにするという仕組みがあります。

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>[1.2,)</version>
    <scope>runtime</scope>
</dependency>

この場合は毎度最新ライブラリをチェックしにいってしまうので、ビルド時間としてそれなりのオーバーヘッドになります。キャッシュ有無にかかわらず、ライブラリのチェックやダウンロードを行うような状況にすることは極力避けるようにしましょう。

CircleCIのキャッシュ機構に頼りきりで、依存関係の最適化を怠ってはいけないということですね。

解決策
  • キャッシュの定期的なクリア
  • キャッシュする対象の見直し
  • プロジェクトの依存ライブラリの見直し

Too much Responsibility Anti Pattern(過剰な責務)

1つのリポジトリで責務が多すぎるリポジトリも考えものです。例えば、サーバサイドとフロントエンドが混合しているようなリポジトリがその代表例です。例えば、フロントエンドしかコミットしてないのにサーバサイドのCIも走ってしまい(その逆も然り)、その結果ビルド数の増加→コンテナ不足になるということが容易に想像できます。

CircleCIの特性上、責務によってリポジトリが分かれているのが理想でしょう。

ただ、複数コンテナ(parallelism)を利用できる余裕があれば、もう少しどうにかできます。例えば、片方のコンテナではサーバサイドを、もう片方ではフロントエンドをという制御ができます。parallelismについては公式を見ると良いでしょう。parallelismのハンドリングのキモとして、以下の2つの環境変数があります。

  • CIRCLE_NODE_TOTAL : 並列で動作させるコンテナ数
  • CIRCLE_NODE_INDEX : このコンテナのインデックス

この2つの変数を駆使すれば、circle.ymlでうまいことバランシングしたり、任意の責務を特定のコンテナに割り当てたりといった制御ができるようになり、ビルド時間の短縮が期待できます。circle.ymlのイメージとしては以下のような感じですね。

test:
  override:
    - case $CIRCLE_NODE_INDEX in 0) mvn test ;; 1) npm test ;; esac:
        parallel: true
解決策
  • 責務によって、リポジトリを分ける
  • 複数コンテナを利用しての並行ビルドの活用

Everything Docker Anti Pattern(何でもかんでもDocker)

CircleCIにはDockerのサポートがあります。これは、CircleCIのコンテナ(LXCベース)の中でDockerをホスティングして、その上にDockerのコンテナを稼働させることができるというものです。

この機能はかなり強力ですが、キャッシュ取得と同様にDockerイメージの取得のコストは発生します。つまり、大きなDockerイメージだとCIの時間には不利ということになります。

Dockerについては、CircleCIでcache_directoriesを設定すれば良いという問題ではありません。現状はcircle.ymlで以下の設定をする必要があります。

dependencies:
  cache_directories:
    - "~/docker"

  override:
    - if [[ -e ~/docker/image.tar ]]; then docker load -i ~/docker/image.tar; fi
    - docker build -t circleci/elasticsearch .
    - mkdir -p ~/docker; docker save circleci/elasticsearch > ~/docker/image.tar

簡単に説明すると、cache_directoriesとdocker load + docker saveの合わせ技です。イメージをS3から取得して、load/saveするコストが発生します。ここで利用するイメージが巨大なものであればあるほど、CIの時間は増えます。

Dockerの利用用途として、データストアのようなミドルウェアの利用が考えられるでしょう。しかし、CircleCIでは代表的なデータストアはDockerを利用せずともサポートされています(以下、公式より抜粋)。

  • postgresql 9.4
  • mysql 5.5.41
  • mongodb 2.4.13
  • riak 1.4.8-1
  • cassandra 2.1.3
  • redis 2.8.19
  • memcached 1.4.13
  • sphinx 2.0.4-release
  • elasticsearch 0.90.2
  • solr 4.3.1
  • beanstalkd 1.4.6
  • couchbase 2.0.0
  • couchdb 1.3.0
  • neo4j 2.1.7
  • rabbitmq 3.4.4

種類は豊富ですが、もしかしたら必要としてバージョンをサポートしてないかもしれません。その場合に安易にDockerを選択すると、ビルド時間との引き換えになることでしょう。

例えば、MySQLの5.6を利用したい!となった場合、自分が試した限りイメージのリストアとdocker loadに平均30秒くらいかかりました。このバージョンのこの機能が使いたい!というのでなければ、コンテナにバンドルされているバージョンのデータストアで妥協する、というのもCIの戦略としては有りだと思います。CIにおいて何を重視するかによってのトレードオフではないでしょうかね。

DockerイメージのキャッシュについてはCircleCI側も課題に感じているようで、今後の改善を期待したいですね。

まとめ

良いサービスだからこそ、アンチパターンを知ろう。