entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

Herokuから ECSに 移行した

こんにちは、インフラ部の id:sue445 です。私事ですが先日GCPの Professional Cloud Architect を取得しました。

そういうわけで今日はGCPではなくAWSの話をします。

tl;dr;

WEBマンガ総選挙 はURL以外全部変わりました。

アプリケーションコードも多少修正したのですが細かい設定変更 *1 やALBやECSで使うヘルスチェック用のエンドポイントを生やしたくらいで、それ以外は変わっていません。

劇的ビフォーアフター

Before (〜2020) After (2021〜)
アプリケーションコード PHP 7 + CodeIgniter 3 PHP 7 + CodeIgniter 3
AP Heroku (usリージョン) ECS + Fargate (東京リージョン)
DB RDS (MySQL 5.7, バージニアリージョン) RDS (MySQL 8.0, 東京リージョン)
Deployment CircleCI CodePipeline + CodeBuild + CodeDeploy
CDN Edge *2(Herokuアドオン) CloudFront
DNS ValueDomain Route53
SSL証明書 Let's Encrypt(証明書は1年に1回手動更新) ACM(証明書は自動更新)
APM NewRelic(Herokuアドオン) Datadog APM

構成

よくあるAWSのアプリの構成です

CloudfrontのoriginがECSなのは変わってるかもしれないですが、移行前だとCDNのoriginがHerokuになっていて移行前後でアプリケーション構成を極力変えたくなかったのでこのような構成をとりました。

移行のモチベーション

移行前のWEBマンガ総選挙はHerokuだったのですが、HerokuからAWSに移行することでパフォーマンス向上と費用圧縮が見込めるのが大きな理由でした。

パフォーマンス向上

Heroku EnterpriseならTokyoリージョンが使えるのですがピクシブではEnterpriseを契約していません。*3

普通のHerokuはusリージョンかeuリージョンのみなのでどちらも日本からは遠いです。

日本国内からアクセスする場合、サーバが海外にあるよりも日本にある方が絶対に早いです。いくらインターネットが進歩しても物理的な近さが圧倒的正義です。

AWSやGCPであれば東京リージョンでの構築が容易です。

また、VPC内にECSとRDSを置くことでアプリケーションからのDB接続がプライベート通信になってセキュアになりつつパフォーマンスが上がるという目論見もありました。

コスト圧縮

HerokuのDynoはそこまでコストになっていなかったのですが、HerokuのCDNアドオンのEdge*4が転送量による従量課金ではなくプランによる段階的な費用だったのでどうしても無駄がありました。(WEBマンガ総選挙の期間が終了してほとんど転送量がないのにアドオン分の費用が発生して割高)

試算した結果、移行することによりAWSの費用が3〜5割前後削減できる見込みがあったのもAWS移行の理由の1つです。

アーキテクチャの採択理由

今回の要件だとAWSとGCPのどちらでもよかったのですが、運用チームがECS + Fargateでのアプリケーション運用に慣れていたのでAWSのECS + Fargate構成を採用しました。

余談ですが同じ構成をGCPで作るとしたらAppEngine(Flexible)かCloud Runを採用したと思います。

やったこと

AWS移行に関して色々やったのでいくつかトピックを挙げます。

移行に伴う開発期間はトータルで1ヶ月半くらいでした。

1. DB作成

移行前はHerokuとの兼ね合いでバージニアリージョンにRDSを作っていましたが、移行後はそのしばりがないので東京リージョンに作りました。

余談ですが手元からmysqlでつないだ時のレイテンシもバージニアと東京とでは全然違いました。

移行前だとHerokuからRDSへの通信がインターネット経由だったのが、移行後だとVPC内にECSとRDSを置けるようになったのでアプリとDB間がインターネットを経由しない通信になりました。

これによりセキュリティとパフォーマンスが両方向上しました。

2. MySQL 5.7 -> 8.0

当初は移行前と同じMySQL 5.7を使う予定だったのですが、5.7のEOLが意外に近かったのに気づきました。

オラクルのドキュメント によるとExtended Support Endはそれぞれ

  • MySQL 5.7: 2023年10月(再来年)
  • MySQL 8.0: 2026年4月

となっています。

先日MySQL 5.6がEOLになったのですがその時にAWSが「RDSで使ってるMySQL 5.6をアプデしなかったら強制アプデしてMySQL 5.7にするよ」というお知らせを出しました。

WEBマンガ総選挙の開催中にこの強制アプデが走ると再起動中にサービスダウンしてしまうので絶対に避けないといけません。

