設定と環境

The Twelve-Factor App(概要・Config・Backing Services・Dev/Prod Parity・ Processes・Port Binding・Admin Processes)を取得して読み、 理解した内容を自分の言葉で書いたもの。本文の引用はしていない。

インフラが**「自作しない・全部コード化する」なら、 こちらはそのコードの中で、何を設定として外に出すか**。

ROADMAP の照合先に 12-Factor App を挙げていたが、 出典として使っていなかったので、ここで実際に読んで書いた。

設定とは何か

デプロイごとに変わりうるものすべて。

データベースの資格情報
外部サービスのアクセスキー
デプロイごとのホスト名

逆に、デプロイで変わらないものは設定ではない。 Rails のルーティング定義や Spring のモジュール結線は デプロイ間で変わらないので、コードに置くのが正しい。

「設定ファイルに書いてあるか」ではなく、 **「デプロイごとに変わるか」**で分ける。

判定法

実用的な試金石が1つある。

このコードベースを、いま公開しても資格情報が漏れないか。

これが通らないなら、設定がコードから分離できていない。

公開する予定がなくても使える。 判定に必要なのは公開ではなく、 「もし公開したら」と問うことだけ。

秘密情報の管理の 「ソースコードに置かない」を、手を動かして確かめられる形にしたもの

「環境」という束にしない

ここが最も非自明な指摘。

development / test / production という名前付きの束で設定をまとめる方式は、 規模に耐えない。

デプロイ先が増えるたびに名前が増える。

staging
qa
joes-staging

そして設定の組み合わせが爆発し、デプロイが脆くなる。

代わりに環境変数に置く。理由は2つ。

言語にも OS にも依存しない
デプロイごとに独立して、細かく管理できる。コード変更も要らない

設定ファイルでも足りない

コード内の定数よりはましだが、リポジトリに入れない設定ファイルにも問題がある。

誤ってコミットされる
あちこちに散らばり、形式もばらばらになる
言語ごとのやり方になり、標準化されない

後ろのサービスは「付け外しできる資源」

バッキングサービスとは、アプリが通常の動作の一部としてネットワーク越しに使うサービス

データストア     MySQL、CouchDB
メッセージング   RabbitMQ、Beanstalkd
メール           Postfix、Postmark
キャッシュ       Memcached
第三者           New Relic、Amazon S3、Google Maps

要点は、コードが自前のものと第三者のものを区別しないこと。 どちらも、設定で指定された接続先つきの資源として扱う。

そうしておくと、ローカルの MySQL を Amazon RDS に差し替えるのが、 設定の変更だけで済む。 コードは変えない。

この性質が効く場面が具体的にある。

侵害されたデータベースを、復元したインスタンスに差し替える。

データの完全性とリストアで 「復元に決定的な外部依存を持っていないか」を確かめる、としたが、 資源として付け外しできる形になっていること自体が、その前提になっている。

開発と本番の差を詰める

歴史的に3つのギャップがある。

ギャップ中身
時間書いたコードが本番に出るまで、数日・数週間・数ヶ月かかる
開発者が書き、運用担当者がデプロイする
道具開発は Nginx / SQLite / macOS、本番は Apache / MySQL / Linux

目指す状態。

従来Twelve-Factor
デプロイの間隔時間
書く人とデプロイする人同じ
開発環境と本番環境乖離しているできるかぎり似せる

アダプタがあるから安全、ではない

ローカルだけ SQLite、本番は PostgreSQL。ローカルはメモリ内キャッシュ、本番は Memcached。

ライブラリや ORM が違いを吸収してくれるので安全に見える。そこが罠。

小さな非互換が出てきて、開発とステージングで動きテストも通ったコードが、本番で落ちる。

しかもこれはアプリケーションの生存期間を通じて積み上がるので、累積の費用が極めて高い。

対処は単純で、ローカルでも本番と同じバッキングサービスを動かす。 Docker のような道具があるので、本番に近い環境を手元に作る負担は以前より小さい。

継続的インテグレーションの 「試験環境をできるかぎり正確に本番に似せる」と同じことを言っている。

プロセスは状態を持たない

プロセスは状態を持たず、何も共有しない。 永続するデータはバッキングサービスへ置く。

ローカルのメモリとファイルシステムは、キャッシュとして使ってよい。 ただし1つのトランザクションの中の、短い間だけ。 ファイルを取得して加工し、結果をデータベースへ入れる、といった範囲。

次のリクエストでも残っている、とは考えない。

複数のプロセスが別々のリクエストを捌く
プロセスが1つでも、デプロイ・設定変更・基盤の移動で再起動すればすべて消える

スティッキーセッションは違反

利用者のセッションをプロセスのメモリに置き、 以降のリクエストを同じプロセスへ送るやり方は、この原則に反する。

置き場所は期限切れを持てるデータストア(Memcached、Redis)

セッション管理で **「セッションIDはサーバー側の索引にすぎない」**としたのは、 この構造を前提にしている。索引の先が特定のプロセスのメモリだと、そこが単一障害点になる。 キャッシュの比較表で Memcached / Redis の用途に「セッションストレージ」が挙がっているのも同じ話。

資産のコンパイルも実行時にやらない。 ビルド段階で済ませる。

ポートで公開する

アプリは完全に自己完結していて、実行環境に Web サーバーを差し込んでもらわない。

自分でポートに束縛して待ち受けることで、HTTP をサービスとして公開する。 Web サーバーのライブラリは、アプリ自身の依存関係の一部になる。

本番ではルーティング層が、公開ホスト名から ポートに束縛されたプロセスへ振り分ける。

そして、あるアプリが別のアプリのバッキングサービスになれる。 使う側の設定に、相手の URL を接続先として書くだけでよい。 前節の「付け外しできる資源」が、自前のサービスにもそのまま当てはまる。

管理タスクも同じ環境で

データベースのマイグレーション、コンソール(REPL)、一度きりのスクリプト。

要求は2つ。

通常の長寿命プロセスと同一の環境で走らせる
同じリリースの、同じコードベースと同じ設定に対して走らせる

管理用のコードは、アプリケーションのコードと一緒に出す。 理由は同期の問題を避けるためで、 古い管理スクリプトを、現在のデプロイに対してうっかり流すことが起きなくなる。

依存関係の隔離も揃える。Web プロセスが bundle exec thin start なら、 マイグレーションも bundle exec rake db:migrate にする。

この要求はスキーマ変更の運用と直結する。 マイグレーションを本番と違う環境から流せば、 ALGORITHM の指定も接続の設定も、確かめたものと違うものになりうる。

12項目のうち、どこが書けているか

12項目すべてに対応が付いた。 対応を取っておく。

項目このリポジトリでの位置
1Codebaseバージョン管理
2Dependencies依存関係 / ビルド
3Configこの文書
4Backing Servicesこの文書
5Build, Release, Runリリースとデプロイ
6Processes(ステートレス)この文書
7Port Bindingこの文書
8Concurrencyキャパシティ計画
9Disposability(速い起動と優雅な停止)部分的。 分散システムは優雅な劣化で、別のもの
10Dev/Prod Parityこの文書 / CI
11Logs可観測性
12Admin Processesこの文書 / スキーマ変更の運用

9番の区別は取り違えやすい。 優雅な停止(処理中のものを畳んでから終わる)と、 優雅な劣化(過負荷で質を落として返す)は別の話。

コスト管理について

書けていない。

キャパシティ計画資源の価格が最適化の目的関数に入るところまでは扱ったが、 費用をどう配賦し、どう抑えるかの実務は範囲外のまま。 一次情報の目処も立てていない。

関連