nazolabo

フリーランスのWebエンジニアが近況や思ったことを発信しています。

ワンポイントTwelve-Factor App(8) : 並行性

この記事は、The Twelve-Factor Appを補足し、実際に現代的なWebアプリケーションで適用する場合の注意点などを紹介するシリーズです。下記の原文を読んだ上でのワンポイント解説になります。

12factor.net

概要

  • プロセスを並べることによってスケールアウトできるようにする
  • プロセス管理ツールによってプロセスを管理する

これは何を表しているか

「6. プロセス」で紹介した設計に即したプロセスであれば、そのままプロセスを違うコンピューター上で複数動作させても問題なく動作するはずです。これにより、プロセスを増やすだけでスケールアウトできるようになります。

また、アプリケーションが自前でデーモン化したりすると、それを共通の仕組みで管理することが困難になります。systemdのようなOSのプロセス管理ツールに任せる・任せられる構成にすることによって、アプリケーションはアプリケーションの動作のみに専念でき、どのような環境でも安全に再起動することが可能になります。

実際に運用する場合

autoscalingのような、自動でコンピューター数が増えるような構成にしたい場合、プロセスの数が不定になり、いつプロセスが増減するかもわからないので、このような構成にしておく必要があります。構成の詳細については他項での説明の通りになります。

通常のWebアプリケーションを通常通りに作成し、ステートレスかつシェアードナッシングな構成になっていれば、そのままプロセスを並べることで並列性を上げることが可能です。とはいえプロセス内の速度は書いたコードに依存しますので、並べられるからといって最適化のようなものが不要になるわけではありません。また、大規模になると「アプリケーションのプロセスは増えるが、それらがアクセスする単一のストレージ(特にDB)が限界になる」という現象が発生します。N+1のようなものには特に注意しましょう。

非Docker環境であれば、開発環境ではForemanのようなツール、本番ではsystemdなどでプロセスを管理することで、アプリケーション側からすると同一の方法でプロセスを管理することができます。Docker環境であれば、開発環境はdocker-compose、本番はECSやk8sといったものを使うことになると思います。

ECSやk8sのようなコンテナオーケストレーションツールでない本番環境の場合、シグナルによってプロセスの再起動が入る場合があります。この際、再起動時にリクエストが来てもエラーにならないように緩やかに再起動する、いわゆるGraceful Restartに対応できているか事前に確認しておきましょう。コンテナオーケストレーションツールの場合でも、リクエストを止める時間とプロセスがタイムアウトする時間を合わせないとユーザーからはエラーレスポンスに見える可能性があります。こちらも必ず確認しておきましょう。

ワンポイントTwelve-Factor App(7) : ポートバインディング

この記事は、The Twelve-Factor Appを補足し、実際に現代的なWebアプリケーションで適用する場合の注意点などを紹介するシリーズです。下記の原文を読んだ上でのワンポイント解説になります。

12factor.net

概要

  • プロセスはポート(TCP/UDP)でのみ通信する。他の通信手段は持たない。

これは何を表しているか

「6. プロセス」で作成したプロセスが他のプロセスと接続する場合は、ポート番号を公開し、TCPまたはUDPによって接続するようにします。シェアードナッシングですので、それ以外の通信手段は持ってはいけません。UNIXドメインソケットや、その他ファイルシステムを使うようなもの、過度に抽象化されたプロトコルなどはNGです。

ポートによる通信は多くのアーキテクチャでサポートされており、特にDocker環境ではポートでコンテナ間通信を行うことが原則となります。

また、ポートを使うことにより、サービスそのものが別のサービスと接続することも簡単になります。いわゆるマイクロサービス的なアプローチを行う場合でも構成が簡単になります。

実際に運用する場合

Dockerでアプリケーションを上げる場合こうなるのですが、1つの役割のアプリケーションは1つのプロセスから起動し、それらはポートで通信するようにしておくと良いです。

1つのアプリケーションが複数のポートを公開すること自体は問題ありません。例えばElasticsearchではHTTP用のポートとノード間通信のポートが別に用意されています。