サービスインした後よりもサービスインする前の方がバージョンアップするリスクが少ないので移行前にMySQL 8.0にバージョンアップすることを決めました。

WEBマンガ総選挙はテーブル数も少なく規模も小さいのでエイってやってバーン!って感じでバージョンアップしました。

MySQL 8.0でハマったこと

MySQL 8.0化でいくつかハマったので書きます。

MySQL 8.0からデフォルトの認証がcaching_sha2_passwordになった

ローカルの docker-compose.yml でMySQLを8.0に上げた時に下記のようなエラーが出ました。

A PHP Error was encountered
Severity: Warning

Message: mysqli::real_connect(): The server requested authentication method unknown to the client [caching_sha2_password]

Filename: mysqli/mysqli_driver.php

Line Number: 201

Backtrace:

File: /app/container/CodeIgniter/application/core/MY_Controller.php
Line: 8
Function: library

File: /app/container/CodeIgniter/application/controllers/Index.php
Line: 6
Function: __construct

File: /app/container/html/index.php
Line: 327
Function: require_once

ローカルだけの問題だったので my.cnf に下記のように mysql_native_password を追加して対応しました。

 [mysqld]
 character-set-server=utf8mb4
 max_allowed_packet = 256M
+default_authentication_plugin=mysql_native_password

ちなみにRDSのMySQL 8.0はデフォルトが mysql_native_password なので問題ありませんでした。*5

RDSのMySQL 8.0からMariaDB 監査プラグインがなくなった

RDSのMySQL 8.0からMariaDB 監査プラグイン(audit logをCloudWatch Logsに流す機能)がなくなりました。

MySQL 8.0でauditの設定を入れてRDSを作ろうとするとTerraformがエラーになって「それはそう」って感じですぐに分かったのですが、auditの設定を消してRDSを作ろうとすると今度は本番環境のRDSは作成できたのですが、最初5.7で作って後から8.0にバージョンアップした方の開発環境のRDSでエラーになってハマりました。

調べたところ、auditを消す差分が発生したのでTerraformが無効化しようとしてるのだが、8.0だと無効化すらできなくてエラーになるという状態でした。 (マジで!?)

Terraformでは開発環境と本番環境で同一のmoduleを使っていて両方の環境で極力同じ構成と設定が使われるようにしてたのですが、enabled_cloudwatch_logs_exports (ログ出力の設定)だけTerraformで差分が出ないように対応しました。(とてもつらい)

awscliから直接無効化するのもダメだったのでAPIレベルでダメだと思います。

$ aws rds modify-db-instance --db-instance-identifier webmanga-sousenkyo-staging --cloudwatch-logs-export-configuration '{"DisableLogTypes":["audit"]}'

An error occurred (InvalidParameterCombination) when calling the ModifyDBInstance operation: You cannot use the log types 'audit' with engine version mysql 8.0.21. For supported log types, see the documentation.

おそらくこの問題を回避するためにはMySQL 8.0に上げる前にMariaDB 監査プラグインを無効化して設定反映するしかないと思います。

3. 本番用のDockerイメージを作成

ローカル開発用のDockerfileをベースにECS用にDockerfileを書き直しました。

本番用のDockerfileを書く時には下記が大変参考になりました。

困ったこと:CodeIgniterがログの標準出力に対応していなかった

Dockerizeする時はログはファイルに出力せずに下記理由により標準出力( /dev/stdout )や標準エラー( /dev/stderr )に出すのが一般的です。

  1. 標準出力か標準エラーに出しておけばDockerの周辺ツール全般*6 がだいたいよしなにやってくれる
  2. コンテナ内のローカルにログファイルをそのまま書き出すとコンテナサイズが肥大化する
  3. コンテナ内に出されたログを別途fluentdなどで送信する必要があって大変

今回ECSで動いてるコンテナのデバッグをするためにログ出力周りを強化したかったのですが、CodeIgniter(WEBマンガ総選挙で使ってるフレームワーク)のLoggerが出力先の ディレクトリ しか指定できない仕様でとても困りました

最終的には https://devcenter.heroku.com/ja/articles/php-logging#codeigniter-3-x を参考に自分でLoggerを作って解決しました。

4. ECS + Fargate + CodePipeline構築

ここが一番苦労しました

ECS + Fargateはメジャーですがその周辺で必要なもの(CodePipelineやCodeBuildなど)も結構あり、公式含めて参考ドキュメントがたくさんありすぎて最初どれを参考にすればいいか道に迷いました。

Terraformで構築するので最終的に https://beyondjapan.com/blog/2020/04/fargate-deploy-flow-terraform/ をベースにしました。

