100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Linux & Shell Scripting
55 minbeginner

Lab — Deploy a GKE Autopilot Cluster with Workload Identity and Cloud SQL

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.

Analogy🏏Cricket
🏏 Think of it like cricket: This script is like the pre-match ground inspection conducted by the match referee, pitch curator, and captains before a Test match begins. Before Rohit Sharma and the opposition captain walk out for the toss, the curator has measured pitch moisture, taken grass length readings, and documented the surface condition — creating a baseline against which any afternoon deterioration can be measured.Just as this structured inspection prevents surprises and creates a documented record, the inventory script creates a documented baseline for a server against which future anomalies can be compared. Just as a ground inspection without a checklist might miss a drainage issue that affects the afternoon session, a server assessment without a structured script might miss a nearly-full disk that causes a midnight deployment failure.The insight is that the value of a structured inspection is not just the current findings but the reproducible method — the same script run tomorrow highlights exactly what changed.

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.

bash
#!/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.

Analogy🏏Cricket
🏏 Think of it like cricket: Creating the GKE cluster and Cloud SQL in parallel is like the BCCI simultaneously preparing the stadium (GKE Autopilot — Google manages the groundskeeping team) and setting up the official scorebook system (Cloud SQL — the authoritative match records database). Both must be operational before the first match begins (before the deployment runs). The Cloud SQL Proxy sidecar is the secure courier service between the dressing room (pod) and the scorebook (database) — it encrypts all communications and handles authentication so the players (application code) never need to know database passwords.
bash
#!/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.

bash
#!/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.

bash
#!/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.
Lesson 35 of 40
0% complete