ポートフォリオサイトのコンテンツをCloudflare R2へ移行しました

2025/7/31

(※)2026年現在、こちらのブログは技術構成を変更しました。

移行の背景と目的

当サイトの一部のコンテンツ(経歴情報など)は、個人情報を含むため、当然githubでは管理できず、これまではローカルから手動でビルドと、デプロイをしていましたが、アップロードの回線速度が遅く、処理に時間を要し、かつ毎回の手動操作が運用上の負担でした。

これを解決し、デプロイをGitと連携して自動化するために、コンテンツ管理をCloudflare R2へ一部移行することにしました。

セキュリティ面でもR2にはメリットがあり、適切なアクセス制御を設定することで、コンテンツを非公開に保ちながら、必要な時だけアプリケーション経由でアクセスできるようになります。

本記事では、これを達成するために行ったR2移行の技術的な経緯や課題、そして解決策についてまとめます。

実装時の技術的課題

実際の移行作業では、想定以上に多くの技術的課題に直面しました。

初期問題OpenNext + R2接続の困難

OpenNextを使用してCloudflare R2に接続する際に、予想以上に深刻な問題が発生しました。 まずはスタンダードなCloudflare WorkersのR2バインディング(getCloudflareContext)を利用した実装を試みました。

const { env } = await getCloudflareContext();
const resume = await env.PORTFOLIO_ASSETS.get("resume.md");

しかし、実際にテストしてみると、

  • pnpm preview --remote 付き → ログが出力されずにハングアップ
  • pnpm preview --remote なし → [unenv] fs.readFile is not implemented yet! エラーが発生
  • 本番環境→バインディングが正常に動作

となり、環境ごとに動作が異なるという問題に直面しました。問題を詳しく調べてみると、以下のことが判明しました。

OpenNextとCloudflare Workersランタイムの互換性問題

  • OpenNextのリモートプレビュー機能と外部ネットワークリクエスト(S3 API)の相性問題

unenvライブラリの制限

  • Cloudflare Workers環境をエミュレートする際のNode.js API制限
  • AWS SDKが内部的にファイルシステムにアクセスしようとしている(おそらく)

解決策の模索 S3互換APIへの切り替え

次にgetCloudflareContext()を使わずS3互換APIを使用することを決定しました。 まず、標準的なAWS SDKを使用した実装を試みました

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";

const s3Client = new S3Client({
  region: "auto",
  endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
  },
});

しかし、これも同じ[unenv] fs.readFile is not implemented yet!エラーが発生しました。多分、というか自信はないですがAWS SDKが内部的にファイルシステムにアクセスしてそうでした。

そこで、直接fetch APIを使ってR2にアクセスしようとしました

// 認証なしの直接アクセス(失敗)
const response = await fetch(`${R2_BUCKET_URL}/resume.md`);
// → 403 Forbidden

この方法が失敗した理由は、s3Clientを使わず、fetchをそのまましようとする場合、AWS署名v4による認証が必要だったからみたいです。

aws4fetchによる最終的な解決

最終的に、AWS署名v4に対応した軽量ライブラリaws4fetchを導入することで問題を解決できました。

A compact AWS client and signing utility for modern JS environments - mhart/aws4fetch
github.com
https://github.com/mhart/aws4fetch
https://opengraph.githubassets.com/443fe5728529f52f609463cf7f0cdaf57a51ebf545d8123e6290aef574cff808/mhart/aws4fetch

aws4fetchは、なんか複雑で正直よくわからん、署名部分をとてもいい感じにしてくれるライブラリです。

実装例

aws4fetchにより、複雑な署名処理が完全に隠蔽され、シンプルな実装が可能になりました

import { AwsClient } from "aws4fetch";

const r2Client = new AwsClient({
  accessKeyId: process.env.R2_ACCESS_KEY_ID,
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
  region: "auto",
});

// ファイルアップロード
const uploadToR2 = async (key, file) => {
  const url = `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET_NAME}/${key}`;

  const response = await r2Client.fetch(url, {
    method: "PUT",
    body: file,
  });

  if (!response.ok) {
    throw new Error(`R2 Upload Error: ${response.status}`);
  }

  return response;
};

const getFromR2 = async (prefix) => {
  // ListObjectsV2で最新バージョンを取得
  const listUrl = `${baseUrl}?list-type=2&prefix=${prefix}_`;
  const listResponse = await r2Client.fetch(listUrl);

  // タイムスタンプでソートして最新を特定
  const latestKey = parseLatestVersion(await listResponse.text());

  // 最新ファイルを取得
  const contentResponse = await r2Client.fetch(`${baseUrl}/${latestKey}`);
  return contentResponse;
};

解決された問題

この実装により、以下の問題がすべて解決されました

  1. OpenNextとの互換性問題 → 根本解決
  2. 開発・本番環境での一貫性 → 同じコードで動作
  3. 軽量で依存関係が少ない → バンドルサイズ削減
  4. Cloudflare Workers環境での安定動作 → unenvエラー回避

なお、CORS設定や認証情報の管理は、Cloudflareのダッシュボードと環境変数で行います。

まとめ

Cloudflare R2への移行により、GitHubと連携した自動デプロイが実現でき、開発効率が大きく向上しました。 課題は多かったものの、S3互換APIを利用することで、環境間の差異を意識することなく安定したコンテンツ配信が可能になりました。

特に、バインディングの環境依存問題は想定以上に複雑でしたが、結果オーライで、汎用性の高いアーキテクチャにたどり着けたと思います。

今後は、この構成をベースに、より柔軟なコンテンツ管理システムを構築していきたいです。

ではまた。


Recent Posts