Logo
CI/CD for Private GKE Clusters via DNS Endpoints

CI/CD for Private GKE Clusters via DNS Endpoints

May 25, 2026
4 min read

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.

Terminal window
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=DISABLED

Key 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 Flask
import 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-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["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/v1
kind: Deployment
metadata:
name: python-poc-deployment
spec:
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: v1
kind: Service
metadata:
name: python-poc-service
spec:
type: LoadBalancer
selector:
app: python-poc
ports:
- port: 80
targetPort: 8080

4. 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-pool

5. 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

  1. Cloud Build Private Pool: Must be peered with the customer VPC network.
  2. GKE Cluster Configuration: The cluster must be launched with --enable-dns-access enabled to bypass transitive peering blocks.