Production Deployment and DevOps

Learning Objectives: By the end of this lesson, you will be able to:

  • Deploy Gin applications to various cloud platforms (AWS, GCP, Azure)
  • Containerize applications with Docker and orchestrate with Kubernetes
  • Implement CI/CD pipelines for automated testing and deployment
  • Set up comprehensive monitoring, logging, and alerting systems
  • Configure load balancing, auto-scaling, and high availability
  • Implement security best practices for production environments
  • Optimize application performance and resource utilization
  • Handle database migrations and zero-downtime deployments

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Have you ever experienced that heart-stopping moment when your application works perfectly in development but crashes mysteriously in production? Or spent sleepless nights wondering if your server is running out of memory while you’re away? Today, we’re going to transform you from someone who “throws code over the wall” into a confident DevOps-minded developer who can deploy, monitor, and maintain production applications with confidence.

Lesson Overview

  • Duration: 150 minutes
  • Prerequisites: Lessons 1-8 completed, basic understanding of Linux, Docker concepts, cloud services
  • Materials Needed: Go 1.25+ for Gin v1.12, Docker, access to cloud platform (AWS/GCP/Azure), Git repository

Proxy safety: Gin trusts all proxies by default. If your deployment uses c.ClientIP() for logging, access control, or rate limiting, configure router.SetTrustedProxies with only your actual ingress IPs/CIDRs; use nil without a proxy. Do not trust arbitrary X-Forwarded-For headers.

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Production Readiness - Checklist and configuration for production environments
  2. Containerization - Docker best practices and multi-stage builds
  3. Cloud Deployment - Deploying to major cloud platforms
  4. CI/CD Pipelines - Automated testing, building, and deployment
  5. Monitoring & Observability - Metrics, logging, tracing, and alerting
  6. Scaling & Performance - Load balancing, auto-scaling, and optimization
  7. Security & Compliance - Production security measures and best practices

Success Criteria:

By the end of this session, you should be able to deploy and maintain a production-ready Gin application with proper monitoring, scaling, and security measures.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

The Production Environment Pyramid

Production readiness involves multiple layers:

  1. Application Layer: Code quality, error handling, performance
  2. Infrastructure Layer: Servers, networking, storage
  3. Platform Layer: Operating systems, runtime environments
  4. Monitoring Layer: Observability, alerting, debugging tools
  5. Security Layer: Authentication, encryption, compliance

Real-World Analogy: Think of launching a rocket - every system must work perfectly because there’s no “undo” button in production. You need multiple safety checks, monitoring systems, and backup plans.

Why This Matters: Production failures can cost businesses millions and damage customer trust. Proper DevOps practices prevent outages and enable rapid response to issues.

The Twelve-Factor App Principles

These principles guide cloud-native application development:

  1. Codebase: One codebase tracked in revision control
  2. Dependencies: Explicitly declare and isolate dependencies
  3. Config: Store config in the environment
  4. Backing Services: Treat backing services as attached resources
  5. Build, Release, Run: Strictly separate build and run stages
  6. Processes: Execute as one or more stateless processes
  7. Port Binding: Export services via port binding
  8. Concurrency: Scale out via the process model
  9. Disposability: Maximize robustness with fast startup and graceful shutdown
  10. Dev/Prod Parity: Keep development and production as similar as possible
  11. Logs: Treat logs as event streams
  12. Admin Processes: Run admin/management tasks as one-off processes

Infrastructure as Code (IaC)

IaC treats infrastructure configuration as code:

  • Version controlled and reviewable
  • Reproducible across environments
  • Automated provisioning and updates
  • Self-documenting architecture

Observability vs. Monitoring

  • Monitoring: Known unknowns (predefined metrics and alerts)
  • Observability: Unknown unknowns (ability to understand system behavior)

Key Terminology

  • SLA (Service Level Agreement): Commitment to specific performance levels (99.9% uptime)
  • SLO (Service Level Objective): Internal goals for service performance
  • SLI (Service Level Indicator): Actual measurements of service performance
  • MTTR (Mean Time To Recovery): Average time to restore service after failure
  • MTBF (Mean Time Before Failure): Average time between system failures
  • Blue-Green Deployment: Two identical production environments for zero-downtime updates
  • Canary Deployment: Gradual rollout to a subset of users before full deployment

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Production Challenges

Configuration Management

