こんにちは!2018年も残すところあと3週間、コネヒト Advent Calendar 2018 - QiitaのDay-9でございます。
ブラック・ラグーンの新刊発売の衝撃がまだ鳴り止まない!の金城(@o0h_)です。 今回はもう5回は読みました、皆さんはどうですか?
コネヒトでは継続的インテグレーションにTravisCIを利用しています。
ローカルの開発環境、プロダクトの本番環境にはDocker(ECS)を利用しています。
本番イメージのビルドやpushもTravisCI上で行っています。
かねてより、「折角DockerベースなのにCIは"別環境"なの悲しい」「良い感じにビルドしたイメージをキャッシュして、デプロイを早くできないか」といった声が上がっていました。
そして、サーバーサイドエンジニアが集い「今四半期中に、CIの活用についての見直しを行おう」という誓いを立てたのです。
よりDockerフレンドリーなCIが他にある中で、Travis CI上でのDocker活用には少し工夫がいるかもな・・という風にも感じております*1。
具体的なtipsが世の中に出回るといいな、と思い今回は現時点で我々が得ている成果・知見についてご報告いたします。
TL;DR
- Docker自体はTravis CIでもサポートされているので難なく使える
- Dockerのビルドをキャッシュするためには、明示的なキャッシュの作成とアップロードが必要
- マルチステージビルドの結果をキャッシュするには、target buildの結果を保存し
--cache-from
オプションを利用したビルドを行う
- TL;DR
- 今回の対象となるDockerfile(前提の共有)
- やりたいこと・目的
- Dockerのイメージビルド時のキャッシュ利用
- ビルド結果のキャッシュを作る
- Travis CI上でキャッシュ済みイメージを利用したビルドを行う
- script/deployでDockerイメージを利用する
- 感想
今回の対象となるDockerfile(前提の共有)
JSとPHPが相乗りするイメージを利用しています。
そのため、ローカル環境でのビルド効率化のために、multi stage buildとBuildKitを利用しています。
これを、「Travis CI上でもばっちり使えるように、頑張ってみようぜ!」というのがこの記事の趣旨です。
【ここで参考になりそうな記事】
- Use multi-stage builds | Docker Documentation
- Docker マルチステージビルドで幸せコンテナライフ / Understanding docker's multi-stage builds - Speaker Deck
- DockerCon参加報告 (`docker build`が30倍以上速くなる話など)
やりたいこと・目的
Travis CIは、以下の用途で利用しています
- push/prごとのテストやスタイルチェックと言ったビルド
- Amazon ECRへのデプロイ
docker build
コマンドの実行(都度ゼロベースでのビルド)- レジストリへのpush、サービスイン
これを、以下のように変更するのが目的です
- push/prごとのテストやスタイルチェックと言ったビルド
- PHP系のテスト等は、Dockerコンテナ上で実行する
- そのために、ビルドに係る処理コストを最小化する
- PHP系のテスト等は、Dockerコンテナ上で実行する
- Amazon ECRへのデプロイ
- レイヤーキャッシュを利用して
docker build
を実行する - レジストリへのpush、サービスイン
- レイヤーキャッシュを利用して
Dockerのイメージビルド時のキャッシュ利用
Travis CIでのビルド時に、Dockerの「前に作ったイメージ」をキャッシュしておくことは可能でしょうか?
基本的には、こちらの記事で触れている通りの方法でDockerコンテナを利用することができます。
【ここで参考になりそうな記事】
multi stage build時のキャッシュについて
ここで注意として、「マルチステージビルドを使っていると、--cache-from
に最終ステージを指定してもレイヤーキャッシュがされない」という問題があります。
具体的には、
- 中間ステージのビルドにキャッシュが利用されない
- そのため、最終ステージの「手前」の部分に変更が生じる
- 手前が変更されているため、以後のレイヤーでもキャッシュを利用しない
かのように見える現象があります。
例えば、以下のようなDockerfileがあったとします
FROM aaa:latest as A # hogehoge FROM bbb:latest as B # fugafuga FROM xxx # piyopiyo
これをビルドします
docker build -t xxx .
さて、ここで使った労力をレジストリにpushしておいて、別ホストでのビルド時にもcacheを使いたい!ですね。
docker pull mine/xxx:latest docker build -t xxx --cache-from mine/xxx:latest
結果は、残念ながら「A」のステージでキャッシュが効きませんでした。。。
という具合です。
これを回避するため、中間ビルドのイメージも個別に生成することにしました
【ここで参考にした記事】
docker save/loadを利用して、multi stage buildでもレイヤーキャッシュの恩恵を受ける
方向を転換し、「中間ステージごとにイメージを保持・読み込みを行い」「ステージごとに、必要なイメージを明示的に --cache-from
指定をして読み込ませる」というものにします。
なお、この段階で、「最終ステージ以外のものをレジストリに上げるのはどうなのかな・・・」という気持ちになったので、ローカルにイメージを置くことにしています。
docker build -t A --target A --cache-from A . docker build -t B --target B --cache-from A --cache-from B . docker build -t xxx --cache-from A --cache-from B --cache-from xxx .
--target
を指定しつつタグ付けを行うことで、imageのsaveができるようになります。
docker save A -o A.tar docker save B -o B.tar docker save xxx -o xxx.tar
saveしたイメージをファイルとして共有することで、他ホストでも読み込み可能になります。
docker load A.tar docker load B.tar docker load xxx.tar
ということで、大まかな方針として「中間ステージ・最終ステージをそれぞれビルドして、セーブして、次のビルドの前にロードする」というやり方に決めました。
【ここで参考にした記事】
ビルド結果のキャッシュを作る
Travis CIは、 .travis.yml
内で「このディレクトリをキャッシュする」というパスを任意に指定することができます。
このディレクトリ下に書き出されたファイルは、次のビルドの際に冒頭で読み込まれ利用可能になるという仕組みです。
それを踏まえて、docker save
の出力先を「キャッシュ対象ディレクトリ」にしてしまえば良さそう。
$HOME/docker
というディレクトリを設け、そこに放り込むことにします。
# .travis.yml cache: directories: - $HOME/docker
Travsi CIは、ビルドのメインとなる script
ステージの後〜 deploy
ステージの間に、キャッシュを作成・クラウド(S3)アップロードを行います。
- OPTIONAL Install apt addons
- OPTIONAL Install cache components
- before_install
- install
- before_script
- script
- OPTIONAL before_cache (for cleaning up cache)
- after_success or after_failure
- OPTIONAL before_deploy
- OPTIONAL deploy
- OPTIONAL after_deploy
- after_script
from: Job Lifecycle - Travis CI
なので、 before_cache
の段階で docker save
を実行してしまえばよいです。*2
# .travis.yml before_cache: > mkdir -p $HOME/docker && rm $HOME/docker/* && docker images -a --filter='dangling=false' --format '{{.Repository}}:{{.Tag}} {{.ID}}' | xargs -n 2 -t sh -c 'docker save $0 -o $HOME/docker/$1.tar'
- cache保持用のディレクトリを(存在しなければ)作り
- 利用されていないファイルが生き残り続けないよう、ディレクトリ配下のファイルを一旦全て削除し
- docker imagsで取得した「存在するイメージ(タグ)」を、ファイル名にIDを使って保存する
- タグを利用すると
/
が入ってきてややこしくなるため、安全かつ一意な文字列としてIDを利用する
- タグを利用すると
これで、「必要そうなイメージをキャッシュに回す」ことができるようになります。
【ここで参考にした記事】
- Docker cache on Travis and Docker 1.12 // Read at G's // A personal blog from Giorgos Logiotatidis.
- My solution is similar - docker save all the images:
image保存のチューニング
しかしながら、これは結構なオーバーヘッドになります。
1つは、docker save自体が結構な時間がかかること。saveするイメージは少ないほうが良いです。
2つ目に、キャッシュを使える場面で活かしきれていないこと。multi stage buildを利用するにあたり「キャッシュを最大限生かせるように」考えていますから、「めったに変更がない中間ステージイメージ」が存在するという状態も作られています。これを「毎回、絶対に作り直す」というのは効率が悪く思います。
そのため、「保存されている内容と今使っている内容がに差異がなければ、そのimageのsaveは省略する」ことで省力化できると効率が良いです。
また、Travis CIのキャッシュの保存・取得は、外部ストレージへのネットワーク経由のアップロード・ダウンロードによって行われます。それを考えると、ファイルサイズが小さい方が有利です。
このタイミングで、出力イメージのgunzip圧縮も行うことにしました。
# .travis.yml before_cache: - cache_threshold=$(date “+%Y%m%d %H:%M”) - docker images -a --filter=‘dangling=false’ --format ‘{{.Repository}}:{{.Tag}} {{.ID}}’ | xargs -n 2 -t sh -c ‘if [ -e $HOME/docker/$1.tar.gz ]; then touch $HOME/docker/$1.tar.gz; else docker save $0 | gzip -2 >$HOME/docker/$1.tar.gz && echo “$0($1) saved”; fi’ - find $HOME/docker/. \! -newermt “$cache_threshold” | xargs rm -rf
- before_cacheに入った時点での日時をメモしておき
- cacheディレクトリ以下のimageそれぞれについて、
- いま利用されているものとIDが同一であれば、
touch
してタイムスタンプだけ更新 - IDが違ったら = 内容に変更が生じていたら、docker saveを行って上書き
- いま利用されているものとIDが同一であれば、
- 1のステップでメモした日時とディレクトリ下のファイルのタイムスタンプを比較、「before_cacheに入る段階より古い」ものを削除
- これで「もう利用してないイメージ」が破棄される
ようにしました。
Travis CI上でキャッシュ済みイメージを利用したビルドを行う
ここまでで、 $HOME/docker
ディレクトリに「使えそうなイメージ」が配置されています。
あとは、これを実際に利用したビルドを行うようにしましょう。
# .travis.yml before_install: - if [[ -d $HOME/docker ]]; then ls $HOME/docker/*.tar.gz | xargs -I {file} sh -c "zcat {file} | docker load"; fi instal: - docker build -t A --target A --cache-from A . - docker build -t B --target B --cache-from A --cache-from B . - docker build -t xxx --cache-from A --cache-from B --cache-from xxx .
こうすることで、以前にビルドしたイメージからうまくキャッシュを利用してくれるようになりました。
script/deployでDockerイメージを利用する
あとは、dockerコマンド経由でのテスト実行や静的解析など、自由に「いつもの環境」での処理実行を行えるようになります。
script
ステージでのコンテナの利用は、先に紹介したTravisのドキュメントをご参照ください。
また、デプロイ用に改めてイメージをビルドする必要がある!という場合も、同様に(必要に応じた)--cache-from
オプションの指定などで対応できると思います。
感想
本記事の内容は、まだまだ改善の余地があるような気もしています・・・それでも、チーム内で求めていた「大体こんな感じ」という水準までは、一旦持ってくることができました。そうして、公開に至っております。
また、Travisの利用するキャッシュサイズが大きくなることや、docker loadに関するオーバーヘッドが大きくなり、プロジェクトによっては今までよりビルドごとに要する時間が大きくなるんだろうな、という印象も受けています。
それでも、チーム内では「テストの確実性が上がる、本場と同等のイメージで動かせる恩恵がある」という事で合意を得ています。
ここでもまた、「やはりDocker使うならイメージが小さいのが正義・・・!」という気持ちを新たにしました。
ということで、次にやりたいのは「イメージをめっちゃダイエットする」です🐶
「ここ、もっとこうした方が良いのでは?」「ここの理解が間違っている!」といった箇所がありましたら、お気軽にフィードバックをいただけると嬉しいです!
話が固くなりましたので、最後にいらすとやさんのホトトギスを見てお別れしたいと思います。
明日は @katsutomu さんが元気な記事を書いてくれます!