Language: 日本語
「コードは正しいのに動かない?」 Cloud Run への環境移植で踏み抜いた 4 つのランタイムトラップ
既存システムや AI ワークフローを新しいクラウド環境(Google Cloud / Cloud Run)へ移植するとき、**「TypeScript の型も通り、ロジックも完全に一致しているのに、なぜか本番環境で動かない……」** という不可解な壁に突き当たることがあります。
既存システムや AI ワークフローを新しいクラウド環境(Google Cloud / Cloud Run)へ移植するとき、「TypeScript の型も通り、ロジックも完全に一致しているのに、なぜか本番環境で動かない……」 という不可解な壁に突き当たることがあります。
ロジック自体の検証や「もっともらしい推測値」への対処(『「型は通るのに品質が狂う」を防ぐ。AI ワークフロー移植で混入する「もっともらしい推測値」の検証法』)とは別に、移植元のコードをどれだけ睨んでも絶対に気づけない「実行環境そのものの差異・落とし穴」 がいくつも潜んでいました。
この記事では、Cloud Run への環境移行で実際に踏み抜いた 4 つのランタイムトラップ(WIF 認証停止、ffmpeg の OOM、Internal Ingress 遮断、コンテナ依存バイナリの欠落) と、その根本的な解決策を解説します。
得られること
- リポジトリ名変更で Workload Identity Federation が全拒否されるデッドロックと復旧手順がわかる
- Cloud Run での ffmpeg 等の重い処理で OOM Kill と状態のハングを防ぐリソース設計が学べる
- Cloud Run 間通信で Internal Ingress と VPC Egress の罠を回避し、IAM 認証に寄せる設計が身につく
- コンテナイメージ内の依存バイナリ・フォントをビルド時に保証する契約テスト(Contract Test)の手法がわかる!
1. リポジトリ改名で WIF が全拒否されるデッドロック
GitHub のリポジトリ名を変更した直後、GitHub Actions から Google Cloud への Workload Identity Federation(WIF)認証が 100% 失敗し、CD パイプラインが完全に停止 しました。
原因: Attribute Condition の不一致
Terraform 等で WIF を構築する際、セキュリティのために GitHub Actions のリポジトリ名を条件に設定します。
# WIF Pool Provider の条件式
assertion.repository == 'my-org/old-repo-name'
リポジトリ名をリネームしたことで、GitHub Actions が発行する OIDC トークンの assertion.repository クレームが新しい名前に変わり、Google Cloud 側がすべてのトークンを「不正なアクセス」として即座に拒否するようになったのが原因でした。
解決策: 帯域外での先行修正とコード追従
「CD パイプラインが認証エラーで動かないため、Terraform で WIF の条件を更新する CD も走らない」という 完全なデッドロック(鶏と卵の問題) に陥ります。
【 デッドロックからの復旧手順 】
1. 帯域外(gcloud CLI / Web コンソール)で WIF の attribute condition を手動更新
2. CD パイプラインの認証を通す
3. Terraform コードの定義を新しいリポジトリ名に追従させてコミット
4. 次回以降の Terraform CI/CD が正常にパスすることを確認
リポジトリや Organization のリネームを行う際は、WIF や IAM の条件式にリポジトリ名がハードコードされていないかを事前に洗い出す必要があります。
2. ffmpeg の 512 MiB OOM Kill と実行ステータスのハング
動画生成ワーカーがレンダリング処理を実行中、何のエラーログも残さずにコンテナが突然死する現象が発生しました。
原因: デフォルトメモリ不足と状態遷移のデッドロック
Cloud Run のデフォルトメモリ設定は 512 MiB です。動画のエンコードや ffmpeg を実行するには圧倒的にメモリが不足しており、Linux カーネルによって OOM Kill(強制終了) されていました。
さらに深刻だったのは、アプリケーションの状態管理です。 ワーカーがクラッシュして再送リクエストが飛んできても、ステートマシン側で「既に RUNNING 状態のタスクは多重実行防止ガードにより拒否する」というフェイルセーフが働いていたため、タスクが永遠に RUNNING のまま止まってしまう という二次災害を引き起こしていました。
解決策
- Cloud Run のメモリと CPU を適切に引き上げ(例: 2 GiB / 2 vCPU など実ワークロードに耐えうる値へ拡張)。
- ワーカーの起動ヘルスチェックと、OOM や異常終了時にタスクを TIMEOUT / FAILED へ適切にフォールバックさせるハートビート監視を導入。
3. Cloud Run 間通信が Google Frontend で 404 遮断される
内部マイクロサービスとして Cloud Run A から Cloud Run B を直接 HTTP 呼び出ししようとしたところ、Google Frontend から 404 Not Found(または 403 Forbidden)で即座に通信が遮断 されました。
原因: Ingress 設定と VPC Egress の不整合
- 呼び出し先(Cloud Run B)の Ingress を
Internal Only(内部トラフィックのみ許可) に設定していた。 - しかし、呼び出し元(Cloud Run A)に Direct VPC Egress や Serverless VPC Access Connector が設定されていなかった。
Cloud Run A からの発信トラフィックはパブリックな Google Frontend を経由するため、Cloud Run B から見ると「外部インターネットからのアクセス」と判定され、Ingress フィルタによって門前払いされていました。
【 通信遮断の構造と解消 】
Before (遮断):
Cloud Run A(VPC Egress なし)
──→ Google Frontend(パブリック経由)
──→ Cloud Run B(Internal only Ingress) ❌ 「外部アクセス」と判定され遮断
After (IAM に寄せる):
Cloud Run A
──→ Google Frontend ──→ Cloud Run B(All Ingress + IAM 認証 🔒)
※ OIDC ID Token を付与し、roles/run.invoker 権限のみで厳格に認可
解決策: ネットワークの場所ではなく IAM 認証に寄せる
サーバーレス環境において、ネットワークトポロジだけでアクセス制御を完結させようとすると構成が複雑化しトラブルの温床になります。
そこで、Cloud Run 間のアクセス制御を 「Cloud Run の Service-to-Service 認証(IAM の roles/run.invoker と OIDC ID Token)」 に一本化しました。これにより、不要な VPC コネクタのコストや設定の不整合を排除し、アイデンティティベースの強固なセキュリティ境界を確立しました。
4. コンテナイメージ内の依存バイナリ・フォント欠落
新しい Studio 用コンテナイメージをデプロイしたところ、実行時に ffmpeg: command not found や、日本語字幕がすべて文字化け・豆腐化するバグが発生しました。
原因: ランタイム依存の未定義
ベースイメージを軽量化(Alpine や distroless)した際、実行設定が要求する ffmpeg、ffprobe、およびフォントパッケージ(Noto Sans 等)が含まれていませんでした。
解決策: コンテナビルド時の「契約テスト(Contract Test)」
「動かしてみて初めてバイナリがないことに気づく」のを防ぐため、Dockerfile のビルドステップおよび CI で、コンテナが満たすべきランタイム契約(Runtime Contract)を自己検証 するテストを組み込みました。
# 必要なバイナリとフォントの配置
RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
# ビルド時・CI での契約検証(実行可能・読み取り可能であることをテスト)
RUN ffmpeg -version > /dev/null 2>&1 || (echo "❌ ffmpeg not executable" && exit 1) \
&& ffprobe -version > /dev/null 2>&1 || (echo "❌ ffprobe not executable" && exit 1) \
&& fc-list : lang=ja | grep -q "Noto" || (echo "❌ Japanese fonts missing" && exit 1)
この契約テストにより、ベースイメージの更新やリファクタリング時にもランタイム依存の欠落をビルド時に 100% 検知できるようになりました。
まとめ: 環境の差分も、移植の差分である
システムの移植作業では、ソースコードのロジックばかりに意識が向きがちですが、「コードが動くランタイム環境の前提条件」を合わせることこそが最も難易度の高い部分 です。
- WIF の条件式: リポジトリ名やブランチ名がハードコードされていないか確認する
- リソースと状態管理: ワークロードに応じたメモリサイジングと、OOM 時のフェイルセーフを用意する
- 認証境界の設計: 複雑なネットワーク Ingress よりも、IAM と ID Token によるアイデンティティ制御に寄せる
- ランタイム契約の自己テスト: コンテナが必要とするバイナリやフォントは、ビルド時に機械的にテストする
「動くはず」という思い込みを捨て、実行環境の境界と契約を 1 つずつ明示的に検証していくことが、クラウド環境への安全な移行を成功させる近道です。