はてなブックマークで利用しているCloudFrontのAWSアカウントを移行した

こんにちは、id:cohalzです。2023年4月に実施したはてなブックマークのメンテナンスではCloudFrontを別のAWSアカウントに移行しました。

この記事ではCloudFrontを別のAWSアカウントに移行した背景とどのように移行したのかを説明します。

はてなブックマークのインフラのこれまで

はてなブックマークのインフラはこのようにCloudFrontと関連リソースだけ別のAWSアカウントで利用していました。

移行前

この状況になっていた経緯をまず説明すると、はてなブックマークでは2018年からオンプレ環境で動かしていたところに追加する形でCloudFrontを利用し始めました。

当時はてなブックマークではAWSをほとんど利用しておらず、全社の共通アカウント(以下旧アカウント)にCloudFrontのディストリビューションを作成しそれを利用していました。

その後アプリケーションをオンプレ環境からAWSに移転するというプロジェクトが動き出しました。

その際AWSアカウントは全社の共通アカウントではなくはてなブックマーク用の個別アカウント(以下新アカウント)を払い出して利用しはじめました。

しかしCloudFrontに関しては既に旧アカウントで元気に動いているという状態であり、アカウントを移行しても別に費用が安くなるなどの明確なメリットがあるわけでもない状態だったのでそのまま放置されて今の状態になっていました。

移行したいモチベーションが出てきた理由

そんなCloudFrontですがアカウント移行を進めたくなった理由が増えてきました。

はてなブックマークでは2022年に全てのインフラ環境をオンプレからAWSに移行するプロジェクトが完了しました。

これによりサービスの費用を全てAWS上で確認できるようになりましたが、実際に確認してみるとCloudFrontとそれ以外で分かれて確認しづらいというのがまず一つありました。

またAWSの新アカウントのリソースに関しては全てAWS CDKで管理されている状態でしたが、旧アカウントではそうなっておらずCloudFrontのみ変更が手作業という状態でした。

この二つによってどうしても普段からCloudFrontとそれ以外という認識で作業をしなければならず認知負荷が高い状態でした。

それとは別にCloudFrontの設定も古く、キャッシュポリシーやオリジンリクエストポリシーといった機能もほとんど活用できていない状態でした。

こういった状況を解消するため、CloudFrontのAWSアカウント移行とAWS CDK管理を行い当たり前の状態を目指すことにしました。

最終的には以下のように別チームが使うS3バケットを残しそれ以外のリソースは全て新アカウントに統一することができました。

移行後

切り替えで設定が変わらないように気を付ける

CloudFrontを移行するにあたって、移行前後で設定が変わらないようにする必要があります。

実は今回のプロジェクトの数年前に一度CloudFrontの移行を進めており、その設定がAWS CDK上で残っていました。

その新アカウントのCloudFrontの設定を確認したところ、変更の多いビヘイビア設定に関しては特に現状と乖離している状態でした。

しかも旧アカウントのCloudFrontは設定変更が手作業であり、数年分の変更の経緯が全て追いやすい形で残っているわけでもないため、差分を確認し丁寧に合わせていくという手順が必要でした。

歴史が長いサービスであるためビヘイビア設定も30個程度と多く存在していたため確認作業だけでも一苦労でした。

確実に設定が同じであると言えるようにするために今回では下の順番で新アカウントのCDK側の設定を合わせていきました。

  • ビヘイビア設定の並びを合わせる
  • 各ビヘイビアの設定を合わせる
  • 全てのビヘイビアでキャッシュポリシーを使うようにする 

まずはビヘイビア設定の並びを合わせるというのを行いました。ビヘイビア設定は優先順位でソートされているため、新設されたパスパターンの設定が優先順位の高いものだとそれ以降の設定の順番がずれてしまいdiffコマンドでは差分を確認しづらい状態でした。

なのでまずはパスパターンの設定を適当でも良いので追加し、その後各ビヘイビアの設定をdiffを見ながら順番に設定を合わせていくという流れで変更をしました。

キャッシュポリシーに移行する

CloudFrontを移行するにあたってキャッシュポリシーやオリジンリクエストポリシーを利用することも要件に含めました。

要件に含めた理由は柔軟な設定ができるようにするためという理由が一番でしたが、AWS CDK側の制約も理由としてありました。

今までAWS CDKでのCloudFrontの設定はCloudFrontWebDistributionを用いて作成されていましたが、これはキャッシュポリシーが使えないという制約がありました。そのためDistributionを用いて作成し直す必要がありました。逆にDistributionではキャッシュポリシーを使わないようなビヘイビア設定を作成できないという仕様がありました。

そのため、移行時に設定が変わらない想定で切り替えを行う前にはまず、旧アカウントのCloudFrontにおいて全てのビヘイビア設定でキャッシュポリシーを使うか使わないかのどちらかに統一する必要がありました。

キャッシュポリシーは便利な機能でありそれをわざわざ使わない理由はないので、移行前に旧アカウントの設定を先に整理し全てのビヘイビア設定でキャッシュポリシーやオリジンリクエストポリシーを使うように設定を変えていきました。

