kubell Creator's Note

ビジネスチャット「Chatwork」のエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

PHPのレジェンドシステムをEC2からKubernetesに移行する話 その5 〜PHPアプリケーションをコンテナ化しよう〜

こんにちは!SRE部のcw-ozakiです。

PHP Conference 2020 Re:bornでPHPでKubernetesを動かすにはどうするべきかという話をしてきました。 speakerdeck.com ただ、25分という短い枠でしたのでKubernetes初心者に向けてということで、だいぶ端折ったのでその5からはそのあたり補足もしていきたいと思います。

という訳で、今回はPHPアプリケーション用のコンテナイメージを作る話です!!

コンテナイメージを作るための前提

docs.docker.com

コンテナイメージを作るさいには上記のベストプラクティスを読みましょう。 このベストプラクティスに則っていれば問題のないイメージを作成することができます。

PHPでコンテナイメージを作るポイント

1. PHPベースイメージを利用する

hub.docker.com

PHPのベースイメージはDocker Hubで提供されているので特別な理由がない限りはこちらを使いましょう。

タグにてCLI、Apache、PHP-FPMのイメージが提供されています。 Webアプリケーションの場合はApacheかNGINX+PHP-FPMの構成になることが多いため、ここはどちらの構成で動かしたいかを決めて利用するイメージを決めてください。

alpine、buster、stretchといったLinuxディストリビューションもタグにて提供されています。 基本的には軽量なalpineでよいのですが、コマンドが/bin/busyboxで再実装されているため他のLinuxディストリビューションと挙動が変わるケースがあります。 Chatworkの場合ではPHPのcheckdnsrr関数を使っており、alpineでは常にtrueを返すケースがあったためstretchを使うようにしています。

github.com bugs.alpinelinux.org

各種ExtensionやPECLの追加方法など、このベースイメージの使い方はDocker HubのREADMEを参照するとわかりやすいのでここでは割愛します。

2. PHPのアプリケーションログを出力する

コンテナアプリケーションではログはSTDOUT/STDERRに出力したものをログとして扱われます。 そのため、定番のMonologとかの場合であれば下記のようにphp://stdout、もしくはphp://stderrにログを出力すればOKです。

<?php
 use Monolog\Logger;
 use Monolog\Handler\StreamHandler;

 $logger = new Logger('containerlog');
 $logger->pushHandler(new StreamHandler('php://stdout', Logger::DEBUG)); 
 $logger->info("output log");

PHP-FPMの場合はこれだけではログが出ないため、合わせてphp-fpm.confを変更します。

[global]
error_log = /proc/self/fd/2
catch_workers_output = yes

この2行を設定することで、PHPアプリケーションがSTDOUT/STDERRに出力したログを/proc/self/fd/2(つまりSTDERR)に出力できます。

また、合わせてlog_limitを調整すると出力するログの長さを、decorate_workers_outputをnoにするとWARNING: [pool www] child 2 said into stderr:といった接頭辞を消すことができます。 合わせて設定するといいでしょう。

3. PHPのエラーログを出力する

もし、PHPエラーをアプリケーションでキャッチしてログに出力していない場合、アプリケーションログの対応だけでは不完全です。 PHPのFatalエラーなどもSTDOUT/STDERRに出力できるようにphp.iniの設定を変更します。

 error_log = /dev/stderr
 log_errors = On
 log_errors_max_len = 16384

このとき注意点としてはPHPエラーは複数行出力されるため、コンテナのログとしては別物として扱われます。 ログの収集基盤としてfluent-bitやfluentdを利用して適切にログを結合するようにしましょう。

4. アプリケーションの設定を注入できるようにする

コンテナイメージを作る上で最も悩ましいのはアプリケーションの設定をどのように注入するかです。

PHPアプリケーションでは主に3つの方法があります。

  • 環境変数で実行時に注入する
  • 設定ファイルをマウントして実行時に注入する
  • コンテナ内に設定ファイルを混入して実行時に環境変数で選択する

これはどれがよいか。というよりはPHPアプリケーションをどのように運用するかで変わります。

環境変数で実行時に注入する

config.php

 <?php
 return [
     'param1' => getenv('APP_PARAM1') ?: die('invalid env from ' . __FILE__ . ' on ' . __LINE__),
     'param2' => getenv('APP_PARAM2') ?: die('invalid env from ' . __FILE__ . ' on ' . __LINE__),
 ];

環境変数で実行時に注入する場合は、上記のように設定ファイル中でgetenvを呼び出し環境変数を設定に注入します。

このときデフォルト値を使ってもかまいませんが、設定漏れを早期に検出するためにdieで死ぬようにした方がより安全です。 また、早期に発見するという意味で環境変数をコード中に持つことはおすすめしません。このconfig.phpを読み込めば環境変数の漏れに気づけるようにするべきです。

注意点としては、この方法では実行しないと環境変数をすべて設定できたか判明しません。 CI/CDのタイミングで環境変数が設定されているかチェックするとより早い段階で検出可能になるため、そこまで作り込むことをおすすめします。

設定ファイルをマウントして実行時に注入する

config.php

 <?php
 return [
     'param1' => 'ABC',
     'param2' => 'DEF',
 ];
$ docker run -v config.php:/app/dir/config.php php ...

設定ファイルをマウントして実行時に注入する場合は事前に設定ファイルを用意してマウントします。 例ではdocker runを使用していますが、KubernetesでもConfigMap/Secretを使うことで同様のことが可能です。

