EC2からFargateへの移行 ~shadow proxyとカナリアリリース~

EC2からFargateへの移行 ~shadow proxyとカナリアリリース~

こんにちは、メディア事業でエンジニアをしている木村洋太です。

昨年のGREE Tech Conferenceでは「LIMIA」のフレームワーク移行プロジェクトにおけるコードの自動修正について話させていただきましたが、今回は同時に行ったインフラ移行について紹介いたします。

EC2からFargateへの移行例は多く存在しているとは思いますが、今回の移行では安全な移行のために、shadow-proxy環境での移行前のテストやEC2とFargateの同時稼働によるカナリアリリースなどさまざまな工夫を行いました。これらの中で得られた知見や失敗をまとめられたらと思っています。

インフラ移行の概要

フレームワーク移行プロジェクト

フレームワーク移行プロジェクトでは、グリーが運営するメディアの一つである「LIMIA」のフレームワークをFuelPHPからLaravelへ移行することを目的としていました。

移行の内容としては、大きく分けて

  • フレームワークの移行: FuelPHP → Laravel
  • 言語のバージョンアップ: PHP7.0 → PHP7.4
  • インフラの移行: EC2 → ECS Fargate

の移行を同時に行ないました。

今回のブログはこのうちのインフラの移行について紹介いたします。

フレームワークの移行における、コードの自動修正の話も個人的にかなり面白い話だと思っているので、そちらも興味があれば是非ご覧ください!

インフラ移行も同時に行なった理由

本来、フレームワーク移行のような大規模なコード変更をする際には同時にインフラ移行をするのはあまり望ましくないと個人的には考えています。これは、問題が起こった際の要因の切り分けにかかるコストが増加したり、切り戻しに時間がかかったり、単純に移行にかかる時間が伸びてしまったりしてしまうからです。

その中でも同時移行を行ったのには以下の理由がありました

  • コードの自動修正のおかげで、他の開発を止めることなく移行を進めることができていたので、移行にかかる期間はそこまで大きな問題ではなかった
  • インフラはコード管理されていたもののビルドやデプロイなど、変更にはコストがかかる状態で、アップデートが止まってしまっていた
  • 開発環境では既にDockerが使用されており、コンテナ環境を一から作成する必要がなかった
  • 管理している他のプロダクトは全てコンテナで運用されており、開発メンバーの知識は十分にあった

インフラの同時移行は大変ではありましたが、この機会にインフラを全てaws-cdkで管理するようにでき、移行自体も大きな問題なく成功できたという点では、同時に移行して良かったと思っています。

安全な移行のために

今回の移行では20万行以上のコードを自動修正した上に、PHPのバージョンアップによる変更やインフラの構成の変更もあっため、QAだけではどうしても全てのバグを探しきれないという問題がありました。

そのため、インフラ移行では

  • リリース前にできる限り本番に近い環境で検証すること
  • 障害が生じてしまった場合でもユーザー影響を可能な限り小さくすること
  • 切り戻しが迅速にできる構成にすること

ができるような方法を検討しました。

本番環境に影響を与えずに事前に本番に近い環境でテストをするという目的については、shadow proxyというものがあることを知り、そちらを試してみることにしました。

リリースについてはカナリアリリースを採用することで、障害発生時の影響をなるべく小さくし、すぐに切り戻しが可能なリリースを実現することを目指しました。

shadow proxy

shadow proxyとは

shadow proxyとは本番環境のリクエストを何かしらの方法で複製し、他のサーバーに送信するようなproxyのことを指しています。

具体的にはhttps://www.slideshare.net/toyama0919/fluentd-49628783を読ませていただき、

  • オートスケールの設定などのインフラの設定の確認
  • QAでは気づききれないようなエラーの確認

を切り替え前に本番環境に近い環境で試せるというメリットから採用することにしました。

検討事項

shadow proxy環境を作成するに当たって、「どこでどのようにリクエストを複製するか」、「どこまで本番と同じ環境を作成するか」をまず考える必要がありました。

「どこでどのように複製をするか」という点では、ngx_http_mirror_moduleやfluent-plugin-http_shadowなど様々な候補があったのですが、いろいろ調べた中でAWSのVPC Traffic Mirroringが良さそうということがわかり、使用することを決めました。

VPC Traffic Mirroringのメリットとしては、

  • 既存のインフラに手を加える必要がないこと
  • 動いている本番環境に影響を与えないこと
  • 転送先にNLBを選べ、proxy部分でスケールさせることも簡単そうなこと
  • AWSのサービスなので設定が少なめでやりたいことができそうなこと

がありました。

「どこまで本番と同じ環境を作成するか」については全リクエストを複製し、RDSも複製して、read/writeまで全てを確認したいところではあったのですが、

  • RDSなどを含め、全く同じ環境を作成するのは時間・料金的にコストがかかりすぎること
  • サービス自体はread heavyであり、負荷やインフラの設定確認はreadのリクエストのみで十分確認できそうなこと

だったため、既存のRDSのリードレプリカを使い、readのリクエストのみを検証することに決めました。

VPC Traffic Mirroring

