はじめに
ふるさと納税のためだけに、NFC端末を導入しようか悩む今日この頃……
どうも、DX開発事業部の小山です。
今回はGoogle Cloudサービスの中でも、Googleの管理するVPC内に所属するサービスをNAT経由でインターネットに接続させる方法について書きたいと思います。
そもそもGoogleの管理するVPC内に所属するサービスって何があるの?
結構ありますが……代表的なもので言うと「Cloud Run」「App Engine」「Cloud Build」等があります。
これらのサービスはその他のGoogle Cloud上のリソース、またはインターネット経由で外部のリソースとアクセスする必要がしばしばあります。
今回はCloud Buildを例に挙げますが……当サービスは外部のGitHubリポジトリと通信する必要があります。
とはいえセキュリティは大事ですよね
ですがやはりインフラエンジニアとしてはパブリックな空間にポン出しデプロイは避けたいところ。
一般的にはプライベート空間に隔離しつつ、インターネット通信を含む外部への通信はNAT経由で実施というやり方を取ります(Cloud Buildだと、プライベートワーカープールという仕組みでプライベートな空間にCloud Buildワーカーを置いておける)
Google CloudにはCloud NATというNAT用のサービスが存在するため、自分は最初それを使えばいいじゃないか!と考えていました。
しかし現実はそう甘くはなかった
先ほどのお話の繰り返しになりますが、Cloud Buildはプライベートワーカープールという仕組みでワーカーをプライベートな空間に置きます。そのままだとパブリックなIPを持たず、プライベートな空間に隔離されたワーカープールはインターネットと通信ができません。そこでユーザー側で作成したVPC(以後VPC-Aと呼称します)とピアリング接続を介して接続し、VPC-A内のCloud NATにアクセスすることでインターネットと接続できるようにします。
ここで問題が発生します。
上記公式ドキュメント曰く
「Cloud NAT ゲートウェイは、単一のリージョンと単一 VPC ネットワーク内のサブネット IP アドレス範囲にと関連付けられています。ある 1 つの VPC ネットワークで作成された Cloud NAT ゲートウェイは、VPC ネットワーク ピアリングで接続された他の VPC ネットワーク内の VM に、NAT を提供できません。ピア ネットワーク内の VM がゲートウェイと同じリージョンに存在する場合も同様です。」
要するに、Cloud BuildプライベートワーカープールはCloud NATを使えないことが判明しました……
本当にNATは不可なのか?
結論から言うとそれは違います。
確かにCloud BuildプライベートワーカープールはCloud NATを使えません。
ですが、NAT自体は他にもやり方があります。
そう、ピアリング接続したVPCにCompute Engineを建ててNATインスタンスを作ればいいのです!
「静的外部 IP を使用してプライベート ネットワーク内の外部リソースにアクセスする」
丁度誂え向きにNATインスタンスを利用して外部と通信する際の作法について書かれている公式ドキュメントを発見しました。
これは試すしかない!!というわけで早速実証してみました。
実証してみた
というわけで、用意したのが以下のcloudbuild.ymlです。
# Cloud Build Private Pool NAT接続テスト
# 目的: NAT経由でインターネット接続できるか確認
steps:
# ステップ1: ルーティングテーブル確認
- name: 'busybox'
id: 'check-routing'
entrypoint: 'sh'
args:
- '-c'
- |
echo "=== Routing Table ==="
route -n
echo "=== Ping NAT instance (10.0.1.2) ==="
ping -c 3 10.0.1.2 || true
echo "=== Ping default gateway ==="
GATEWAY=$$(route -n | grep '^0.0.0.0' | awk '{print $$2}')
ping -c 3 $$GATEWAY || true
timeout: '30s'
# ステップ2: 送信元IPアドレスの確認
- name: 'gcr.io/cloud-builders/curl'
id: 'check-source-ip'
args:
- '-s'
- 'https://ipinfo.io'
timeout: '30s'
# ステップ3: 結果サマリー
- name: 'ubuntu'
id: 'test-summary'
entrypoint: 'bash'
args:
- '-c'
- |
echo "========================================="
echo "NAT接続テスト完了"
echo "========================================="
echo "✓ 送信元IP: 上記ステップ2で確認"
echo ""
echo "※ 送信元IPがNATインスタンスの外部IPと一致していればNAT経由で接続できています"
echo "========================================="
# タイムアウト設定
timeout: '300s'
https://ipinfo.ioにアクセスすることで、このワーカーはどのIPアドレスで外部通信しているのかが判明します。
その時にNATインスタンスと化したCompute Engineの外部IPアドレスが出てくれば、正しくNAT経由で外部通信をしていることが実証されます。
(そもそも通信できている時点でNATが機能していることになるのですが、そこはご愛嬌……)
カスタムルートもその他のリソース → NATとNAT → インターネットの2つを用意!

