Deploying to a private GKE cluster from an internal CI/CD pipeline usually requires proxy VMs, bastion hosts, or complex VPN routing. Cloud Build DNS endpoints eliminate that overhead by letting workers reach the private control plane directly over Private Service Connect.
The transitive VPC peering problem
VPC network peering in GCP is not transitive. If the VPC hosting your Cloud Build private worker pool (VPC A) peers with your application VPC (VPC B), and your application VPC peers with the GKE control plane VPC (VPC C), Cloud Build cannot route traffic directly to the GKE control plane.
graph TD
subgraph VPCA ["VPC A (Cloud Build Pool)"]
CB["Cloud Build Worker"]
end
subgraph VPCB ["VPC B (Your VPC)"]
InternalIP["Internal Route Gateway"]
end
subgraph VPCC ["VPC C (GKE Master VPC)"]
GKE["GKE Control Plane"]
end
VPCA <-->|"VPC Peering"| VPCB
VPCB <-->|"VPC Peering"| VPCC
CB -.->|"Blocked: non-transitive"| GKE
style VPCA fill:#F8F9FA,stroke:#DADCE0,color:#1a1a1a
style VPCB fill:#E8F0FE,stroke:#4285F4,color:#1a1a1a
style VPCC fill:#FCE8E6,stroke:#EA4335,color:#1a1a1a
style CB fill:#34A853,stroke:#1E7E34,color:#fff
style GKE fill:#1A73E8,stroke:#0D47A1,color:#fff
Private Service Connect with GKE DNS access
Enabling DNS access creates a Private Service Connect (PSC) endpoint inside VPC B that exposes the GKE control plane API. GKE creates a private Cloud DNS zone in your network so any peered network, including VPC A, can resolve the control plane endpoint and send requests directly to it.
graph TD
subgraph VPCA ["VPC A (Cloud Build Pool)"]
CB["Cloud Build Worker"]
end
subgraph VPCB ["VPC B (Your VPC)"]
PSC["PSC Endpoint<br/>(VPC Internal IP)"]
DNS["Private DNS Zone<br/>(*.gke.private)"]
end
subgraph VPCC ["VPC C (GKE Master VPC)"]
GKE["GKE Control Plane"]
end
VPCA <-->|"VPC Peering"| VPCB
VPCB <-->|"VPC Peering"| VPCC
CB -->|"1. Resolves DNS"| DNS
CB -->|"2. Sends API request"| PSC
PSC -->|"3. Bridges tunnel"| GKE
style VPCA fill:#F8F9FA,stroke:#DADCE0,color:#1a1a1a
style VPCB fill:#E8F0FE,stroke:#4285F4,color:#1a1a1a
style VPCC fill:#FCE8E6,stroke:#EA4335,color:#1a1a1a
style CB fill:#34A853,stroke:#1E7E34,color:#fff
style GKE fill:#1A73E8,stroke:#0D47A1,color:#fff
style PSC fill:#F9AB00,stroke:#E37400,color:#1a1a1a
Note - Private worker pool required
Cloud Build runs inside a Google-managed tenant network. Deploying to a private GKE cluster requires a private worker pool peered to your VPC. Default public workers cannot route traffic to internal PSC endpoints.
1. Create the private GKE cluster
This command provisions an Autopilot cluster isolated from the public internet. The --enable-dns-access flag provisions a local DNS endpoint inside the VPC.
gcloud beta container clusters create-auto "autopilot-cluster" \ --project "sidekick-1024" \ --region "us-central1" \ --release-channel "regular" \ --enable-private-nodes \ --enable-dns-access \ --no-enable-ip-access \ --no-enable-google-cloud-access \ --network "default" \ --subnetwork "default" \ --cluster-ipv4-cidr "/17" \ --binauthz-evaluation-mode=DISABLEDKey networking flags
--enable-dns-access. Configures the control plane behind a Private Service Connect endpoint with a local VPC DNS record.--no-enable-ip-access. Disables the public IP endpoint on the control plane.--no-enable-google-cloud-access. Blocks all public Google Cloud IP access, allowing traffic only from inside your peered VPC.
2. Application source code
A minimal Flask application to test deployments:
app.py
from flask import Flaskimport os
app = Flask(__name__)
@app.route('/')def hello(): version = os.environ.get('VERSION', 'v1.0') return f"Hello from Private GKE! (Version: {version})\nDeployed via Cloud Build DNS Endpoint."
if __name__ == '__main__': app.run(host='0.0.0.0', port=8080)Dockerfile
FROM python:3.9-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY . .EXPOSE 8080CMD ["python", "app.py"]3. Kubernetes manifests
Save the deployment and service definitions in k8s/app.yaml. The pipeline replaces PYTHON_IMAGE_PLACEHOLDER with the image digest during build.
apiVersion: apps/v1kind: Deploymentmetadata: name: python-poc-deploymentspec: replicas: 2 selector: matchLabels: app: python-poc template: metadata: labels: app: python-poc spec: containers: - name: python-poc-container image: PYTHON_IMAGE_PLACEHOLDER ports: - containerPort: 8080---apiVersion: v1kind: Servicemetadata: name: python-poc-servicespec: type: LoadBalancer selector: app: python-poc ports: - port: 80 targetPort: 80804. Pipeline configuration
The cloudbuild.yaml configuration runs on a private worker pool. It runs authentication and deployment in a single step so kubectl can reuse the credentials generated by gcloud.
steps: # 1. Build the Docker image - name: 'gcr.io/cloud-builders/docker' id: build args: - build - -t - '$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO_NAME/python-poc:$SHORT_SHA' - -t - '$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO_NAME/python-poc:latest' - .
# 2. Push to Artifact Registry - name: 'gcr.io/cloud-builders/docker' id: push args: - push - --all-tags - '$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO_NAME/python-poc' waitFor: [build]
# 3. Auth & Deploy (Combined Step to share ~/.kube/config) - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk' id: deploy entrypoint: 'bash' args: - '-c' - | # Fetch credentials using the DNS endpoint (PSC) gcloud container clusters get-credentials $_CLUSTER_NAME \ --region $_REGION \ --dns-endpoint
# Inject the unique image tag into the manifest sed -i "s|PYTHON_IMAGE_PLACEHOLDER|$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO_NAME/python-poc:$SHORT_SHA|g" k8s/app.yaml
# Apply the update kubectl apply -f k8s/app.yaml waitFor: [push]
images: - '$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO_NAME/python-poc:$SHORT_SHA' - '$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO_NAME/python-poc:latest'
options: pool: name: 'projects/$PROJECT_ID/locations/$_REGION/privatePools/$_PRIVATE_POOL_NAME'
substitutions: _REGION: us-central1 _REPO_NAME: poc-repo _CLUSTER_NAME: autopilot-cluster _PRIVATE_POOL_NAME: worker-pool5. Deployment prerequisites
Important - Prerequisites
Before triggering the pipeline, verify these IAM permissions and network routes.
IAM roles for Cloud Build
Grant these roles to the Cloud Build service account ([PROJECT_NUMBER]@cloudbuild.gserviceaccount.com):
roles/artifactregistry.writer. Allows the build runner to push container images to Artifact Registry.roles/container.developer. Allows Cloud Build to fetch cluster credentials and apply Kubernetes manifests.
Network configuration
- Peer your Cloud Build private worker pool with your VPC network.
- Verify the GKE cluster was created with
--enable-dns-access. Without this flag, Cloud Build cannot resolve or reach the control plane endpoint across peered networks.