Development vs. production configurations differ significantly:

  • Database connections: Different hosts, credentials, connection pools
  • External services: Different API endpoints, rate limits
  • Security settings: HTTPS enforcement, CORS policies
  • Performance settings: Timeouts, buffer sizes, worker counts

Scalability Considerations

Production applications must handle varying loads:

  • Horizontal scaling: Adding more server instances
  • Vertical scaling: Increasing server resources
  • Auto-scaling: Automatic adjustment based on metrics
  • Load distribution: Balancing requests across instances

Failure Modes

Production systems face unique challenges:

  • Network partitions: Services become unreachable
  • Resource exhaustion: Memory leaks, disk full, CPU overload
  • Cascading failures: One service failure affects others
  • Data corruption: Inconsistent state across systems
  • Security breaches: Unauthorized access attempts

Deployment Strategies

Different approaches for updating production systems:

  1. Blue-Green: Switch entire environment at once
  2. Rolling: Update instances one by one
  3. Canary: Test with small user percentage first
  4. A/B Testing: Compare different versions simultaneously

Common Misconceptions

  1. Misconception: “If it works in development, it will work in production” Reality: Production has different data volumes, network conditions, and failure modes that don’t exist in development.

  2. Misconception: “More monitoring means better observability” Reality: Too many metrics can create noise. Focus on meaningful indicators that help diagnose and prevent issues.

  3. Misconception: “Kubernetes is always the right choice for deployment” Reality: Simple applications might benefit from simpler deployment models. Choose complexity that matches your needs.

5. Live Coding Demonstration (20-25 minutes)

Example 1: Production-Ready Application Structure

// cmd/server/main.go
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
    
    "github.com/gin-gonic/gin"
    "github.com/your-app/internal/config"
    "github.com/your-app/internal/database"
    "github.com/your-app/internal/handlers"
    "github.com/your-app/internal/middleware"
    "github.com/your-app/internal/monitoring"
    "github.com/your-app/internal/services"
)

func main() {
    // Load configuration
    cfg, err := config.Load()
    if err != nil {
        log.Fatal("Failed to load configuration:", err)
    }
    
    // Setup logging
    logger := setupLogger(cfg.LogLevel)
    
    // Setup database
    db, err := database.NewConnection(cfg.Database)
    if err != nil {
        logger.Fatal("Failed to connect to database:", err)
    }
    defer database.Close(db)
    
    // Run migrations
    if err := database.Migrate(db); err != nil {
        logger.Fatal("Failed to run migrations:", err)
    }
    
    // Setup monitoring
    metrics := monitoring.NewMetrics()
    tracer := monitoring.NewTracer(cfg.Jaeger)
    
    // Setup services
    userService := services.NewUserService(db, logger)
    authService := services.NewAuthService(cfg.JWT, logger)
    
    // Setup handlers
    userHandler := handlers.NewUserHandler(userService, logger)
    authHandler := handlers.NewAuthHandler(authService, logger)
    
    // Setup Gin router
    gin.SetMode(cfg.GinMode)
    router := gin.New()
    
    // Add middleware
    router.Use(middleware.RequestID())
    router.Use(middleware.Logger(logger))
    router.Use(middleware.Recovery(logger))
    router.Use(middleware.CORS(cfg.CORS))
    router.Use(middleware.Security())
    router.Use(middleware.Metrics(metrics))
    router.Use(middleware.Tracing(tracer))
    
    // Health checks
    router.GET("/health", handlers.HealthCheck(db))
    router.GET("/ready", handlers.ReadinessCheck(db))
    router.GET("/metrics", gin.WrapH(metrics.Handler()))
    
    // API routes
    v1 := router.Group("/api/v1")
    {
        // Auth routes
        auth := v1.Group("/auth")
        {
            auth.POST("/login", authHandler.Login)
            auth.POST("/register", authHandler.Register)
            auth.POST("/refresh", authHandler.RefreshToken)
        }
        
        // Protected routes
        protected := v1.Group("")
        protected.Use(middleware.Auth(authService))
        {
            users := protected.Group("/users")
            {
                users.GET("", userHandler.ListUsers)
                users.GET("/:id", userHandler.GetUser)
                users.PUT("/:id", userHandler.UpdateUser)
                users.DELETE("/:id", userHandler.DeleteUser)
            }
        }
    }
    
    // Setup HTTP server
    server := &http.Server{
        Addr:         ":" + cfg.Port,
        Handler:      router,
        ReadTimeout:  cfg.Server.ReadTimeout,
        WriteTimeout: cfg.Server.WriteTimeout,
        IdleTimeout:  cfg.Server.IdleTimeout,
    }
    
    // Start server in goroutine
    go func() {
        logger.Info("Starting server on port " + cfg.Port)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            logger.Fatal("Server failed to start:", err)
        }
    }()
    
    // Wait for interrupt signal for graceful shutdown
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    logger.Info("Shutting down server...")
    
    // Give outstanding requests 30 seconds to complete
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := server.Shutdown(ctx); err != nil {
        logger.Error("Server forced to shutdown:", err)
    }
    
    logger.Info("Server exited")
}

