ブログ執筆において、画像の用意と管理は最大の「重力」です。 キャプチャを取り、リサイズし、ストレージに上げ、URLを貼る。この摩擦が、書く意欲をじわじわと削いでいきます。

筆者は以前のブログ(Blogger)時代にこの問題を解決するシステムを構築し、現在のAstro製ブログでもその思想とコードを引き継いでいます。 この記事は、「ブログ執筆システムを自作・拡張しているエンジニア」を対象とした、摩擦ゼロの画像パイプライン構築ガイドです。

なぜ Cloudflare R2 なのか

最大の理由は 「配信コストが無料(Egress 0円)」 である点です。 S3互換APIが使えるため boto3 などの既存ツールがそのまま利用でき、どれだけアクセスがあってもストレージ料金しかかかりません。

また、「キャッシュコントロールの容易さ」 も大きな利点です。 AWS S3 + CloudFront の構成では、画像を削除した後にCDN側のキャッシュパージ(Invalidation)が必要で、これに追加料金やタイムラグが発生することがあります。 一方、R2はCloudflareのエッジネットワークと統合されているため、オブジェクトを削除すれば即座に取り下げが行われ、運用の手間が圧倒的に少なくなります。

セットアップの「急所」攻略

R2 の設定は S3 に似ていますが、Cloudflare 独自の UI 構造により、いくつか迷いやすいポイントがあります。

1. バケットの作成

受け皿となるバケットを作成します。

R2バケット作成画面:位置情報は『自動』、クラスは『Standard』でOK R2バケット作成時の推奨設定

名前を決め、位置情報を 「自動(アジア太平洋)」 、ストレージクラスを 「Standard」 に設定します。

2. 公開ドメインの接続

バケットの中身を外部に公開するため、ドメインを紐付けます。ここが最初の迷いどころです。

R2バケットの設定画面:『公開アクセス』セクションにある『カスタムドメイン』を探す 『R2 > バケット詳細 > 設定 > カスタムドメイン』から進むのが正解

Cloudflare のサイドメニューにある一般的な「ドメイン」から設定しようとすると、無関係な DNS スキャンなどが始まってしまいます。

正解は、「R2 > バケット詳細 > 設定 > カスタムドメイン」 という、バケット個別の設定画面から接続することです。

3. APIトークンの場所

スクリプトから操作するための「鍵」を発行します。このメニューはやや深い場所にあります。

APIトークンの隠れ場所:R2トップの最下部『Account Details』にあるManageボタン APIトークン管理画面への入り口

R2 トップ画面の最下部にある 「Account Details」 セクションの右端、「Manage」 ボタンの中に隠れています。

4. 権限設定

発行画面では 「アカウント API トークン」 (上側)を選択します。

権限設定の正解:『オブジェクト読み取りと書き込み』を選択し、管理者権限は不要 自動同期に必要な最小限の権限設定

ここで最も重要なのがアクセス許可です。デフォルトの「読み取り専用」ではアップロードができません。必ず 「オブジェクト読み取りと書き込み」 を選択してください。

5. 情報の確保

発行完了画面に一度だけ表示される「3点セット」を控えます。

秘密の鍵:発行直後にしか表示されない3つの重要情報(ID、シークレット、エンドポイント) 発行された認証情報は即座にメモが必要

  • アクセスキー ID
  • シークレットアクセスキー
  • S3エンドポイント (S3 API)

実装:システムへの組み込み

取得した情報を環境変数に落とし込み、プログラムから呼び出します。

1. .env の記述例

.env に、汎用的な形式で値を保存します。

# Cloudflare R2 Settings
R2_ACCESS_KEY_ID=xxx (アクセスキーID)
R2_SECRET_ACCESS_KEY=yyy (シークレットアクセスキー)
R2_ENDPOINT_URL=https://<account_id>.r2.cloudflarestorage.com
R2_BUCKET_NAME=my-bucket-name
R2_PUBLIC_URL_PREFIX=https://static.example.com/

2. 最小構成コード

boto3 を使って R2 に接続し、ファイルをアップロード・削除する完全な Python コードです。 依存ライブラリ (boto3, python-dotenv) をインストールした上で実行します。

uv pip install boto3 python-dotenv
# r2_upload.py
import boto3
import os
import mimetypes
from dotenv import load_dotenv

# .envファイルを読み込む
load_dotenv()

class R2Uploader:
    def __init__(self):
        # 環境変数が読み込まれているか確認
        if not os.getenv('R2_ENDPOINT_URL'):
            raise ValueError("R2 configuration not found in .env")

        self.s3 = boto3.client(
            's3',
            endpoint_url=os.getenv('R2_ENDPOINT_URL'),
            aws_access_key_id=os.getenv('R2_ACCESS_KEY_ID'),
            aws_secret_access_key=os.getenv('R2_SECRET_ACCESS_KEY'),
            region_name='auto'
        )
        self.bucket_name = os.getenv('R2_BUCKET_NAME')

    def upload(self, file_path, object_name):
        content_type, _ = mimetypes.guess_type(file_path)
        if content_type is None:
            content_type = 'application/octet-stream'

        try:
            self.s3.upload_file(
                file_path, 
                self.bucket_name, 
                object_name,
                ExtraArgs={'ContentType': content_type}
            )
            print(f"Uploaded: {object_name}")
            return True
        except Exception as e:
            print(f"Failed to upload {file_path}: {e}")
            return False

    def delete(self, object_name):
        try:
            self.s3.delete_object(Bucket=self.bucket_name, Key=object_name)
            print(f"Deleted: {object_name}")
            return True
        except Exception as e:
            print(f"Failed to delete {object_name}: {e}")
            return False

if __name__ == "__main__":
    # 使用例: ダミー画像を作成してアップロード→削除テスト
    uploader = R2Uploader()
    test_key = "uploads/test.png"
    
    # 1x1ピクセルのダミーPNGを作成
    dummy_png = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82'
    with open("test.png", "wb") as f:
        f.write(dummy_png)
    
    if uploader.upload("test.png", test_key):
        # 成功したら削除まで確認(クリーンアップ)
        uploader.delete(test_key)
    
    # ローカルファイルのお掃除
    os.remove("test.png")

自動化のワークフロー

セットアップが完了すれば、あとはパイプラインの構築です。 筆者は以下の運用を採用しています。

  1. 執筆: マークダウンの所定のフォルダ(またはクリップボードから貼り付け)に画像を置く。
  2. パス解決: ビルドスクリプトが画像を検知し、![alt](./image.png) のような相対パスを認識する。
  3. アップロード: 変更があった画像のみを R2 に自動アップロードする。
  4. リンク置換: 最終的なHTML(またはデプロイされるMarkdown)のリンクを https://static.a3ro.cc/... に書き換える。

この仕組みによって、「画像のURL」を意識することなく、ローカルにある画像をただ貼るだけで執筆を完結できています。

まとめ

仕組みを整える目的は、常に一つ。「明日もまた、一行でも多く書くため」です。

Cloudflare R2 という低コストなインフラと、自動化の仕組みを組み合わせる。この工夫の積み重ねが、読者にとっても、書き手にとっても心地よい執筆環境を作り上げてくれるはずです。