Cloud Runの外部IPアドレスを固定する
概要 #
Cloud Run単体では固定の外部IPアドレスは存在せず、デプロイごと・インスタンスごとにIPアドレスが動的に変更される。
外部APIで特定のIPアドレスからのみリクエストを許可している場合、デフォルトのCloud Runだと要件を満たせずリクエストができない状態になってしまう。
全体像 #
flowchart LR
subgraph GCP[Google Cloud(同一リージョン)]
subgraph CR[Cloud Run サービス]
i1[インスタンス #1]
i2[インスタンス #2]
iN[インスタンス #N]
end
i1 -- "Direct VPC egress(すべてのトラフィック)" --> VPC[(VPC サブネット)]
i2 -- "Direct VPC egress(すべてのトラフィック)" --> VPC
iN -- "Direct VPC egress(すべてのトラフィック)" --> VPC
VPC --> Router[Cloud Router]
Router --> NAT[Cloud NAT]
NAT -- "固定送信元IPで外部へ" --> IPs[(静的 外部 IPv4(1..n))]
IPs --> Internet[インターネット]
end
Internet --> Partner[相手先サービス(IP許可リスト:静的IP群)]
- Cloud Run -> VPC (Direct VPC egress) -> Cloud NAT (静的外部IP) -> 外部API
- 外部API側で許可してもらうのはCloud NATに手動割り当てした静的
外部IPv4(リージョン)
事前確認 #
- リージョン: Cloud Run, サブネット, Cloud Router/NATを同一リージョンにする
- 今回は
asia-northeast1
- 今回は
- VPC/サブネット: どのネットワーク経由で出すか決めておく
defaultを使用する
- 権限:
compute.networkUserが必要
手順 #
VPCの設定 #
- 外部静的IPを予約する
- VPCネットワーク→IPアドレス→IPアドレスを予約
- 種別: 外部/IPv4
- スコープ: リージョン(Cloud NATと同じリージョン、asia-northeast1)
- Cloud Routerを作成
- ハイブリッド接続→Cloud Router→作成
- ネットワーク、リージョンを上記と合わせる
- Cloud NATを作成
- ネットワークサービス→Cloud NAT→作成
- NATタイプ:公開
- Cloud Routerの選択
- ネットワーク・リージョン: 上記と合わせる
- クラウドルーター: 上記で作成したものを指定
- Cloud NATマッピング
- VM インスタンス、GKE ノード、サーバーレス
- ソースIPバージョン: IPv4サブネット範囲
- 「すべてのサブネットのプライマリとセカンダリの範囲」を選択
- Cloud NAT IP アドレス: 手動を選択
- 上記で予約した静的IPを割り当て
Cloud Runの設定 #
vpc経由にする必要があるのでCloud Runサービス設定を変更
- 対象のCloud Runを開き「新しいリビジョンの編集とデプロイ」へ
- ネットワーキングタブを開く
- アウトバウンド トラフィック用のVPCに接続するをチェック
- ネットワーク・サブネットを上記と同じものへ
- トラフィックルーティング: 全てのトラフィックをVPCにルーティングする(Direct VPC egress)を設定
- アウトバウンド トラフィック用のVPCに接続するをチェック
- このリビジョンをすぐに利用するにチェックを行いデプロイ
- ネットワーキングタブを開く
確認方法 #
Cloud Runでテスト用のエンドポイントを作成し、そのエンドポイント上でhttps://curlmyip.orgへリクエストしレスポンスをチェックするのが楽
func (h *HealthHandler) TestExternalAPI(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(os.Stderr, "DEBUG: Getting external IP address\n")
resp, err := http.Get("https://curlmyip.org")
if err != nil {
fmt.Fprintf(os.Stderr, "DEBUG: Failed to get IP: %v\n", err)
http.Error(w, fmt.Sprintf("Failed to get external IP: %v", err), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "DEBUG: Failed to read response: %v\n", err)
http.Error(w, fmt.Sprintf("Failed to read response: %v", err), http.StatusInternalServerError)
return
}
externalIP := strings.TrimSpace(string(body))
fmt.Fprintf(os.Stderr, "DEBUG: External IP is: %s\n", externalIP)
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf("Cloud Run external IP: %s", externalIP)))
}これをhttp.HandleFuncにわたせばOkey
注意点 #
- stagingとproductionのCloud Runを分けている場合、VPC/サブネットを別にしないといけない
- サブネットだけ分離すれば同一VPCで行うことは可能
- 結論としてめんどくさいからVPCごと分離したほうが分かりやすい
- ただし作業量は増える→terraform化すれば問題なしだがね