Introduction

Kubernetes has become the de facto standard for orchestrating containerized applications in production environments. Its powerful features for scaling, self-healing, and declarative configuration make it an excellent platform for deploying Go microservices. In this hands-on guide, we’ll walk through the complete process of deploying a set of Go microservices on Kubernetes, from containerization to production deployment.

Go is particularly well-suited for containerized environments due to its small binary sizes, minimal dependencies, and excellent performance characteristics. We’ll leverage these strengths while exploring Kubernetes features that help build resilient, scalable applications.

Prerequisites

Before getting started, ensure you have:

  1. A basic understanding of Go programming
  2. Docker installed locally
  3. kubectl configured to connect to a Kubernetes cluster
  4. A code editor of your choice
  5. Basic familiarity with YAML

If you don’t have a Kubernetes cluster yet, you can set up a local one using Minikube, Kind, or Docker Desktop.

Our Example Application

For this guide, we’ll build and deploy a simple e-commerce microservices application consisting of:

  1. Product Service: Manages product information and inventory
  2. User Service: Handles user authentication and profile management
  3. Order Service: Processes customer orders
  4. Frontend: A simple web interface for the application

Each service will be written in Go, containerized, and deployed to Kubernetes.

Part 1: Building Containerized Go Microservices

Product Service

Let’s start by building a simple product service. Create a new directory called product-service and add the following files:

// main.go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
    "strconv"
    "sync"
)

type Product struct {
    ID          string  `json:"id"`
    Name        string  `json:"name"`
    Description string  `json:"description"`
    Price       float64 `json:"price"`
    Stock       int     `json:"stock"`
}

var (
    products = []Product{
        {ID: "1", Name: "Go Gopher Plush Toy", Description: "Cute Gopher plush toy", Price: 19.99, Stock: 100},
        {ID: "2", Name: "Kubernetes Cookbook", Description: "Guide to K8s deployment", Price: 39.99, Stock: 50},
        {ID: "3", Name: "Mechanical Keyboard", Description: "Programmer's keyboard", Price: 129.99, Stock: 30},
    }
    mu sync.RWMutex
)

func getProducts(w http.ResponseWriter, r *http.Request) {
    mu.RLock()
    defer mu.RUnlock()
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(products)
}

func getProduct(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Path[len("/products/"):]
    
    mu.RLock()
    defer mu.RUnlock()
    
    for _, product := range products {
        if product.ID == id {
            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(product)
            return
        }
    }
    
    w.WriteHeader(http.StatusNotFound)
    w.Write([]byte(`{"error": "Product not found"}`))
}

func health(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status": "ok"}`))
}

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    http.HandleFunc("/products", getProducts)
    http.HandleFunc("/products/", getProduct)
    http.HandleFunc("/health", health)
    
    log.Printf("Product service starting on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

Create a Dockerfile in the same directory:

FROM golang:1.20-alpine AS builder

WORKDIR /app
COPY . .

RUN go mod init product-service
RUN go build -o product-service .

FROM alpine:3.17

WORKDIR /app
COPY --from=builder /app/product-service .

EXPOSE 8080
CMD ["./product-service"]

Now, build and push the Docker image:

docker build -t your-registry/product-service:v1 .
docker push your-registry/product-service:v1

User Service

Create a user-service directory with these files:

// main.go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
    "sync"
)

type User struct {
    ID       string `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
}

var (
    users = []User{
        {ID: "1", Username: "johndoe", Email: "john@example.com"},
        {ID: "2", Username: "janedoe", Email: "jane@example.com"},
    }
    mu sync.RWMutex
)

func getUsers(w http.ResponseWriter, r *http.Request) {
    mu.RLock()
    defer mu.RUnlock()
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Path[len("/users/"):]
    
    mu.RLock()
    defer mu.RUnlock()
    
    for _, user := range users {
        if user.ID == id {
            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(user)
            return
        }
    }
    
    w.WriteHeader(http.StatusNotFound)
    w.Write([]byte(`{"error": "User not found"}`))
}

func health(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status": "ok"}`))
}

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    http.HandleFunc("/users", getUsers)
    http.HandleFunc("/users/", getUser)
    http.HandleFunc("/health", health)
    
    log.Printf("User service starting on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

Create a Dockerfile similar to the one for the product service, then build and push the image.

Order Service

The order service will be more complex as it needs to communicate with both the product and user services:

// main.go
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "sync"
    "time"
)

