CircleCI 2.0を使うようにするだけで、こんなに速くなるとは夢にも思わなかった!

みなさんこんにちは。Tokyo Otaku Mode(TOM)でエンジニアをやっています @pchw です。

先日TOMの開発チームは定期的に行われる開発合宿をやってきました。今回は1年ぶりに鎌倉での開発合宿でした。

k0011840
せっかくだし、息抜きに海岸を散歩するかという絵

今回は、

  • CircleCI 2.0 移行
  • mongoose ライブラリのバージョンアップ
  • jade -> pugへの移行
  • Vue.js のバージョンアップ・整理

というテーマを決め、チームに分かれて開発を行いました。

この記事は、CircleCI 2.0 移行 のチームの記事になります。

さて、TOMでは合宿前までは CircleCI 1.0 を使っていました。
色んな記事で

  • 「早くなった」
  • 「キャッシュのコントロールが細かく出来るようになった」
  • 「Docker!」

などの前評判を聴いていましたが、「うちに導入してどれぐらい変わるのかねえ」という感じでした。

ひとまず、合宿ではまず公式の Migrating ガイドであるMigrating from 1.0 to 2.0を元に進めることにしました。

  • circle.yml.circleci/config.yml に移動
  • environment(環境変数)周りの移行(全部文字列にする)
  • working_directory の指定
  • steps という所に処理を書いていく
  • 最低限、以下のようなキーを埋めていけば、何もしないpass状態にもっていける
    • version
    • jobs
    • working_directory
    • build
    • steps

という所まではすんなりと進みました。

その後、既存のcircle.ymlで行っていた処理を見直しながら.circleci/config.ymlへCircleCI 2.0の書式に従って移行するという作業に移ります。

CI実行環境の選択

.circleci/config.ymlを記述するにあたり、まず CI 実行環境をExecutor Typeから選択する必要があります。(Executer Type には dockermachine があります。詳しくは Choosing an Executor Type をご覧ください。)

Execute Type: machine の場合

Executer Type として machine を選択すると Virtual Machine が実行環境となります。Docker 起動では無くなりますので、後述するような.circleci/config.yml で複数の Docker コンテナを指定する方法など、Docker Origin な設定はすべて実施できなくなります。(Virtual Machine には Docker が入っているようですので Docker や Docker Compose 等を独自に組み込んで似たような CI タスクを実行することは可能なはずです。)

Executer Type: docker の場合

Executer Type として docker を選択すると .circleci/config.yml で指定された Docker image が Primary image として実行され各 CI タスクは基本的にすべて Primary image 上で実行されます。

Executer Type に docker を選択する場合、CI に使用する Docke Image を自分で指定しなければなりません。

使用する Docke Image は、Public Repository に置かれた Docker Image ならどれでも使用できます。

などの選択があります。

なお Private Repository を使用したい場合は、Docker を起動できる Docker Image をまず起動し、docker login する必要があります。各タスク実行時も docker run する必要がありますので、CircleCI 2.0 でサポートされる Docker 関係の機能は使用できません。詳しくは Using Private Images and Docker Registries をご覧ください。

各種オフィシャル Docker Image を使用する場合
CI 実行に必要な各種ライブラリや追加で必要となるプログラミング言語等のリソースを .circleci/config.ymlsteps でインストールしていく必要があります。

独自に作成した Docker Image を使用する場合
CI 起動時に必要となるリソースは全てインストールされた状態となっていますので、.circleci/config.yml では CI タスクのみを記述すれば良い形になります。またプレインストール処理が無くなりますので CI の実行時間の短縮が期待できる利点がありますが、アプリの修正内容や production 環境に合わせて Docker Image を更新していく必要があります。

TOMでの選択
諸々検討の結果、Tokyo Otaku Mode では独自に CI 用の Docker Image を作成することにしました。
必要なモジュールをインストールした Image を用意し、DockerHub へ置いておいてそれをimageで指定しその後の処理をstepsに記述することになりました。
なるべくstepsに書く量を少なくしたかったというのが主な理由です。また、その Image を流用してゆくゆくは開発環境をサッと構築し直せるといいなという目論見もあったりはします。

Docker Imageを作るためのDockerfileの作成

さて、必要なモジュールをインストールした Docker Image を作成して Docker Hub にアップロードするにしても、まずは Dockerfile を作る必要があります。
ここは試行錯誤があったのですが、設定を大きく変えると CI が失敗する可能性がありましたので、ベースの Image には CircleCI 1.0 に合わせて Ubuntu 14.04 を選び、以下のモジュールをインストールするように記述しました。

  • MongoDB
  • PostgreSQL
  • Ruby
  • Node.js
  • npm モジュールで必要となるパッケージ
    • libkrb5-dev
    • sysstat
    • imagemagick
  • 上記インストールに必要となるモジュール群

