What You'll Build
In this lab you will provision a GKE Autopilot cluster with Workload Identity enabled, create a Cloud SQL PostgreSQL instance accessible only via the Cloud SQL Proxy sidecar, deploy the cricket analytics API as a Kubernetes Deployment with the proxy sidecar pattern, configure Workload Identity so the pods access Cloud SQL and Cloud Storage without service account keys, and verify the end-to-end connection from the Kubernetes pod through the Cloud SQL Proxy to the PostgreSQL database. This lab demonstrates the production-grade GKE security pattern used by data-intensive organisations on GCP and reinforces the Workload Identity concept learned in M5 Lesson 3.
Prerequisites
- GCP project with billing enabled — gcloud config set project PROJECT_ID
- gcloud CLI installed and authenticated — gcloud auth login
- kubectl installed — gcloud components install kubectl
- Completed M5 Lessons 3-4: GCP IAM, GKE, Cloud SQL and Workload Identity concepts
- APIs enabled: container.googleapis.com, sqladmin.googleapis.com, storage-api.googleapis.com
Setup — Enable APIs and Create Service Accounts
Enable the required GCP APIs and create the GCP service account that the Kubernetes pods will impersonate via Workload Identity. Unlike AWS IAM Roles for EC2 (where the role is attached to the instance), GKE Workload Identity requires an explicit binding between a Kubernetes ServiceAccount and a GCP ServiceAccount.
#!/bin/bash
set -euo pipefail
PROJECT=$(gcloud config get-value project)
REGION='asia-south1'
CLUSTER='cricket-autopilot'
NAMESPACE='cricket'
K8S_SA='cricket-api-ksa'
GCP_SA='cricket-api-gsa'
echo "Project: ${PROJECT} Region: ${REGION}"
echo
echo '=== Enable required APIs ==='
gcloud services enable \
container.googleapis.com \
sqladmin.googleapis.com \
storage-api.googleapis.com \
iam.googleapis.com \
--project="$PROJECT"
echo 'APIs enabled'
echo
echo '=== Create GCP Service Account ==='
gcloud iam service-accounts create "$GCP_SA" \
--display-name='Cricket API GCP Service Account' \
--project="$PROJECT" 2>/dev/null || echo 'SA already exists'
GCP_SA_EMAIL="${GCP_SA}@${PROJECT}.iam.gserviceaccount.com"
echo "GCP SA: ${GCP_SA_EMAIL}"
# Grant roles — least privilege
# Cloud SQL Client: connect via Cloud SQL Proxy
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="serviceAccount:${GCP_SA_EMAIL}" \
--role='roles/cloudsql.client' \
--condition=None
# Storage Object Viewer: read match data from GCS
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="serviceAccount:${GCP_SA_EMAIL}" \
--role='roles/storage.objectViewer' \
--condition=None
echo 'IAM roles granted'
# Save config
cat > /tmp/gke_lab.env << ENV
PROJECT='${PROJECT}'
REGION='${REGION}'
CLUSTER='${CLUSTER}'
NAMESPACE='${NAMESPACE}'
K8S_SA='${K8S_SA}'
GCP_SA='${GCP_SA}'
GCP_SA_EMAIL='${GCP_SA_EMAIL}'
ENV
echo 'Config saved'Step 1 — GKE Autopilot Cluster and Cloud SQL
Create the GKE Autopilot cluster with Workload Identity enabled and the Cloud SQL PostgreSQL instance. These are created in parallel because they have no dependency on each other — the cluster takes approximately 5 minutes and Cloud SQL takes approximately 10 minutes. Both must be ready before the Kubernetes deployment can be configured.
#!/bin/bash
source /tmp/gke_lab.env
echo '=== Create GKE Autopilot cluster (runs in background) ==='
gcloud container clusters create-auto "$CLUSTER" \
--region="$REGION" \
--project="$PROJECT" \
--workload-pool="${PROJECT}.svc.id.goog" &
CLUSTER_PID=$!
echo '=== Create Cloud SQL PostgreSQL (runs in parallel) ==='
DB_INSTANCE='cricket-db-lab'
gcloud sql instances create "$DB_INSTANCE" \
--database-version=POSTGRES_16 \
--tier=db-f1-micro \
--region="$REGION" \
--project="$PROJECT" \
--no-assign-ip \
--require-ssl 2>/dev/null || echo 'Instance already exists'
# Create database and user
gcloud sql databases create cricket_analytics \
--instance="$DB_INSTANCE" \
--project="$PROJECT" 2>/dev/null || true
DB_PASS=$(openssl rand -base64 24)
gcloud sql users create cricket_app \
--instance="$DB_INSTANCE" \
--password="$DB_PASS" \
--project="$PROJECT" 2>/dev/null || true
CONNECTION_NAME=$(gcloud sql instances describe "$DB_INSTANCE" \
--project="$PROJECT" \
--format='value(connectionName)')
echo "Cloud SQL connection name: ${CONNECTION_NAME}"
# Wait for GKE cluster to be ready
echo 'Waiting for GKE cluster...'
wait $CLUSTER_PID
echo 'GKE cluster ready'
# Get kubectl credentials
gcloud container clusters get-credentials "$CLUSTER" \
--region="$REGION" --project="$PROJECT"
kubectl get nodes
# Save DB config
cat >> /tmp/gke_lab.env << ENV
DB_INSTANCE='${DB_INSTANCE}'
DB_PASS='${DB_PASS}'
CONNECTION_NAME='${CONNECTION_NAME}'
ENV
echo 'Step 1 complete'Step 2 — Workload Identity Binding and Kubernetes Deployment
Configure the Workload Identity binding between the Kubernetes ServiceAccount and the GCP ServiceAccount, then deploy the cricket API with the Cloud SQL Proxy sidecar. The proxy sidecar handles all database authentication — the application connects to localhost:5432 as if connecting to a local PostgreSQL instance.
#!/bin/bash
source /tmp/gke_lab.env
echo '=== Configure Workload Identity ==='
# Create Kubernetes namespace
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
# Create Kubernetes Service Account
kubectl create serviceaccount "$K8S_SA" \
--namespace "$NAMESPACE" \
--dry-run=client -o yaml | kubectl apply -f -
# Annotate K8s SA with GCP SA
kubectl annotate serviceaccount "$K8S_SA" \
--namespace "$NAMESPACE" \
"iam.gke.io/gcp-service-account=${GCP_SA_EMAIL}" \
--overwrite
# Bind GCP SA to K8s SA (Workload Identity binding)
gcloud iam service-accounts add-iam-policy-binding "$GCP_SA_EMAIL" \
--role='roles/iam.workloadIdentityUser' \
--member="serviceAccount:${PROJECT}.svc.id.goog[${NAMESPACE}/${K8S_SA}]" \
--project="$PROJECT"
echo 'Workload Identity configured'
# Create Kubernetes secret for DB password
kubectl create secret generic cricket-db-secret \
--from-literal=password="$DB_PASS" \
--namespace="$NAMESPACE" \
--dry-run=client -o yaml | kubectl apply -f -
echo
echo '=== Deploy cricket API with Cloud SQL Proxy sidecar ==='
cat << MANIFEST | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: cricket-api
namespace: ${NAMESPACE}
spec:
replicas: 2
selector:
matchLabels:
app: cricket-api
template:
metadata:
labels:
app: cricket-api
spec:
serviceAccountName: ${K8S_SA}
containers:
- name: api
image: python:3.11-slim
command: ["python3", "-c"]
args:
- |
import http.server, json, os
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'status':'healthy','cloud':'gcp','region':'${REGION}'}).encode())
def log_message(self, *a): pass
http.server.HTTPServer(('', 8080), H).serve_forever()
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: '127.0.0.1'
- name: DB_PORT
value: '5432'
- name: DB_NAME
value: 'cricket_analytics'
- name: DB_USER
value: 'cricket_app'
- name: DB_PASS
valueFrom:
secretKeyRef:
name: cricket-db-secret
key: password
resources:
requests:
memory: 256Mi
cpu: 250m
- name: cloud-sql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.35.4
command:
- /cloud_sql_proxy
- -instances=${CONNECTION_NAME}=tcp:5432
- -use_http_health_check
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
resources:
requests:
memory: 128Mi
cpu: 100m
---
apiVersion: v1
kind: Service
metadata:
name: cricket-api-svc
namespace: ${NAMESPACE}
spec:
selector:
app: cricket-api
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
MANIFEST
echo 'Deployment applied — waiting for pods...'Step 3 — Verification and Cleanup
Verify the deployment is healthy, the Cloud SQL Proxy sidecar is running, and the Workload Identity is functioning correctly by checking that the pods can access GCP services without credentials. Then clean up all resources to avoid billing.
#!/bin/bash
source /tmp/gke_lab.env
echo '=== Verify deployment ==='
# Wait for pods to be running
kubectl rollout status deployment/cricket-api -n "$NAMESPACE" --timeout=180s
# Check pod status
kubectl get pods -n "$NAMESPACE" -o wide
# Check both containers in each pod
kubectl get pods -n "$NAMESPACE" -o jsonpath='{.items[*].status.containerStatuses[*].name}'
echo
# Get an individual pod name
POD=$(kubectl get pod -n "$NAMESPACE" -l app=cricket-api -o jsonpath='{.items[0].metadata.name}')
echo "Checking pod: ${POD}"
# Verify Workload Identity: check the GCP identity the pod is using
kubectl exec "$POD" -n "$NAMESPACE" -c api -- \
curl -sf 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email' \
-H 'Metadata-Flavor: Google' 2>/dev/null || echo 'Metadata check requires gke metadata server'
# Verify Cloud SQL proxy is listening on 127.0.0.1:5432
kubectl exec "$POD" -n "$NAMESPACE" -c api -- \
nc -zv 127.0.0.1 5432 2>&1 || echo 'nc not available — check proxy logs'
# Check Cloud SQL Proxy logs
kubectl logs "$POD" -n "$NAMESPACE" -c cloud-sql-proxy | tail -5
# Get LoadBalancer external IP
echo
echo '=== Service external IP (may take 2-3 minutes) ==='
kubectl get service cricket-api-svc -n "$NAMESPACE"
EXTERNAL_IP=$(kubectl get service cricket-api-svc -n "$NAMESPACE" \
--output='jsonpath={.status.loadBalancer.ingress[0].ip}' 2>/dev/null)
[[ -n "$EXTERNAL_IP" ]] && curl -sf "http://${EXTERNAL_IP}/" | python3 -m json.tool || \
echo 'External IP not assigned yet — check: kubectl get svc -n cricket'
echo
echo '=== GKE Autopilot vs EKS comparison ==='
cat << 'COMPARE'
GKE Autopilot vs AWS EKS:
Feature GKE Autopilot AWS EKS
------- ------------- -------
Control plane cost FREE $0.10/hour ($73/month)
Node management Google manages nodes You manage nodes OR use Fargate
Billing model Per pod vCPU+memory Per node instance-hour
Workload Identity Built-in (IRSA equivalent) IRSA (additional config)
Kubernetes versions Latest, auto-upgraded Manual version management
Security hardening Built-in (CIS benchmark) Requires manual configuration
Scaling Automatic node provisioning ASG-based node scaling
For cricket analytics:
- GKE Autopilot: simpler, cheaper for small/variable workloads (no idle node cost)
- EKS: more ecosystem tooling, better AWS service integration (SQS, SNS native)
COMPARE
echo
echo '=== CLEANUP ==='
gcloud container clusters delete "$CLUSTER" \
--region="$REGION" \
--project="$PROJECT" \
--quiet --async
gcloud sql instances delete "$DB_INSTANCE" \
--project="$PROJECT" \
--quiet
gcloud iam service-accounts delete "$GCP_SA_EMAIL" \
--project="$PROJECT" \
--quiet 2>/dev/null || true
rm -f /tmp/gke_lab.env
echo 'Cleanup initiated — cluster deletion takes ~5 minutes'Warning: GKE Autopilot clusters charge for pod vCPU and memory while pods are running, even though you do not see worker nodes in the console. Cloud SQL db-f1-micro charges approximately $0.012/hour. A GKE Autopilot cluster with 2 replicas of 250m CPU and 256Mi memory costs approximately $0.025/hour in compute. Delete both resources immediately after the lab. The gcloud container clusters delete command is asynchronous (--async flag) — verify deletion in the GCP console after a few minutes to confirm the cluster is gone.
Extension Challenge: Extend this lab with three production enhancements: (1) Replace the simple Python HTTP server with a FastAPI application that actually connects to PostgreSQL using asyncpg and returns live cricket score data from the database — this completes the end-to-end data flow through the Cloud SQL Proxy; (2) Add Cloud Armor (GCP's WAF, equivalent to AWS WAF) in front of the LoadBalancer to filter malicious traffic and rate-limit requests per IP; (3) Configure Horizontal Pod Autoscaling based on CPU utilisation — kubectl autoscale deployment cricket-api --cpu-percent=50 --min=2 --max=10 — and verify that GKE Autopilot automatically provisions additional nodes as the HPA scales pods beyond the current capacity.
- GKE Autopilot control plane is free — Google manages node provisioning, upgrades and security hardening; billing is per pod vCPU and memory second, not per node hour.
- Workload Identity requires three steps: annotate the Kubernetes ServiceAccount with the GCP SA email, bind the GCP SA with roles/iam.workloadIdentityUser for the K8s SA, and use the K8s SA in the pod spec.
- Cloud SQL Proxy sidecar connects to Cloud SQL over the Google internal network using IAM authentication — the application connects to localhost:5432 with no public database IP and no credential management in the pod.
- Create time-consuming resources (GKE cluster and Cloud SQL) in parallel using bash background processes — this reduces total lab setup time from 15 minutes to 10 minutes.
- GKE Autopilot automatically provisions nodes when new pods are scheduled that exceed current node capacity — the cluster autoscaler is built into Autopilot with no configuration required.
- Delete GCP labs immediately after completion — GKE clusters and Cloud SQL instances both incur per-hour charges, and forgetting to clean up is the most common source of unexpected GCP bills for learners.