type Order struct {
    ID        string    `json:"id"`
    UserID    string    `json:"user_id"`
    Products  []Product `json:"products"`
    Total     float64   `json:"total"`
    Status    string    `json:"status"`
    CreatedAt time.Time `json:"created_at"`
}

type Product struct {
    ID       string  `json:"id"`
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Quantity int     `json:"quantity"`
}

type User struct {
    ID       string `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
}

var (
    orders = []Order{
        {
            ID:     "1",
            UserID: "1",
            Products: []Product{
                {ID: "1", Name: "Go Gopher Plush Toy", Price: 19.99, Quantity: 2},
            },
            Total:     39.98,
            Status:    "completed",
            CreatedAt: time.Now().Add(-24 * time.Hour),
        },
    }
    mu sync.RWMutex
)

func getOrders(w http.ResponseWriter, r *http.Request) {
    mu.RLock()
    defer mu.RUnlock()
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(orders)
}

func getOrder(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Path[len("/orders/"):]
    
    mu.RLock()
    defer mu.RUnlock()
    
    for _, order := range orders {
        if order.ID == id {
            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(order)
            return
        }
    }
    
    w.WriteHeader(http.StatusNotFound)
    w.Write([]byte(`{"error": "Order not found"}`))
}

func createOrder(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        w.WriteHeader(http.StatusMethodNotAllowed)
        return
    }
    
    var newOrder Order
    if err := json.NewDecoder(r.Body).Decode(&newOrder); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte(`{"error": "Invalid request"}`))
        return
    }
    
    // Validate user exists
    userServiceURL := os.Getenv("USER_SERVICE_URL")
    if userServiceURL == "" {
        userServiceURL = "http://user-service:8080"
    }
    
    resp, err := http.Get(fmt.Sprintf("%s/users/%s", userServiceURL, newOrder.UserID))
    if err != nil || resp.StatusCode != http.StatusOK {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte(`{"error": "Invalid user"}`))
        return
    }
    
    // Calculate total
    newOrder.ID = fmt.Sprintf("%d", len(orders)+1)
    newOrder.CreatedAt = time.Now()
    newOrder.Status = "pending"
    
    mu.Lock()
    orders = append(orders, newOrder)
    mu.Unlock()
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(newOrder)
}

func health(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status": "ok"}`))
}

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    http.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodGet {
            getOrders(w, r)
        } else if r.Method == http.MethodPost {
            createOrder(w, r)
        } else {
            w.WriteHeader(http.StatusMethodNotAllowed)
        }
    })
    http.HandleFunc("/orders/", getOrder)
    http.HandleFunc("/health", health)
    
    log.Printf("Order service starting on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

Create a Dockerfile for this service as well.

Frontend Service

For simplicity, we’ll create a basic Go web server that serves static HTML and makes API calls to our services:

// main.go
package main

import (
    "html/template"
    "log"
    "net/http"
    "os"
)

type PageData struct {
    Title string
}

func home(w http.ResponseWriter, r *http.Request) {
    tmpl, err := template.ParseFiles("templates/home.html")
    if err != nil {
        log.Printf("Error parsing template: %v", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    
    data := PageData{
        Title: "E-commerce Microservices Demo",
    }
    
    tmpl.Execute(w, data)
}

func health(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status": "ok"}`))
}

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    http.HandleFunc("/", home)
    http.HandleFunc("/health", health)
    
    // Serve static files
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))
    
    log.Printf("Frontend service starting on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

Create a basic templates/home.html file and a static directory with some CSS.

Part 2: Kubernetes Deployment Configuration

Now that we have our containerized services, let’s create the Kubernetes configuration files to deploy them.

Namespace

First, let’s create a dedicated namespace for our application:

# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: ecommerce

Apply it with:

kubectl apply -f namespace.yaml

ConfigMaps and Secrets

Let’s create ConfigMaps and Secrets for our application:

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: ecommerce
data:
  PRODUCT_SERVICE_URL: "http://product-service:8080"
  USER_SERVICE_URL: "http://user-service:8080"
  ORDER_SERVICE_URL: "http://order-service:8080"
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: ecommerce
type: Opaque
data:
  # Note: Values should be base64 encoded
  JWT_SECRET: c2VjcmV0LWtleQ== # "secret-key" in base64

Product Service Deployment

Create the Kubernetes deployment and service for the product service:

# product-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
  namespace: ecommerce
spec:
  replicas: 2
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
    spec:
      containers:
      - name: product-service
        image: your-registry/product-service:v1
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: "0.5"
            memory: "512Mi"
          requests:
            cpu: "0.1"
            memory: "128Mi"
        env:
        - name: PORT
          value: "8080"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: product-service
  namespace: ecommerce
spec:
  selector:
    app: product-service
  ports:
  - port: 8080
    targetPort: 8080
  type: ClusterIP

User Service Deployment

Create the deployment and service for the user service:

# user-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  namespace: ecommerce
spec:
  replicas: 2
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: your-registry/user-service:v1
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: "0.5"
            memory: "512Mi"
          requests:
            cpu: "0.1"
            memory: "128Mi"
        env:
        - name: PORT
          value: "8080"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
  namespace: ecommerce
spec:
  selector:
    app: user-service
  ports:
  - port: 8080
    targetPort: 8080
  type: ClusterIP

Order Service Deployment

Create the deployment and service for the order service:

# order-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: ecommerce
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: your-registry/order-service:v1
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: "0.5"
            memory: "512Mi"
          requests:
            cpu: "0.1"
            memory: "128Mi"
        env:
        - name: PORT
          value: "8080"
        - name: USER_SERVICE_URL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: USER_SERVICE_URL
        - name: PRODUCT_SERVICE_URL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: PRODUCT_SERVICE_URL
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: ecommerce
spec:
  selector:
    app: order-service
  ports:
  - port: 8080
    targetPort: 8080
  type: ClusterIP

Frontend Service Deployment

Finally, create the deployment and service for the frontend:

# frontend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: ecommerce
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: your-registry/frontend:v1
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: "0.5"
            memory: "512Mi"
          requests:
            cpu: "0.1"
            memory: "128Mi"
        env:
        - name: PORT
          value: "8080"
        - name: PRODUCT_SERVICE_URL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: PRODUCT_SERVICE_URL
        - name: USER_SERVICE_URL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: USER_SERVICE_URL
        - name: ORDER_SERVICE_URL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: ORDER_SERVICE_URL
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: ecommerce
spec:
  selector:
    app: frontend
  ports:
  - port: 80
    targetPort: 8080
  type: ClusterIP

Ingress Configuration

To expose our frontend service to external traffic, we’ll create an Ingress resource:

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ecommerce-ingress
  namespace: ecommerce
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: ecommerce.example.com  # Replace with your domain
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend
            port:
              number: 80
      - path: /api/products
        pathType: Prefix
        backend:
          service:
            name: product-service
            port:
              number: 8080
      - path: /api/users
        pathType: Prefix
        backend:
          service:
            name: user-service
            port:
              number: 8080
      - path: /api/orders
        pathType: Prefix
        backend:
          service:
            name: order-service
            port:
              number: 8080

Part 3: Deploying to Kubernetes

Now let’s deploy our application to Kubernetes:

# Apply all configurations
kubectl apply -f namespace.yaml
kubectl apply -f configmap.yaml
kubectl apply -f product-service.yaml
kubectl apply -f user-service.yaml
kubectl apply -f order-service.yaml
kubectl apply -f frontend.yaml
kubectl apply -f ingress.yaml

Verify that all pods are running:

kubectl get pods -n ecommerce

You should see output similar to:

NAME                               READY   STATUS    RESTARTS   AGE
frontend-7d9f6d8f44-2xvqp          1/1     Running   0          45s
frontend-7d9f6d8f44-l8mxz          1/1     Running   0          45s
order-service-6c9fb5f5d7-8gqvn     1/1     Running   0          60s
order-service-6c9fb5f5d7-vq2cz     1/1     Running   0          60s
product-service-7c8f94f4c7-kpd8h   1/1     Running   0          75s
product-service-7c8f94f4c7-xvbdq   1/1     Running   0          75s
user-service-5d87bf6cd9-29nhp      1/1     Running   0          67s
user-service-5d87bf6cd9-wgrz4      1/1     Running   0          67s

Part 4: Advanced Kubernetes Features for Go Applications

Horizontal Pod Autoscaling

Go applications are lightweight and can scale efficiently. Let’s add horizontal pod autoscaling to our product service:

# product-service-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: product-service-hpa
  namespace: ecommerce
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: product-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Apply the HPA:

kubectl apply -f product-service-hpa.yaml

Persistent Storage for Go Applications

While our simple services don’t need persistent storage, in a real-world scenario, you might need it for databases or file storage. Here’s an example of a PersistentVolumeClaim and how to use it with a Go application:

# product-storage.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: product-data
  namespace: ecommerce
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

Then update the deployment to use it:

# Updated section from product-service.yaml
spec:
  containers:
  - name: product-service
    # ...other configuration...
    volumeMounts:
    - name: product-data
      mountPath: /app/data
  volumes:
  - name: product-data
    persistentVolumeClaim:
      claimName: product-data

Implementing Health Checks in Go

We already added basic health checks to our services, but let’s improve them with more detailed readiness and liveness probes. Here’s an expanded health check handler for Go:

func health(w http.ResponseWriter, r *http.Request) {
    // Check dependencies
    dbHealthy := checkDatabaseConnection()
    dependenciesHealthy := checkExternalServices()
    
    if r.URL.Path == "/health/live" {
        // Liveness check - just verify the app is running
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"status": "ok"}`))
        return
    }
    
    // Readiness check - verify it can service requests
    if !dbHealthy || !dependenciesHealthy {
        w.WriteHeader(http.StatusServiceUnavailable)
        w.Write([]byte(`{"status": "not ready", "message": "dependencies not available"}`))
        return
    }
    
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status": "ready"}`))
}

