Cloudflare Workers + R2環境移行時に遭遇した小ネタ


こちらは[Cloudflare Advent Calendar 2023](https://qiita.com/advent-calendar/2023/cloudflare) の14日目の記事になります。

今年、大変お世話になったCloudflareということで Advent Calendarに軽い気持ちで申し込んでしまったのですが、ネタ（HonoとD1とBrowser Renderingなどを使ってゴニョゴニョしたい）の仕込みが間に合わなかったので、
半年前にやっていたCloudflare R2移行の小ネタの紹介とさせてください。

---

# Cloudflare Workers + R2環境への移行について

CloudFront + S3環境から Cloudflare + R2に移行することになった経緯については、
以下の前回の記事とスライドを参照いただければと思います。

https://qiita.com/teckl/items/4314aa210a689d08e759

https://speakerdeck.com/teckl/cloudflare-workers-r2-migration

今回はこの中で紹介していなかったちょっとした小ネタや失敗談について4つほど紹介させていただきます。
（あまり技術的な内容ではないのであしからず。。）

なお、上記記事の内容は 2023-03 に移行したサービスでの話がメインでしたが、
本記事はその後の 2023-04 に移行した別のサービスでのお話になります。

---

# 小ネタ1. スマホ用画像の対応について
元々、このサービスでは以下のような画像URLになっていました。

1枚の画像をアップロードすると、以下のように4種類の画像がS3上にアップロードされるようになっていました。

- 元画像
    - https://image.example.jp/s/e/sample/aaaa.jpg
- 元画像のサムネイル画像
  - https://image.example.jp/s/e/sample/aaaa-s.jpg
- ガラケー・スマホ用画像（lite画像1）
    - https://image.example.jp/lite/s/e/sample/aaaa.jpg
- ガラケー・スマホ用画像のサムネイル画像（lite画像2）
    - https://image.example.jp/lite/s/e/sample/aaaa-s.jpg

## 4つの画像が存在している経緯
1つの画像アップロードにつき4種類の画像が存在していた理由ですが、
20年近く前から存在しているサービスのため、
この `/lite/` 画像は当時のガラケー用の画像やスマホ専用として設計されたものかと思います。

「Cloudflare Workers + R2環境」に移行する前は「CloudFront + S3環境」で、
その更に前はオンプレ環境で動いていました。

その当時はまだ2つの静的画像のみで、 `/lite/` の画像については動的にリサイズを行って表示を行っていました。

### オンプレ時代
オンプレ時代の lite画像ですが、パスが `/lite/` の場合、
Nginx側で
`ngx_http_image_filter_module` 
というモジュールを使って動的にリサイズを行っていました。

https://nginx.org/en/docs/http/ngx_http_image_filter_module.html

```conf
    image_filter_buffer 2M;
    proxy_max_temp_file_size 1000m; #default
    proxy_ignore_headers Cache-Control;
    proxy_buffers 8 64k;
    proxy_buffer_size 64k;

    location /lite/ {
        #add_header X-My-Header "$host";

        alias "$data_dir_by_host";

        set $max_width 200;
        image_filter resize $max_width '-';
        expires 30d;

        error_page 415 = @empty;
    }

    location @empty {
        empty_gif;
    }
```

### CloudFront + S3時代

その後、オンプレのファイルサーバ運用がいろいろと大変になったため、CloudFront + S3に移行する事になりました。
S3ではNginxの動的リサイズのような事が出来ないため、
元画像がS3アップロードされるとトリガーでAWS Lambda 側でリサイズを行い、
自動でS3の `/lite/` 配下のパスにリサイズした画像を設置するようにしていました。

### Cloudflare + R2時代
今回、Cloudflare + R2環境に移行するにあたり、
この `/lite/` 画像をどうするべきか悩みました。

素直に考えるとS3のバケット構成をそのままR2に持っていけばいいのですが、
そのままだと AWS Lambda のようなトリガー起動でリサイズの仕組みは難しそう…
かと言ってサーバ側でリサイズして4種類の画像をPUTするのもなんか微妙だしなぁ…
と、Workers側でなんとかリサイズ出来ないかと調べていると、自分と同じ悩みの方が見つかりました。

https://community.cloudflare.com/t/image-resize-from-r2-bucket-source/481816

R2バケットから取得する際にリサイズも出来ると最高なのですが、オブジェクトストレージの仕組み上やはり難しく、
[Cloudflare Image Resizing](https://developers.cloudflare.com/images/image-resizing/) を使うしか方法が無さそうでした。

Cloudflare Image Resizingは個人的には使ってみたいものの、今回Cloudflare + R2に移行を決めた最大の目的がコスト削減であったため、諦めて別の方法を検討しました。

### 結局どうしたのか？
結論から言うと、 `/lite/` の画像を廃止して、Workers でURL自体はマッピングして元画像を表示する事にしました。

```javascript
	if (objectKey.match(/lite\//i)) {
	  // Lite images are designed to display the original image.
	  objectKey = objectKey.replace("lite/", "");
	}
  
	// If not in cache, get it from R2
	const object = await env.MY_BUCKET.get(objectKey);
```

廃止を選択した理由は以下になります。

- ガラケーのサポート自体を終了したこと
- スマホ用としても200pxのような低画質の `/lite/` の役割は既に終わったと判断
    - 現代ではiPhone 3Gの頃のようなパケット制限の厳しい時代ではなくなっていること
- lite廃止によってR2のオブジェクト容量が削減可能になり、よりコスト削減効果が見込めること

これにより、サーバ側の（移行期間中のダブルライトを含む）アップロード処理もだいぶシンプルにすることができ、コスト削減にもなりました。
また、最初はこの変更によってユーザクレームが来たらどうしようかとちょっと考えてはいましたが、現在になってもこの件についての問い合わせは来ていないので、廃止した判断は良かったと考えています。

なお、S3 からR2への移行自体は rclone の `--exclude` で素直に除外する事ができました。

```
$ rclone copy -v  --exclude 'lite/'  s3:jp.example.image/  r2:jp-example-image/
```

---

# 小ネタ2. 【失敗談】 PV計測タグの対応について

こちらはCloudflare Workers + R2移行の失敗談です。
このサービスはレガシーなサービスのため、各オーナー毎へのアクセス解析機能として、
いわゆる小さな1x1の透明GIFを用いて、サーバ側で集計してアクセス解析を提供していました。

こちらもCloudFront経由でオリジンサーバにリクエストを送信していたため、
当初は素朴にCloudflare Workersで同じような事をしようと考えました。

この集計用のgifは以下の様なURLになっています。

`https://xxx.example.jp/img/imp.gif`

計測用のタグのため、CDN側にキャッシュされては困ってしまうため、
キャッシュはしたくない、また、確実にオリジンの集計サーバに届いてほしいため、
以下のようにしました。

カスタムドメインで配信している場合、デフォルトだとGIFなども含めてキャッシュが有効になっています。
https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#default-cached-file-extensions

これを避けるため、Page Rulesで、Cache Levelを `Bypass` にすることでキャッシュさせないように設定しました。

![PageRule_example.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28477/ceb5ecc3-e739-291a-ca0e-95626d4fb77b.png)

そしてWorkers側では以下のようにオリジンの集計サーバにホストを書き換えてリクエストを送信するようにしました。

```javascript
	if (objectKey.match(/img\/imp\.gif/)) {
	  // imp.gif cache is not cached by Page Rules.
	  url.hostname = 'xxx.example-origin.jp';
	  url.protocol = 'http:';  // origin host is http only
	  return fetch(url.toString(), request);
	}
```

このアイデアは、公式の Bulk origin override という例を見て設定してみたものになります。

https://developers.cloudflare.com/workers/examples/bulk-origin-proxy/

これらにより、これまで通り、Cloudflare CDN経由で別オリジンに対してimpを計測することが出来るようになったのですが、
よく考えてみるとこれは実現自体は出来るものの、悪手でした。
その理由としては以下のようになります。

- キャッシュさせたくない、かつオリジンにプロキシしたい場合にはWorkersを通す必要は無いのでは？
- 計測タグはそれなりのリクエスト数となるため、無理にWorkersを使う必要性は…？
    - Workersのリクエスト料金は非常に安いとは言え、件数が増えるとそれなりのコストになってしまう
- 透明gif自体はオリジンサーバが返すため、R2に置く必要も無かった

ということで、上記のWorkers + Page Rules でのプロキシ案はボツとなってしまいました。

ただ、一応こういう事も出来はしました、という備忘録として書いてみました。

---

# 小ネタ3. 【失敗談】共通の静的アセットのWorkers利用は不要かもしれない件
こちらも失敗談です。
前回の記事やスライドで紹介した例は、ユーザさんが任意でアップロードしたファイルのCloudflare Workers + R2による画像配信の成功例でしたが、
こちらで紹介するのはサービス共通で使う静的アセットファイル（共通のcss,js,画像等）の例となります。

移行を検討していた当初は、既存のS3にあるものはすべてをR2に移行して、Workersで配信しようと考えていました。
ところが、よくよく考えてみると、無理にWorkersで配信するのは微妙ではないかという事になりました。

結果として共通の静的アセットは、Workersを使って配信しなくても良いのではないかということになりました。
理由としては以下となります。


- 移行検討当時はR2は直接公開が行えず、Workersを使って公開させることが出来なかったためWorkersは必須だった
    - しかし現在ではR2は公開バケット機能と、カスタムドメイン機能が可能となっている
        - この公開バケットを利用すればWorkers無しでも十分Cloudflare CDNの恩恵は受けられる
        - 細かいキャッシュ制御なども不要で、シンプルに全公開すれば良いため
- 静的アセットのようなファイルの場合、Workersを使っての複雑なルーティングをしなくて済む
- 先程の小ネタ2.同様に、Workersを使う必要がない場合は素直に公開バケットを使った方がコスト的にも優しい

ということで、当初はR2にあるファイルはなんでもWorkersで配信しようと考えていたのですが、
結果としてWorkersを使うべき箇所は適材適所にやるのが良いのかなと考えたものとなります。

多くの場合はR2の公開バケット機能だけで大体のことは出来てしまうかもしれません。

https://dev.classmethod.jp/articles/cloudflare-r2-ga/

---

# 小ネタ4. R2移行後の最新状況について

前回の記事からのアップデートです。

## ライフサイクルルールの設定について

ねんがんのR2のライフサイクルルールですが、以前はベータ版が準備中でしたが、その後無事にGAされたため、現在は削除時のゴミ箱用のR2バケットに一定期間経過後に自動で削除されるように設定させていただいています。
これはありがたい。。

https://developers.cloudflare.com/r2/buckets/object-lifecycles/

## R2のAPIトークンの制限について
R2のAPIトークンですが、3月当時はバケット単位でのトークン制限は行えなかったはずですが、
現在はバケット単位の制御などが細かく行えるようになっていました。
また、パーミッションもAdminとObjectの権限が分かれて、より細かく扱うことが出来そうです。
これはありがたい。。

https://community.cloudflare.com/t/r2-token-per-bucket/389067/12

## R2 への移行方法について
当時は、R2への移行は R2 Migrator(R2 Super Slurper) と rclone の2択だったと思うのですが、
現在ではSippyという方法もあるようです。（まだオープンベータのようなのでこれからに期待）

https://developers.cloudflare.com/r2/data-migration/

https://developers.cloudflare.com/r2/data-migration/sippy/

また、「プログレッシブ移行」というやり方もあるようで、
これを使うとGoogle Cloud Storage (GCS) やS3から段階的に移行が行えるようです。

https://qiita.com/khayama/items/51a6817281f6049ca463

一度に移行するといろいろな罠があるので、既存の環境を残しつつ段階的に移行を試すことができそうで、
このプログレッシブ移行は有力そうですね…！

---

# まとめ

Cloudflare Workers + R2移行時にあった小ネタと失敗談、R2の最新状況などを紹介させていただきました。
もっとこうすればよかったのでは？などのツッコミがあればいただけるとありがたいです。

Cloudflare Workers + R2ですが、当時あったいくつかの問題点もこの半年で日々解決され、
かなり使いやすい状態になってきているのを感じています。
コスト的に非常に優秀であり、可用性・耐久性・機能面でもかなりこなれてきているのを実感するので、もっと多くの人がCloudflare Workers + R2環境を使ってほしいなぁと思いました。

最近は別件の移行対応が佳境になっており、あまりCloudflare Workers関連をチェック出来ていませんでしたが、日々進化が激しくめっちゃ楽しそうなのでまたいろいろと個人的にも触ってみようと思います。