func setupLogger(level string) *log.Logger {
    // Setup structured logging (use logrus, zap, or similar in real applications)
    return log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile)
}

// internal/config/config.go
package config

import (
    "fmt"
    "os"
    "strconv"
    "time"
)

// Config holds all configuration for the application
type Config struct {
    Port     string         `json:"port"`
    GinMode  string         `json:"gin_mode"`
    LogLevel string         `json:"log_level"`
    Database DatabaseConfig `json:"database"`
    JWT      JWTConfig      `json:"jwt"`
    Server   ServerConfig   `json:"server"`
    CORS     CORSConfig     `json:"cors"`
    Jaeger   JaegerConfig   `json:"jaeger"`
}

type DatabaseConfig struct {
    Host         string `json:"host"`
    Port         string `json:"port"`
    User         string `json:"user"`
    Password     string `json:"-"` // Do not serialize credentials.
    Name         string `json:"name"`
    SSLMode      string `json:"ssl_mode"`
    MaxOpenConns int    `json:"max_open_conns"`
    MaxIdleConns int    `json:"max_idle_conns"`
    MaxLifetime  time.Duration `json:"max_lifetime"`
}

type JWTConfig struct {
    SecretKey            string        `json:"-"`
    AccessTokenDuration  time.Duration `json:"access_token_duration"`
    RefreshTokenDuration time.Duration `json:"refresh_token_duration"`
    Issuer              string        `json:"issuer"`
}

type ServerConfig struct {
    ReadTimeout  time.Duration `json:"read_timeout"`
    WriteTimeout time.Duration `json:"write_timeout"`
    IdleTimeout  time.Duration `json:"idle_timeout"`
}

type CORSConfig struct {
    AllowedOrigins []string `json:"allowed_origins"`
    AllowedMethods []string `json:"allowed_methods"`
    AllowedHeaders []string `json:"allowed_headers"`
}

type JaegerConfig struct {
    Endpoint    string  `json:"endpoint"`
    ServiceName string  `json:"service_name"`
    SampleRate  float64 `json:"sample_rate"`
}

// Load loads configuration from environment variables
func Load() (*Config, error) {
    config := &Config{
        Port:     getEnv("PORT", "8080"),
        GinMode:  getEnv("GIN_MODE", "release"),
        LogLevel: getEnv("LOG_LEVEL", "info"),
        
        Database: DatabaseConfig{
            Host:         getEnv("DB_HOST", "localhost"),
            Port:         getEnv("DB_PORT", "5432"),
            User:         getEnv("DB_USER", "postgres"),
            Password:     getEnv("DB_PASSWORD", ""),
            Name:         getEnv("DB_NAME", "myapp"),
            SSLMode:      getEnv("DB_SSL_MODE", "require"), // explicitly disable only for local Compose without TLS
            MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
            MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 5),
            MaxLifetime:  getEnvAsDuration("DB_MAX_LIFETIME", "1h"),
        },
        
        JWT: JWTConfig{
            SecretKey:            getRequiredEnv("JWT_SECRET_KEY"),
            AccessTokenDuration:  getEnvAsDuration("JWT_ACCESS_DURATION", "15m"),
            RefreshTokenDuration: getEnvAsDuration("JWT_REFRESH_DURATION", "168h"), // time.ParseDuration has no "d" unit
            Issuer:              getEnv("JWT_ISSUER", "myapp"),
        },
        
        Server: ServerConfig{
            ReadTimeout:  getEnvAsDuration("SERVER_READ_TIMEOUT", "10s"),
            WriteTimeout: getEnvAsDuration("SERVER_WRITE_TIMEOUT", "10s"),
            IdleTimeout:  getEnvAsDuration("SERVER_IDLE_TIMEOUT", "120s"),
        },
        
        CORS: CORSConfig{
            AllowedOrigins: getEnvAsSlice("CORS_ALLOWED_ORIGINS", []string{}), // no cross-origin access by default
            AllowedMethods: getEnvAsSlice("CORS_ALLOWED_METHODS", []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
            AllowedHeaders: getEnvAsSlice("CORS_ALLOWED_HEADERS", []string{"*"}),
        },
        
        Jaeger: JaegerConfig{
            Endpoint:    getEnv("JAEGER_ENDPOINT", "http://localhost:14268/api/traces"),
            ServiceName: getEnv("JAEGER_SERVICE_NAME", "myapp"),
            SampleRate:  getEnvAsFloat("JAEGER_SAMPLE_RATE", 0.1),
        },
    }
    
    // Validate required configuration
    if err := config.Validate(); err != nil {
        return nil, err
    }
    
    return config, nil
}