func checkDatabaseConnection() bool {
    // In a real app, check your DB connection
    return true
}

func checkExternalServices() bool {
    // Check if dependent services are available
    return true
}

Then update the probes in your deployment:

livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 15
  periodSeconds: 20
readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

Graceful Shutdown in Go for Kubernetes

Kubernetes sends SIGTERM signals to pods when they need to be terminated. Make sure your Go applications handle this gracefully:

func main() {
    // ... initialization code ...
    
    server := &http.Server{
        Addr:    ":" + port,
        Handler: router,
    }
    
    // Start server in a goroutine so we can handle shutdown
    go func() {
        log.Printf("Server starting on port %s", port)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server failed: %v", err)
        }
    }()
    
    // Set up channel to listen for signals
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    
    // Block until signal is received
    <-quit
    log.Println("Server shutting down...")
    
    // Create context with timeout for outstanding requests
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    // Attempt graceful shutdown
    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }
    
    log.Println("Server exited gracefully")
}

This ensures that ongoing requests are completed before the pod is terminated.

Part 5: Monitoring Go Applications in Kubernetes

Prometheus Metrics in Go

Let’s add Prometheus metrics to our Go services using the prometheus/client_golang package:

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    httpRequestsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "endpoint", "status"},
    )
    
    httpRequestDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "Duration of HTTP requests in seconds",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "endpoint"},
    )
)

