ブログ執筆において、画像の用意と管理は最大の「重力」です。 キャプチャを取り、リサイズし、ストレージに上げ、URLを貼る。この摩擦が、書く意欲をじわじわと削いでいきます。
筆者は以前のブログ(Blogger)時代にこの問題を解決するシステムを構築し、現在のAstro製ブログでもその思想とコードを引き継いでいます。 この記事は、「ブログ執筆システムを自作・拡張しているエンジニア」を対象とした、摩擦ゼロの画像パイプライン構築ガイドです。
なぜ Cloudflare R2 なのか
最大の理由は 「配信コストが無料(Egress 0円)」 である点です。
S3互換APIが使えるため boto3 などの既存ツールがそのまま利用でき、どれだけアクセスがあってもストレージ料金しかかかりません。
また、「キャッシュコントロールの容易さ」 も大きな利点です。 AWS S3 + CloudFront の構成では、画像を削除した後にCDN側のキャッシュパージ(Invalidation)が必要で、これに追加料金やタイムラグが発生することがあります。 一方、R2はCloudflareのエッジネットワークと統合されているため、オブジェクトを削除すれば即座に取り下げが行われ、運用の手間が圧倒的に少なくなります。
セットアップの「急所」攻略
R2 の設定は S3 に似ていますが、Cloudflare 独自の UI 構造により、いくつか迷いやすいポイントがあります。
1. バケットの作成
受け皿となるバケットを作成します。
R2バケット作成時の推奨設定
名前を決め、位置情報を 「自動(アジア太平洋)」 、ストレージクラスを 「Standard」 に設定します。
2. 公開ドメインの接続
バケットの中身を外部に公開するため、ドメインを紐付けます。ここが最初の迷いどころです。
『R2 > バケット詳細 > 設定 > カスタムドメイン』から進むのが正解
Cloudflare のサイドメニューにある一般的な「ドメイン」から設定しようとすると、無関係な DNS スキャンなどが始まってしまいます。
正解は、「R2 > バケット詳細 > 設定 > カスタムドメイン」 という、バケット個別の設定画面から接続することです。
3. APIトークンの場所
スクリプトから操作するための「鍵」を発行します。このメニューはやや深い場所にあります。
APIトークン管理画面への入り口
R2 トップ画面の最下部にある 「Account Details」 セクションの右端、「Manage」 ボタンの中に隠れています。
4. 権限設定
発行画面では 「アカウント API トークン」 (上側)を選択します。
自動同期に必要な最小限の権限設定
ここで最も重要なのがアクセス許可です。デフォルトの「読み取り専用」ではアップロードができません。必ず 「オブジェクト読み取りと書き込み」 を選択してください。
5. 情報の確保
発行完了画面に一度だけ表示される「3点セット」を控えます。
発行された認証情報は即座にメモが必要
- アクセスキー 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")
自動化のワークフロー
セットアップが完了すれば、あとはパイプラインの構築です。 筆者は以下の運用を採用しています。
- 執筆: マークダウンの所定のフォルダ(またはクリップボードから貼り付け)に画像を置く。
- パス解決: ビルドスクリプトが画像を検知し、
のような相対パスを認識する。 - アップロード: 変更があった画像のみを R2 に自動アップロードする。
- リンク置換: 最終的なHTML(またはデプロイされるMarkdown)のリンクを
https://static.a3ro.cc/...に書き換える。
この仕組みによって、「画像のURL」を意識することなく、ローカルにある画像をただ貼るだけで執筆を完結できています。
まとめ
仕組みを整える目的は、常に一つ。「明日もまた、一行でも多く書くため」です。
Cloudflare R2 という低コストなインフラと、自動化の仕組みを組み合わせる。この工夫の積み重ねが、読者にとっても、書き手にとっても心地よい執筆環境を作り上げてくれるはずです。