// Validate ensures all required configuration is present
func (c *Config) Validate() error {
    if c.Database.Password == "" {
        return fmt.Errorf("database password is required")
    }
    
    if c.JWT.SecretKey == "" {
        return fmt.Errorf("JWT secret key is required")
    }
    
    return nil
}

// Helper functions for environment variable parsing
func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

func getRequiredEnv(key string) string {
    value := os.Getenv(key)
    if value == "" {
        panic(fmt.Sprintf("Required environment variable %s is not set", key))
    }
    return value
}

func getEnvAsInt(key string, defaultValue int) int {
    valueStr := getEnv(key, "")
    if valueStr == "" {
        return defaultValue
    }
    
    value, err := strconv.Atoi(valueStr)
    if err != nil {
        return defaultValue
    }
    
    return value
}

func getEnvAsDuration(key string, defaultValue string) time.Duration {
    valueStr := getEnv(key, defaultValue)
    value, err := time.ParseDuration(valueStr)
    if err != nil {
        panic(fmt.Sprintf("Invalid duration for %s: %s", key, valueStr))
    }
    return value
}

func getEnvAsFloat(key string, defaultValue float64) float64 {
    valueStr := getEnv(key, "")
    if valueStr == "" {
        return defaultValue
    }
    
    value, err := strconv.ParseFloat(valueStr, 64)
    if err != nil {
        return defaultValue
    }
    
    return value
}

func getEnvAsSlice(key string, defaultValue []string) []string {
    valueStr := getEnv(key, "")
    if valueStr == "" {
        return defaultValue
    }
    
    // Simple comma-separated parsing (use proper CSV parser for complex cases)
    return strings.Split(valueStr, ",")
}

// handlers/health.go
package handlers

import (
    "net/http"
    "time"
    
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

// HealthCheck returns basic health status
func HealthCheck(db *gorm.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status":    "healthy",
            "timestamp": time.Now().UTC(),
            "version":   os.Getenv("APP_VERSION"), // Set during build
        })
    }
}

// ReadinessCheck verifies application is ready to serve traffic
func ReadinessCheck(db *gorm.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        // Check database connectivity
        sqlDB, err := db.DB()
        if err != nil {
            c.JSON(http.StatusServiceUnavailable, gin.H{
                "status": "not ready",
                "reason": "database connection error",
                "error":  err.Error(),
            })
            return
        }
        
        if err := sqlDB.Ping(); err != nil {
            c.JSON(http.StatusServiceUnavailable, gin.H{
                "status": "not ready",
                "reason": "database ping failed",
                "error":  err.Error(),
            })
            return
        }
        
        // Add other readiness checks here (Redis, external APIs, etc.)
        
        c.JSON(http.StatusOK, gin.H{
            "status":    "ready",
            "timestamp": time.Now().UTC(),
            "checks": gin.H{
                "database": "ok",
            },
        })
    }
}

Example 2: Comprehensive Dockerfile and Docker Compose

# Build stage
FROM golang:1.27-alpine AS builder

# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata

# Set working directory
WORKDIR /build

# Copy go mod and sum files
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy source code
COPY . .

# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags='-w -s' \
    -o main cmd/server/main.go

# Final stage
FROM scratch AS production

# Copy CA certificates
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# Copy binary
COPY --from=builder /build/main /main

# If the application loads HTML templates/static assets from disk, copy those
# directories from the builder too and set its working directory accordingly.

# Create non-root user
USER 65534:65534

# Expose port
EXPOSE 8080

