nazolabo

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

ワンポイント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つ増えるために挙動が複雑になります。ちゃんと把握していれば怖くないので、どこで何が動いているのかを意識しながら書くようにしましょう。

WEB+DB PRESS Vol.95で特集記事を書きました

こちらではお久しぶりです。nazoです。

この度、現在所属しているUUUM株式会社のメンバーと、HTTPに関する特集記事をWEB+DB PRESS Vol.95に書きました。2016年10月22日発売です。

雑誌記事の執筆というのは人生初で、わずか8ページ程度かと思いきや執筆はハードでした。本一冊書くのとかどれだけ大変なんだよと思いましたが、いつかは挑戦したいものです。

私のパートはHTTPヘッダの詳細についてということで、このページ数で何を伝えるべきか考えたのですが、この特集に興味がある人がどのようなこと知ることができると嬉しいのかを考えて厳選したつもりです。

他の記事も興味深いものばかりですので、書店で見かけたらよろしくお願いします。

APCキャッシュを安全に扱うSafeApcを作った

https://github.com/nazo/safeapc

なにこれ?

APC(APCu)のユーザーキャッシュ(アプリから指定するキャッシュ。ソースコードのキャッシュではない)を「そこそこ安全に」扱うための簡単なラッパーです。

どう使うの?

packagistに登録してあるので普通にcomposerから入れてください。

基本的には普通にapc_fetchとかするのがSafeApc::get等に変わっただけです。

あと、リクエストの最初に、以下を入れる必要があります。

// キャッシュで使用するリクエスト開始時間を指定
SafeApc::setCacheStartTime($_SERVER['REQUEST_TIME']);

// キャッシュのバージョン番号を指定(この例では外部ファイルから)
SafeApc::setCacheVersionKey(file_get_contents('apc_version'));

これだと何がいいの?

これにより、APCキャッシュの問題2つが改善されます。

ttlの時間は常にtime()を参照する問題

APCttlを指定した場合、そのttlの判定は、常に「APC関数を呼び出した時点でのtime()の戻り値」となります。

通常、Webアプリでは、処理中の内部時間がずれてしまうと内部時間の一貫性がなくなるため、リクエスト開始時刻をそのリクエスト中の時間とし、リクエスト中の処理が1秒以上経過しても、全ての更新時刻などはリクエスト開始時刻で扱うことが多いですが、APCttlはそこは当然無視されるので、ttlを厳密に扱おうとした場合に、微妙にずれる、ということが発生します。

例えば、以下のような感じです。

// このキャッシュは1秒だけ持たせよう
apc_store('key', 'value', 1);

// 適当な処理
sleep(3);

// 同一リクエスト中だしさっきのキャッシュ残ってるよね?→ない!
apc_fetch('key');

これを回避するために、ttlは86400で固定し、PHP側で生存時間を判定するようにしています。なんで86400固定なのかは後述。

全キャッシュクリアは高負荷だよ問題

APCの全キャッシュクリアは、同プロセスのAPC処理を全てブロックします。このため、確実に他の処理に悪影響が発生します。

また、PHP5.3以前&apache2 mod_phpでは、このブロックされている途中にmax_execution_time超え等で強制終了されると、ロックを開放せずに全プロセスを巻き込んで死ぬという挙動があります。(このへんの話

どちらにしても、全キャッシュクリアは危険なので、キャッシュクリアは2つの方法を組み合わせて行っています。というか、行ってください。

  • 無期限設定でも、必ず86400秒(1日)経過で自動で消える。(変更できます)
    • この時、86400秒で一斉に消えると高負荷になるので、ランダムで+0〜30秒増やしています。これによりキャッシュクリアのタイミングが、よりバラつくようになります。
    • デフォルトが86400秒なのには特に意味はありませんが、なんとなく1日くらい残ってくれれば大丈夫かなーという感じで指定しています。
  • キャッシュのバージョンを変えることによって、そもそも前のキャッシュキーにアクセスしないようにしている。
    • 内部的にはキャッシュのキーは「バージョン#本来のキー」という文字列になります。そのため、バージョンを変えると、古いキーには絶対にアクセスしません。
    • これはこれで、古いキーが1日残り続ける、という問題はあります。
    • 従って、「SafeApc::setCacheVersionKeyに与える引数を変える=キャッシュクリア」となります。
    • ついでに言うと、通常APCのキャッシュクリアは、Webサーバーのプロセス上で行わないといけないので、コマンドラインからの実行には一手間必要でしたが、この方法だとただバージョン番号を変えるだけなので、簡単にキャッシュクリアをコマンドライン上で行うことができます。

必要な設定

  • apc.user_ttlは何でもいいので必ず設定してください。0だと、メモリが溢れた場合に、apc_storettlを無視して全キャッシュクリアを行うという問題があります。
  • 削除を行わない方針のため、比較的メモリ使用量が増えやすいです。apc.shm_sizeは、「キャッシュの最大量 * 想定される1日のキャッシュクリア回数」くらいのサイズを指定しておきましょう。
  • 内部的にはapc_fetch等を呼んでいるので、APCuで使う場合はAPC互換モードでビルドしてください。(Enable full APC compatibility [yes] : yes)

まとめ

ここまで書いておいてアレですが、APCキャッシュは外部との通信がない分高速ですが、基本的にmemcachedかredisでいいと思います。APCだと複数サーバあるとその台数分キャッシュ生成処理が走るし。状況次第ですね。

某所で散々検証した内容なので、理論的には問題ないはずですが、このコード自体は全く使っていないしろくにテストしていないので、バグがあれば教えてください。