各ビヘイビアでどのようなポリシーを設定すれば良いかに関してはAWS CLIの結果からjqでグルーピングをすることで比較的簡単に調べることができました。

aws cloudfront get-distribution --id XXX | jq '[.Distribution.DistributionConfig.CacheBehaviors.Items[].ForwardedValues] | unique | length'

ビヘイビア設定のForwardedValuesを順番に見ていき、必要そうなキャッシュポリシーを新旧両方のCloudFrontで作成し使うように設定を追加しました。

キャッシュポリシーを使った設定がちゃんと同一であるかについての確認もしたのでそれの紹介をしていきます。

キャッシュポリシーを使ったビヘイビア設定ではForwardedValuesがnullとなり、CachePolicyIdもAWSアカウントが違うことによって異なる値になるので単純には比較できません。

{
  "PathPattern": "/js/bookmark_button.js",
  "ForwardedValues": null,
- "CachePolicyId": "xxx"
+ "CachePolicyId": "yyy"
},

これらの中身が同一であることを調べるのに今回は下記の方法で確認しました。

  • 新旧アカウントで同じ名前でキャッシュポリシーを作成する(CachePolicyAとする)
  • 新旧アカウントの各キャッシュポリシーについてaws cloudfront get-cache-policy-config を使ってキャッシュポリシー自体のdiffを確認し、同一のキャッシュポリシー名で実際の設定が変わらないことを確認
  • aws cloudfront get-distribution-config の出力のうちキャッシュポリシーID(xxxとyyy)をそれぞれのキャッシュポリシー名(CachePolicyA)に置換して同一のキャッシュポリシー名を使っていることを確認
  • オリジンリクエストポリシーも同様に上記の順番で確認する

ここまで設定を合わせることで新旧アカウントの結果の差分はdiffコマンドでもかなり確認しやすいものになりました。そこから少しずつ差分を減らしていき移行の準備を進めました。

移行方法について検討する

以上の設定変更を行ったことで新旧両アカウントのCloudFrontがほぼ同じ設定になり、あとは移行するだけになりました。

同一アカウントでCloudFrontを移行する場合は aws cloudfront associate-alias というコマンドが利用できますが、今回は別のAWSアカウントに移行するため使えません。

またドキュメントで案内されている別の方法として、新しいCloudFrontの代替ドメイン名にワイルドカードを設定し、DNS設定を切り替えるという方法もありました。

docs.aws.amazon.com

今回の移行では b.hatena.ne.jp やその他複数のドメインが対象だったので、*.hatena.ne.jp などそれぞれ対応するワイルドカード証明書が新アカウントのACM上で作成できる必要がありました。

しかしサービス個別のAWSアカウントに一時的とはいえ *.hatena.ne.jp といった非常に範囲の広い証明書を用意したくない、別チームにDNS設定の変更を依頼する必要があるなど複数の理由から今回は採用しませんでした。

これら以外の方法でCloudFrontを別のAWSアカウントに移行する手段は調べたところ二つありました。

1つ目の方法はAWSサポートに連絡してAWSサポートに連絡して、代替ドメイン名を別のAWSアカウントのCloudFrontに移動してもらうという方法です。

この方法ではダウンタイムなしで移行することができますが、欠点として実施タイミングがAWSのサポート側の作業に依存してしまうというデメリットがあります。

実施タイミングをこちらで制御できないことで、問題があったときの切り戻しタイミングも制御できず復旧に時間が掛かる可能性があり、この方法は新旧二つのCloudFrontの設定が同一である保証がないと選びづらいものでした。

2つ目の方法は旧アカウントで代替ドメイン名を削除、新アカウントで代替ドメイン追加の切り替え作業を行うという方法です。

こちらの方法は旧アカウントで代替ドメイン名を削除してから新アカウントで代替ドメインを追加するまでの間はサービスが全体的に閲覧できなくなるという大きなデメリットがありますが、実施およびダウンタイムのタイミングを完全に制御できるというメリットがあります。

こちらの方法をstaging環境で実際に試したところダウンタイムは10s程度と短いものであったため、停止メンテナンスを入れてダウンタイムを許容しつつ採用することにしました。

この方法を採用した別の理由として、Aurora MySQL 2.10などの廃止による対応で停止メンテナンスを必要としていたタイミングでと重なっていたというのもあり、そこに合わせる形で実施できたというもありました。

ちなみに今回のメンテナンスではCloudFrontを含む合計9つのコンポーネントの切り替えを全て行う大掛かりなメンテナンスとなりました。

AWS CLIでCloudFrontを移行する手順を作成する

メンテナンス状態にして切り替えるとは言ってもサービス全体が閲覧できない状態になるためダウンタイムが短いに越したことありません。

切り替え手順をstaging環境でちゃんと確認したい、変更を素早く行いたい、旧アカウントのCloudFrontはCDKで管理していない、などの理由から今回はAWS CLIを使って代替ドメイン名を付け替えるスクリプトを作成しました。