# Run the binary
ENTRYPOINT ["/main"]

The static build above is suitable for a pure-Go PostgreSQL driver. The SQLite driver gorm.io/driver/sqlite uses CGO: choose a CGO-enabled build and a runtime image with matching C libraries, or use a compatible pure-Go driver. A scratch image has no shell or curl; add a health check only if the binary actually implements a health-check command, or configure an orchestrator HTTP probe against /health.

# compose.yaml (illustrative; provide strong secrets via an uncommitted .env or secret manager)

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    ports:
      - "8080:8080"
    environment:
      - PORT=8080
      - GIN_MODE=release
      - DB_HOST=postgres
      - DB_USER=myapp
      - DB_PASSWORD=${DB_PASSWORD:?set DB_PASSWORD}
      - DB_SSL_MODE=disable # local Compose PostgreSQL; use TLS for external DBs
      - DB_NAME=myapp_production
      - JWT_SECRET_KEY=${JWT_SECRET_KEY:?set JWT_SECRET_KEY}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - app-network

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=myapp_production
      - POSTGRES_USER=myapp
      - POSTGRES_PASSWORD=${DB_PASSWORD:?set DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    restart: unless-stopped
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp -d myapp_production"]
      interval: 30s
      timeout: 10s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - app-network

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--storage.tsdb.retention.time=200h'
      - '--web.enable-lifecycle'
    restart: unless-stopped
    networks:
      - app-network

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?set GRAFANA_ADMIN_PASSWORD}
      - GF_USERS_ALLOW_SIGN_UP=false
    restart: unless-stopped
    networks:
      - app-network

  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"
      - "14268:14268"
    environment:
      - COLLECTOR_ZIPKIN_HTTP_PORT=9411
    restart: unless-stopped
    networks:
      - app-network

volumes:
  postgres_data:
  redis_data:
  prometheus_data:
  grafana_data:

networks:
  app-network:
    driver: bridge

Save the following in nginx.conf (do not append it to the Compose YAML):

This Compose stack is illustrative: supply init.sql, nginx.conf, prometheus.yml, certificates and strong secrets before starting it; do not expose observability ports publicly without access controls. Pin immutable image versions/digests rather than latest for repeatable deployments.

# nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream app {
        server app:8080;
    }

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;

    # SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    server {
        listen 80;
        server_name your-domain.com;
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name your-domain.com;

        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;

        # Security headers
        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";

        # API endpoints with rate limiting
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://app;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Login endpoint with stricter rate limiting  
        location /api/v1/auth/login {
            limit_req zone=login burst=5 nodelay;
            proxy_pass http://app;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # Health checks (no rate limiting)
        location /health {
            proxy_pass http://app;
            access_log off;
        }

        location /ready {
            proxy_pass http://app;
            access_log off;
        }
    }
}

Example 3: CI/CD Pipeline with GitHub Actions

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

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

