こんにちは、CyberOwl内定者バイトの佐藤です。
CyberOwlでは一部社内サイトをOpenStackで稼働しているオンプレサーバーで運用していました。しかし、オンプレサーバーのインフラ変更によりサーバー移転が必要になりました。そこでこの機会を利用して、インフラをオンプレサーバーからCyberOwlが現在メインで使用しているAWS ECSに変更することにしました。
今回の記事では、ECS移転作業で起きた体験談を書いていこうと思います。
今回のプロジェクトは以下の2点が大きく求められました。
今回のプロジェクトではオンプレサーバーのインフラ更新日が決まっていたため、ECSへの移転期日が存在しました。
そして移転後に正しく動いているのかの確認のために、期日よりも一ヶ月ほど早い移転を求められました。
すでにオンプレサーバーで正常に稼働できているため、書き換えを控えるのが無難です。
また、1つ目の速度にも関連しますが、古のフレームワークを扱っているので修正しようとするとドキュメントを読む工数がかかります。
加えて、これから古のフレームワークを触る可能性は低いので、当時のフレームワーク特有の機能の知見を得ることはそこまでメリットはありません。
また、移転サイトの中には重要なシステムが存在していたため、その中身はなるべく書き換えたくありませんでした。
以上の理由から、問題が起きた際は極力ソースコードはいじらずにインフラレイヤーで解決することが求められました。
今回携わった社内サイトはPHP5.5で実装されていました。
また、フレームワークは以下のもので実装されていました。
・laravel 4.2
・laravel 5.1
・CodeIgniter 2.1
今年の2月にlaravel 9がリリースされましたが、laravel 4.2は8年前、laravel 5.1は7年前にリリースされており、かなり昔のフレームワークということがわかります。
CodeIgniter2.1だと約10年前のフレームワークです。
実装を行ううえで、いくつか問題が生じたので一部抜粋して紹介したいと思います。
旧システムの中身をそのまま残したいため、cronはコンテナ内部で行なうように実装しました。
cronを実行すると、dotenvの環境変数はcronのプロセスに渡すことができます。(docker-composeのenvはできません)
しかし、laravel 4.1ではdotenvの読み込みを対応しておらず、env.phpというphpファイルで環境変数を渡します。そのためcronでdotenvを使用していないcronには以下のようにcron実行前に環境変数を渡す仕組みを利用して解決しました。
* * * * * APP_ENV=production /usr/local/bin/php /app/index.php schedule:run
古のフレームワークなので、ログの書き込みは基本fileで、標準出力にするためには追加設定が必要でした。特に苦労したのはcronのログ表示です。
よくあるのが以下の記述をしてコンテナプロセスを維持する方法です。
cron.d
* * * * * echo "Hello" > /dev/null 2>&1
script
cron && tail -f /dev/null
しかし、laravelのschedule機能を用いる場合どこにログが吐き出されるのかわかりませんでした。laravelのログ出力を標準出力に設定をしたのちに、/dev/stdoutを監視してもcronのログを標準出力することができませんでした。そのため、ファイルにログを書き出して監視することにしました。
最終的に以下の形で実装して動作させています。
cron.d
* * * * * /usr/local/bin/php /app/artisan schedule:run
30 14 * * * rm -rf /app/storage/logs/laravel.log && touch /app/storage/logs/laravel.log
script
mkdir -p /app/storage/logs && \
touch /app/storage/logs/laravel.log && \
crontab /etc/cron.d/sample && \
cron && tail -F /app/storage/logs/laravel.log
laravelの設定で/app/storage/logs/laravel.logにログを書き出させて、tailコマンドで監視させています。また、ファイルに書き出しすぎるとメモリが圧迫されるので1日1回ファイルの削除と作成をしています。
tailコマンドが監視中のファイル削除後にも正常に稼働するために以下の工夫をしています。
tail -F /app/storage/logs/laravel.log
tailコマンドのFオプションはログローテーション用のオプションです。ファイル名変更後、監視対象ファイルが新規作成されても対象ファイルへの監視が継続されます。
定期的にファイルの中身を空白にする実装も試したのですが、ファイルにログは書き込まれるものの、5分間に一度しかログとして表示されない挙動を起こしました。
AWS Application Load Balancer(以下ALB)とLaravelを組み合わせて使うと、Laravelが80番ポートでアクセスするALBをクライアントだと勘違いして、ソースコードの埋め込みをHTTPで行います。
そのため、HTTPSでアクセスした場合にもHTTPで参照するので、ブラウザ側でMixed Content Errorが発生します。
Mixed Content ErrorによってHTTPのファイルは安全でないと判断され、ロードされなくなります。
これを解決するには、ALBにアクセスしたクライアントのプロトコル情報などを取得する必要があります。正しいクライアント情報を取得するために、laravelではTrustedProxy というライブラリが存在します。このライブラリはlaravel 5.5から標準で搭載されていますが、今回のフレームワークでは自前でインストールする必要がありました。
また、GitHubのwiki の紹介とはインストール後の設定方法が少し異なっていたので注意が必要なので紹介します。
Service Providerへの登録方法が異なります。Providerの名前が異なるので以下のように記述します。
'providers' => array(
# other providers omitted
'Fideloper\Proxy\ProxyServiceProvider', // 'Fideloper\Proxy\TrustedProxyServiceProvider'ではない
);
そして、デフォルトでproxyが * に設定されているため、php artisan publish:vendorせず、ALBの問題は解決することができます。
こちらは概ねwikiの手順通りなのですが、proxyの指定方法が異なります。このバージョンだと * ではなく、** で指定するので注意してください。
<?php
return [
'proxies' => [
'**', // * だけにしない
],
// These are defaults already set in the config:
'headers' => [
(defined('Illuminate\Http\Request::HEADER_FORWARDED') ? Illuminate\Http\Request::HEADER_FORWARDED : 'forwarded') => null,
\Illuminate\Http\Request::HEADER_CLIENT_IP => 'X_FORWARDED_FOR',
\Illuminate\Http\Request::HEADER_CLIENT_HOST => null,
\Illuminate\Http\Request::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO',
\Illuminate\Http\Request::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT',
]
];
ECSでコンテナをデプロイする際にserver healthの設定は必須です。
当初はlaravelのルーティングを使って対応していました。しかし、サイトの一部でsession情報をテーブルに保存していたため、sessionテーブルにserver healthの情報が大量に溢れてしまいました。
そこで、nginxを使ってserver healthのレスポンスを返すことにしました。
server {
...
location /server-health {
default_type text/plain;
access_log off;
return 200 'OK';
}
...
}
一旦これで対応できたのですが、一部のサイトでBasic認証が必要になりました。
ソースコードの変更を控えるため、同じくnginxでBasic認証を実装したのですが、server healthのアクセスをするたびにBasic認証が必要になってしまいました。
そのため以下のようにserver healthの時のみ認証を外す処理を追加して対応しました。
server {
...
location /server-health {
default_type text/plain;
access_log off;
return 200 'OK';
satisfy any;
allow all;
}
auth_basic "Restricted";
auth_basic_user_file "/etc/nginx/.htpasswd";
...
}
1つのサイトでECSデプロイ完了後にページ読み込み速度がかなり遅くなってしまいました。(約5000msほど)
調査をしてみたところ、ソースコード上でN+1問題が発生していることがわかりました。旧サーバーで速度が遅くなかった理由は、旧サーバーとDBサーバーが同じデータセンターで、物理的に距離が近かったことが推測されます。
ECSのデプロイは異なるAZのサブネットを指定していたので、物理的に距離が離れていました。つまり距離による小さなアクセス遅延と、それが何回も繰り返された結果が大幅な速度低下に繋がっていました。
解決策として物理的に距離を近づけてあげました。
まずdbサーバーを配置するAZを1つに固定します。この時、RDSのサーバーレスを使用する場合はdbサーバーのAZを指定することができないためこの方法はできません。
また、今回のECSはBlue/Greenデプロイを採用しています。2つのデプロイサブネットを、固定したRDSのAZのサブネットに統一しました。
これによってデプロイをしてもDBサーバーと同じAZにいることになり物理的距離を常に近く保つことができます。
サブネットが1つになるためインフラの堅牢性は下がりますが、ソースコードを書き換えずに実現できる方法の1つとして紹介させていただきます。この変更を施すことで、読み込み速度を5000msから500msほどに改善することができました。
移転作業を通じて以下の2点を教訓として学ぶことができました。
すごい普通のことを言っていますが、今回の移転作業でとても身に染みました。
特にログ1つ標準出力に出すだけでもここまで苦労するとは思ってもいませんでした...。
また、laravelのログは出るが、phpのエラーログはでない場合も存在しました。本当に現状のログだけで大丈夫なのか疑問に持つ習慣をつけることができました。
本番環境でエラーが発生した際に、それがローカルでも再現可能なのかを確認することが、デバッグの方向性を決定する上で重要だと実感しました。
再現性があることで、そのバグがコードの問題なのか、AWSの問題なのか判断がつくからです。
また、ECSでエラーが起きているimageをpullして調査するなどデバッグの手法も学ぶことができてよかったです。
移転作業では複数のサイト移転に携わらせていただきましたが、AWSへのデプロイ手順はほとんど同じものでした。
そのため今後はCloud FormationやTerraformを使ってAWSインフラの作成自動化を行い、新規サービスのインフラ手順を簡略化したいと思います。
※2022年3月29日時点