NATインスタンスのIPは「34.85.59.7」ですね。
この値が返ってくれば、疎通確認は成功です!

さて、では早速以下コマンドを実行して動かしてみましょう……!!
gcloud builds submit \ --config=cloudbuild.yaml \ --worker-pool=projects/PROJECT_ID/locations/REGION/workerPools/POOL_NAME \ --no-source
---------------------------------------- REMOTE BUILD OUTPUT ---------------------------------------- starting build "42e7b581-932a-426b-adbc-feaef5801e8a" FETCHSOURCE BUILD Starting Step #0 - "check-routing" Step #0 - "check-routing": Pulling image: busybox Step #0 - "check-routing": Using default tag: latest Step #0 - "check-routing": latest: Pulling from library/busybox Step #0 - "check-routing": Digest: sha256:b3255e7dfbcd10cb367af0d409747d511aeb66dfac98cf30e97e87e4207dd76f Step #0 - "check-routing": Status: Downloaded newer image for busybox:latest Step #0 - "check-routing": docker.io/library/busybox:latest Step #0 - "check-routing": === Routing Table === Step #0 - "check-routing": Kernel IP routing table Step #0 - "check-routing": Destination Gateway Genmask Flags Metric Ref Use Iface Step #0 - "check-routing": 0.0.0.0 192.168.10.1 0.0.0.0 UG 0 0 0 eth0 Step #0 - "check-routing": 192.168.10.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 Step #0 - "check-routing": === Ping NAT instance (10.0.1.2) === Step #0 - "check-routing": PING 10.0.1.2 (10.0.1.2): 56 data bytes Step #0 - "check-routing": Step #0 - "check-routing": --- 10.0.1.2 ping statistics --- Step #0 - "check-routing": 3 packets transmitted, 0 packets received, 100% packet loss Step #0 - "check-routing": === Ping default gateway === Step #0 - "check-routing": PING 192.168.10.1 (192.168.10.1): 56 data bytes Step #0 - "check-routing": 64 bytes from 192.168.10.1: seq=0 ttl=64 time=0.091 ms Step #0 - "check-routing": 64 bytes from 192.168.10.1: seq=1 ttl=64 time=0.090 ms Step #0 - "check-routing": 64 bytes from 192.168.10.1: seq=2 ttl=64 time=0.095 ms Step #0 - "check-routing": Step #0 - "check-routing": --- 192.168.10.1 ping statistics --- Step #0 - "check-routing": 3 packets transmitted, 3 packets received, 0% packet loss Step #0 - "check-routing": round-trip min/avg/max = 0.090/0.092/0.095 ms Finished Step #0 - "check-routing" Starting Step #1 - "check-source-ip" Step #1 - "check-source-ip": Already have image (with digest): gcr.io/cloud-builders/curl Step #1 - "check-source-ip": Step #1 - "check-source-ip": ***** NOTICE ***** Step #1 - "check-source-ip": Step #1 - "check-source-ip": Supported `curl` versions can be found in the various images available at Step #1 - "check-source-ip": https://console.cloud.google.com/launcher/details/google/ubuntu1604. Step #1 - "check-source-ip": Step #1 - "check-source-ip": ***** END OF NOTICE ***** Step #1 - "check-source-ip": Finished Step #1 - "check-source-ip" ERROR ERROR: build step 1 "gcr.io/cloud-builders/curl" failed: context deadline exceeded ----------------------------------------------------------------------------------------------------- BUILD FAILURE: Build step failure: build step 1 "gcr.io/cloud-builders/curl" failed: context deadline exceeded ERROR: (gcloud.builds.submit) build 42e7b581-932a-426b-adbc-feaef5801e8a completed with status "FAILURE"
……エラーが出ましたね……
curlはタイムアウトしていますし、NATインスタンスへのpingが通っていないということは、NAT経由で外部通信ができていないということになります。
おかしいですね……
試しにテスト用のCompute Engineを建てて、curl https://ipinfo.ioしてみましょう。
![]()
……成功していますね……
ちゃんとIP(34.85.59.7)が返ってきたということは、NAT経由で外部通信ができているということになります。
テスト用のCompute Engineは外部通信出来ていて
プライベートワーカープールは外部通信出来ていない。
これはおかしいですね…………
困った時は公式ドキュメント
「静的外部 IP を使用してプライベート ネットワーク内の外部リソースにアクセスする」
先ほども紹介した公式ドキュメントに気になる記述が。
「注: プライベート プールから NAT ゲートウェイ VM にすべてのトラフィックをインターネット(0.0.0.0/0)に転送する場合、4 つの異なるルーティング ルールを指定する必要があります。0.0.0.0/0 アドレス空間を模倣するには、0.0.0.0/1 と 128.0.0.0/1 の両方に範囲を設定する必要があります。」
改めて、カスタムルートを見てみましょう。