env:
  GO_VERSION: '1.27'
  DOCKER_REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
      
      redis:
        image: redis:7
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

    steps:
    - uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v6
      with:
        go-version: ${{ env.GO_VERSION }}

    - name: Cache Go modules
      uses: actions/cache@v3
      with:
        path: |
          ~/.cache/go-build
          ~/go/pkg/mod
        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
        restore-keys: |
          ${{ runner.os }}-go-

    - name: Install dependencies
      run: go mod download

    - name: Run linting
      uses: golangci/golangci-lint-action@v3
      with:
        version: latest
        args: --timeout=5m

    - name: Run security scan
      uses: securecodewarrior/github-action-gosec@master
      with:
        args: ./...

    - name: Run tests
      env:
        DB_HOST: localhost
        DB_PORT: 5432
        DB_USER: postgres
        DB_PASSWORD: postgres
        DB_NAME: test_db
        REDIS_HOST: localhost
        REDIS_PORT: 6379
        JWT_SECRET_KEY: test-secret-key
      run: |
        go test -v -race -coverprofile=coverage.out ./...
        go tool cover -html=coverage.out -o coverage.html

    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.out
        flags: unittests

    - name: Run integration tests
      env:
        INTEGRATION_TEST: true
        DB_HOST: localhost
        DB_PORT: 5432
        DB_USER: postgres
        DB_PASSWORD: postgres
        DB_NAME: test_db
      run: go test -v -tags=integration ./tests/integration/...

  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'push'

    steps:
    - uses: actions/checkout@v4

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.DOCKER_REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=sha,prefix={{branch}}-
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        build-args: |
          VERSION=${{ github.sha }}
          BUILD_TIME=${{ github.event.head_commit.timestamp }}

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    environment: staging

    steps:
    - uses: actions/checkout@v4

    - name: Deploy to staging
      env:
        KUBECONFIG_DATA: ${{ secrets.STAGING_KUBECONFIG }}
        IMAGE_TAG: develop-${{ github.sha }}
      run: |
        echo "$KUBECONFIG_DATA" | base64 -d > kubeconfig
        export KUBECONFIG=kubeconfig
        
        # Update deployment image
        kubectl set image deployment/myapp-staging myapp=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
        
        # Wait for rollout to complete
        kubectl rollout status deployment/myapp-staging --timeout=300s
        
        # Run smoke tests
        kubectl run smoke-test --rm -i --restart=Never --image=curlimages/curl -- \
          curl -f http://myapp-staging-service/health

  deploy-production:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production

    steps:
    - uses: actions/checkout@v4

    - name: Deploy to production
      env:
        KUBECONFIG_DATA: ${{ secrets.PRODUCTION_KUBECONFIG }}
        IMAGE_TAG: main-${{ github.sha }}
      run: |
        echo "$KUBECONFIG_DATA" | base64 -d > kubeconfig
        export KUBECONFIG=kubeconfig
        
        # Blue-green deployment
        ./scripts/blue-green-deploy.sh ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}

  notification:
    needs: [test, build, deploy-staging, deploy-production]
    runs-on: ubuntu-latest
    if: always()

    steps:
    - name: Notify Slack
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        channel: '#deployments'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}
        fields: repo,message,commit,author,action,eventName,ref,workflow

# scripts/blue-green-deploy.sh
#!/bin/bash
set -e

IMAGE_TAG=$1
NAMESPACE=${NAMESPACE:-default}
APP_NAME=${APP_NAME:-myapp}

echo "Starting blue-green deployment for $IMAGE_TAG"

# Get current active deployment
CURRENT_COLOR=$(kubectl get service $APP_NAME -o jsonpath='{.spec.selector.color}')
echo "Current active color: $CURRENT_COLOR"

# Determine new color
if [ "$CURRENT_COLOR" = "blue" ]; then
    NEW_COLOR="green"
else
    NEW_COLOR="blue"
fi

echo "Deploying to color: $NEW_COLOR"

# Update deployment with new image
kubectl set image deployment/$APP_NAME-$NEW_COLOR $APP_NAME=$IMAGE_TAG

# Wait for deployment to be ready
echo "Waiting for deployment to be ready..."
kubectl rollout status deployment/$APP_NAME-$NEW_COLOR --timeout=300s

# Run health checks
echo "Running health checks..."
kubectl run health-check-$NEW_COLOR --rm -i --restart=Never --image=curlimages/curl -- \
    curl -f http://$APP_NAME-$NEW_COLOR-service/health

# Switch traffic to new deployment
echo "Switching traffic to $NEW_COLOR"
kubectl patch service $APP_NAME -p '{"spec":{"selector":{"color":"'$NEW_COLOR'"}}}'

echo "Blue-green deployment completed successfully"

6. Hands-On Practice (30-40 minutes)

Exercise 1: Kubernetes Deployment (15 minutes)

Objective: Deploy your Gin application to Kubernetes with proper configuration

Instructions:

  1. Create Kubernetes manifests for deployment, service, configmap, and secrets
  2. Set up horizontal pod autoscaler (HPA)
  3. Configure persistent volumes for database
  4. Add ingress controller for external access
  5. Implement readiness and liveness probes

Starter Code:

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080
        env:
        - name: PORT
          value: "8080"
        # TODO: Add environment variables from ConfigMap and Secret
        # TODO: Add resource limits and requests
        # TODO: Add readiness and liveness probes

Solution:

# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  PORT: "8080"
  GIN_MODE: "release"
  LOG_LEVEL: "info"
  DB_HOST: "postgres-service"
  DB_PORT: "5432"
  DB_NAME: "myapp"
  REDIS_HOST: "redis-service"
  REDIS_PORT: "6379"

---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: myapp-secret
type: Opaque
data:
  DB_PASSWORD: bXlhcHBfcGFzc3dvcmQ=  # base64 encoded
  JWT_SECRET_KEY: c3VwZXItc2VjcmV0LWp3dC1rZXk=  # base64 encoded