func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Create a response writer that captures the status code
        ww := &responseWriterCapture{ResponseWriter: w, statusCode: http.StatusOK}
        
        // Call the next handler
        next.ServeHTTP(ww, r)
        
        // Record metrics
        duration := time.Since(start).Seconds()
        httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, fmt.Sprintf("%d", ww.statusCode)).Inc()
        httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
    })
}

// Response writer that captures the status code
type responseWriterCapture struct {
    http.ResponseWriter
    statusCode int
}

func (rwc *responseWriterCapture) WriteHeader(statusCode int) {
    rwc.statusCode = statusCode
    rwc.ResponseWriter.WriteHeader(statusCode)
}

func main() {
    // ... other initialization ...
    
    // Add Prometheus metrics endpoint
    http.Handle("/metrics", promhttp.Handler())
    
    // Apply metrics middleware to all routes
    router := http.NewServeMux()
    router.Handle("/products", metricsMiddleware(http.HandlerFunc(getProducts)))
    router.Handle("/products/", metricsMiddleware(http.HandlerFunc(getProduct)))
    router.Handle("/health", metricsMiddleware(http.HandlerFunc(health)))
    router.Handle("/metrics", promhttp.Handler())
    
    // ... server startup code ...
}

Now update your service to expose the metrics port:

# product-service.yaml update
apiVersion: v1
kind: Service
metadata:
  name: product-service
  namespace: ecommerce
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "8080"
    prometheus.io/path: "/metrics"
spec:
  selector:
    app: product-service
  ports:
  - name: http
    port: 8080
    targetPort: 8080
  - name: metrics
    port: 9090
    targetPort: 8080
  type: ClusterIP

Deploying Prometheus and Grafana

Let’s set up monitoring with Prometheus and Grafana:

