CircleCI 2.0を使うようにするだけで、こんなに速くなるとは夢にも思わなかった!
みなさんこんにちは。Tokyo Otaku Mode(TOM)でエンジニアをやっています @pchw です。
先日TOMの開発チームは定期的に行われる開発合宿をやってきました。今回は1年ぶりに鎌倉での開発合宿でした。
せっかくだし、息抜きに海岸を散歩するかという絵
今回は、
- 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
には docker
と machine
があります。詳しくは 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 ならどれでも使用できます。
- Docker Hub から各種言語がインストールされたオフィシャル Docker Image
- CircleCI 1.0 の Build Image(https://circleci.com/docs/1.0/build-image-trusty/ 参照)
- 独自に作成した 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.yml
の steps
でインストールしていく必要があります。
独自に作成した 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_INDEX
やCIRCLE_NODE_TOTAL
という環境変数が使えるため、以下のように記述することで複数コンテナでテストを分担して並行して進めることが出来ました。1
2
3
4if [ $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_cache
と restore_cache
の組み合わせでキャッシュを制御します。
現在 TOM では以下のような設定で npm モジュールと gem パッケージをブランチ別にキャッシュしています。
restore_cache
でキャッシュをリストアしてから npm install
と bundle 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-shrinkwrap や yarn を使用していないため 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.yml
に add_ssh_keys というフィールドを追加して、サーバに接続するための鍵のフィンガープリントを指定します。フィンガープリントは CircleCI の「SSH Permissions」というページで鍵を登録すると取得できます。
1 | - add_ssh_keys: |
次に ssh-agent を立ち上げて鍵を登録します。上記の add_ssh_keys
が実行されると、 ~/.ssh/
以下に id_rsa_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
といった鍵ファイルが生成されます。この鍵ファイルを ssh-add
することで、CircleCI から SSH でサーバに接続することができるようになります。
1 | - run: |
ローカルでの実行
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