GCP for DevOps — Project: GKE Autopilot CI/CD Pipeline

3 minute read

Project 2: GKE Autopilot CI/CD Pipeline

Build a complete CI/CD pipeline that automatically builds container images, stores them in Artifact Registry, and deploys to a GKE Autopilot cluster using Cloud Build and Cloud Deploy.

Architecture

GKE CI/CD Pipeline:
┌──────────┐    ┌──────────────┐    ┌──────────────┐
│  GitHub   │───▶│  Cloud Build │───▶│  Artifact    │
│  Push     │    │  Trigger     │    │  Registry    │
└──────────┘    └──────┬───────┘    └──────┬───────┘
                       │                    │
                       ▼                    ▼
                ┌──────────────┐    ┌──────────────┐
                │  Cloud       │───▶│  GKE         │
                │  Deploy      │    │  Autopilot   │
                │              │    │              │
                │  dev → stg   │    │  ┌────────┐ │
                │   → prod     │    │  │ Pods   │ │
                └──────────────┘    │  └────────┘ │
                                    └──────────────┘

Step 1: Enable Required APIs

$ gcloud services enable \
    container.googleapis.com \
    cloudbuild.googleapis.com \
    clouddeploy.googleapis.com \
    artifactregistry.googleapis.com \
    --project=my-project
Operation finished successfully.

Step 2: Create Artifact Registry Repository

$ gcloud artifacts repositories create app-images \
    --repository-format=docker \
    --location=us-central1 \
    --description="Application container images"
Created repository [app-images].

Step 3: Create GKE Autopilot Cluster

$ gcloud container clusters create-auto gke-autopilot-dev \
    --region=us-central1 \
    --project=my-project
Creating cluster gke-autopilot-dev...done.

# Get credentials
$ gcloud container clusters get-credentials gke-autopilot-dev \
    --region=us-central1
Fetching cluster endpoint and auth data.
kubeconfig entry generated for gke-autopilot-dev.

Step 4: Sample Application

Create a simple Go web application:

$ mkdir -p sample-app && cd sample-app
// main.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        version := os.Getenv("APP_VERSION")
        if version == "" {
            version = "unknown"
        }
        fmt.Fprintf(w, "Hello from GKE Autopilot! Version: %s\n", version)
    })

    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprint(w, "OK")
    })

    log.Printf("Starting server on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}
# Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY main.go .
RUN go build -o server main.go

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]

Step 5: Kubernetes Manifests

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-app
  labels:
    app: sample-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
      - name: sample-app
        image: us-central1-docker.pkg.dev/my-project/app-images/sample-app:latest
        ports:
        - containerPort: 8080
        env:
        - name: APP_VERSION
          value: "1.0.0"
        resources:
          requests:
            cpu: "250m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 3
---
apiVersion: v1
kind: Service
metadata:
  name: sample-app
spec:
  type: LoadBalancer
  selector:
    app: sample-app
  ports:
  - port: 80
    targetPort: 8080

Step 6: Cloud Build Configuration

# cloudbuild.yaml
steps:
  # Build the container image
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '-t'
      - 'us-central1-docker.pkg.dev/$PROJECT_ID/app-images/sample-app:$SHORT_SHA'
      - '-t'
      - 'us-central1-docker.pkg.dev/$PROJECT_ID/app-images/sample-app:latest'
      - '.'

  # Push images to Artifact Registry
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', '--all-tags', 'us-central1-docker.pkg.dev/$PROJECT_ID/app-images/sample-app']

  # Deploy to GKE
  - name: 'gcr.io/cloud-builders/gke-deploy'
    args:
      - 'run'
      - '--filename=k8s/'
      - '--image=us-central1-docker.pkg.dev/$PROJECT_ID/app-images/sample-app:$SHORT_SHA'
      - '--location=us-central1'
      - '--cluster=gke-autopilot-dev'

options:
  logging: CLOUD_LOGGING_ONLY

images:
  - 'us-central1-docker.pkg.dev/$PROJECT_ID/app-images/sample-app:$SHORT_SHA'
  - 'us-central1-docker.pkg.dev/$PROJECT_ID/app-images/sample-app:latest'

Step 7: Create Build Trigger

# Connect your GitHub repository first (via Console)
# Then create a trigger
$ gcloud builds triggers create github \
    --name=sample-app-deploy \
    --repo-name=sample-app \
    --repo-owner=my-github-user \
    --branch-pattern="^main$" \
    --build-config=cloudbuild.yaml
Created [sample-app-deploy].

# Or trigger a manual build
$ gcloud builds submit \
    --config=cloudbuild.yaml \
    --project=my-project .

Step 8: Set Up Cloud Deploy (Progressive Delivery)

# clouddeploy.yaml
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
  name: sample-app-pipeline
description: "Sample app deployment pipeline"
serialPipeline:
  stages:
  - targetId: dev
    profiles: [dev]
  - targetId: staging
    profiles: [staging]
  - targetId: production
    profiles: [production]
    strategy:
      canary:
        runtimeConfig:
          kubernetes:
            serviceNetworking:
              service: "sample-app"
              deployment: "sample-app"
        canaryDeployment:
          percentages: [25, 50, 75]
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
  name: dev
description: "Development cluster"
gke:
  cluster: projects/my-project/locations/us-central1/clusters/gke-autopilot-dev
# Register the pipeline
$ gcloud deploy apply --file=clouddeploy.yaml --region=us-central1
Waiting for the operation to finish...done.

# Create a release
$ gcloud deploy releases create release-001 \
    --delivery-pipeline=sample-app-pipeline \
    --region=us-central1 \
    --images=sample-app=us-central1-docker.pkg.dev/my-project/app-images/sample-app:abc123
Created release [release-001].

Step 9: Verify Deployment

# Check deployment status
$ kubectl get deployments
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
sample-app   3/3     3            3           2m

# Check service external IP
$ kubectl get svc sample-app
NAME         TYPE           CLUSTER-IP    EXTERNAL-IP    PORT(S)
sample-app   LoadBalancer   10.0.1.100    35.192.0.50    80:30080/TCP

# Test the application
$ curl http://35.192.0.50
Hello from GKE Autopilot! Version: 1.0.0

Clean Up

# Delete Kubernetes resources
$ kubectl delete -f k8s/

# Delete GKE cluster
$ gcloud container clusters delete gke-autopilot-dev \
    --region=us-central1 --quiet

# Delete Artifact Registry
$ gcloud artifacts repositories delete app-images \
    --location=us-central1 --quiet

# Delete Cloud Deploy pipeline
$ gcloud deploy delivery-pipelines delete sample-app-pipeline \
    --region=us-central1 --force --quiet

Key Learnings

  • GKE Autopilot for hands-off node management
  • Cloud Build for automated container builds
  • Artifact Registry as a private container registry
  • Cloud Deploy for progressive delivery with canary stages
  • Kubernetes liveness/readiness probes for health checks

Next → Serverless REST API

Updated: