nazolabo

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

ワンポイント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したものを使用するようにしましょう。

ワンポイントTwelve-Factor App(1) : コードベース

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

12factor.net

概要

  • 単一のアプリケーション(システム)は単一のコードベースのみで成立する。
  • 単一のコードベースは単一のバージョン管理システムで変更を追跡している。
  • 環境によってコードを切り替えるようなことをしてはいけない。
  • 同一のコードを共有する複数のアプリケーションがある場合、それは依存関係管理ツールで管理する。

これは何を表しているか

「このアプリケーションはこのコードベースのみである」と明示しておくことによって、システムが明快になります。

これに違反しているというのはどういうことでしょうか。まず、「1アプリケーションが複数のコードベースで構成されている」というケースはどうなるでしょうか。それらのコードベースがそれぞれアプリケーションとして動作するものであれば、それは分散システム、現代的に言うのであればマイクロサービスみたいなものになります。その場合、各アプリケーションそれぞれが12Factor Appに即しているのであれば問題ありません。そうでない場合、システムが絡み合っているような状態になり、どこまでがどのアプリケーションなのかを把握するのが困難になります。

「複数のアプリケーションが1つのコードベース」というのは、ライブラリのようなものの場合になります。このような場合は、「どこまでがライブラリでどこまでがアプリケーションなのか」を把握するのが困難になるので、依存関係解決ツールを使用して明確に分離すべきです。

また、1つのアプリケーションを「ステージング」「本番」といった感じで複数の環境に配置する場合でも、必ず同一のコードをそれぞれの環境に展開するようにすべきです。「ステージングバージョン」「本番バージョン」というようなコードを作るべきではありません。これは「3. 設定」や「10. 開発/本番一致」などで重要になってきます。

実際に運用する場合

ここは書いてある通りですが、1アプリケーション=1リポジトリとし、外部ライブラリは依存関係解決ツールを利用しましょう。

前述の通り、「ステージング」や「本番」みたいな環境ごとで微妙に違うコードベース(ブランチなど)を用意するのもNGです。これらは設定で分離されるべきです。「3. 設定」で解説します。単一のコードベースでそれぞれの環境にデプロイすることで、環境間の

例外のケースとしては、OpenAPIやgRPCといった複数のアプリケーションにまたがる定義ファイルがあると思います。ただしこれらはそもそもプログラムではないので、いいのでないかという気はします。そこから生成されたコードはアプリケーションと同一リポジトリに格納しましょう。

Nuxt.jsにおけるサーバーサイドレンダリングの挙動とライフサイクル

Nuxt.jsというかSSR関係を触る際にまず疑問になるのは「どこまでがサーバーサイドレンダリング (SSR) でどこからがクライアントサイドレンダリング (CSR) なの?」ということだと思います。

基本

簡単に言えば、ブラウザでURL直接指定で開けばSSR、それ以降はCSR、ということになります。

当たり前ですが、サーバーサイドレンダリングはサーバーでの実行なので、windowオブジェクトも存在しないし、そこから外部サーバー(APIサーバーなど)にアクセスする際はそこからのアクセスになります。そのままではブラウザのcookieも利用できません。

axios-moduleは何をしているのか

上記の通り、SSRになった場合とCSRになった場合は通信経路も変わるしcookieの取扱も変わります。

これらを真面目に対応すると結構大変なのですが、 axios-module を使うと大体のことを解決してくれます。

axios-moduleは大きく以下のようなことを行ってくれます。

  • SSRの場合とCSRの場合でエンドポイントを切り替える
  • クライアントサイドのHTTPヘッダ(cookieなど)をサーバーにそのまま透過的に送信する

他にもいろいろありますが、重要なのはこの2点で、これにより通信時のレンダリング場所がサーバーかクライアントかを意識せずにAPIアクセス処理を書くことができるようになります。Nuxtで開発する場合は必須と言っても過言ではないでしょう。

なお、複数のエンドポイントを指定したりはできないので、API通信先は1箇所に固定されることになります。複数のエンドポイントが発生しそうな場合はクライアント(Nuxt)だけで解決せず、BFF (Backends For Frontends) パターンを用いて処理するのが良いでしょう。

ライフサイクル

Nuxtを使うとコンポーネントにメソッドが増えたり、ミドルウェアプラグインのような仕組みが追加されます。これらがSSRの場合とCSRの場合でどのように呼ばれるかは使ってみないとわからないところです。

そこで、サンプルプロジェクト を用意し、調査してみました。

初回アクセス (SSR)

そこからの遷移 (CSR)

つまりどういうことか

Nuxtでアプリを作る際は、SSRでもCSRでも問題なく表示できるように作る必要があります。また、クローラーなどからのアクセスは常にSSRになるため、クローラーなどに拾ってほしい情報はSSRの段階で確定させる必要があります。

一方で、windowオブジェクトにアクセスするもの、例えばlocalStorageを使うようなものはクライアントサイドのみでしか実行することはできません。 mounted 以外で書く場合は process.server 変数などを使って分岐させる必要があります。

特に注意したいのが created で( beforeCreate も)、SSR時とCSR時の両方で呼ばれます。普通に書くと二重に処理してしまうことになるので、意識して書かないと意図しない挙動になる可能性があります。

まとめ

このあたりの挙動は分散して書かれてはいるものの、1ページにまとまっているものが見つからなかったため、今回用意してみました。

SSRは便利ではあるものの、どうしても層が1つ増えるために挙動が複雑になります。ちゃんと把握していれば怖くないので、どこで何が動いているのかを意識しながら書くようにしましょう。