コンテナ時代の Twelve-Factor App
Twelve-Factor App (Adam Wiggins, 2012) を読んで感動したので、あらためてコンテナ技術が流行する 2016 年現在の Web アプリケーション環境を意識しながら覚え書き、まとめです。
本記事は主に Web アプリケーション開発者、Web アプリケーションインフラ構築に関わる方を対象としています。
Twelve-Factor App
Heroku が提唱するソフトウェア開発および CI 環境構築における方法論で、下記を大目的とした 12 の指針からなる。
- プロジェクトに新しく加わった開発者が要する時間とコストを最小化する
- 実行環境間での移植性を最大化する
- サーバー管理やシステム管理を不要なものにする
- 開発環境と本番環境の差異を最小限にし、アジリティを最大化する継続的デプロイを可能にする
- ツール、アーキテクチャ、開発プラクティスを大幅に変更することなくスケールアップできる
今回は特に Docker による Web アプリケーション開発および CI 環境構築を意識して、以下の 8 つの指針を抜粋し、要約と共に Docker への適用例 を考察する。
- 継続的デプロイのためのアプリケーション構成
- I . コードベース
- III. 設定
- V . ビルド、リリース、実行
- アプリケーションプロセスの管理とスケール
- VI. プロセス
- VIII. 並行性
- IX. 廃棄容易性
- 運用
- XI. ログ
- XII. 管理プロセス
継続的デプロイのためのアプリケーション構成
アジリティの高い、身軽なアプリケーション構成 のための方針、および共通概念。
I. コードベース
- バージョン管理された コードベース と、一対一対応するアプリケーション
- 一つのコードベース・アプリケーションは、複数の デプロイ (ステージング環境、本番環境、開発環境) を生ずる
III. 設定
アンチパターンとして、コードへの定数べた書き、Rails の database.yml への値のべた書きが挙げられている。良しとされない理由としては、
- 誤ってリポジトリにチェックインされやすい
- いろんな場所、いろんなフォーマットに散乱しがち
ことが挙げられている。
V. ビルド、リリース、実行
ビルド、リリース、実行の 3 つのステージを厳密に分離する。
- ビルドステージ
- 特定バージョンのコードリポジトリから ビルド (実行可能な塊) への変換
- リリースステージ
- ビルドに対してデプロイの設定 (環境毎に異なる値、前節参照) を結合し、リリース を作成する
- 実行ステージ
- リリースに対して、必要なプロセスを起動することでアプリケーションを 実行 する
ビルドステージでは、バイナリやアセットファイルのコンパイル、依存ライブラリの取得などが行われる。リリースステージによって環境依存の設定が埋め込まれ、いつでも実行できる状態となる。
ビルドには環境に依存する値 (RAILS_ENV や DB ホスト / パスワード) は含まれない。 このことは逆に、コードベースからデプロイに至るプロセスを、設定を介して "環境に依存しないビルド" と "環境の依存があるリリース" の二段階の概念に分けた、とも考えられる。
🐳 Docker への適用
- 1 つのコードベースから 1 つのアプリケーションを成す Docker イメージ群を生成する
- nginx イメージ & rails イメージなど
- ビルドステージでは、コンパイルや依存ライブラリの取得と同時に、Dockerfile のビルドを行う
- Docker イメージ群は環境依存の値を含まない
- リリースステージで設定 (環境変数) を埋め込むことで、実行可能になる
- Docker Compose の docker-compose.yml や、ECS の Task Definition によって実現する
- 実行ステージでは Docker Compose や、コンテナオーケストレーション (Kubernates / ECS / Swarm など) を用いてコンテナ群を実行する
アプリケーションプロセスの管理とスケール
スケーラブルなアプリケーションプロセスが満たすべき要件。
VI. プロセス
- アプリケーションの実行は 1 つ以上のプロセスからなる
- プロセスは ステートレス である
- プロセスは シェアードナッシング である
永続化する必要のある全てのデータは、ステートフルなバックエンドサービス (DB など) に格納する。
VIII. 並行性
プロセスはアプリケーション実行の最小単位である。
- アプリケーションのエントリーポイントは、プロセスの単なる起動である
- プロセスをデーモン化する機能、PID を吐く機能などをアプリケーション自体に持たせるべきではない
- 統合的に、OS の標準機能やプロセスマネージャに任せる (Upstart, launchd, foreman)
- プロセス単位でスケールアウトする
- シェアードナッシングなプロセスは容易に並行動作可能
IX. 廃棄容易性
コードのデプロイやプロセスの再配置 (スケーリング) のためにプロセスはいつでも再起動される可能性があり、それに備えなければならない。
- プロセスは SIGTERM シグナルに対して、グレースフルにシャットダウンする
- Web プロセス: ポートのリッスンを停止、処理中のリクエストの完了を待ち、シャットダウン
- ワーカープロセス: ジョブをキューに戻す
- 突然の死 (SIGTERM 無し) に対しても堅牢であるべき
- プロセスの起動時間を最小化する
🐳 Docker への適用
プロセス = コンテナ (or コンテナ内で起動するプロセス) と読み替える。
- コンテナは原則ステートレス・シェアードナッシングとする
- 永続化領域 (VOLUME) は慎重に扱う必要がある
- 例外的にステートフルなコンテナ (データベースなど) を扱う場合は、その他のステートレスなコンテナとは明確に分離する
- コンテナ内で起動するプロセスはデーモン化しない
- コンテナプロセスの管理はコンテナの外側でコンテナオーケストレーションシステムが担う
- コンテナは SIGTERM を正しくハンドリングする
- コンテナの起動時間を最小化する
運用
本番運用のための統一的なインタフェースの指針。
XI. ログ
XII. 管理プロセス
一回きりの管理・メンテナンスタスクについて。
- REPL シェル (rails console など) や単発タスク (DB マイグレート、Rake タスク など) を、本番環境で実行できるようにしておく
- このためのコードは、アプリケーションコードと同じコードベースで管理し、一緒にデプロイする
- 同期の問題を避けるため
🐳 Docker への適用
- コンテナ内のプロセスはログストリームを標準出力に書き出す
- Docker の Logging Driver は、標準出力をハンドリングすることを想定している
- Docker イメージはアプリケーションプロセスの実行と共に、REPL シェルやタスクの実行を想定するべき
bundle exec rake XXX
やbundle exec rails console
- 例えば postgres イメージは、以下の機能を同時に提供している (Best practices for writing Dockerfiles)
docker run postgres
: postgres の起動docker run --rm -it postgres bash
: シェルの起動