皆さんは、The Tweleve-Factor App
をご存知だろうか?
これはHerokuの中の人が書いた、Webアプリケーションを使いやすい形でスケーラブルにするための方法論である。簡単にいえばコンテナで動かしたいアプリケーションが守っておくとよいレシピ集であると言える。
今回これを取り上げた背景としては、実はDockerコンテナをメインにした本番でのインフラ運用を考えた時に、アプリケーションがこの12の要素を満たしていることが重要だと最近ひしひし感じているから。
実際、自分が働いているところが運営しているサービス Wantedlyは、もともとずっとHerokuで運営していて、最近AWSに移行し、現在Dockerコンテナの上で動いている。この移行を約1ヶ月半で実現できた大きな要因として、Herokuの上に乗っていたことで知らず知らずのうちに12factor appに沿っていたというのがあると思う。今後、色々なものをDockerコンテナとして運用していくにあたって、ベストプラクティスを正しく理解しておくのが今後もより良いインフラを作っていく上で大切そう。
以下、12factorの内容を簡単にまとめてみたメモ。
簡略化のために、Rubyでの例以外書かなかった。本家ではPythonやPerlなどについての例も出ていたりする。
I. コードベース (Codebase)
One codebase tracked in revision control, many deploys
(c.f. git push heroku master
で動く設計)
コードベースはgit
などのバージョン管理システムで管理しよう。
そして、サーバーで動くアプリとコードベースは常に1対1の関係であるべきで、複数のコードベースをまとめて1つのアプリにしたり、1つのコードベースで複数のアプリを管理してはいけない。
共通のコードが有る場合は共有ライブラリを作って、II.Dependenciesに則って管理する。つまりgem
にする。
1つのコードベースであれば、その中で違うバージョンのproductionとstagingを作ってデプロイをしてOK。
II. 依存関係 (Dependencies)
Explicitly declare and isolate dependencies
(c.f. heroku buildpack)
全ての依存関係は明示的に書かれるべきであり、システム全体に入っている暗黙的なパッケージの存在(ImageMagickやcurl等)を前提にしてはいけない。
つまりGemfile
を用いて依存を記述し、bundle exec
を用いて依存を分離する。
ImegeMagick
などのツールが必要な場合は、vendorアプリとしてアプリ内に含めてしまうと良い。
III. 設定 (Config)
Store config in the environment
(c.f. heroku config
)
プロダクション、ステージング、ディベロップメントなどの環境によって異なる設定
- DBやMemcachedなどのバックエンドサービス
- AWSやTwitterなどのCredential
- デプロイ毎に変化させないといけない値
は全て環境変数として管理し、決してコードに入れてはいけない。(開発環境はdotenv
gemを使うと便利。)
ただし、例えば、config/routes.rb
などはここで言う"Config"には含めず、コードベースで管理する。プロダクション、ステージングなどの環境やデプロイ毎に変わるものは環境変数、変わらないものはコードベースに入れるという風に考えるとわかりやすい。
環境変数に入れる以外の方法として、config/database.yml
のようにversion管理対象外のファイルに書き出しておくという方法もあるが、あまり良くない。間違ってチェックインしてしまったり、1箇所で管理できなくなったり、言語/フレームワーク依存になってしまう。こういった設定を読み込む方式は、development
, test
, production
みたいなグループごとに設定するが、この仕組だけでは、もっと個別のデモ環境(例:joes-staging
)などを作りたいとなった時に破綻していってしまう。環境変数に入れ、各変数を個別に設定していくほうがよりスケールする。
IV. バックエンドサービス (Backing Services)
Treat backing services as attached resources
(c.f. 各Heroku Add-onの利用の仕方)
DB、SMTPサーバー、New Relic、Amazon S3、Twitter API、などのアプリケーションを動かすのに必要な補助的なサービスはリソースとして扱い、環境変数に入れたURLを元にアクセスする。
この時、ローカルで立てたDBやSMTPサーバーも同様にURLで扱い、Third Partyのサービスと区別しない。
例えば、2つのDBにつなぐ必要がある場合、それぞれを1つのリソースとして扱い、アタッチする。こうするとあるDBが壊れた場合、それのみ新しいものを作り、アタッチしなおせば、コード変更なしで対応することが出来る。
V. ビルド、リリース、ラン(Build, release, run)
Strictly separate build and run stages
(c.f. git push heroku
の時のログ)
サーバー(ローカル以外)ではビルド、リリース、ランのフェーズを明確に分ける必要がある。
- ビルド:コードレポジトリを実行可能な形式にする。サードパーティーのコードをフェッチして依存解決し、バイナリとアセットをコンパイルする。
- リリース:ビルドステージでできたビルドと、そのデプロイに関する設定を統合し、各サーバマシン上ですぐに実行できる状態にする。
- ラン:実際にサーバーの実行環境でアプリを起動する。これはプロセスのまとまりをそのリリースの状態で立ち上げることで実現される。
(ここの部分は、本家の記述が少し古い気がしていて、ランのフェーズはプロセスの立ち上げと、リクエストを古い方のプロセスから新しい方のプロセスを使うように切り替えることの2つに分かれている気がする。実際Herokuもあるタイミングからデプロイ時切り替えがすごくスムーズになったので記述時から変更されている可能性が高い。)
このようにステージを分けることで、ロールバック、クラッシュしたプロセスのリスタートなどが高速に行える状態をキープしたまま、ビルド時に重い複雑な処理を行うことが可能になる。
VI. プロセス (Processes)
Execute the app as one or more stateless processes
(c.f. heroku ps
, Heroku AppとAdd-onの住み分け)
アプリ本体はステートレスなプロセスにし、何かを共有してはいけない。ステートを持つ情報はDBなどのサービスに保存する。
メモリやファイルシステムは、キャッシュとしてのみ使い、あるファイルが将来にわたって存続し続けることを仮定してはならない。同一人物からのリクエストでも別のサーバー/プロセスが処理する事があるし、新しいデプロイによりローカルなステートが消えてしまうこともある。
assetはビルド時にコンパイルし、runtimeではやらない方が良い。
システムによっては状態をメモリに保存する“sticky sessions”というものがあるが、保存期限のあるデータを使う必要があるのなら、MemcachedやRedisを用いたほうが良い。
VII. ポートバインディング (Port binding)
Export services via port binding
(c.f. herokuのweb dyno, heroku/rails_serve_static_assets gemのREADME)
Webサーバーとなるようなコンテナアプリを考える。12factor appであるそのコンテナは、完全に自己完結していなければならず、実行時に何かと結合することでwebサーバーとして成り立つものであってはいけない。
この時、HTTPポートを公開し、そこに来たリクエストを処理する方式を採用すると良い。こうすると、開発環境では、http://localhost:3000/
などでアクセスできるし、本番環境ではルーティングレイヤがうまく公開されているポートとコンテナのポートをバインドすればよい(例えば80番のアクセスをコンテナの3000番につなげる)。
これは、例えばRubyではThin
などのWebサーバーのライブラリを組み込んでおくことで実現される。
こうすると、このwebサーバーコンテナが別のサービスのバックエンドサービスとなることも出来る。なお、HTTPであることは必須ではなく、例えばRedisは独自プロトコルを使っている。
VIII. 並行性 (Concurrency)
Scale out via the process model
(heroku ps scale
)
スレッドではなく、プロセスを用いてスケールアウトする。
スレッドだとメモリを共有する上、何がどのくらい動いているのかが把握しづらい。
これに対し、プロセスは何も共有しないし、どのタイプのプロセスがどのくらい動いているかを見ることで、メモリやCPUの資源をうまく分配する設計もしやすくなる。
もちろん、各プロセスが、その中でスレッドやasync/evented modelを用いて多重化をするのはOK。言っているのは、複数台でのスケールアウトを考える時に、プロセスを1つの単位とした構成にしないと難しいよいうこと。
なお、プロセスの管理(アウトプットストリーム、プロセスのクラッシュ、リスタートなどの管理)は、OSレベルのプロセス管理システムに任せるべきで、デーモンにしたりpdiを書き出したりしてはいけない。
IX. 廃棄容易性 (Disposability)
Maximize robustness with fast startup and graceful shutdown
(c.f. heroku restart
/heroku deploy
時の挙動)
それぞれのプロセスは、秒単位でパッと立ち上げられて、パッと捨てられる構成にしなければならない。
特にプロセスのスタートに要する時間は大切で、これが速いとスケールアップがあるプロセスを別のマシンに移動する事が容易になる。
SIGTERM
を受け取ってプロセスは終了する時は、新しいリクエストを受け付けるのをやめ、現在行っている処理は完了して、お行儀よく終了する必要がある。
これを実現するには、Webのリクエストは数秒で終わるような設計、ずっとポーリングしているようなリクエストがある場合、クライアント側で接続が失われたらユーザーに気付かれないように再接続要求をするように設計する必要がある。
Workerの場合は、Jobの処理はQueueシステムを用い、Lockする仕組みの場合はLockを返して死ぬようにする。各JobはTransactionなどで囲い、何度失敗しても問題がないようにしたり、冪等性のある設計にしたりしなければならない。
本来は、SIGTERM
ではない、ハードウェア障害などに起因する突然の終了にも頑健でなければならない。クライアントの接続が切れたらJobをQueueに戻すといったBeanstalkdのようなバックエンドを使うことが推奨される。
X. 開発/本番一致 (Dev/prod parity)
Keep development, staging, and production as similar as possible
開発環境と、production, stagingなどといったサーバー環境を出来る限り同じ環境にしよう。
昔は
- 時間:書いたコードが何日か経ってから製品版になる
- 人:開発する人とサーバーにデプロイする人は違う
- ツール:開発環境はSQLiteとMac、サーバーはMySQLとLinuxなどと異なる
といったことが普通だったが
今は
- 時間:数分から数時間で書いたコードが本番にデプロイされる
- 人:開発する人とデプロイする人が同じ
- ツール:ローカルとサーバーでほぼ同じ
となっているのが良い。
これは、homebrew、Chef/Puppet、Vagrantなどのツールの進化によって可能になった。なので、安心して継続的デプロイするために、開発用のローカル環境と本番用のサーバー環境はできるだけ一緒にしよう。
XI. ログ (Logs)
Treat logs as event streams
(c.f. heroku logs -t
, logging用add-onの使い方)
ログはファイル書き出しなどではなくイベントストリームとして扱う。
要するに、アプリはstdout
にログを吐き出すだけでよく、ログの保存先やルーティングは考えなくて良い。
こうするとローカルの開発環境ではログはターミナル上で見えるし、サーバー上ではLogplexやFluentdなどの別の独立したサービスがアウトプットストリームを管理する。これらのルーターは最終的にFileにログをストアしてもいいし、リアルタイムにターミナルにtailとして送り出しても良い。更にはログを検索できるようなサービスに送ることも可能になる。
XII. 管理プロセス (Admin processes)
Run admin/management tasks as one-off processes
(c.f. heroku run
)
管理やメンテナンス等の作業は一度だけ実行されるプロセスとして扱うべきである。具体例としては、rails c
やrails r
、rake db:migrate
などが挙げられる。
この時、一時的な管理プロセスをrails s
などの長く走っているプロセスと同じ環境で走らせてあげるのが大切。このために、Admin用のスクリプトなども同じコードベースに入れ、同期の問題が発生しない様にする。