Article 24 – GKE in Production

“But it works on my machine!” — the five words that started it all. Your developer’s code runs fine on their laptop but fails in production because of a missing library or a different OS version. So you containerize everything with Docker, packaging the app with all its dependencies into a neat box that runs identically anywhere.

But now you have a new problem. Instead of a few VMs, you have hundreds of containers. Who deploys them? How do they find each other? What happens when one crashes at 3 AM? You need an orchestrator. You need Kubernetes. And in Google Cloud, the managed Kubernetes service is GKE (Google Kubernetes Engine).

What GKE Actually Manages

Kubernetes has two halves:

  • Control Plane (the brains) — Makes all scheduling decisions, handles failures, manages the API. In GKE, Google manages this entirely. They keep it available, patched, and secure.
  • Nodes (the brawn) — Worker machines (GCE VMs) that run your containers. You manage these, but GKE gives you powerful tools.
Standard vs. Autopilot

Your first big decision.

GKE Standard — The hands-on experience. You decide:
– Machine types for nodes
– Number of nodes
– Autoscaling configuration
– Upgrade schedules

You manage node pools — groups of nodes sharing the same configuration. Maximum flexibility, more operational responsibility.

GKE Autopilot — Google manages both the control plane AND the nodes. You do not create or manage nodes at all. You deploy your containers, and GKE provisions the compute automatically. You pay per-pod for CPU, memory, and storage requested — not for underlying VMs.

Autopilot is a truly serverless Kubernetes experience. For most workloads, it dramatically reduces operations and can be more cost-efficient.

# Create an Autopilot cluster
gcloud container clusters create-auto my-cluster \
    --region=us-central1

# Create a Standard cluster
gcloud container clusters create my-cluster \
    --num-nodes=3 \
    --zone=us-central1-a
Core Kubernetes Objects

Whether Standard or Autopilot, you use the same K8s API objects.

Pod — The smallest deployable unit. A wrapper around one or more containers. 95% of the time, it is one container per Pod. Pods are ephemeral — they get created, destroyed, and replaced constantly.

Deployment — The blueprint. You declare: “I want 3 replicas of my app using this container image.” The Deployment creates and maintains those 3 Pods. If one crashes, it automatically creates a replacement. Self-healing and scalable.

Service — The stable front door. Each Pod has an internal, unstable IP. If a Pod is replaced, the IP changes. A Service provides a single, stable IP and DNS name that load-balances traffic across the healthy Pods behind it. The most important type:

  • LoadBalancer — When you create a Service of type LoadBalancer in GKE, it automatically provisions a real Google Cloud Load Balancer with a public IP. This is the primary way to expose your app to the internet.
GKE Ingress — One IP, Multiple Services

A LoadBalancer Service gives one external IP to one set of Pods. But what if you have api-service, web-frontend, and admin-portal in the same cluster? You do not want a separate load balancer for each.

Ingress uses a single Global Cloud HTTP(S) Load Balancer to route traffic to different Services based on rules:

  • api.your-app.comapi-service
  • www.your-app.comweb-frontend
  • www.your-app.com/adminadmin-panel

One load balancer, one IP, multiple services. Cost-effective and clean.

Autoscaling — Two Levels

Horizontal Pod Autoscaler (HPA) — Scales your Pods. “If average CPU across my web-server Pods exceeds 60%, add more Pods.” The HPA adjusts the replica count in your Deployment automatically.

Cluster Autoscaler — Scales your Nodes (Standard mode only). What if HPA wants 100 Pods but your 3 nodes are full? The Cluster Autoscaler detects pending Pods and adds new nodes. When load decreases, it consolidates Pods and removes unneeded nodes to save money. In Autopilot, this happens transparently.

Workload Identity — Secure Access to Cloud Services

Your Pod needs to access a Cloud Storage bucket. How does it authenticate? You could download a service account key and store it as a Kubernetes Secret, but key management is a security nightmare.