php-fpmの場合もUNIXドメインソケットを使わずポートでlistenすることによってFastCGIで通信することができますので、手前にnginxなどを置いてポート通信することで要件を満たすことができます。

ワンポイントTwelve-Factor App(6) : プロセス

この記事は、The Twelve-Factor Appを補足し、実際に現代的なWebアプリケーションで適用する場合の注意点などを紹介するシリーズです。下記の原文を読んだ上でのワンポイント解説になります。

12factor.net

概要

  • アプリケーションは単一または複数のプロセスによって構成される
  • アプリケーションプロセスは状態を持ってはいけない・または状態をいつ破棄されても問題がないシステムになっている

これは何を表しているか

「Twelve-Factorのプロセスはステートレスかつシェアードナッシング」とありますが、ステートレスは文字通り状態を持たないことです。シェアードナッシングというのは、プロセスがお互いにリソースを共有しておらず、例えばどれか1つが死んでも他に影響を与えないような構造のことを指します。

プロセスはいつ勝手に再起動されても問題がないような構造にしておくことで、手軽に扱えるようになります。状態を持っていたり再起動に不安がある構造だと、障害発生時にオペレーションが煩雑になり、そもそもデプロイ方法も煩雑になることが予想されます。

ユーザーを特定するためのセッションのような仕組みは、MemcachedやRedisといった外部ストレージを利用することで永続化することができます。プロセス内メモリで保存していると、再起動した時点でセッションが全て初期化されてしまい、ユーザーに再ログインさせる必要などが発生します。

実際に運用する場合

Webアプリケーションにおいてプロセス内のメモリを永続的に信頼するようなコードはそもそも書くことが難しいので、標準的な構成であれば問題がないと思います。メモリを永続化する仕組み、例えばPHPAPCのようなものを使う場合は、それを完全に信頼せず、いつ初期化されていても問題がない設計にしておくべきです。

フレームワークの組み込みのキャッシュ機構で、キャッシュの保存先をローカルのメモリや外部ストレージから指定できるようなものがありますが、破棄されて問題がないキャッシュであればメモリに保存する選択肢も悪くはありません。ローカルのメモリにキャッシュする場合、高速ではあるもののプロセス毎にキャッシュが作られることになりますので、キャッシュの利用頻度や計算速度などを考慮して選択しましょう。

セッションに関しては前述の通りMemcachedやRedisといった外部ストレージに保存すべきです。こちらもフレームワークに設定方法があると思いますので、事前に確認しておきましょう。

ワンポイントTwelve-Factor App(5) : ビルド、リリース、実行

この記事は、The Twelve-Factor Appを補足し、実際に現代的なWebアプリケーションで適用する場合の注意点などを紹介するシリーズです。下記の原文を読んだ上でのワンポイント解説になります。

12factor.net

概要

  • ビルドはコンパイル(ビルド)・依存関係の解決など
  • リリースはビルド結果に設定を足す
  • 実行はプロセスの起動
  • これらを全て明確に分離する

これは何を表しているか

「1. コードベース」で定義されたコードは、単一のビルドと対応します。ビルドでは依存関係解決ツール(「2. 依存関係」参照)での依存関係解決などを行います。この時点では実行環境に固有の設定(「3. 設定」参照)は登場してはいけません。

ビルドに対し、「3. 設定」で定義された設定を組み合わせたものが、単一のリリースとなります。リリースは一意の名前を持っており、変更することができません。変更するには追記する必要があります。

リリースされたものは実行ステージで実行されます。実行ステージではサーバーの再起動やクラッシュ時に自動で再起動したりするようなことがあるので、ここでは余計なことはせずに最小限の動作のみにしておく必要があります。ビルドステージでのエラーは実環境に影響を与えないので捕捉が簡単ですが、実行ステージは既に外部サーバーで動いているのでエラーの捕捉が困難です。

これらの作業が確実に分離されていることによって、他の項の条件も満たしやすくなり、構造も明確になります。

実際に運用する場合