この方法では設定ファイルごと注入できるため、自由度が高く、融通がかなりききます。

その代わりにこの設定ファイルの正しさをどのように担保するのか難しい問題があります。 例えばKubernetesではYAML中でこのファイルを管理するのでPHPとしての正しさの検証をどうするか、複数環境で設定が漏れていないかどのように検証するのかなどです。

コンテナ内に設定ファイルを混入して実行時に環境変数で選択する

prod.php

 <?php
 return [
     'param1' => 'ABC',
     'param2' => 'DEF',
 ];

dev.php

 <?php
 return [
     'param1' => 'ABC',
     'param2' => 'DEF',
 ];

config.php

$config = requre(getenv('APP_STAGE') . '.php')

コンテナ内に設定ファイルを混入して実行時に環境変数で選択する場合は、上記のようにあらかじめ設定ファイルを用意しておいて実行時にどの設定を読むのか決定します。

実行時に注入しないため、コンテナを作るさいに設定のズレなどを検知しやすく扱いやすいです。 その代わりに設定の変更するたびにコンテナを作り直さなければいけないため、可搬性が下がることになります。

また、秘密情報をどのように管理するかについても考える必要があります。 この設定ファイルはGitにコミットされるものなので、秘密情報をそのままコミットするわけにはいきません。 なんらかの暗号化をかけて起動時に復号するか、Amazon KMSやHaschicorp Vaultといった秘密情報を管理できるところに保管して管理するか、そこだけ環境変数にして起動時に注入するかなどいくつかの選択肢があります。

どの方法を選択するのか

まず、明確に基準になるのがアプリケーションの環境が固定なのかどうかです。 例えばdev、test、stage、productionといったように環境が固定でこれ以上増えないというなら、コンテナ内に設定ファイルを混入して実行時に環境変数で選択するをおすすめします。 設定ファイルが1箇所に固まっている方が使い勝手はいいのでこの形がよいです。

もし、負荷試験用の環境やセキュリティ診断用の環境など頻繁にあげたい場合には環境変数で実行時に注入するか、設定ファイルをマウントして実行時に注入するかかを選択してください。 個人的には環境変数の方がトータルで構築しやすいかなとは思っていますが一長一短です。構築、運用しやすいのはどちらか考えて決定してください。

Chatworkでは環境を頻繁にあげたいという要求があり基本的には環境変数で注入できるようにしています。 ただ、一部の設定ファイルで環境変数化ができないところがあり、そういったところで設定を変えたいときは設定ファイルをマウントして実行時に注入する形です。 1

デプロイのタイミングで環境変数が全て設定されているかをチェックすることで、環境変数がズレる問題に対処しています。 将来的にはCIのタイミングでKubernetesのマニフェストをチェックし、環境変数の抜け漏れをチェックできればと考えていますがまだそこまでは構築できていません。

5. PHPアプリケーションのコードをコンテナに混入する

PHPのコンテナにPHPアプリケーションのコードをコンテナに混入するかどうかについてはKubernetes上でどのような構成を取るかで変わります。 このあたりは次回詳しく解説しますが、どのようにPHPアプリケーションコードをコンテナに混入するかは共通的な考え方になります。

  • 不要なコードは含めない
  • イメージレイヤーキャッシュを活用する

大きく分けるとこの2点に気を使ってください。

まず、テストコードや開発用の依存を外すことでコンテナを軽量化することができます。 .dockerignoreを使ってテストコードは除外する、composer installするときは--no-devをつけるなど忘れないようにしてください。 このコンテナサイズがデプロイの時間にダイレクトにかかるので、コンテナサイズはできる限り小さくしましょう。

次にイメージレイヤーキャッシュを活用しましょう。 例えば

COPY composer.json /appdir/composer.json
COPY composer.lock /appdir/composer.lock
RUN composer install --no-dev

COPY app /appdir/app

というようにcomposerのだけをCOPYすることで、時間のかかるcomposer installをcomposerの変更がない限りイメージレイヤーキャッシュで手順をスキップすることができます。 DockerのCOPYはtimestamp以外の変更が入ったときにCache Bustingが発生するため、composerとアプリケーションのコードをまとめてCOPYするのは悪手です。 また、テストコードだけ変更したさいにコンテナイメージを再作成しないように、.dockerignoreで外すというのもイメージレイヤーキャッシュを活用する上で重要になります。

まとめ

  • コンテナイメージを作る上で一番悩ましいのは注入性をどう確保するか!腕の見せ所!!
  • コンテナイメージは小さく、早く作ろう!!!
  • PHPアプリケーション特有なのはログぐらいでコンテナイメージを作るのは難しくない!!!

という訳で、PHPアプリケーションをコンテナ化するときに考えるポイントについて記載してみました。 次回はPHPのレジェンドシステムをEC2からKubernetesに移行する話で最重要になる、Kubernetes上にどのようにPHPアプリケーションを展開するかご紹介したいと思います。

www.wantedly.com ChatworkではSREメンバーを絶賛募集中です! このKubernetes化の話やChatworkの各種インフラや体制など興味があればカジュアルに話を聞きにきちゃってください。

PHPのレジェンドシステムをEC2からKubernetesに移行する話シリーズ

creators-note.chatwork.com creators-note.chatwork.com creators-note.chatwork.com creators-note.chatwork.com


  1. dev/test/stage/prodで設定の配列のキーが全部違うという辛いことがあり断念