LoginSignup
31
15

CloudFront + S3環境をCloudflare R2 + Workers環境に移行した話

Last updated at Posted at 2023-03-17

【2023-12-14追記】
本記事の内容のさらにその後の小ネタなどを以下に書きました。

【2023-07-20追記】
本記事の内容のその後を追加して、

Workers Tech Talks #1
https://workers-tech.connpass.com/event/287490/

というイベントで発表を行いました。
最新の情報に若干更新を行っておりますので、ご興味ある方はこちらも参照をよろしくお願いします。


YAPC::Kyoto 2023 がいよいよ今週末に開催されるということで、
それを祝して(?)開催された昨日の吉祥寺.pm32

で以下のLTをさせていただきました。

本記事はこのLT資料の元ネタとなったものとなります。

最近行っていた、Cloudflare R2 + Workers環境への移行について紹介します。

Cloudflare Workersとは

Cloudflare WorkersとはCDNのエッジでJavaScriptを実行できるサーバーレスなサービスです。
Lambda@EdgeみたいなFaaSで、リージョンの概念は無く全世界のエッジで実行されます。
Service Workerが動作し、簡単なアプリケーションであればサーバレスで動作可能です。

Cloudflare R2とは

Cloudflare R2はAmazon S3互換のオブジェクトストレージで、エグレス料金がかからず、非常にリーズナブルな料金で利用が可能です。
2022年9月にGA され、S3と比較すると安く利用する事が可能です。

CloudFront + S3環境から Cloudflare + R2に移行することになった経緯

最近の急激な円安の影響でコスト増加し、トラフィックとストレージのコストをできるだけ抑える必要があったため、
今回はCloudFront + S3 + Nginx環境からCloudflare + R2 + Workersの構成に切り替える事になりました。

移行前の構成

※これは簡易的なイメージです。
Basic認証機能は現状あまり使われていなかったため、このタイミングで廃止してシンプルな構成に変更することにしました。

Cloudflare + R2への移行の概要

大まかには、以下のような作業が必要でした。

  1. Workers機能の実装
  2. アプリケーションからS3とR2のダブルライト実装(両方のストレージに書き込み/削除)
  3. S3からR2への全データコピー
  4. キャッシュパージの実装
  5. ロギングの設定
  6. 整合性チェック
  7. 切り替え(NSの委任)
  8. 残作業

今回はこれらの中でハマった事や工夫した箇所などを紹介しようと思います。


1. Workers機能実装

R2のGAのタイミングで、公開バケットという機能が提供されるようになったため、シンプルなバケットの公開であればWorkersを通さなくても配信できるようになりました。

しかし、現在利用しているS3のバケットは、以下のようにパスのマッピングをしてやる必要があるため、
公開バケット機能そのままでは使えないことがわかりました。

現在のURLとバケットのイメージ

このため、Workersを使ってR2のパスを動的に作成して配信させるようにすることにしました。

WorkersでR2の画像を配信する

の記事などを参考に、
wranglerのデプロイ環境などを整え、
シンプルに配信するようにしました。

今回はシンプルな画像配信メインだったので使用しませんでしたが、ちょっと込み入った事をしたい場合はゆーすけべーさんによる超高速なCloudflare Workersのフレームワーク Hono[炎] などを検討すると良さそうです。

Cacheについて

画像配信が中心になるため、これまでのようにキャッシュしたいと考えました。
Cloudflare Workersのキャッシュは複数の方法があるようです。

  1. fetch cf
  2. Cache · Cloudflare Workers docs
  3. KV · Cloudflare Workers docs
  4. Using Durable Objects · Cloudflare Workers docs

ここはCloudFrontのキャッシュとかなり異なるので、手っ取り早くイメージを掴みたい場合は以下の記事などが参考になるかと思います。

キャッシュヒット率などを考えると理想的にはKVがあるのですが、
思ったよりもコストがかかる可能性があるため、今回はCache APIを使ってキャッシュするようにしてみました。

公式のCache APIのサンプル実装

を元に、前述のようなバケットパスをHostヘッダーに応じて動的にパスを作るようにしてファイルを配信するようにWorkersを作成しました。