もうちょいサクッと作れるかと思ったのですがTerraformだけでもかなりでかいボリュームになって大変でした。GCPだとCloud RunかApp Engineで実現することになるのですが、過去の経験だとそこまで大変じゃなかった記憶です。

5. CDN作成

移行前だとHerokuアドオンの兼ね合いで https://xxxxxxxx.cloudfront.net のようなCloudFrontのURLをそのまま使っていたのですが*7、移行後はDNSもCDNもAWSなので cdn.webmanga-sousenkyo.com のようなDNSを作ってそこにCloudFrontを紐付けました

6. ACMで証明書を発行

ALBとCloudFrontでリージョンが変わるので下記のように2種類の証明書を発行しました

  • webmanga-sousenkyo.com, *.webmanga-sousenkyo.com:東京リージョン
    • アプリケーションの実行環境が東京リージョンでALBも東京リージョンなので。(ALBと同じリージョンで証明書を作る必要がある)
  • cdn.webmanga-sousenkyo.com : バージニアリージョン
    • CloudfrontでACMの証明書を使うにはバージニアリージョンで証明書を発行する必要がるため

今回ACMで証明書を発行したことにより

のように一石三鳥で色々嬉しいことがありました

7. DNS切り替え

レジストラに登録してたネームサーバをValueDomainからRoute53に切り替えました

開発環境と本番環境のDNSを一度に切り替えるのは怖かったので

  1. 移行後のDNS(Route53)にzoneを作成
  2. 移行前のDNSに新開発環境のALBをCNAMEレコードで登録して移行後のDNSに新開発環境のALBをAレコードで登録し、新旧両方のネームサーバで見れるようにした
  3. 本番環境構築後、移行後のDNSに新本番環境のALBをAレコードで登録
    • /etc/hosts にALBのIPアドレスを書けば見れる状態
  4. 最後の最後にネームサーバを切り替え

のような手順をとりました。

このおかげでWEBマンガ総選挙をサービスダウン無しでHerokuからAWSに完全移行できました。

ハマったこと

NSを切り替え後、DNSのTTLが過ぎても旧環境にリクエストがいく現象があってハマってました。

「予め移行前のDNSでTTLを短めに設定したからヨシ!」と思っていたらNSの方にもキャッシュがあるとは思ってませんでした(恥

2日後に旧環境のリクエストが消えたことを確認できたのでようやく削除できました。

幸いだったのはサイトの更新が全く無い時に移行を行ったことで新旧どっちのDNSにいっても同じものが見えるのでユーザ影響がなかったことです。

移行結果

良くなったこと

前述の通り費用が3〜5割くらい削減できました。

また、WEBマンガ総選挙のロケーションがアメリカから日本になったことにより アプリケーションコードをほぼ変更せずに レイテンシが10倍くらい早くなりました。

  • 移行前:日本<->アメリカの通信で1秒前後
  • 移行後:日本<->日本の通信で70〜120ms前後

悪くなったこと

デプロイの時間がちょっと延びましたが、それ以外は特に不満点はないです。

  • Before: Herokuへのpushで1分半
  • After: 7~8分(docker buildとBlue/Green deploymentで各4分前後)
    • 前者はCodeBuild側でdocker buildのレイヤーキャッシュが効いてくれれば短縮できるのだが後者は無理っぽい

学び

完全にまっさらな状態でインフラを構築するよりも、本番のアプリケーションをダウンタイム無く移行する方が色々考えることが多くて 正直吐きそうだった とても学びが多かったです。

*1:Sentryのエラーの送信先を分けたりハードコーディングされていた設定値を環境変数に移動など

*2:https://elements.heroku.com/addons/edge

*3:Heroku Enterpriseを契約する手もあったのですが、社内でHerokuを採用してるアプリがWEBマンガ総選挙だけだったので他アプリと運用方法をあわせたいという理由でボツになりました

*4:https://elements.heroku.com/addons/edge

*5:https://dev.classmethod.jp/articles/rds-mysql-8-default-auth-plugin-is-mysql_native_password/

*6: docker logs や kubectl logs やCloudWatch LogsやCloud Loggingなど諸々

*7:HerokuアドオンがCloudfrontのラッパみたいになってた

20191219021925
sue445
2018年7月に中途入社。CIおじさん。好きな言葉は「全ての手作業を自動化」。 好きなアニメは女児アニメ全般(プリキュア・プリティーシリーズ・アイカツ)とサザエさん。 多数のgemをOSSで公開 https://rubygems.org/profiles/sue445 。代表作はプリキュアのRuby実装のrubicure (https://github.com/sue445/rubicure)