# prometheus.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
    scrape_configs:
      - job_name: 'kubernetes-pods'
        kubernetes_sd_configs:
          - role: pod
        relabel_configs:
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
            action: keep
            regex: true
          - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
            action: replace
            target_label: __metrics_path__
            regex: (.+)
          - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
            action: replace
            regex: (.+):(?:\d+);(\d+)
            replacement: ${1}:${2}
            target_label: __address__
          - source_labels: [__meta_kubernetes_namespace]
            action: replace
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_pod_name]
            action: replace
            target_label: kubernetes_pod_name

Part 6: CI/CD Pipeline for Go Microservices on Kubernetes

Here’s a sample GitHub Actions workflow for our Go microservices:

# .github/workflows/ci-cd.yaml
name: CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service: [product-service, user-service, order-service, frontend]
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: '1.20'
    
    - name: Build and Test
      run: |
        cd ${{ matrix.service }}
        go mod init ${{ matrix.service }}
        go mod tidy
        go build -v ./...
        go test -v ./...
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Login to DockerHub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
    
    - name: Build and push
      uses: docker/build-push-action@v3
      with:
        context: ./${{ matrix.service }}
        push: true
        tags: your-registry/${{ matrix.service }}:${{ github.sha }},your-registry/${{ matrix.service }}:latest
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up kubectl
      uses: azure/setup-kubectl@v3
      with:
        version: 'latest'
    
    - name: Set up kubeconfig
      run: |
        mkdir -p $HOME/.kube
        echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config
        chmod 600 $HOME/.kube/config
    
    - name: Update Kubernetes deployments with new image
      run: |
        kubectl set image deployment/product-service product-service=your-registry/product-service:${{ github.sha }} -n ecommerce
        kubectl set image deployment/user-service user-service=your-registry/user-service:${{ github.sha }} -n ecommerce
        kubectl set image deployment/order-service order-service=your-registry/order-service:${{ github.sha }} -n ecommerce
        kubectl set image deployment/frontend frontend=your-registry/frontend:${{ github.sha }} -n ecommerce
    
    - name: Verify deployments
      run: |
        kubectl rollout status deployment/product-service -n ecommerce
        kubectl rollout status deployment/user-service -n ecommerce
        kubectl rollout status deployment/order-service -n ecommerce
        kubectl rollout status deployment/frontend -n ecommerce

Part 7: Security Best Practices for Go Applications in Kubernetes

Network Policies

Restrict communication between pods with network policies:

# network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: ecommerce
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: product-service-policy
  namespace: ecommerce
spec:
  podSelector:
    matchLabels:
      app: product-service
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    - podSelector:
        matchLabels:
          app: order-service
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

Security Context

Add security context to your deployments:

# Updated security section in product-service.yaml
spec:
  containers:
  - name: product-service
    # ...other config...
    securityContext:
      readOnlyRootFilesystem: true
      runAsNonRoot: true
      runAsUser: 10001
      capabilities:
        drop:
        - ALL

Secure Secrets Management

Use a dedicated secrets management solution like HashiCorp Vault instead of Kubernetes secrets for sensitive information. Here’s an example of integrating with Vault:

  1. Install the Vault Injector:
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault --set "injector.enabled=true"
  1. Update your deployment to use the Vault injector:
# Updated product-service.yaml with Vault annotations
metadata:
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/agent-inject-secret-database-config.json: "database/creds/product-service"
    vault.hashicorp.com/role: "product-service"

Conclusion

In this comprehensive guide, we’ve covered the complete process of deploying Go microservices to Kubernetes. From building containerized services to setting up monitoring, scaling, and security, you now have the knowledge to deploy robust Go applications on Kubernetes.

The combination of Go’s efficiency and Kubernetes’ orchestration capabilities creates a powerful platform for building scalable, resilient microservices.

Key takeaways:

  1. Go’s small binary sizes and minimal runtime dependencies make it ideal for containerization
  2. Kubernetes provides the orchestration layer to manage deployments, scaling, and networking
  3. Health checks and graceful shutdown are essential for reliable Go applications in Kubernetes
  4. Monitoring with Prometheus enables visibility into your applications’ performance
  5. Security must be considered at all layers: network, container, and application

As you continue building and deploying Go applications on Kubernetes, remember that the ecosystem continues to evolve. Stay updated with the latest best practices in both Go and Kubernetes to ensure your applications remain secure, performant, and maintainable.

Happy deploying!