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:
- Production Readiness - Checklist and configuration for production environments
- Containerization - Docker best practices and multi-stage builds
- Cloud Deployment - Deploying to major cloud platforms
- CI/CD Pipelines - Automated testing, building, and deployment
- Monitoring & Observability - Metrics, logging, tracing, and alerting
- Scaling & Performance - Load balancing, auto-scaling, and optimization
- 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:
- Application Layer: Code quality, error handling, performance
- Infrastructure Layer: Servers, networking, storage
- Platform Layer: Operating systems, runtime environments
- Monitoring Layer: Observability, alerting, debugging tools
- 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:
- Codebase: One codebase tracked in revision control
- Dependencies: Explicitly declare and isolate dependencies
- Config: Store config in the environment
- Backing Services: Treat backing services as attached resources
- Build, Release, Run: Strictly separate build and run stages
- Processes: Execute as one or more stateless processes
- Port Binding: Export services via port binding
- Concurrency: Scale out via the process model
- Disposability: Maximize robustness with fast startup and graceful shutdown
- Dev/Prod Parity: Keep development and production as similar as possible
- Logs: Treat logs as event streams
- 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:
- Blue-Green: Switch entire environment at once
- Rolling: Update instances one by one
- Canary: Test with small user percentage first
- A/B Testing: Compare different versions simultaneously
Common Misconceptions
-
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.
-
Misconception: “More monitoring means better observability” Reality: Too many metrics can create noise. Focus on meaningful indicators that help diagnose and prevent issues.
-
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:
- Create Kubernetes manifests for deployment, service, configmap, and secrets
- Set up horizontal pod autoscaler (HPA)
- Configure persistent volumes for database
- Add ingress controller for external access
- 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
-
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
-
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
-
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
- Production readiness requires proper configuration, monitoring, and deployment strategies
- Containerization enables consistent deployments across environments
- CI/CD pipelines automate testing and deployment while reducing human error
- Monitoring and observability are essential for maintaining reliable services
Quick Review Questions
- What are the key components of a production-ready Gin application?
- How do you implement zero-downtime deployments?
- What monitoring metrics are most important for web applications?
- 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