Apache httpd + PHP + php-fpm + redis + MySQL で水平分散クラスタを組むのは、Web業界やゲーム業界のサーバー・サイド・エンジニアにとっては、超メジャーの構成なので、Bluemix の CloudFoundryコンテナで検証したメモです。 前回のBluemix CFコンテナのPHP と DBaaS MySQLを繋いでアプリを動かしてみた の続編です。(1) 残念ながら、この構成は、Bluemix上では、アンチ・パターン、すなわち、やってはいけない設計であることが判明しました。 対策案についても検証しましたが、Bluemixのロードバランサー(gorouter)には、セッション・アフォニティの機能が実装されているので、ストアは必須では無い感じですね。
目指す構成
今回は、次の構成を構築します。 普段使い慣れた Mac の開発環境を利用して、開発とテストを実施して、完成したコードをBluemix へプッシュする様にしていきます。 複数のPHPのサーバー間で、セッション・オブジェクトを共有するために、Redis をセッション・ストアーとして利用します。 次のGoogleトレンドのグラフの様に、以前は、memcached が主流でしたが、近年は Redis が主流となっている様で、RedisとPHP連携に関する資料は簡単に入手することができます。(6)〜(11)
それから、RDBMSには、MySQLを利用します。 Compose の Redisは 2ノード構成(12)、MySQLは 3ノード構成(13) となっています。
PHP ビルドパックは、Bluemix CFコンテナのPHP と DBaaS MySQLを繋いでアプリを動かしてみた の記事で利用した CloudFoundry をベースにしたカスタム・ビルドパックを利用します。 複数のPHP ビルドパックとロードバランサーの設定は、次のスクリーン・コピーの様に、インスタンス数を表すアイコンの + マークをクリックするだけで、処理能力を増強できます。 それぞれのインスタンスへ要求を分配するロードバランサー (gorouter) は、Bluemixに組み込まれており、追加の費用は発生しません。
Bluemix組み込みのロードバランサー (gorouter)は、応答でJSESSIONID Cookieを返すようにアプリを設定することで、常に同じCFランタイムへ要求を転送するセッション・アフィニティを提供します。(2) このため、セッション・ストアを設定しなくても、複数のウェブページ間でセッションを継続する事ができます。 セッション・ストアが無い場合、CFランタイムがクラッシュすると、他のCFランタイムと共有されていないために、セッション情報を失ってしまいます。 この課題に対応するために、Redis を設定します。
サンプル・アプリケーションのセットアップ
この構成で動作させるサンプル・アプリケーションは、GitHub https://github.com/takara9/php_sample_apl/tree/store-redis に置いてあります。また、アプリを動作させるまでの手順は README.md に書いてありますから、実際に実行して確認ができます。
$ git clone -b store-redis https://github.com/takara9/php_sample_apl
このリポジトリには、共有のセッション・ストアを利用しない版、CleaDB版、PostgreSQL版のブランチがありますので、参考にしてください。
MySQLとの接続
PHPランタイム(コンテナ)から MySQLサーバーへ接続する方法は、Bluemix CFコンテナのPHPとDBaaS MySQLを繋いでアプリを動かしてみた を参照してください。 このテスト用アプリでは、PHP7 PDO を利用して MySQL へ暗号化通信で接続しています。(14) また、PHPとMySQLの連携に関する資料は、PHPマニュアルを含め簡単に参照することができます。(15)〜(19)
Redisとの接続 (セッション・ストア)
PHPのセッション・ストアの設定方法には、下記の2つの方法があります。 本番稼働する際は、もちろん前者ですが、後者は試す場合にはとても便利な方法ですね。
- php.ini に設定する方法
- phpスクリプトから一時的に変更する方法
php.iniの設定する方法
下記の二つをビルドパックの https://github.com/cloudfoundry/php-buildpack/defaults/config/php/7.1.x/php.ini に設定します。 もちろん、CloudFoundry のビルドパックを修正できませんから、フォークしてから、自分のリポジトリで変更します。
session.save_handler = redis
auth 以下は置き換えてください。 ここで、Public な GitHubに登録すると、redis のユーザー・パスワードを公開してまうので、Private に登録する必要があります。
session.save_path = "tcp://sl-us-south-1-portal.5.dblayer.com:18638?auth=****************"
phpスクリプトから一時的に変更する方法
サンプルアプリの中で、末尾の方の以下2行を参照してください。 この ini_set を利用して、php.ini の内容をオーバーライドできます。
ini_set('session.save_handler', 'redis');
ini_set('session.save_path','tcp://sl-us-south-1-portal.5.dblayer.com:18638?auth=****************');
PHPマニュアル SessionHandlerクラス http://php.net/manual/ja/class.sessionhandler.php
セキュリティの課題
セッション・ストアとの通信を捉えることができれば、入力したパスワードを含めた機密情報など、すべての情報を横から取得できると言っても過言では無い。 そこで、CFランタイムとRedisの通信が、暗号化されているか確認しました。これはローカル環境に構築したビルドパックと同等な Apache httpd + php の環境に、tcpdumpを仕掛けてパケットをダンプしたものです。 次のスクリーンコピーの画面右側のACSIIダンプに注目してください。 パスワードの内容が丸見えです。
この点は、Compose の Redis マニュアルにも、記載されています。(3)
セキュリティに関する注意
デフォルトでは、Redisへのすべての接続は暗号化されていません。 これは、Redisのセキュリティページを引用するためです。
Redisは、信頼できる環境内の信頼できるクライアントによってアクセスされるように設計されています。 これは、通常、Redisインスタンスを直接インターネットに公開することや、信頼できないクライアントがRedis TCPポートまたはUNIXソケットに直接アクセスできる環境に直接公開することをお勧めしません。
Composeでは、暗号化されていないインターネットにRedisの展開を公開できますが、IPアドレスをホワイトリストに登録して公開を制限するか、サーバーと他のシステムとの間のトラフィックを暗号化するSSHトンネルを使用するオプションを利用できます Redisデータベース。 デフォルトでは、システムには暗号化されていないTCPポータルが設定されているので、接続を開始し、ホワイトリストを使ってアクセスを制御する方法を説明します。
Redis SSHトンネルでRedis for SSHトンネルを設定する方法について読むことができます。(4)
RedisのSSHトンネルを有効化するための設定は、Compose のコンソールにアクセスする事で対応できますが、2017年7月 現在、Compose コンソールと Bluemixの統合推進中であるため、Bluemix からオーダーしたRedisのSSHを有効にする画面が提供されていません。
現在は、Redisへの接続時のパスワードを平文テキスト(クリア・テキスト)で流してしまうため、Bluemix CFランタイムから、Redisをセッションストアとして利用することは、適切では無いと言えると思います。しかし、インターネット上へ流すと言っても、厳重な物理セキュリティが確保された Bluemix PaaS が存在する Bluemix Infrastructure (旧SoftLayer)の基盤に存在しているものなので、パケットを盗み見られる可能性は、皆無と言っても良いと思います。この可能性の問題と、適切な対策が講じられているかの問題は別ですから、適切とは言えないと思います。
Redis セキュリティ課題の解決策(案)
もし、PHPのセッション・ストアに保存するデータを暗号化できるとしたら、暗号化通信を実施した程度にリスクが緩和されることになります。ここで諦めるのは、ちょっと悔しいので、対策について検討してみました。 PHPには、セッション・ストアに対するアクセスをオーバーライドする機能(5)があります。 この機能のマニュアルには、ストアへの書き込みで AES-256で暗号化、読み込みで復号するサンプルコードがついています。 そこで、サンプル・アプリに組み込んでテストしました。
次のスクリーンコピーは、前述のユーザーIDとパスワードが確認できたのと同じ区間でのデータです。 AES-256の暗号化によって、内容を知ることができません。
プログラムの主な追加部分は、GitHub https://github.com/takara9/php_sample_apl/blob/master/htdocs/store_encrypter.php のコードです。 次のコードの最後の行で、session_start()をコールす前に、redisと接続した後に、session_set_save_handler()で暗号機能を拡張したセッションハンドラーを定義することで、対応することができます。
79
80 ini_set('session.save_handler', 'redis');
81 ini_set('session.save_path','tcp://sl-us-south-1-portal.5.dblayer.com:18638?auth=EYZQOJCWBKCZCGIU');
82
83
84 $key = 'secret_string';
85 $handler = new EncryptedSessionHandler($key);
86 session_set_save_handler($handler, true);
87
88 session_start();
まとめ
Bluemix の ロードバランサー(gorouter)には、クッキーのJSESSIONIDを追跡するセッション・アフィニティの機能が動作しており、複数のインスタンスを実行していても、ログイン時に確立したセッションは、常に同じインスタンスへ転送されるため、共通のストアを持たなくても、インスタンスが再起動するなどのイベントが無い限り、アプリケーションは、正常に動作することができます。
Redisをセッションストアにする場合、アプリが稼働するCFコンテナとRedisの間の通信は、暗号化されないため、そのままでは利用できません。そのため、PHPのセッションハンドラーに、暗号機能を追加して、セッション・オブジェクトの秘匿性を確保することはできます。 つまり、通信の暗号化ではなく、書き込みデータの暗号化によって、通信が暗号化されるのと同程度の秘匿性を保証することはできます。しかし、Redisサーバーと接続する際のパスワードが保護されないため、避けるべき構成と考えます。
今回、本当に実現できるか、実装レベルの実験して、資料を調べて解った事実でした。 Bluemix (Cloudant)と PHPアプリケーションは、親和性が高いと思うので、さらに地均ししていきたいと思います。
雑感1 MacでPHP実行環境構築
PHPのコードを書いて、動作を確認する環境としては、Bluemixのエディタを利用するよりも、Macを開発環境として利用するのが、やはり便利です。 MacOSには、最初からApache httpd と PHP がインストールされているので、これらを有効に使う様にします。または、Vagrant と Virtual Box 上の仮装サーバーに環境を作っても良いと思います。(20)〜(23)
雑感2
インターネットで検索して、資料を調べるなかで、NTTコミュニケーションの Cloud'n の資料が出てきました。(24) CloudFoundry を活かす利用法として、PHP,Ruby,Java といった開発言語に注目していることが伺えました。
参考資料
(1) Bluemix CFコンテナのPHPとDBaaS MySQLを繋いでアプリを動かしてみた http://qiita.com/MahoTakara/items/598fc3452be7dc712af4
(2) Cloud Foundry Documentation Session Affinity https://docs.cloudfoundry.org/concepts/http-routing.html#sessions
(3) Compose Documentation A note concerning security https://help.compose.com/docs/connecting-to-redis-2
(4) Compose Documentation Redis SSH Tunnels https://help.compose.com/docs/redis-ssh-tunnels
(5) PHPマニュアル SessionHandlerクラス http://php.net/manual/ja/class.sessionhandler.php
(6) PHPセッションをPhpRedisに保存する http://qiita.com/zurazurataicho/items/a71437412410955afd44
(7) UbuntuかつPHP7でphpredisを使えるようにする http://qiita.com/shinkuFencer/items/72f2617fb1db2134e340
(8) #9 Phpのsession Storeをredisにする http://rrreeeyyy.com/blog/2012/06/12/9-php-session-store-redis/
(9) DiditalOcean How to Set Up a Redis Server as a Session Handler for PHP on Ubuntu 16.04 https://www.digitalocean.com/community/tutorials/how-to-set-up-a-redis-server-as-a-session-handler-for-php-on-ubuntu-16-04
(10) PHP のセッション管理に Redis + phpredis 拡張を使う https://www.psi-net.co.jp/blog/?p=929
(11) PHP Documentation セッション処理 http://php.net/manual/ja/book.session.php
(12) Compose Configuration https://help.compose.com/docs/redis-on-compose#section-compose-configuration
(13) Compose MySQL Durability https://www.compose.com/databases/mysql
(14) Bluemix CFコンテナのPHPとDBaaS MySQLを繋いでアプリを動かしてみた http://qiita.com/MahoTakara/items/598fc3452be7dc712af4
(15) PHP Documentation MySQL ドライバおよびプラグイン http://php.net/manual/ja/set.mysqlinfo.php
(16) PHP Documentation MySQL 用 PHP ドライバの概要 どの API を使うか http://php.net/manual/ja/mysqlinfo.api.choosing.php
(17) PHP【保存版!!】PHPからMySQLに接続する方法etc【データベース】 http://qiita.com/icelandnono/items/0ae83baa779c293897a5
(18) w3schools.com PHP Connect to MySQL https://www.w3schools.com/php/php_mysql_connect.asp
(19) ポンクソフト PHPでMySQLを使う - PHP入門 http://ponk.jp/php/basic/php_mysql
(20) MacでApache+PHPを有効にする http://qiita.com/mazgi/items/42458878e75bf1abc1c4
(21) Macでローカルサーバを立ち上げる方法 http://qiita.com/shuntaro_tamura/items/bdabcb77926dc92617b1
(22) Apache2.4で「client denied by server configuration」というエラーが。。。 https://www.deep-deep.jp/blog_engineer/archives/4225
(23) MacにRedisをインストールする http://qiita.com/checkpoint/items/58b9b0193c0c46400eeb
(24) Cloud'n マニュアル http://www.cloudn-service.com/guide/manuals/html/paas-v2/deploy/index.html