nazolabo

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

ワンポイントTwelve-Factor App(11) : ログ

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

12factor.net

概要

  • ログは全て標準出力に出す
  • (通常は)1行を1イベントとする
  • 標準出力に出したログはログルーターが適切に取り扱う

これは何を表しているか

ほとんどのWebアプリケーションフレームワークは、ログはデフォルトではファイルに出力するようになっています。しかし、「9. 廃棄容易性」といった点から、ファイルに出力してしまった場合は廃棄された場合の取り扱いが困難になってしまいます。

標準出力を唯一のログ出力先にしておくことで、そのアプリケーションがどのように動いているかに関わらず、共通でログを取り扱うことができます。標準出力に出したログはfluentdなどのログルーター(ログコレクター)で扱うことで、アプリケーションとは別にログの取り扱い方を決めることができます。特にDockerでは標準出力に出したログをDockerロギングドライバ経由で柔軟に取り扱うことができます。

実際に運用する場合

Webに限らずアプリケーションでログの取り扱いは最重要です。特に運用段階に入ると、ログのわかりやすさで障害時の対応の速さが大きく変わります。

ログをファイルに出力してしまうと、環境によって取り扱い難易度が変わってしまいます。fluendで取るという理由で一時出力先をファイルにするという場合もなくはないですが、基本的には標準出力に出しておくことでどの環境でも共通で取り扱うことができます。特にDocker環境では標準出力に出しておかないとDocker側のロギングドライバで取り扱うことができないので、必ず標準出力に出すようにしましょう。

設定で標準出力にログを出す仕組みがないログシステムでも、 /dev/stdout に送ることで標準出力に出すことが可能です。

開発環境ではターミナルにログが出ていれば通常は十分だと思いますので、標準出力をそのままターミナルに流しておきましょう。

1行1イベントとなると、CSV/TSVのような形式だとそのログの各項目が何かを判定することができなくなってしまいます。ヘッダ情報を足すにはLTSVでもいいですが、階層構造などを考えるとJSON形式で出しておくことで様々なプラットフォームでの取り扱いが簡単になります。nginxなどのログも含め、基本的にはJSON形式で出力するのが良いでしょう。

本項とは直接関係ありませんが、セキュリティの観点からパスワードなどをログに出さないようにする・問い合わせから追跡しやすいようにユーザーIDやエラーのスタックトーレスをログに出しておく、などの細かい点を抑えておくと良いです。

ワンポイントTwelve-Factor App(10) : 開発/本番一致

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

12factor.net

概要

  • 開発環境と本番環境の差異を最小限にする
  • 似たようなバックエンドを吸収するアダプタのようなものは使わない・使ってもバックエンドは開発と本番で同種にする

これは何を表しているか

「手元では動いたが本番で動かない」というのは開発あるあるです。これを防ぐには、開発環境と本番環境がほぼ同一になっているのがベストです。

環境の差異というのは、大きく「コードそのものの差異」と「周辺環境の差異」の2つに分かれます。

「コードそのものの差異」というのは、コードベースが本番と開発で大きく離れてしまうということです。リリースのできない大きな開発が続くとこのような状態になります。小さなコード量を短時間で少しずつリリースし、なるべく本番と開発のコードの量の差を減らすことが重要です。リリースの単位が大きくなると、影響範囲も大きくなり、障害の検知の難易度が上がります。細かくリリースすることで細かなフィードバックを得ることができます。

「周辺環境の差異」は、例えば「キャッシュストレージに開発ではメモリを使うが本番ではRedisを使う」「ファイルストレージに開発ではローカルファイルシステムを使うが本番ではAWS S3を使う」といったことが考えられます。ActiveRecordのようなものを使うと、DBすら「開発ではSQLite、本番ではMySQL」のような構成が可能になります(現実にはかなり厳しいですが)。しかし、これらは似ているようで違うものなので、詳細な挙動の差異により「本番だけ動かない」といったケースが発生する恐れがあります。「似ているから」「アダプタが吸収してくれるから」といって開発と本番で違うバックエンドを使うことは避けたほうがよく、避けることにより「本番で動かない」可能性を最小限にすることができます。