---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080
        envFrom:
        - configMapRef:
            name: myapp-config
        - secretRef:
            name: myapp-secret
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10

---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8080
  type: ClusterIP

---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  annotations:
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - api.yourdomain.com
    secretName: myapp-tls
  rules:
  - host: api.yourdomain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp-service
            port:
              number: 80

Exercise 2: Monitoring and Alerting Setup (15 minutes)

Objective: Implement comprehensive monitoring with Prometheus and Grafana

Requirements:

  • Set up custom metrics in your Gin application
  • Create Prometheus configuration to scrape metrics
  • Build Grafana dashboards for key metrics
  • Configure alerting rules for critical conditions
  • Set up notification channels (Slack, email, PagerDuty)

Hints:

  • Use prometheus/client_golang for custom metrics
  • Create metrics for HTTP requests, database operations, and business logic
  • Set up SLI/SLO-based alerting rules

Exercise 3: Advanced Deployment Strategy (10 minutes)

Objective: Implement canary deployment with automatic rollback

Challenge: Create a deployment strategy that:

  • Gradually routes traffic to new version (10%, 50%, 100%)
  • Monitors error rates and performance metrics
  • Automatically rolls back if metrics exceed thresholds
  • Provides manual approval gates for production
  • Logs all deployment activities for audit

Extension Ideas:

  • Add feature flags for gradual feature rollout
  • Implement A/B testing infrastructure
  • Create deployment approval workflows
  • Add chaos engineering for resilience testing

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Container images are too large and slow to deploy Cause: Including unnecessary dependencies or not using multi-stage builds Solution: Use alpine base images, multi-stage builds, and .dockerignore files

  2. Problem: Applications fail after deployment due to missing configuration Cause: Hardcoded configuration or missing environment variables Solution: Use configuration management tools and validate required config on startup

  3. Problem: Database connections fail in production Cause: Connection pool configuration or network security rules Solution: Properly configure connection pools and network policies

Debugging Tips

  • Use kubectl logs and describe commands for Kubernetes troubleshooting
  • Implement structured logging with correlation IDs
  • Set up distributed tracing for complex request flows
  • Use health checks and readiness probes effectively
  • Monitor resource usage and set appropriate limits

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Follow the twelve-factor app principles for cloud-native applications
  • Implement proper secrets management (never commit secrets to code)
  • Use infrastructure as code for reproducible deployments
  • Set up proper monitoring, logging, and alerting before going to production
  • Implement proper backup and disaster recovery procedures

Next Steps in Your Learning Journey

  • Learn advanced Kubernetes concepts (operators, custom resources, service mesh)
  • Study cloud-native patterns (circuit breakers, bulkheads, timeouts)
  • Explore GitOps workflows with ArgoCD or Flux
  • Learn about service mesh technologies (Istio, Linkerd)
  • Study site reliability engineering (SRE) practices and error budgets

9. Summary and Review (5 minutes)

Key Takeaways

  1. Production readiness requires proper configuration, monitoring, and deployment strategies
  2. Containerization enables consistent deployments across environments
  3. CI/CD pipelines automate testing and deployment while reducing human error
  4. Monitoring and observability are essential for maintaining reliable services

Quick Review Questions

  1. What are the key components of a production-ready Gin application?
  2. How do you implement zero-downtime deployments?
  3. What monitoring metrics are most important for web applications?
  4. How do you secure containerized applications in production?

For Instructors

Teaching Tips

  • Use real production examples to show the consequences of poor deployment practices
  • Demonstrate actual deployment failures and how to recover from them
  • Show monitoring dashboards during simulated incidents
  • Emphasize the importance of automation and reproducibility
  • Connect DevOps practices to business impact (uptime, customer satisfaction, costs)

Assessment Ideas

  • Deploy a complete application stack to a cloud provider
  • Set up comprehensive monitoring and alerting systems
  • Create CI/CD pipelines with proper testing and security checks
  • Implement disaster recovery procedures and test them
  • Design high-availability architecture for specific business requirements

Adaptation Notes

  • For beginners: Focus on basic deployment concepts before advanced orchestration
  • For advanced students: Introduce service mesh and advanced cloud-native patterns
  • Online delivery: Provide cloud sandbox environments for hands-on practice
  • Time management: Kubernetes and monitoring sections can be extended based on audience needs