細かいですが、特定の拡張子の場合のみ正規表現で判定して Cache-Control: private でキャッシュさせないようにする事などもWorkersなので気軽に記述することが可能です。

ここまででR2バケット内の画像をキャッシュしてWorkersで返す事が可能な事を確認したら次に進みます。


2. アプリケーションからS3とR2のダブルライト実装(両方のストレージに書き込み/削除)

以前、オンプレ環境からS3に移行した際はバイナリログ的なログを用意して、デイリーで差分コピーを行っていました。
今回はすでに両方ともクラウド環境のため、S3からR2へのコピーにも都度コストがかってしまいます。

このため、定期的にS3からR2へ差分コピーを行う方法だとコスト的に厳しいという問題があり、
また出来る限りサービスを停止させたくないため、ダブルライト方式を採用することにしました。

R2は元々S3互換のため、アプリケーション側はさほど大きな改修にならずにいけました。
S3との主な違いとしては、R2には region の概念が存在しないので auto を指定する点、SSLが必須な点、エンドポイントを指定する点などとなっていました。

    # Amazon S3
    $attr{client} = Amazon::S3::Thin->new({
        aws_access_key_id     => $aws_access_key_id,
        aws_secret_access_key => $aws_secret_access_key,
        region                => 'ap-northeast-1',
    });

    # Cloudflare R2
    $attr{second_client} = Amazon::S3::Thin->new({
        aws_access_key_id     => $cloudflare_access_key_id,
        aws_secret_access_key => $cloudflare_secret_access_key,
        region                => 'auto',
        secure                => 1,
        endpoint_url          => $r2_endpoint_url,
    });

アプリケーション側でのダブルライトの仕組みが正常に動作する事を確認後、
いよいよバケットのフルコピーを行っていきました。


3. S3からR2への全データコピー

ダブルライトが正常に稼働した後、S3からR2へのバケットコピーを行っていきます。
それなりのサイズのバケットの場合、今のところ大きく分けて2つの方法があると思います。

方法1. R2 Migrator(R2 Super Slurper) で移行

R2 Migratorは本日時点ではまだBeta版であり、オブジェクトのサイズやオブジェクト数などにいくつか制限があります。

方法2. rclone で移行

rclone とはオープンソースのコマンドラインベースのファイル同期および転送ツールです。

公式でもR2でrcloneを使用する方法を紹介しています。

R2 MigratorがまだBeta版であり、オブジェクト数・サイズなどの点で懸念があるのとまだ不安定な可能性がありそうな事、またコピーの細かい制御が出来るか不明だったため、
今回はこちらのrcloneでデータコピーを行いました。
簡単にコツなどを紹介します。

rclone動作環境について

コピー元のS3バケットが東京リージョンのため、東京リージョンに c5n.4xlarge の移行用EC2インスタンスを用意しました。(※c5nはネットワーク帯域が最大100Gbpsになっています。)

rcloneのコマンドオプションなど

rcloneには数多くのオプションがありますが、以下のサイトなどを参考に、最適となりそうなオプションを調整しました。

https://rclone.org/flags/ より、いくつか抜粋しておきます。

--transfers              int Number of file transfers to run in parallel (default 4)
--s3-upload-concurrency  int Concurrency for multipart uploads (default 4)
--s3-chunk-size          SizeSuffix Chunk size to use for uploading (default 5Mi)
--s3-disable-checksum    Don't store MD5 checksum with object metadata
--no-check-dest          Don't check the destination, copy regardless
--ignore-checksum        Skip post copy check of checksums

移行対象のバケットはアルファベット順に並んでいるため、
簡単なrcloneのラッパー的なPerlスクリプトを用意して rcloneコマンドを生成して時間・ログ・コストを計測しながら順番にコピーしていきました。

階層がそれなりに多いため、アルファベット順の第2階層までをコピー対象のパスとしてrcloneを 100並列 x 6 = 600並列くらいで徐々に増やしながら流していきました。

バケットパスの階層を取得するには、

$ rclone lsf xxxx-bucket/0/

みたいにすると下層のパスがリストで取得可能です。
参考までに使用したスクリプトのgistを貼っておきます。

