docker
docker-compose
docker-for-mac
CircleCI2.0

PLAID Advent Calendar 2018 2日目の記事です。

今回は、とあるDockerの構築技法(series build)をご紹介します。
なにかと言うと、開発環境〜本番環境までのDocker Imageをどのように構成・構築するか?という問題に対する、一つの手法についての解説です。

ここしばらく、弊社KARTEの開発環境のDocker化の続きとして、CI〜本番環境への適用も視野に入れたビルドシステムの見直しを行っていたのですが、そのノウハウをまとめたものです。

前半では思想を、後半では簡略化したコードを元に解説していこうと思います。

サンプルとして「とあるプロジェクト」を作成しました。
(いくつか良くない所があるので、ちょこちょこ直すかもしれないです)

PLAID社でのDocker利用について

弊社サービスのKARTEでは開発環境のDocker化は完了しています。Docker for Macを使って実行環境を構築するようになっており、また、CircleCI 2.0を利用してContainerベースのE2Eテストを動かしていています。

本番環境ではサブのシステムのみですがKubernetesを使い始めていて、全体として徐々にこちらに寄せていく流れが見えつつある……ような……気がします。

自分自身のことを言うと、Dockerは2011年の後半辺りから気になりだしてちょいちょい触っており、最近では開発環境のDocker化を社内で進めるなど、Docker好きとして知られております。(参考:イマドキのDocker力をつけるPLAID式チュートリアル

「環境」について

前提として、Nodeを使ったとあるWebアプリのプロジェクトの構成を考えます。リポジトリ運用はgit-flow、製品なのでprivateリポジトリです(サンプルはpublicですが)。

「環境」についてはだいたいどこも似たような構成になると思いますが、「開発環境」「CI環境」「検証環境」「本番環境」の4つとおき、まずは「環境のタイプ」についてそれぞれの求めるものや特徴を考えてみます。

これらを一貫した環境として作成・管理することができれば、Dockerの強みが最大限に活きてくるのではないでしょうか。

開発環境

開発時でもサーバサイドの実装はコンテナ内で動かしたい昨今です。環境依存を下げ、使用する開発PCが散らかるのを避けることができます。Docker for Macとgit, bash以外に依存がないのが理想だと思います。

構成管理はdocker-compose.yml。DBなどはデータVolumeを作った永続化が利用できます。

また開発環境なので、ソースコードの編集更新をテスト用に起動したサーバに直接反映させないといけません。とはいえコンテナの中でエディタを動かしたくはないので(昔やってましたが)、ホスト側のファイルシステムをコンテナ内にマウントするHost Volumeの機能を使うと便利です。

ちなみにKARTEの開発環境では、よく使う操作にbashで作った専用のkarteコマンドをつくったのですが、不評でした……。

CI環境

ここでは完全に動く機能一式の他に、テストツールを動かすための開発ツール類も必要になります。また、頻繁にビルドが繰り返されるので少しでも効率化しておきたい箇所です。

開発環境と異なり、データの初期投入の処理も考えなければならないので、その辺りも念頭に置く必要があります。今回のサンプルでは特に工夫はありませんが、KARTE向けのフルセットでは環境の初期設定専用のコンテナを作るというテクニックを使っています。

E2EテストになってくるとDBを含め複数のコンテナの組み合わせで記述することになるので、構成管理の機能もかなり重要です。

CircleCI 2.0ではDockerコンテナが使えるようになっているので便利です。.circleci/config.ymlなどでテストランナーと構成管理を行うことができます。2.1の方がきれいに書けるらしいですが、今回は2.0で作業しました。構成管理としては若干クセがあって使いづらいところもありますが、テストランナーとしての機能は便利なのでテストの実行もdocker-compose化するかはなかなか悩ましいところです。

検証環境

本番とは独立した検証環境です。ここではシステム外部からのE2Eテストと、手動テストを行います。サーバの構成などは極力本番に似せてあり、環境設定やFixtureなどは投入済みの環境です。

スケールが小さい以外は本番と同じなので割愛します。

本番環境

GKEのクラスタなどで作られる本番環境。

ここではImageに無駄がなく配布データ量が小さいことが求められるかと思います。Imageのサイズはスケールアウトの際の応答性に結構効いてきます。また、「テストが通ったImageをそのまま実行する」というのは結構重要なポイントで、Dockerの強みといえるものではないかなと思います。

実運用に関しては、設定ファイルなどをどのように管理するかという辺りを考える必要があります。Kubernetesには設定ファイルを配布する機能があるのでその辺りを活用することになると思います。

弊社のKARTEの場合、システムのメインの部分はコンテナとして運用していないので、今回の記事はこの部分に関してエアプです。

Imageのビルドについて

いろいろな考え方があると思うのですが、だいたい次のことを意識すると思います。

  • Imageを最小にする
  • 処理時間はなるべく短く
  • ソースコードからの生成物を無駄なく作る
  • インストールなどの処理はなるべく重複しないようにする
  • できるかぎりシンプルにする

最後の一つのテーマがとにかく他のテーマとぶつかるので、トレードオフをいろいろ考えないといけません。この記事を書いたのも、この辺のバランスがノウハウなのではないかなと思ったからに他なりません。定番が確立すれば楽になっていくのだと思うのですが。

今回は、徐々に大きくしながら作って、最後に必須のものだけ取り出す手法をとっており、5つのイメージを2つのDockerHubリポジトリに作成します。

ここからは「とあるプロジェクト」の動くコードを元に解説していきます。コード内にもコメントで解説をつけているので、そちらも参照してみてください。

ビルドスクリプト

CI毎に実行されるスクリプトで、次の5つのImageを連続して作成します。

  1. 最小ベースImage -- 実行に最低限必要なライブラリのみ
  2. 開発・CIベースImage -- 開発やCIの実行に必要なツールを含めたもの
  3. リリースImage -- ソースコード類やNodeのモジュール類を含んだもの
  4. CI Image -- 実働可能なフルセットのImage
  5. 本番Image -- 実働可能な最小セットのImage

すべてが毎回実行されるわけではなく、1, 2, 3は存在しないとき、5は必要なときだけ実行されます。1〜4までは一つ前のImageをベースに作成されますが、5だけは1と4のマージによって作成します。

それぞれのImageの詳細について解説していきます。

最小ベースImage

本番環境用のミニマムな構成です。「実行環境」のみを作るのがポイント。ここでは、本番環境での動作に必要なミドルウェアのみをインストールします。make, gccなどのよく使う開発ツールも入れません。

動作環境自体を更新することは稀なので、projectのバージョンとは独立してバージョン管理します。(今回はNodeのオフィシャルパッケージではなくて、別途Installする形にしてありますが、深い意味はないです)

開発・CIベースImage

最小ベースImageを元に、ソースコードのビルドやモジュールのインストール、Fixtureの設定などを行うためのライブラリやコマンドなどを追加したものです。ソースコードが必要な部分以外のすべてが入っています。

この時点ではまだリポジトリのcloneは行いません。「頻繁に更新しない部分」をまとめたものだからです。projectのバージョンとは独立してバージョン管理します。今回はビルド用のスクリプト内で構築していますが、2つのベースImageについてはDockerHubでビルドするのも良いと思います。

開発環境ではこのImageを使い、ソースコードについてはHost Volumeを利用してマウントします。サーバサイドが依存する外部ライブラリがコンテナ内にあるので、Nodeのモジュールのインストールも内部で行います。node_modulesに対してVolumeを切ってありますが好みによると思います。ホスト側をどのように使うかによっても変わってくるでしょう。

deploy.keyというファイルが入っていてちょっと賛否分かれそうですが……そして今回は公開リポジトリなので意味ないのですが、「自分自身をread-onlyで取得できる鍵」を入れています。これをImageの中に焼き込むことで、その後の処理を簡略化しています。(当たり前ですが鍵は今回専用に作ったものです)

リリースImage

リリースごとに作られる、CI用Imageの元になるImageです。(実働可能であることは必須でないので名前は微妙です)

開発・CIベースImageを元に、git cloneからスタートし、まっさらな状態から構築します。

基本的にはキャッシュ可能なデータや処理をすべて行うフェーズで、CI Imageを無駄なく作るためのベースとして利用します。MPEG2でいうIフレームです(わかりにくい)。たとえば、node_modulesはほとんどのブランチで更新がなかったり差分の更新で済むので、このImageに埋め込みます。

CI Image

CIが回るたびに作業ブランチの最新の状態で作成されるImageです。このImageには、実行に必要なすべて+テストを実行するためのすべてを含みます。タグはCIの実行IDなどが良いでしょう。CircleCIならCIRCLE_WORKFLOW_WORKSPACE_IDを使うのがコツです。

ビルドが行われる回数が多いので、ブランチが分岐した直近のリリースImageからの差分として作成します。MPEG2でいうPフレームです(わかりにくい)。リリースImageでnode_modulesなどをインストール済みなので、package.jsonに差分がない場合はnpm installを省略するテクニックが有用です。gitのコマンドにもコツがあり、これでかなり小さく効率的に差分を取得できます。

考え方としては同じブランチの一つ前のCI Imageをベースにする方法もあるとは思うのですが、以前やろうとして複雑な実装になったので今回はやりません。

ソースコードをビルドするようなものはすべてここでビルドします。そのためgitのリポジトリにはビルド後のファイルは含めません。

なお、今回はちょっと作りきれなかったので、CircleCI自体の解説は省略します。
(ファイルだけはあるけど...)

本番Image

検証環境・本番に配布してそのまま利用することができる最小のImageです。フルサイズであるCI Imageから不要なものを消し、必要なものだけを最小ベースImageとマージします。

このマージ作業はDockerfileでbuilderが使えるようになってものすごく楽になったところです。

CI Imageをbuilderとして呼び出し、npm prune --productionなどで掃除した後に、最小実行ベースイメージにコピーして完成です。

ちなみに、まだ本番投入されていないので、この機能はエアプです。(まだバグがあるかも)

むすび

Dockerを開発環境〜本番環境まで活用するために必要な、Image作成フローのアイデアについて紹介させていただきました。

この辺りのことに関しては、実のところ開発環境Docker化の話の前後辺りからずーっと考えていたのですが、やり方のバリエーションも多く、何をどこでと考え始めるとかなり悩んでしまってなかなか進められなかった部分でした。今回なんとか形になったので個人的にはかなりスッキリしました。

これからまだいろいろ揉んでいく段階なので、続きについてもご紹介できると良いなと思っております。

では。

(おまけ)とあるプロジェクトのこと

今回のサンプルは、今後自分で使うときのベースにしたくていろいろ地味に作り込んであります。frontendでrollup.jsを使っていたりとか。

本当はE2EとかCircleCIとの連携周りもしっかり書きたかったのですが、予定より時間をオーバーしてしまったので一旦ここまでとさせていただきます。やりかけな感じではありますがCircleCIのファイルは入っていたりします。

「とあるプロジェクト」の簡単な使い方はREADME.mdに書いてありますが、今後も動く状態はキープするので、各種コードの作例としてご活用頂ければと思います。