「3. 設定」でも出てきた通り、環境毎に設定ファイルを作ってしまうと環境の差異が大きくなります。開発環境ではデバッグ用ツールなどで多少の差異が生まれることもありますが、極力どの環境もほぼ同じの構成にするようにしましょう。

実際に運用する場合

リリースのサイクルを早めるには、開発手法の見直しが必要です。大きな機能の開発の場合でも、例えば他に影響を与えないコードは先にリリースしてしまう・バックエンドの処理だけ先にリリースしてしまうという方法でリリースの粒度を小さくすることができます。

環境の差異に関しては、開発環境ではDockerを使うことで本番とほぼ同等のバックエンドサービスを用意することができます。S3のようなマネージドサービスでも最近はminioのような互換システムがありますので(金とセキュリティが問題なければ開発時もS3を使うのが一番良いです)、通常の開発時ではdocker-composeで必要なバックエンドサービスを一発でローカルで起動できる状態にすることは容易になったと思います。

開発環境を本番に近づけるにはDocker上(あるいはもっと厳密な仮想環境)で動かすのが良いですが、実際の開発時にアプリケーションがDocker上で動くと処理速度などの点で不利なことが多いので使わないこともあります。その場合でも、例えば非開発者がローカルで動作確認したいという場合を考慮して、 docker-compose up の1コマンドだけで確実に上がる環境を用意しておくと良いです。その環境が用意できると本番でDockerで運用するのも簡単になります。

開発者が全Docker環境を使うかどうかは好みの問題があると思いますが、私は前述の通りDockerでアプリを動かすのは速度面などで不利になることが多いので、「MySQLなどのミドルウェアのみDockerで用意し、PHPRuby、Node.jsといった環境はホスト側で直接起動する」という構成が一番良いと思っており、2バージョンで起動できる環境を用意するようにしています。またこの構成だとgit pre-commit hookなども使いやすいところが良いと思います。

Dockerは仮想化ツールとは違いますので、カーネルのバージョンなどで埋められない差異が多少存在することは頭に入れておきましょう。しかし、一般的な開発でそこが問題になることはあまりありません。

ワンポイントTwelve-Factor App(9) : 廃棄容易性

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

12factor.net

概要

  • プロセスはいつでも廃棄されて良いものとする
  • ワーカープロセスなどでは、再入可能な設計にしておく

これは何を表しているか

普通のプロセスに関しては「6. プロセス」や「8. 並行性」にかかれている内容と同様です。並行性が高ければ廃棄容易性も自動的に高いものになります。

いやゆるキューから起動する遅延実行処理のような仕組みがある場合、それらの処理が途中で死んでも再実行できるか、同じ処理が複数回走っても正常な結果になるかといったことに気をつけて設計する’必要があります。

実際に運用する場合

いわゆるautoscalingみたいなことをする場合、現在動作しているアプリケーション環境は突然死んだりします。そのためローカルストレージは突然破棄されたりします。

どうしてもローカルストレージを使わないといけないということもなくはないですが、ファイルは基本的にS3などのオブジェクトストレージに保存するようにし、ログは前述の通り標準出力からDockerのロギングドライバ等を経由し外部ストレージに保存するというようにし、ローカルストレージに依存しない環境にしましょう。NFSなどで永続ストレージをマウントする方法もありますが、取り扱いが難しいので最終手段にしておくべきです。

Webアプリケーションのユーザーからのリクエストで時間のかかる処理を書いてしまうとユーザーを待たせてしまうことになってしまうため、時間のかかる処理はキューに逃がして遅延実行させるということがよくあります。例えば決済処理を遅延処理した際に途中で突然死して再実行した場合に二重決済になるといったことは障害のよくある例です。メールが2回送信されてしまうといったこともよくあります。遅延実行時に限ったことではないですが、重要な処理は再実行が可能になっているかといったことは必ず確認しておきましょう。

ワンポイント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 でテストをすると、ビルドの成果物に影響を与えずテストだけ行うことができます。

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