rclone_s3_to_r2_sync.pl

# 動作イメージ
[ec2-user@xxx]$ time carton exec -- ./rclone_s3_to_r2_sync.pl
2023-02-06T17:05:24 [INFO] start - rclone copy --transfers 100 --s3-upload-concurrency 100 \
--no-check-dest --ignore-checksum --s3-disable-checksum  -P \
 s3:xxxx-production/0/0-/  r2:xxxx-production/0/0-/

rclone の罠

ちなみに rclone には copysync コマンドの2種類がありますが、
rsync と違って sync を使うとデフォルトで rsync --delete の削除挙動となるので注意しましょう。
一回限りのフルコピーでは copy を使うと良いと思います。 --dry-run (-n) も可能です。

コピーにかかるR2操作関連のコストチェック

巨大なバケットをコピーする時はどれだけのオブジェクト操作が実行されるのかを予めチェックする必要があります。S3 Storage Lensなどを使って予め予測をすると良さそうです。

Class A Operations(ListObjects, PutObjectなど)、Class B Operations(HeadObject, GetObjectなど)がどれだけかかるか、事前に計画を立てながら監視するようにしました。

CloudflareではGraphQL APIが用意されているため、
以下の記事で紹介されていたように、R2のメトリクスをGrafanaで確認出来るようにしてみました。

NAT Gateway課金の罠

バケットコピー作業開始当初、素朴にrcloneを開始したものの、思ったよりもAWS側の料金が跳ね上がってしまう問題がありました。
調査したところ、作業用のEC2がプライベートサブネットに属していて、そこからS3をR2にコピーしたため外部Cloudflareへのアウトバウンド通信がNAT Gateway経由になってしまうというものでした。

これを回避するにはEC2をパブリックサブネットに置くか、VPC Endpointを利用するとNAT Gateway経由にならなくて済むので切り替えを行いました。

以下の記事がとても参考になりました。

こういった作業をする際は、短期間で一気にコピーを行わず、最初は少しずつ作業するようにして Cost Explorerを細かくチェックすると良いと思います。


4. キャッシュパージの実装

Cloudflare Cache APIを使ってキャッシュしている場合はキャッシュのパージをCloudflare APIを使って行う必要があります。

CloudFrontのInvalidationsとは違い、Cloudflareのキャッシュパージリクエスト自体は無料ということなのでコストは気にせずに素朴に削除したファイルのURLを適宜キューに入れて投げるようにしました。


5. ロギングの設定

移行前のCloudFrontでは、特定の拡張子のリクエスト件数などを定期的に集計してDBに入れるといった事を行っていました。
Cloudflareでは Logpush という機能があるため、これを利用しました。(LogpushはEnterpriseプランのみ使用可能です)

以下の記事を参考に、

必要なログのフィルタルールを定義して、R2バケットにログを溜め込むようにし、
定期的にAPIで取り込みを行うようにしました。
以下のAPIを利用すると必要な期間のみのログが取得可能です。


6. 整合性チェック

ここまでの手順でS3からR2へのコピーが完了しており、
アプリケーションからはS3とR2の両方に書き込みがされているため、基本的にはバケットの中身は一緒となっている「はず」です。
が、実際に一緒のレスポンスが返ってくれるのか、コピー漏れやファイルの欠損などは無いのかの整合性の確認を行うようにしました。

Cloudflare Workersをカスタムドメインで使用するにはDNSのゾーン委任が必要なため、本番用ドメインを使ってテストが行えません。正確にはPartial (CNAME) Setupというのを使って工夫すれば可能ですが、ちょっと複雑になります。
Partial (CNAME) setupについては以下の記事に詳しく紹介されていました。

今回は、一旦本番とは仮のドメインを用意して、事前に見比べ出来るようにテストを行いました。

  • production.example.net (本番用ドメイン)
  • production.example.com (実際には使用されていない整合性チェック用ドメイン)

Workersのカスタムドメインは複数指定可能なため、本番環境で使用していないドメインを別途用意し、
バケットに存在するであろうランダムに対象URLを生成して実際にレスポンスコードや Content-Type, Content-Length などの比較を行うスクリプトを用意しました。