ビルドした Docker Image は Docker Hub にアップロードします。Docker ID を作成し docker push すればアップロードできます。(詳しくは Overview of Docker Hub 等をご覧ください。)

余談ですが、Tokyo Otaku Mode では Android アプリのビルドも CircleCI で行なっており、別途アプリの CI 用のイメージも作成しています。

なお、この Image に含まなかったデータベースや elasticsearch などはオフィシャルのImageを使ってimageと並列に以下のように記述しました。こうすることで別コンテナとして動きます。
このように記述したimageから作られたコンテナに対しては localhost〜 でアクセス出来るようになっているため、開発環境のマシンでデータベースを立てているのと同じようにアクセスすることが出来ます。

stepsの記述

続いてstepsの記述を行なっていきます。
既存のcircle.ymlでは、

  • ソースの取得(自動)
  • 複数コンテナによるテスト
  • 初期データをデータベースに投入
  • 特定ブランチでの staging への Deploy
  • 本番への Deploy
    という処理が記述されていました。
    これをそれぞれstepsに変換していく必要があります。

ソースの取得処理

ソースの取得は CircleCI 1.0 では自動的に行われていましたが、CircleCI 2.0 では
- checkoutと書くことでそこの行を処理するタイミングでソースコードの取得処理が行われます。
つまり、checkout の記述場所によって、ある処理の後にソースコードの取得を行うなど、柔軟にconfig.ymlを書く側がコントロール出来るようになりました。

複数コンテナによるテストの振り分け

複数コンテナによるテストは、ほぼ変わりません。CircleCI 2.0 でも引き続きCIRCLE_NODE_INDEXCIRCLE_NODE_TOTALという環境変数が使えるため、以下のように記述することで複数コンテナでテストを分担して並行して進めることが出来ました。

1
2
3
4
if [ $CIRCLE_NODE_TOTAL -gt 1 ]; then
node_modules/.bin/grunt test:container$((CIRCLE_NODE_INDEX + 1))
else
node_modules/.bin/grunt test

ほかの使える環境変数に関しては、CircleCI 2.0 Environment Variables - CircleCIに記述されています。

Staging環境のための処理の分岐

staging 環境用の branch のみ処理を行いたい/行いたくない等の分岐も同様に環境変数のCIRCLE_BRANCHを用いて判定することで実現できます。

1
2
3
4
5
6
7
- run:
name: Run test
command: |
if [[ $CIRCLE_BRANCH = 'release' || $CIRCLE_BRANCH =~ ^staging/ ]]; then
:
else
node_modules/.bin/grunt test

データベースへの初期データの投入

初期データの投入に関しては、単純にスクリプトを実行すればよかったので、run命令によって

1
2
3
- run:
name: Insert init data
command: mongo ut ./init_data.js

と書けました。

データベースサービスの起動待ち

テストをいざ実行しようとすると、MongoDB に接続出来ない旨のエラーが出るようになりました。
これは、imageのセクションで指定した MongoDB がstepsでテストが実行されるまでに起動して接続できる状態になっていないのが原因でした。
そのため、

1
2
3
4
5
6
7
8
9
10
- run:
name: Waiting for MongoDB to be ready
command: |
for i in `seq 1 15`;
do
nc -z localhost 27017 && echo Success && exit 0
echo -n .
sleep 1
done
echo Failed waiting for MongoDB && exit 1

のように MongoDB に接続を試行して接続が出来るかを確かめる処理をstepsの初めに入れて対処しました。

キャッシュ

CircleCI 1.0 と同様に、プロダクトが依存しているサードパーティパッケージライブラリ等をキャッシュすることができます。CircleCI 1.0 では cache_directories によりキャッシュ対象を制御していましたが、CircleCI 2.0 では save_cacherestore_cache の組み合わせでキャッシュを制御します。

現在 TOM では以下のような設定で npm モジュールと gem パッケージをブランチ別にキャッシュしています。

restore_cache でキャッシュをリストアしてから npm installbundle install を実行し、paths で指定したディレクトリを save_cache でキャッシュします。paths には working_directory からの相対パスか絶対パスでキャッシュしたいディレクトリを指定します。

キャッシュの key を変えることで複数のキャッシュを切り替えて使用することも可能です。キャッシュの切り替え方法や他の詳細については Caching in CircleCI をご覧ください。

