回線環境で挙動を分ける — Cloudflare Workerが使えない開発環境でのWordPress画像直接アップロードフォールバック

回線環境で挙動を分ける

システムの物理的な配置や回線環境(回線速度、ポート制限)によって、最適なデータフローは変わる。 今回は、本番環境の「細いモバイル回線」用の画像アップロード設計が、ローカル開発環境(太い回線)で不都合を起こしていた問題と、それを解消したフォールバック実装の話をする。


Concept Image

今回やったこと / この記事について

WordPress(netouyonews 等)に記事を投稿する際、本文中のR2画像URLをWordPressメディアライブラリに移植するスクリプト(wp_post.py)を改修した。 Cloudflare Worker が未設定の開発環境でも、WordPressの認証情報があれば直接ローカルから画像をダウンロード&アップロードして差し替えるフォールバック(_direct_r2_to_wp)を追加した。


スタジオの日常会話

Mix: 「Zashさん、開発用マシンで記事のテスト投稿をすると、画像がWordPressメディアに取り込まれず、R2の直リンクのままになっちゃいます。設定漏れですかね?」

Zash: 「あー、それは本番サーバー(M1)用の対策が裏目に出てる。M1はモバイル回線(上り)が細くて、直接WPにデカい画像を送ると524エラーでタイムアウトするから、Cloudflare Workerに中継(migrate)させてるんだ。でも開発機にはWorkerの設定をしてないから、素通りしてたな。」

Mix: 「なるほど。でも開発機の回線は十分太いから、直接WPにアップロードしちゃえば解決しますね!」

Zash: 「そうだな。環境変数がないなら手元で叩かせる。Worker未設定なら直接送る、っていう自動フォールバックを仕込もう。」


背景と課題:M1モバイル回線の「細さ」とWorkerの役割

私たちの本番データ収集サーバー(M1)は、接続環境の都合で上り回線の帯域が非常に狭い。 この環境から数MBの画像をWordPressメディアAPI(/wp-json/wp/v2/media)へ直接アップロードしようとすると、送信に時間がかかりすぎて 524 Gateway Timeout が頻発していた。

これを防ぐため、以下の非同期構成を取っている。

  1. 画像を一度軽量な R2 にアップロードする(上りは最小限で済む)。
  2. Cloudflare Worker(/migrate)に「このR2画像をWordPressにインポートしてくれ」とリクエストを投げる。
  3. Worker(強固なネットワーク)が非同期でR2から画像を落とし、WordPressメディアにアップロードし、差し替えたURLを返す。

これによって、M1サーバーは細い回線でも安定して画像をWPに移植できていた。


開発環境での「不都合」

しかし、この設計は「Worker(MATOME_IMAGE_INGEST_URL)が正しく設定されていること」が前提となる。

ローカル開発環境や、テスト用の別環境(M5のworktree等)では、通常この中継用Workerの設定(APIキーやエンドポイント)は用意されていない。その結果、画像移植スクリプトは警告を出して処理をスキップし、記事内の画像は R2の直リンクURLのまま投稿 されていた。

これでは、開発環境での動作検証が不完全になるだけでなく、本番へそのままコピーした際にR2へ無駄なアクセスが集中したり、後から画像リンクが切れるリスクが生じる。


解決策:直接アップロードへの自動フォールバック

開発マシンの回線は十分に太い(光回線や社内LAN)。それならば、Workerを中継せずとも、スクリプトが直接 「R2からダウンロード」➔「WordPressにアップロード」 を実行してしまえばいい。

そこで、wp_post.py に以下のフォールバック処理を実装した。

def _direct_r2_to_wp(content, urls, wp_url, wp_user, wp_pass, http) -> tuple[str, int, int]:
    """R2 画像を直接 DL ➔ WP メディアにアップロードして URL 差し替え(Worker 不要・太い回線向け)。
    Worker は M1 のモバイル上り 524 対策。compose は M5 実行なので直接アップロードで十分。"""
    uploaded = failed = 0
    for r2_url in urls:
        try:
            img = http.get(r2_url, timeout=30)
            if img.status_code != 200:
                failed += 1
                continue
            fn = (r2_url.rsplit("/", 1)[-1] or "img.webp").split("?")[0]
            ctype = img.headers.get("Content-Type", "image/webp")
            r = http.post(
                f"{wp_url}/wp-json/wp/v2/media", data=img.content,
                headers={"Content-Disposition": f'attachment; filename="{fn}"', "Content-Type": ctype},
                auth=(wp_user, wp_pass), timeout=120)
            if r.status_code in (200, 201) and r.json().get("source_url"):
                content = content.replace(r2_url, r.json()["source_url"])
                uploaded += 1
            else:
                print(f"    [WARN] WP media {r.status_code}: {r.text[:120]}")
                failed += 1
        except Exception as e:
            print(f"    [WARN] 直接アップロード失敗: {e}")
            failed += 1
    return content, uploaded, failed

そして、呼び出し元の upload_r2_images_to_wp で、Workerの設定がない場合にこのフォールバックを実行するように分岐させた。

    ingest_url = os.environ.get("MATOME_IMAGE_INGEST_URL", "").strip()
    ingest_key = os.environ.get("INGEST_KEY", "").strip()
    if not ingest_url or not ingest_key:
        if wp_url and wp_user and wp_pass:
            print(f"    [info] Worker未設定 ➔ R2画像{len(urls)}枚を直接WPメディアにアップロード")
            return _direct_r2_to_wp(content, urls, wp_url, wp_user, wp_pass, http)
        print("    [WARN] Worker未設定かつWP認証なし ➔ R2 URL のまま投稿")
        return content, 0, len(urls)

結果と学び

この改修により、環境ごとの挙動は以下のように綺麗に整理された。

実行環境Worker設定回線速度挙動
本番 (M1)あり細いWorker経由で非同期移植(524回避)
開発 (M5等)なし太いローカル直接DL&WPアップロード
認証なし検証なしR2 URL のままフォールバック保存

テスト環境で既存の下書き記事をパブリッシュしたところ、Worker未設定の状態でも、R2から落とされた画像が綺麗にWordPressのメディアライブラリへと移植され、記事本文の画像URLも正しく書き換わった。

ネットワークやインフラの制約をコードで解決しようとすると、つい一律の「美しい設計」を強制したくなる。しかし、現実の開発現場では「開発環境の手軽さ」と「本番環境の極限状態」を両立させなければならない。 「設定がないなら、手元にある太い回線で力技を通す」という泥臭いフォールバックこそが、システムの機動力を支えるネクタイの結び目のようなものだ。


更新履歴

  • 2026-06-13: 新規追加。開発環境での画像移植バグ調整ログとして公開。

訂正履歴

  • 現時点で訂正はありません。

ZashStudio 技術監修 2026-06-13