アルファベット単位での対象パスのランダム抽出には、 ORDER BY RAND() で十分現実的に返ってきたのでそれを使用しました。(件数やインデックスによるため、事前にexplainは必須)

SELECT hostname FROM target_table WHERE hostname LIKE ? ORDER BY rand() LIMIT 1

本題からはちょっとそれますが、今回移行対象のバケットは複数種類あったため、この整合性チェックにより一部バケットのコピー漏れを検知して対応することができました。


7. 切り替え(NSの委任)

本番環境の切り替えは実作業としては以下の流れになります。
停止メンテナンスをしないで済むように、Partial (CNAME) setupを利用して切り替えをおこないました。

  1. Partial (CNAME) setupで本番用ドメインを認証させる
    • 元々Route53を利用していたので、Route53側で cloudflare-verify.example.net のようなTXTレコードを作成
  2. Cloudflare側のDNSに予め既存のRoute53と同じレコードをすべて設定しておく
    • Route53側のCloudFrontのAliasレコードと同じレコードをCloudflare側にもCNAMEで登録しておく。この際、「Proxied」ではなく「DNS only」とすること。
  3. Workersの Custom Domainを設定
  4. Workersの wrangler.toml 内の custom_domain も設定し、wrangler publish でデプロイする
  5. Workersの本番用Custom Domainが Initializing から Active に変わるのを待つ
    • Activeになったら、curl等で確認
      • curl -I -X GET 'https://hoge.example.net/test.png' --resolve 'hoge.example.net:443:${cloudflare-IP}'
  6. ここまで問題無かったら、いよいよNSの委任作業を行う
    • CNAME Setup から Full Setup に変更する
    • Route53側にNSレコードを作成して、Cloudflareに委任
    • 事前に入れておいた cloudfrontのレコードを workers に変更する。この際、DNS onlyからProxiedに変更
  7. wrangler tail でログを監視する

ここまでで切り替えは完了となりました。

なお、もし想定外の問題が起きて切り戻しを行う場合は、Cloudflare側DNSの Workers のCNAMEを元の cloudfrontのALIASレコードに変更(かつ ProxiedDNS only に)することで切り戻しが可能だと考えています。

【2023-04-29追記】 対象ドメインがZone Apex(ネイキッドドメイン)の場合

上記の手順はサブドメイン単位のドメイン切り替えの際のNS委任手順となっていました。
移行する対象ドメインが、Zone Apex(ネイキッドドメイン)の場合は、もう少し注意する点があります。

後日に行った別の案件の話ですが、Route53 からCloudflare DNSにZone ApexのNSレコードを切り替えたのにも関わらず、WHOISになかなか反映されない、古いSOAレコードがキャッシュされ続けてしまう、カスタムドメインの追加時になかなか「Initializing」から「Active」になってくれないといった問題がありました。

追加したカスタムドメインが「Initializing」から「Active」になってくれない問題

Workersからカスタムドメインの追加を行うと、裏側のSSL/TLS - Edge Certificatesという箇所で、Edge Certificatesの発行処理をマネージドで実行してくれます。
ここでLet's Encrypt証明書を自動で取得しようとしてくれているようですが、「Pending Validation (TXT)」のままずっと止まってしまう、という問題で、以下のページに近い状態でした。

https://community.cloudflare.com/t/ssl-tls-certificate-pending-validation-txt/279458
https://community.cloudflare.com/t/community-tip-best-practices-for-certificate-provisioning/44230

最初はいわゆるSOAレコードのネガティブキャッシュ問題かと考え、86400秒(1日)以上待ってみたものの、状態が変わらず。
このため、ドメインを管理しているレジストラに対し、「管理コンソールからドメインのネームサーバの変更をしたのにも関わらず、1日以上経過(この時点では3日ほど経過)してもWhoisのName ServerがRoute53のままになっています。」と問い合わせを行いました。
すると、レジストラ側で何かの不備があった(?)ようで、問い合わせ後のやり取りの末にネームサーバの反映を行っていただけました。
これによってName ServerもRoute53 からCloudflare DNSに移行でき、Whois反映から数時間ほどでEdge Certificatesも無事取得が行え、無事にカスタムドメインも「Active」となってくれました。