キャッシュは key により制御可能で、 projectname-{{ .Branch }}
と指定することでブランチ単位のキャッシュを取得していますが、 {{ .Revision }}
により Git のコミット単位での取得や、{{ checksum "filename" }} により指定ファイルのチェックサム単位での取得を行うことができます。(key に指定可能な他の値については Configuration Reference: save_cache をご覧ください。)

当初 {{ checksum "filename" }} でのキャッシュを検討していたのですが、npm-shrinkwrapyarn を使用していないため npm モジュールのロックファイルが存在しない点と、gem パッケージを後述するデプロイ時のみで使用しているため大した量が無い点から、最終的にブランチ単位でのキャッシュを選択しました。

なお、CircleCI 2.0 ではキャッシュは Docker machine 上で作成されるようで、Cirlce CI 1.0 にあった Rebuild without cache メニューが無くなり、キャッシュディレクトリにもアクセスできなくなっているようですので、不完全な内容や誤った内容のキャッシュが作成されてしまった場合は restore_cache をコメントアウト等で無効にしてからリビルドしキャッシュを再作成する必要があるようです。

Deploy

SSH Keyの設定

TOMでは「Capistrano」のデプロイコマンドを CircleCI 上で実行してデプロイを行っています。CircleCI 2.0からサーバに SSH で接続するための鍵の設定方法が変わったため、再設定をしました。

まず鍵を登録します。 .circleci/config.ymladd_ssh_keys というフィールドを追加して、サーバに接続するための鍵のフィンガープリントを指定します。フィンガープリントは CircleCI の「SSH Permissions」というページで鍵を登録すると取得できます。

1
2
3
- add_ssh_keys:
fingerprints:
- "xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx"

次に ssh-agent を立ち上げて鍵を登録します。上記の add_ssh_keys が実行されると、 ~/.ssh/ 以下に id_rsa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx といった鍵ファイルが生成されます。この鍵ファイルを ssh-add することで、CircleCI から SSH でサーバに接続することができるようになります。

1
2
3
4
5
6
7
8
- run:
name: Start ssh-agent
command: |
ssh-agent -s > ~/.ssh_agent_conf
source ~/.ssh_agent_conf
for _k in $(ls ${HOME}/.ssh/id_*); do
ssh-add ${_k} || true
done

ローカルでの実行

CircleCI 2.0では SSH で有効にして実行(CI が動いたあとにコンテナにSSHで接続してデバッグ出来る)の機能が無くなってしまった代わりにローカルで同様に CI を実行する事ができます。

ローカルでの実行には、ローカルに Docker を入れる必要があります。
Install Docker for Mac - Docker Documentation にダウンロードリンクや手順があるので、それに沿ってインストールします。(stableで良いです)

その後、circleciコマンドを入れます。

1
$ curl -o /usr/local/bin/circleci https://circle-downloads.s3.amazonaws.com/releases/build_agent_wrapper/circleci && chmod +x /usr/local/bin/circleci

を実行することで、circleciコマンドがインストール出来ます。

circleciコマンドには、.circleci/config.ymlの文法チェックを行ってくれるコマンドがあり、正しい場合は以下のようになります。

1
2
3
$ circleci config validate
config file is valid

1
$ circleci build

とすることで、ローカルで CI を実行することができます。

ローカルでの実行に関する本家のドキュメントはRunning Jobs Locally - CircleCI に置いてあります。現時点ではキャッシュ等の制限などがあり、上記のドキュメントに明記されていますが、今後のアップデートで変化していくと思われます。

速度について

上記の手順によって全ての既存のタスクがstepsへ置き換える事ができ、テストの実行が行われるようになりました。
そこで、既存の CircleCI 1.0 の時と CI の実行時間を比べたところ、20分前後から15分前後へ改善しました。
特に CircleCI 1.0 の時と比べて意図的に速度改善を施しては居ない(キャッシュ等も CircleCI 1.0 の時も勝手に行われていたはず)のですが、これだけ改善するのは驚きでした。

まとめ

今回の開発合宿で、CircleCI 2.0への移行を行いました。
結果として、無事移行は成功して

  • CI の柔軟性が増した
  • CI 実行速度が上がった
    という利点を受けることができました。

TOM ではフロントエンジニア・サーバーエンジニアを募集していますので、興味がございましたら以下のリンクから話を聞きにいらして下さい。(CircleCI を普段使っていても使っていなくても結構です)

フロントエンドエンジニア
https://www.wantedly.com/projects/53079

サーバーサイドエンジニア
https://www.wantedly.com/projects/8214

商品企画
https://www.wantedly.com/projects/100553

インターン
https://www.wantedly.com/projects/101256