VPC Traffic Mirroringは特定のインスタンスのeniをsourceとして、そこを流れるトラフィックを別のeniやNLBに送信できる仕組みです。詳しくはドキュメントを参照してください。

トラフィックはVXLANヘッダーでカプセル化されて送られてくるためそれを処理する必要がありますが、基本的にはhttps://github.com/aws-samples/http-requests-mirroringをほぼそのまま使用することで転送できました。また、このsampleではproxy部分のインスタンスの数を変更でき、スケールも容易だったのが良かったです。

細かいところでは

  • (本番環境は大丈夫だったが、)テストのために開発環境で試そうとして、使用していたインスタンスタイプがVPC Traffic Mirroringの対象ではなかった
  • sample自体に軽微なバグ https://github.com/aws-samples/http-requests-mirroring/pull/3があった

などで少し手間取りましたが、概ねスムーズに進めることができました。

read onlyな環境の作成

shadow proxyを考える上では、サービスの外部へのアクセスを把握して制限することは重要でした。例えば、二重にモニタリングデータやアクセスログが送信されてしまったり、二重に外部APIにリクエストが送信されるためにquotaの問題が生じたりしてしまうからです。

今回はサービスで使用しているRDS, DynamoDB, Memcachedに対してのreadのみのアクセスを許可し、他は全て制限するような環境を構築しました。

具体的には

  • RDS, ElastiCache以外の全てのoutboundを制限したセキュリティグループの作成
  • DBはread onlyのユーザーを常に使用するように、他AWSのリソースはread onlyのポリシーに変更したロールを作成
  • Memcachedについてはwriteが起こらないようにコード変更
  • 上記の権限変更で起こるエラーについてはハンドリングするようにコード変更

などを行いました。これらにより、本番環境のデータに影響を与えないshadow proxy環境を作成できました。

この設定で考慮が漏れていた点としては、起動などに必要なAWSリソースへのアクセスもできなくなってしまい、タスクの起動やログの出力に失敗してしまうことでした。

これについてはVPCエンドポイントを作成することで解決が可能で、ssm, ssmmessages, ecr.dkr, ecr.api, logs, s3のVPCエンドポイントを作成しました。VPCエンドポイント自体はVPC内の他のリソースにも影響を与えるため、追加する場合にはセキュリティグループの設定などに十分に注意してください(念の為、VPCエンドポイントを作成したsubnetのrejectのflowlogを確認していました)。

shadow proxyをやってみて

shadow proxy環境を実際に動かしてみて

などを事前に検知することができました。

また、EC2の頃は1インスタンスにphp-fpmとnginxを両方載せ、CPUやメモリなどある程度大きめなインスタンスを使用しているのに対し、Fargate環境ではコンテナを分け、メモリも小さくするなど構成の変更があり、これに合わせてphp-fpmやnginxの設定も変更しています。その設定の調整やタスク数の調整、負荷の見積りを行うのにも役に立ちました。

このように、事前に本番環境に近い環境でテストすることができたという点では役に立つことも多かったのですが、予定よりも環境の構築に時間がかかってしまったことや、「LIMIA」のインフラの設定がそこまでシビアではないこと、後述するカナリアリリースで確認できる範囲と一部被ってしまっていることから、費用対効果としては微妙だったのではないかというのが移行を終えての個人的な感想です。

トラフィックがより多いサービスでのインフラ構築・設定、負荷の確認やより慎重な移行が必要となるサービスでは、さらにshadow proxyの恩恵を受けることができるかもしれません。

カナリアリリース

カナリアリリースの方法の検討

AWSでカナリアリリースを行う方法は大きく分けて、Route53の加重ルーティングによる方法とALBの加重ターゲットグループを利用する方法の2つがあり、どちらも実用例を探すことができます。

今回の移行ではフレームワーク移行も行なっており、カナリアリリースを行うには認証情報やCSRFトークンなどのCookie関連の共通化をどうするかという問題がありました。これらの問題について、認証情報は後述するコード変更により同期を行い、CSRFトークンはALBのSticky Session機能を使用することで解決することに決定しました。

Sticky Sessionを使用するため、必然的にカナリアリリースの方法としてはALBの加重ターゲットグループを採用することになりました。

ログイン状態の同期

ログイン状態の同期についてはセキュリティにも関わる部分なので詳細は省略しますが、

  • 同じKeyのCookieを使用
  • FuelPHPとLaravelどちらのフレームワークで生成された認証情報でも読み取れるようにコード変更

を行いました。これらにより、どちらの環境でログインしてもセッションが維持される状態を実現できました。

加重ターゲットグループの設定

加重ターゲットグループは既存の本番環境のALBの設定を変更する必要があります。事前に開発環境でテストを行い、加重の設定方法やsticky sessionの挙動、切り戻し方法などを確認しました。(記事を書く際にも再度調査したので大丈夫かとは思いますが、間違いなどあればご指摘ください)

