This guide documents the setup for a fully private GKE cluster that remains accessible to internal CI/CD pipelines without needing proxy VMs or complex VPN configurations.
The Problem: Transitive VPC Peering
By default, VPC network peering in GCP is not transitive. If the VPC hosting your Cloud Build Private Worker Pool (VPC A) is peered with your customer VPC (VPC B), and your customer VPC is peered 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 (No Transitive Routing)"| 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
The Solution: Private Service Connect (PSC) with GKE DNS Access
When enabling DNS access, GCP provisions a Private Service Connect (PSC) endpoint inside VPC B that exposes the GKE master API. The control plane generates a local DNS zone in your network so that any peered VPC (like VPC A) can resolve and communicate directly to the master via this endpoint.
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 Requirement
Since Cloud Build runs in a separate Google-managed tenant network, deploying to a private GKE cluster requires a Private Worker Pool peered to your customer VPC. Standard (public) worker pools cannot route to internal PSC endpoints.
1. Provisioning the Private GKE Cluster
The following command creates an Autopilot cluster that is completely isolated from the public internet. It uses the --enable-dns-access flag to create a local DNS endpoint within 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: Enables the DNS-based control plane (PSC), allowing the cluster to be reached via a local VPC IP.--no-enable-ip-access: Disables the public IP endpoint for the control plane.--no-enable-google-cloud-access: Ensures only traffic originating from within your specific VPC (like a Private Worker Pool) can reach the master.
2. Application Source Code
A basic Python Flask application used for the Proof of Concept (POC).
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
Stored in k8s/app.yaml. Note the PYTHON_IMAGE_PLACEHOLDER which is dynamically replaced by the pipeline.
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. CI/CD Pipeline Configuration
The cloudbuild.yaml file is configured to run on a Private Worker Pool. It combines authentication and deployment into a single step to maintain the container’s credential context.
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 Pre-requisites
Important - Pre-requisites
Before triggering the pipeline, ensure the following IAM roles and network paths are established:
IAM Roles for Cloud Build Service Account
Grant these roles to the Cloud Build Service Account ([PROJECT_NUMBER]@cloudbuild.gserviceaccount.com):
- Artifact Registry Writer (
roles/artifactregistry.writer): Required to push Docker images. - Kubernetes Engine Developer (
roles/container.developer): Required to fetch cluster credentials and apply configurations.
Networking Configuration
- Cloud Build Private Pool: Must be peered with the customer VPC network.
- GKE Cluster Configuration: The cluster must be launched with
--enable-dns-accessenabled to bypass transitive peering blocks.