The right way is Workload Identity. It links a Kubernetes Service Account (KSA) to a Google Cloud IAM Service Account (GSA). Your Pod inherits the IAM permissions without a JSON key. No secrets to rotate. No keys to leak.

Artifact Registry — Where Your Images Live

Your Deployment specifies a container image like my-app:v1.2.3. For production, you want a private, secure, integrated registry. Artifact Registry is Google Cloud’s managed service for this.

  • Private storage within your project
  • Automatic vulnerability scanning for known security issues
  • Fast image pulls from within GCP
# Create an Artifact Registry repo
gcloud artifacts repositories create my-repo \
    --repository-format=docker \
    --location=us-central1
Persistent Storage for Stateful Apps

Containers are ephemeral. If a database Pod restarts, its data is gone. For stateful workloads, you use PersistentVolumes:

  1. Developer creates a PersistentVolumeClaim (PVC) — “I need 100GB of SSD storage”
  2. GKE dynamically provisions a GCP Persistent Disk through a StorageClass
  3. The disk is represented as a PersistentVolume (PV), bound to the PVC
  4. The Pod mounts the PVC

If the Pod crashes and restarts on a different node, Kubernetes detaches the disk and reattaches it to the new node. Data survives.

Private Clusters

For production security, your GKE worker nodes should have only private IPs. In a private cluster, nodes cannot be reached from the public internet. This significantly reduces your attack surface.

If Pods need internet access (for pulling external images or downloading updates), configure Cloud NAT in the VPC — just like we covered in the networking article.

# Create a private cluster
gcloud container clusters create my-secure-cluster \
    --enable-private-nodes \
    --master-ipv4-cidr=172.16.0.0/28 \
    --enable-master-authorized-networks \
    --master-authorized-networks=10.0.0.0/8
ConfigMaps and Secrets

Configuration should not be hardcoded into your container image. Database connection strings, API keys, feature flags — keep these separate.

  • ConfigMaps — Non-sensitive configuration (environment variables, config files)
  • Secrets — Sensitive data (passwords, API keys, tokens). Base64 encoded by default; integrate with Cloud KMS for real encryption at rest.

Pods consume them as environment variables or mounted files.

Common Pitfalls and Best Practices

Pitfall: Using the default node service account (has Editor permissions — way too broad).
Best Practice: Use Workload Identity with dedicated, least-privilege IAM service accounts.

Pitfall: Using :latest image tag. Deployments become unpredictable.
Best Practice: Always use specific, immutable tags (my-app:v1.2.3) or digests (my-app@sha256:...).

Pitfall: No resource requests/limits on containers. “Noisy neighbor” problem, unreliable autoscaling.
Best Practice: Always define CPU and memory requests and limits.

Pitfall: Choosing Standard when your team lacks Kubernetes operations experience.
Best Practice: Start with Autopilot. Move to Standard only when you have a specific need for the extra control.

Pitfall: Hardcoding sensitive data in container images.
Best Practice: Use ConfigMaps and Secrets. Manage them separately from application code.

Pitfall: Exposing nodes to the public internet.
Best Practice: Use Private Clusters in production with Cloud NAT for egress.

Quick Reference
# -- gcloud (cluster management) --
gcloud container clusters create-auto [CLUSTER] --region=[REGION]
gcloud container clusters create [CLUSTER] --num-nodes=3 --zone=[ZONE]
gcloud container clusters get-credentials [CLUSTER] --region=[REGION]
gcloud container clusters resize [CLUSTER] --node-pool=[POOL] --num-nodes=5

# -- kubectl (workload management) --
kubectl apply -f my-deployment.yaml    # Apply config
kubectl get pods                        # List pods
kubectl get services                    # List services
kubectl get ingress                     # List ingresses
kubectl logs [POD_NAME]                 # Get pod logs
kubectl exec -it [POD] -- /bin/sh       # Shell into pod
kubectl scale deployment/[NAME] --replicas=5  # Scale