送信先IP範囲が0.0.0.0/0のものしかないですね……
どうやら原因はここみたいなので、その他のリソース → NATとNAT → インターネットの2つのルートにそれぞれ0.0.0.0/1と128.0.0.0/1の送信先IP範囲をもつカスタムルートを用意します。

修正も完了したので、改めてテストしてみましょう。
もう一度Cloud Buildプライベートワーカープールを使用して、手元のcloudbuild.ymlを実行してみます。
gcloud builds submit \ --config=cloudbuild.yaml \ --worker-pool=projects/PROJECT_ID/locations/REGION/workerPools/POOL_NAME \ --no-source
---------------------------------------- REMOTE BUILD OUTPUT ----------------------------------------
starting build "2a474e8c-f839-4af1-93d8-1da23fe36115"
FETCHSOURCE
BUILD
Starting Step #0 - "check-routing"
Step #0 - "check-routing": Pulling image: busybox
Step #0 - "check-routing": Using default tag: latest
Step #0 - "check-routing": latest: Pulling from library/busybox
Step #0 - "check-routing": Digest: sha256:b3255e7dfbcd10cb367af0d409747d511aeb66dfac98cf30e97e87e4207dd76f
Step #0 - "check-routing": Status: Downloaded newer image for busybox:latest
Step #0 - "check-routing": docker.io/library/busybox:latest
Step #0 - "check-routing": === Routing Table ===
Step #0 - "check-routing": Kernel IP routing table
Step #0 - "check-routing": Destination Gateway Genmask Flags Metric Ref Use Iface
Step #0 - "check-routing": 0.0.0.0 192.168.10.1 0.0.0.0 UG 0 0 0 eth0
Step #0 - "check-routing": 192.168.10.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
Step #0 - "check-routing": === Ping NAT instance (10.0.1.2) ===
Step #0 - "check-routing": PING 10.0.1.2 (10.0.1.2): 56 data bytes
Step #0 - "check-routing":
Step #0 - "check-routing": --- 10.0.1.2 ping statistics ---
Step #0 - "check-routing": 3 packets transmitted, 0 packets received, 100% packet loss
Step #0 - "check-routing": === Ping default gateway ===
Step #0 - "check-routing": PING 192.168.10.1 (192.168.10.1): 56 data bytes
Step #0 - "check-routing": 64 bytes from 192.168.10.1: seq=0 ttl=64 time=0.084 ms
Step #0 - "check-routing": 64 bytes from 192.168.10.1: seq=1 ttl=64 time=0.077 ms
Step #0 - "check-routing": 64 bytes from 192.168.10.1: seq=2 ttl=64 time=0.136 ms
Step #0 - "check-routing":
Step #0 - "check-routing": --- 192.168.10.1 ping statistics ---
Step #0 - "check-routing": 3 packets transmitted, 3 packets received, 0% packet loss
Step #0 - "check-routing": round-trip min/avg/max = 0.077/0.099/0.136 ms
Finished Step #0 - "check-routing"
Starting Step #1 - "check-source-ip"
Step #1 - "check-source-ip": Already have image (with digest): gcr.io/cloud-builders/curl
Step #1 - "check-source-ip": {
Step #1 - "check-source-ip": "ip": "34.85.59.7",
Step #1 - "check-source-ip": "hostname": "7.59.85.34.bc.googleusercontent.com",
Step #1 - "check-source-ip": "city": "Tokyo",
Step #1 - "check-source-ip": "region": "Tokyo",
Step #1 - "check-source-ip": "country": "JP",
Step #1 - "check-source-ip": "org": "AS396982 Google LLC",
Step #1 - "check-source-ip": "timezone": "Asia/Tokyo",
Step #1 - "check-source-ip": "readme": "https://ipinfo.io/missingauth"
Finished Step #1 - "check-source-ip"
Starting Step #2 - "test-summary"
Step #2 - "test-summary": Pulling image: ubuntu
Step #2 - "test-summary": Using default tag: latest
Step #2 - "test-summary": latest: Pulling from library/ubuntu
Step #2 - "test-summary": 01d7766a2e4a: Pulling fs layer
Step #2 - "test-summary": 01d7766a2e4a: Verifying Checksum
Step #2 - "test-summary": 01d7766a2e4a: Download complete
Step #2 - "test-summary": 01d7766a2e4a: Pull complete
Step #2 - "test-summary": Digest: sha256:d1e2e92c075e5ca139d51a140fff46f84315c0fdce203eab2807c7e495eff4f9
Step #2 - "test-summary": Status: Downloaded newer image for ubuntu:latest
Step #2 - "test-summary": docker.io/library/ubuntu:latest
Step #2 - "test-summary": =========================================
Step #2 - "test-summary": NAT接続テスト完了
Step #2 - "test-summary": =========================================
Step #2 - "test-summary": ✓ 送信元IP: 上記ステップ2で確認
Step #2 - "test-summary":
Step #2 - "test-summary": ※ 送信元IPがNATインスタンスの外部IPと一致していればNAT経由で接続できています
Step #2 - "test-summary": =========================================
Finished Step #2 - "test-summary"
PUSH
DONE
Step #1 - "check-source-ip": }
-----------------------------------------------------------------------------------------------------
ID CREATE_TIME DURATION SOURCE IMAGES STATUS
2a474e8c-f839-4af1-93d8-1da23fe36115 2026-02-25T12:52:52+00:00 25S - - SUCCESS
成功しました!!
正しくNATインスタンスの外部IPが出力されていますね。
余談(なんで0.0.0.0/0じゃ通信できなかったの?)
何故0.0.0.0/0ではCloud Buildの通信がうまくいかなかったのか……
何故0.0.0.0/1と128.0.0.0/1ではCloud Buildの通信がうまくいったのか……
まず仕様として、Cloud BuildはGoogle管理のVPC内にホストされています。
そして、Google管理のVPCはインターネットとの通信を行うためのルートとして、0.0.0.0/0が既に定義されています。
よって、こちら側で0.0.0.0/0を「NATインスタンスを通ってインターネットに出る」ように定義しても既存の0.0.0.0/0を使うため、そちらにトラフィックが流れることはありません。
そこで、0.0.0.0/1と128.0.0.0/1の出番です。
0.0.0.0/0は全IP空間を指し、そのレンジは0.0.0.0〜255.255.255.255となっています。
ここから0.0.0.0〜127.255.255.255を0.0.0.0/1が、128.0.0.0〜255.255.255.255を128.0.0.0/1がカバーすることで、擬似的に0.0.0.0/0と同じように全IP空間を指すことができます。
さらにインターネットルーティングにおいてはプレフィックス長(/1や/0のCIDR表記におけるスラッシュの後の数字)が長い(具体的な)宛先が優先されるため、事前に0.0.0.0/0で定義されているインターネット向けのルートではなく、0.0.0.0/1と128.0.0.0/1で定義されたNATを経由するインターネット向けのルートが優先されて、通信ができるようになります。
おわりに
今回は、NATインスタンスを利用して、Cloud BuildプライベートワーカープールのNATを実施する方法をご紹介しました。
本記事が皆様のお役に立てれば幸いです。
最後までお読みいただきありがとうございます。