OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

BEAR.Sunday を GAE flex 環境で使う際の tips

このようなことを偉そうにもつぶやいてしまったので、この記事を書く運びとなりました。

改めましてこんばんわ @kalibora です。

弊社では BEAR.Sunday を Google App Engine の Flexible Environment(以下 GAE)上で動かしているのですが、 実はどーにもこーにもずっと解決出来ない問題がありました。

それはGAEのオートスケールでインスタンスが増える際に、下記のような Ray.DI 起因だと思われるエラーがたまに出る。 というものです。

Dummy\Exception(Argument 1 passed to Dummy\Foo::__construct() must be an instance of Dummy\Bar, integer given, called in /app/var/tmp/prod-hal-api-app/Dummy_Foo-.php on line 5)

一部ファイル名やパスを書き換えていますが、 Ray.DI に生成を任せているクラスの引数に、期待通りのクラスではなく integer が与えられた。という内容のエラーです。

なぜこの様な事が起きるのか詳細は理解できていないのですが、

(本番環境では同時に多数のアクセスが来て、最初のアクセスでオンデマンドにコンパイルしている最中に、また次のリクエストが来て・・・という具合でエラーになるのでしょうか?)

プロダクション | BEAR.Sundayデプロイ の項目には下記のような記述があります。

セットアップを行う際にvendor/bin/bear.compileスクリプトを使ってプロジェクトをウオームアップすることができます。

コンパイルスクリプトはDI/AOP用の動的に作成されるファイルやアノテーションなどの静的なキャッシュファイルを全て事前に作成します。

全てのクラスでインジェクションを行うのでランタイムでDIのエラーが出ることもありません。

また.envには一般にAPIキーやパスワードなどのクレデンシャル情報が含まれますが、内容は全てPHPファイルに取り込まれるのでコンパイル後に消去可能です。コンパイルはdeployをより高速で安全にします。

ということで、本番環境ではコンパイルすべきだということが分かりました。

では GAE 環境へのデプロイではどのタイミングでコンパイルすべきでしょうか?

GAE のデプロイ

そもそも GAE のデプロイの流れを理解していないといけないので少し内部の挙動を追いました。

gcloud app deploy コマンドを打つと、内部では下記のような処理が走るようです。

  1. app.yaml の skip_files なども加味しつつ ソースファイルを src.tgz に固めて Google Cloud Storage にアップロード
  2. Google Cloud Build でビルド(docker イメージを生成)
  3. デプロイ(生成された docker イメージを使って新バージョンを立ち上げ)
  4. プロモート(新バージョンにトラフィックを切り替え)

2.3. のタイミングでコンパイルできれば良さそうですが、 3. の際にカスタム処理を入れる方法は私の方では見つけられませんでした。

2. のタイミングでは composer install 処理が走るので、 composer の post-install-cmd でコンパイル処理を追加すれば、 生成される docker イメージに vendor ファイルと共にコンパイルされたファイル(tmp/{context}/...)も追加されるので都合が良さそうです。

ですが、ここでいくつか自分たちが遭遇した罠(というか勝手に自分でハマっただけですが)がありました。

罠1. Cloud Build 時に includes している環境変数は使われない

GAE の app.yaml では includes を使って env_variables を別のファイルに書くことも可能で、共通の環境変数はそこで定義していたのですが、 これは Cloud Build 時に使われませんでした。

これに関しては下記のソースコードを見ると分かります。

https://github.com/GoogleCloudPlatform/php-docker/blob/master/builder/gen-dockerfile/src/Builder/GenFilesCommand.php

ということで、 includes は使わないことにしました。

罠2. Doctrine2 が勝手にコンパイル時にDB接続しに行く罠

さて、これで解決かと思いきや Doctrine2 ユーザーへの罠もありました。

DI のコンパイル時に本番環境のデータベース(Cloud SQL)に接続しに行こうとして、 Cloud Build 環境では接続出来ずにエラーになる。

というものです。

実際にデプロイされたGAE環境ではもちろん本番データベースに接続できるものの、Cloud Build環境では接続できないようなので(普通接続する必要はないので問題ない)どうしたものかと悩みましたが、

結局この原因は Automatic platform version detection という接続先のデータベースを自動で判定する機能が原因だったので、 この機能をやめ、明示的に接続先のプラットフォームを指定するようにしました。

罠3. src の タイムスタンプがコンパイル時と docker イメージ作成時で異なる?

ここまでで、Cloud Build 時に生成される docker イメージにコンパイルされたファイルを含めることができました。

これで最初のデプロイ時でもオートスケールでインスタンスが増えた際でも、コンパイル済みのファイルが最初から用意されていることになります。 (最初のリクエスト時にコンパイルで待たされることはありません!エラーも出ない!)

と思いきや、もう1つ遭遇した罠がありました。

プロダクション | BEAR.Sundayコンテキスト の項目には下記のような注意書きがあります。

重要: プロダクションではディプロイ毎に$appを再生成する必要があります。

$app を再生成するには src/ ディレクトリのタイムスタンプを変更します。 BEAR.Sundayはそれを検知して $apptmp/{context}/di のDI/AOPファイルの再生成を行います。

これは逆に言えば

src/ ディレクトリのタイムスタンプが変わると $apptmp/{context}/di のDI/AOPファイルが再生成される。

ということになります。

せっかく作った tmp/{context}/di のファイルを消して再生成したくはないので、 コンパイル時の src/ のタイムスタンプと docker イメージ内に含まれる src/ のタイムスタンプが同じでないといけません。

ですが私が試したところ、どうも1秒くらいズレるようです。 この原因や根本的に合わせる方法は分かりませんでした。

ですので、 Bootstrap クラスを独自のものに差し替え、そのクラスでは src/ のタイムスタンプを直接見るのではなく、 タイムスタンプを保存したファイルがあれば、それを見るようにしました。 (コンパイル時に src/ のタイムスタンプをファイルに出力しておく)

これで GAE デプロイ後も /tmp/{context} ディレクトリが削除されることなく、使い続けることができるようになりました。

まとめ

  1. composer の post-install-cmdvendor/bin/bear.compile を呼ぶようにする
  2. Cloud Build 環境と実際にデプロイして動作する環境の差異に注意する
    • Cloud Build 時は includes した環境変数が使えない
    • データベースに接続できる/できない
  3. デプロイ後にDI関連のファイルが再生成されないようにする
    • 自分は src/ のタイムスタンプを固定したが、単純に tmp/{context}/.do_not_clear ファイルを配置するだけでよかったかもしれない(未検証)

はい。そんな感じで現場からは以上です。