ビルドはCIサービスで行われます。ビルドになるべく複雑な内容を入れておくことで、ビルドに失敗するとCIがエラーを返してくれ、リリース前に問題を検知することができます。

テストもCIサービスのビルド(厳密にはビルドとリリースの間)ステージに入れておくことで、同様にCIがエラーを返してくれます。ビルドはコードベースに対して単一で、その成果物に対してテストをすることで、「テストが通ったのに本番で動かない」という状況を回避することができます。Dockerコンテナとしてビルドしたものに対して docker run でテストをすると、ビルドの成果物に影響を与えずテストだけ行うことができます。

リリース及び実行はデプロイ環境によって様々ですので、各環境に合わせて構築してください。

ワンポイントTwelve-Factor App(4) : バックエンドサービス

この記事は、The Twelve-Factor Appを補足し、実際に現代的なWebアプリケーションで適用する場合の注意点などを紹介するシリーズです。下記の原文を読んだ上でのワンポイント解説になります。

12factor.net

概要

  • バックエンドサービスは「アタッチされたリソース」であり、簡単にデタッチもできる
  • ローカルで動作するサービスと外部で提供されているサービスを区別せず、エンドポイントを切り替えると簡単に切り替わるようにする

これは何を表しているか

例えば、自分で用意したMySQLと、AWSのRDSは同じように扱えるようにすべきです。RDS専用のAPIを使う必要がある場合は、それはRDSでありMySQLではなくなります。

「3. 設定」で、設定は環境変数に分離すると解説しておりますが、ここで登場する「アタッチされたリソース」は、全て設定=環境変数で接続先が切り替えれるように設計することで、簡単にバックエンドサービスを切り替えることができます。コード内に含まれていると、サービスを変更したい場合などに変更することができなくなります。

実際に運用する場合

実運用で一番困るのがメールです。SMTP自体は汎用的なプロトコルではあるのですが、メールに関するサービスは独自APIが多く、それらに対応することが余儀なくされます。また、メールは本番以外では送信されてほしくないことが多く、環境によって大幅に挙動が変わることがあります。

開発環境では接続先をMailHog)のようなサービスにすることによってメールを飛ばないようにできます。APIが必要な部分は、APIキーが指定されていた場合のみ動作するような設計にしておくのが良いでしょう。

S3のようなオブジェクトストレージの取り扱いや、キャッシュストレージなどの扱いは、「10. 開発/本番一致」で解説します。いずれにせよ、設定=環境変数で各環境を切り替えれるようにしておくべきです。

ワンポイントTwelve-Factor App(3) : 設定

この記事は、The Twelve-Factor Appを補足し、実際に現代的なWebアプリケーションで適用する場合の注意点などを紹介するシリーズです。下記の原文を読んだ上でのワンポイント解説になります。

12factor.net

概要

  • 設定は環境変数のみで分岐する
  • 設定はコードに含めない

これは何を表しているか

「1. コードベース」では、「ステージング」や「本番」といった環境によってコードを分けたりしてはいけないとありましたが、コードの中に入っている設定は別です。

設定とは、例えばデータベースの接続先、APIのキーといったものから、稼働環境のURLや、環境によって違う全ての値が対象となります。

フレームワークによっては、それ自体に環境毎に設定ファイルを作る機能が用意されている場合がありますが、それらを使ってしまうと環境毎に設定ファイルが必要になってしまい、環境を作るたびにコミットが生まれます。また、修正にもコミットして修正が必要になってしまいます。このような環境の違いがコードそのものに含まれていると、違う環境で動かすことが困難になってしまいますので、環境要因によるものはコードに含めてはいけません。

コードに含めないとなるとどこか別の場所に保存しないといけませんが、環境変数を使用することで多くの環境で共通で設定を差し込むことができます。特にsystemdやDockerでは環境変数の注入が簡単になっておりますので、積極的に利用することができます。

実際に運用する場合