sticky session+加重ターゲットグループの確認できた重要そうな仕様としては

  • 加重ルーティングやsitckinessの設定後若干のラグ(10秒以内程度)がある
  • 加重は1~999の値で設定できる(0.1%からのカナリアリリースが可能)
  • stickinessの設定で一度「AWSALBTG」のCookieを渡されて振り分けられた後は、加重を変更してもsticky sessionのstickiness durationが切れるまでは振り分けが維持される
    • これは加重の割合を0にしても振り分け続けられるため、加重を0にするだけでは切り戻しはできないことに注意
    • 一度sticky sessionをオフにして再度オンにし直した場合でも維持される
  • Cookieのexpireは1週間だが、これはstickiness durationとは無関係

これらから、stickiness durationについてはカナリアリリースの割合を変更していくタイミングなども考慮し、1日に設定しました。つまり、加重の設定変更から1日後にはすでに振り分けられていたユーザーも再度振り分けられて、設定した加重に到達するようなイメージです。

また、切り戻しについてはsticky sessionをオフにした上で、加重を0にすれば良いということがわかり、反映も数秒で行われるので安心感がありました。

カナリアリリースをやってみて

カナリアリリースでは

  • QAでは確認しきれなかったエラー
  • 特有の設定、ユーザーでのエラー
  • PHPのバージョンアップによるエラー

などの検知をしつつ、これらを最小限のユーザー影響でリリースできたという点で大いに役に立ちました。

全く異なる二つの環境でのカナリアリリースという少し特殊な試みで、あまり前例もありませんでしたが、開発環境でさまざまな検証を事前に行うことができ、大きな問題もなく終えることができました。細かな修正は行なったものの、切り戻しもなく無事に進めることができたのは本当によかったです。

これらの移行で得られた知見については、例えばPHPからGolangへの言語の変更のような、大きなリプレイスでも応用できるのではないかと考えています。

失敗

全てが上手くいったかのように見えたカナリアリリースでしたが、100%リリース完了後にカナリアリリース由来の障害を起こしてしまいました。具体的には、Cookieが二重に保存されてしまうことで、Webでログアウトできない状態が生じてしまいました。

Cookieが二重でクライアント側で保存されてしまった原因は複雑なのですが、順を追って説明していきますと

  • 旧環境のCookieが(不要かつ指定すべきでないのにも関わらず)ドメイン属性を指定していた
  • 旧環境のCookieのexpireの設定が適切でなかった

これらのCookieの設定ミスにより、

  1. 旧環境でログインし、Cookieがセットされる
  2. 旧環境のセッション情報の有効期限が切れる
  3. 新環境から見ると旧環境のCookieが残っているため、セッション情報を参照するが無効化されている(ログアウトされている判定)
  4. 新環境で再ログイン
  5. 旧環境のCookieはDomain属性が指定されているため上書きされず、別のCookieとして保存される
    1. https://datatracker.ietf.org/doc/html/rfc6265#section-5.3 の11.参照
11. If the cookie store contains a cookie with the same name, domain, and path as the newly created cookie: 1. Let old-cookie be the existing cookie with the same name, domain, and path as the newly created cookie. (Notice that this algorithm maintains the invariant that there is at most one such cookie.) 2. If the newly created cookie was received from a "non-HTTP" API and the old-cookie's http-only-flag is set, abort these steps and ignore the newly created cookie entirely. 3. Update the creation-time of the newly created cookie to match the creation-time of the old-cookie. 4. Remove the old-cookie from the cookie store.

これらにより、Cookieが二重に登録される状態が生じてしまいました。

この状態の厄介なこととしては、新環境からは旧環境のCookieを削除や変更ができない状態にも関わらず、クライアント側は一致するパスが長い物から順にCookieの順番をソートし、サーバー側は同じKeyの中では優先度の最も高いCookieしか取得しないため、Domain属性のついている旧環境のCookieが常に読み込まれてしまうことでした。

これらの検証結果から、カナリアリリース終了後に新環境でCookieのKeyを新しいものに切り替れば解決できることがわかり、すぐにリリースを行ないました。

テストやQAでは、2環境間でログインが同期されていることや切り替えながらログイン・ログアウトが可能なことは確認できていたのですが、上の事象を再現できるような旧環境でログイン→新環境でログアウト→新環境でログイン→新環境でログアウト(できない)というようなテストまではできていませんでした。そもそも、移行時にCookieの設定についてより慎重な調査と検証を行えていればこの問題が生じることはなかったのでそこは大きな反省点です。

この障害については開発環境で早い段階で気づくことができ、事前にCookieと認証周りについてある程度の検証を行えていたために、すぐに原因を究明し解決できたのは不幸中の幸いではありました。また、ユーザー影響についても(切り替えでログアウトが発生したこと以外は)ほぼ内部ライターの方々のみに留めることができました。

最後に

EC2からFargateへの移行において、shadow proxyとカナリアリリースを採用した事例について紹介させていただきました。失敗はありつつも、全体的には検討段階で考えていた通りにおおむね進めることができ、無事移行に成功することができました。

個人的にはこのような大規模なインフラの移行やAWS Fargateでの一からのインフラの構築など、なかなか経験できないような経験をすることができ、とても運が良かったと思っています。また、安全な移行なためにできることはないかと様々な検討をし、実際にやってみたことで技術的にも多くのことを学ぶことができました。