作成したスクリプトの工夫として、旧アカウントから新アカウントに切り替えるだけではなく新アカウントから旧アカウントに変えるという逆向きの切り替えも簡単に行えるような形で作成したというのがありました。 逆向きの切り替えもサポートしたことで問題が起きた時の切り戻しが誰でも簡単に行いやすくなった他、staging環境での切り替えの検証を何回も行いやすくなったなどのメリットもありました。

AWS CLIによるCloudFrontの設定の変更方法はこの記事を参考に aws cloudfront get-distribution-config の結果を取得・書き換えたのちに aws cloudfront update-distribution で反映するという方法を使いました。

blog.serverworks.co.jp

上の記事では変更したいパラメータをsedで書き換えていましたが、代替ドメイン名(Aliases)のように複数行で書かれたJSONのプロパティをsedで書き換えるのは難しいのでjqの更新演算子 |= を使って代替ドメイン名だけを変更するようにしました。

例えばCloudFrontから代替ドメイン名設定を削除するのはこのようなシェルスクリプトで実現できます。

etag=$(aws cloudfront get-distribution-config --id $source_distribution_id | jq '.ETag')
aws cloudfront get-distribution-config --id $source_distribution_id | jq '.DistributionConfig | .Aliases |= {"Quantity": 0}' > $distribution_config_file
aws cloudfront update-distribution --id $source_distribution_id --distribution-config file://$distribution_config_file--if-match $etag

代替ドメイン名設定を追加するコマンドも上のAliasesの部分だけを変えるだけで設定できます。

代替ドメイン名を削除・追加の一連の流れを1つのシェルスクリプトで実行するという形で実際の切り替えを行いました。

本番環境を切り替えた時のダウンタイムはstaging環境と同様に10s程度で完了することができました。

アクセスログを配送する部分も移行する

こうしてCloudFront自体を切り替えることに成功しましたが、この切り替えのタイミングではアクセスログの保存先は変えず図のようにそのまま旧アカウントのS3バケットを利用しました。

CloudFrontだけ移行した状態

もし移行の際にS3バケットも同時に切り替える場合、アクセスログの配送に問題があった場合にCloudFrontの切り戻しを選択肢に入れなければなりません。

切り替え時にリスクとなるものはできるだけ減らしたいという理由で、S3バケットの変更はCloudFrontの切り替えの後に行うようにしました。

切り替えをしなかったことで新アカウントのCloudFrontから引き続き旧アカウントのS3に書き込めるかを事前に確認する必要が出てきますが、こちらに関してはCloudFrontの切り替えを行う前に確認作業を行うことができるので安全です。

また生ログを保存する部分だけではなく、送られたログをパーティション分割された形に移動させるLambdaおよびAthenaのテーブルも移行しました。

Athenaのテーブル定義に関しても下の記事を参考にしつつ見直しました。

zenn.dev

パーティション射影を利用できるようにした他、パーティションの単位も今までは1日単位だったのを1時間単位に変更し、細かい単位で検索できるようになりました。

ログを移動させるLambdaの切り替えも安全に切り替えました。

Athenaで使うパーティションの単位を変更したことなどもあり、Lambdaの実装も多少ではありますが変更した箇所がありました。

変更する箇所が増えることで動かなくなる可能性も増えていくため、新アカウントのLambdaでは新アカウントのS3バケットだけではなく旧アカウントのS3バケットにもダブルライトの形で引き続きログを2種類の場所に移動するようにしました。

これによりCloudFrontから保存するバケットを切り替えしても旧アカウントのAthenaは引き続き使える状態を維持し、ここでも切り替えリスクを減らせるようになりました。

新アカウントのS3バケットとLambda・Athenaのテーブル定義を作成できた後は、CloudFrontからそのS3バケットを利用して良いかの最終確認も行いました。

S3バケットを切り替える前に、まず手元からCloudFrontのログを模したオブジェクトを手動でアップロードしました。 そのログがLambdaの実行対象となり適切にパーティション分割された場所に配送されAthenaで検索ができるまでを確認しました。ここまで確認したのちにCloudFrontからその新アカウントのS3バケットを利用するように切り替えました。

丁寧すぎるくらいの確認ではありましたが、アクセスログが正しく配送されることはサービスが稼働していることと同じくらい大事なことだと個人的には思っているのでしっかり確認をしていきました。

ここまでの確認をした結果問題なくログの配送も新アカウントに切り替えることができ、そのコンポーネントも全てAWS CDKで管理できるようになりました。

まとめ

CloudFrontを別のAWSアカウントに移行する手法とその際に気をつけるポイントを紹介しました。

今回、インフラ構成の経緯を追いづらいところもあり、長年サービスを運用していく上ではInfrastructure as Codeが重要になってくると再認識したプロジェクトでした。

はてなブックマークのインフラも全てAWSアカウントに移し終わり、Infrastructure as Codeも徹底できた今ようやく長くサービスを継続させるための当たり前のラインまで到達できたと感じています。

はてなでは長くサービスを運用するために活躍できる仲間を募集しています。