設定をコードに含めないことによって、設定だけを書き換えて好きな環境でアプリケーションを起動することができるようになります。パスワードなどの機密情報をGitHub等にpushしてしまうと、そこから機密情報が漏れるといったリスクもありますので、設定が切り出されているのはセキュリティ的な点からも好ましい構成になります。

Dockerを使っている場合は前述の通り簡単に環境変数を扱うことができますが、開発環境でDockerを使わない場合はdotenvのようなツールを使って.envファイルを読めるようにするか、direnvのようなツールで注入する必要があります。

どう環境変数化するかについては、例えば「本番環境では動くけどステージングでは動かない」みたいなものを用意したい場合は、「なぜ動かしたくないのか」という理由を考え、そこの切り分けは「本番かステージングか」ではなく「それを動かす理由」の環境変数で切り替えるのが良いでしょう。

また、nginxやphp-fpmといった既成のサーバーアプリケーションの場合、起動前に環境変数を読むのが難しいですが、これらも環境変数で設定を切り分けるべきです。これらの場合は envsubst コマンドや、類似のテンプレートエンジンで設定ファイルを事前にレンダリングしてから起動するという手法で解決することができます。

フロントエンドの場合、「ビルド時に必要な設定」と「実行時に必要な設定」が存在します。ビルドはCIなどで自動で行い、実行はWebサーバー上で行われますので、それぞれ必要なタイミングが変わります。特にフロントエンドは構造上ビルド後に環境変数を読むことは困難ですが、12Factor Appではビルド結果を環境毎に分けてはいけませんので、起動時に一手間かける必要があります。

ワンポイントTwelve-Factor App(2) : 依存関係

この記事は、The Twelve-Factor Appを補足し、実際に現代的なWebアプリケーションで適用する場合の注意点などを紹介するシリーズです。下記の原文を読んだ上でのワンポイント解説になります。

12factor.net

概要

  • 依存関係を依存関係解決ツールを使って解決する
  • 暗黙に依存関係が存在することは許容しない

これは何を表しているか

いわゆるbundlerやcomposerといった依存関係解決ツールを使えという話です。これにより「それらは外部のものである」というのが明確になります。

「1. コードベース」と同様、1つのアプリケーションは1つのコードベースであるべきです。ライブラリのようなものは別のコードベースになるので、それを解決するために依存関係解決ツールを使うべきです。

また、依存関係解決ツールを使う場合でも、いわゆるグローバルにインストールするようなことはせず、必ずアプリケーションのローカルにインストールされるようにし、事前にインストールする必要がないようにしておきましょう。

依存関係には、ネイティブのライブラリ(curlImageMagickなど)を要求する場合もありますが、これらもアプリケーションに同梱していない場合は、バージョンアップによって動かなくなってしまうということがあります。これらもアプリケーションに同梱しておくのが良いです。Railsでnokogiriに苦戦した方は多いと思います。

実際に運用する場合

bundlerやcomposerといった依存関係解決ツールに関しては普通に使っていれば問題ないと思います。現代において依存関係解決ツールが存在しない言語は希少だと思いますので、お使いの言語のものを適切に利用してください。ほとんどのメジャーなフレームワークを使えば、デフォルトでそれらが適用されると思います。

依存関係のインストールに必要なネイティブのライブラリに関しては、Dockerを使うことによってそれらも簡単に固定バージョンのものを用意することが可能です。開発初期はどの環境でも最新バージョンを入手するのが簡単なのですが、開発が数年になると当時のバージョンがOSのパッケージシステムで入らず動かなくなるといったことが多々存在します。

現実的にはDockerを常に使うとは限りませんし、特に開発中はシステムのライブラリをそのまま使うことが多いので、その場合は依存バージョンをREADMEなどに記載することによって暫定的に回避するなどの対策を取っておきましょう。

フロントエンドで、CDNで配布されているものを<link>タグや<script>タグでそのまま使うケースがありますが、どこで何が読まれているのかを把握することが困難になってしまいます。フロントエンドも必ずnpmやyarn等で管理し、本番環境では1つのファイルにpackしたものを使用するようにしましょう。