いわゆる上位DNSの「DNS浸透問題」というものだと思いますが、自分もあまり詳しい分野ではないため、以下のような記事なども参照いただければと思います。

NSレコードの変更時に知っておきたいTTL(キャッシュ)と上位DNSの話


8. 残作業

ここまででCloudflare R2 + Workersへの切り替えは完了となりますが、残作業がいくつかあります。

  • Cloudflare R2 + Workers環境に切り替え後、しばらくの期間並行稼動で様子を見つつ、切り戻しの必要が無くなったと判断出来たタイミングになったらアプリケーション側のS3とR2へのダブルライトの仕組みを廃止し、R2のみ書き込みのシングル構成に変更する。
  • 必要があればライフサイクルルール同等処理の組み込みなど
  • S3バケットやCloudFrontのディストリビューションなど、関連するリソースのアーカイブ・もしくは削除などを行う

以上で切り替え作業は終了となります。


その他小ネタ

小ネタ1. LWP(libwww-perl)の UserAgentがデフォルトで拒否されている問題

Perl使いとしては最初知らずにちょっとハマった点を紹介します。

Cloudflare全般のデフォルト設定として、
Perlの標準HTTPクライアントであるLWPが拒否されてしまっていますw

$ curl -X GET -I  https://blog.cloudflare.com/
HTTP/2 200

$ curl -X GET -I  https://blog.cloudflare.com/ -A libwww-perl
HTTP/2 403 

$ curl -X GET -I  https://blog.cloudflare.com/ -A libwww-per
HTTP/2 200

この問題は以下の Browser Integrity Check という仕組みのようで、その昔スパマーがこのLWPのUAを悪用したケースが多かったんだと思われます。。

このBrowser Integrity Checkは、Websites - Security - Settings から機能のON/OFFが可能となっています。
もしくは、OFFにするのではなく、特定のUAを許可したい場合はWAFのCustome Ruleから任意のUser-Agentを追加する方法もありそうでした。

が、今回はなるべくCloudflare標準に合わせたかったため、

my $ua = LWP::UserAgent->new;
$ua->agent('curl/7.29.0');

とするように対応しました。

また、Furlをご利用の方は心配しなくて良さそうですw

$ perl -MFurl -E 'say Furl->new->get("https://blog.cloudflare.com/")->status_line'
200 OK

小ネタ2. WebPの対応が簡単に設定可能

こちらはハマった問題ではないのですが、すごいと思ったので紹介します。

Cloudflare Polishという機能があり、Pro以上のプランであれば、ポチポチするだけでWebP対応が出来てしまいます…!

アプリケーション側でWebP対応をしようとすると結構大変だったりするので、これは素晴らしいですね。
圧縮による劣化の問題や、Exif除去の問題などもあるので、導入の際は慎重に検討する必要がありますが、
切り替えも簡単なので、使用が可能なのであればぜひ検討したいところです。


まとめ

CloudFront + S3環境から Cloudflare R2 + Workersに移行する方法の一例を紹介しました。
まだR2はGAされてからさほど経過していないため、予期しない問題などもあるかもしれませんが、
コスト削減の観点からすると結構なインパクトがあると思います。

R2 Migratorが今後GAされればS3からR2への移行方法ももっとシンプルになりそうです。

また、今回は一つの事例として紹介しましたが、間違っている点やもっと良いやり方などあるかと思います。
まだ自分もCloudflareに触れてからまだ間もないので、誤りがありましたら適宜修正させていただきますので、遠慮なくツッコミをいただけるとありがたいです。

【2023-03-20追記】
LT内では触れることができませんでしたが、
本件の遂行にあたり、エンタープライズプランの契約の際に不明な箇所の対応方針について、Cloudflare Japan様から様々な技術的サポートをいただきました。
本件は一つの事例として共有させていただきましたが、この場を借りて改めてお礼申し上げます。

【2023-04-29追記】
Zone Apexの場合の移行時の注意点、R2のObject lifecyclesについてなどの追記を行いました。

31
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
15