Dockerに関するメモ
Dockerと付き合っていく上で考えるべきことのメモ
The Twelve-Factor App を熟読
Dockerは要するにこのTwelve-Factor Appを実現するためのインターフェースなので、 選択に悩んだらここに戻ってくると良い。
- コードベース (Codebase)
バージョン管理されている1つのコードベースと複数のデプロイ
- 依存関係 (Dependencies)
依存関係を明示的に宣言し分離する - 設定 (Config)
設定を環境変数に格納する - バックエンドサービス (Backing Services)
バックエンドサービスをアタッチされたリソースとして扱う - ビルド、リリース、実行 (Build, release, run)
ビルド、リリース、実行の3つのステージを厳密に分離する - プロセス (Processes)
アプリケーションを1つもしくは複数のステートレスなプロセスとして実行する - ポートバインディング (Port binding)
ポートバインディングを通してサービスを公開する - 並行性 (Concurrency)
プロセスモデルによってスケールアウトする - 廃棄容易性 (Disposability)
高速な起動とグレースフルシャットダウンで堅牢性を最大化する - 開発/本番一致 (Dev/prod parity)
開発、ステージング、本番環境をできるだけ一致させた状態を保つ - ログ (Logs)
ログをイベントストリームとして扱う - 管理プロセス (Admin processes)
管理タスクを1回限りのプロセスとして実行する
baseの選択
Official Imageは docker-library/official-imagesでオープンソースとして適切にアップデートされているため、、 これをもとにしていれば、自前のDockerfile内部での明示的なapt-get upgrade/yum update等は不要である。
たとえば、Ubuntu Trustyであれば、 tianon/docker-brew-ubuntu-coreの trusty/DockerfileにDockerfile一式がある。 Ubuntuがdailyで公開している Ubuntu Core をupdate.shで 取り込んだtrusty-core-amd64.tar.gzにDocker用の調整を加えている。 したがって、心配であれば official-images/library/ubuntu をチェックすれば、十分に新しいベースイメージかどうかが把握できる。
ubuntu:precise にさらにもう一段階、日付等のタグが付与できると、DockerfileからFROMで指定する際に、 ベースイメージのバージョンを確実に指定できるが、そこまでは用意されていないので、 手動でdocker pullしなおしてからbuildしなおせばよいはず(未確認)。
設定の注入
コンテナ内のプロセスとvolumeで完結するのであれば関係無いが、 外部(DBサーバの接続情報や外部API)へ接続する場合は、その設定情報を注入してあげる必要がある。
旧来多かったのは、全てのパターンの設定情報をソースコードレポジトリに含めておき環境名(RAILS_ENV等)で切り替えるパターンだが、 外部公開したり、部品として広く再利用するような場合には、 全ての設定情報そのものをdocker run時に環境変数で注入する必要がある。
ビルド済みのコンテナに設定を持ち組む方法は3つある。
- 環境変数(-e)
例) docker run -e VARNAME="VALUE"
Docker Container Linking も環境変数の一種 - CMD
ENDPOINTとの適切な組み合わせが前提 - 外部の構成管理DB(CMDB)
etcd等に構成管理DBを用意し、そこから取得する。ただし構成管理DBへの接続情報は環境変数等で持ち込む必要がある。
Twelve-Factorが「設定を環境変数に格納する」と書いているとおり、 設定注入には環境変数を用いるのが望ましいだろう。
(TODO) あまりに設定内容が多い場合はどうしようか……。
データストア以外にデータを置かない
アプリケーション自体のエラーログはSTDERR/STDOUTに吐いて、 開発環境ではコンソールで直接確認し、STG/PRDではfluentd等を使って適切に回収するのが良いと思われる。
アクセスログ等はFluentd(等のストリーミング基盤)前提でよさそう。 環境変数で設定が注入されなかった場合にどうするかだけ考えないといけないかも。
再利用・外部公開するのであれば、Fluentdでログを送る際に一般にコンセンサスが取れているtagを付与するべきである。
管理プロセスの実行
1回限りの管理プロセスは、アプリケーションの通常の長時間実行されるプロセスと全く同じ環境で実行されるべきである。これらのプロセスは、あるリリースに対して実行され、そのリリースに対して実行されるすべてのプロセスと同じコードベースと設定を使う。管理用のコードは、同期の問題を避けるためにアプリケーションコードと一緒にデプロイされるべきである。
同じ依存関係分離技術をすべてのプロセスタイプに利用するべきである。例えば、もしRubyのWebプロセスがコマンド bundle exec thin start を使うのであれば、データベースのマイグレーションには bundle exec rake db:migrate を使うべきである。同様に、Virtualenvを使っているPythonプログラムは、仮想環境内の bin/python をTornado Webサーバーとすべての manage.py 管理プロセスの両方で利用するべきである。
CMDは、この管理プロセスの起動を指定するのに用いられるべきである。
上の例であれば、以下のようなイメージ。
ENDPOINT="bundle exec" CMD="thin start"
マイグレーションをするときは、
docker run rake db:migrate
実用上は、もう少し抽象度を上げてstart, migrateなどのキーワードを受け付ける起動スクリプトをENDPOINTとして用意するのが良いかもしれない。
1プロセス論争
理想としては、ENDPOINTとして指定されるプロセスの生存期間=コンテナであるのが望ましい。
6 . プロセス (Processes)
アプリケーションを1つもしくは複数のステートレスなプロセスとして実行する
ただし、それはあくまでベストであり、1つの機能を実現するために複数プロセスが必要であれば、 無理矢理コンテナを分離してvolume等でくっつけるよりは、1コンテナ内で複数プロセスを動かす方が良い。 あくまで、コンテナ間はリソースハンドラ(URL等)のみを媒介とした疎結合を保つべきである。
Docker公式ドキュメントでもSupervisorが紹介されている。
その一方で、これから設計するアプリケーションであれば、 1プロセス(とその子孫プロセス)で完結するアーキテクチャを選択するのが良いだろう。
言うまでもなく、SSHDを目的とするコンテナ(例えば踏み台サーバ)でも無い限りは、コンテナとしてデバッグ用のSSHDを上げるべきでは無い。docker exec しろ。
実行権限
通常のアプリケーションに関して言えば、Dockerfileでのコンテナ作成プロセスが終わったら、コンテナ実行時にはrootで動くプロセスは「一切」不要なはず。なので、USERディレクティブでCMD/ENTRYPOINTの実行権限を設定して一般ユーザでプロセスが起動するようにするだけで無く、イメージ内のSUIDビットも全て落とした方が良い。
このあたりは、既存の適切にchrootされたアプリケーション配布パッケージを参考にするとよいのかも。 そもそもアプリケーションの実行権限では、仕様としてアプリケーションが操作するファイル以外には一切書き込みできないべきである。
設定の注入に「外部の構成管理DB(CMDB)」を追加