Skip to content

🚀 Deployment

"If it's not deployed, it doesn't exist."
Go's single binary deployment là một trong những điểm mạnh lớn nhất của ngôn ngữ.

🐳 Docker Multi-Stage Builds

Optimal Dockerfile

dockerfile
# Stage 1: Build
FROM golang:1.22-alpine AS builder

# Install git (for go mod download) and ca-certificates (for HTTPS)
RUN apk add --no-cache git ca-certificates tzdata

WORKDIR /app

# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy source
COPY . .

# Build with optimizations
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-w -s -X main.version=${VERSION:-dev}" \
    -o /app/server \
    ./cmd/server

# Stage 2: Runtime
FROM scratch

# Copy certificates and timezone data
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

# Copy binary
COPY --from=builder /app/server /server

# Non-root user (numeric for scratch)
USER 65534:65534

EXPOSE 8080

ENTRYPOINT ["/server"]

Build Flags Explained

bash
CGO_ENABLED=0    # Disable C bindings → static binary
GOOS=linux       # Target OS
GOARCH=amd64     # Target architecture

-ldflags="-w -s" # Strip debug info → smaller binary
  -w             # Omit DWARF symbol table
  -s             # Omit symbol table

-X main.version=v1.0.0  # Inject version at build time

⚔️ Tradeoff: Base Image Selection

ImageSizeSecurityFeaturesWhen to Use
scratch0 MB★★★★★NoneStatic Go binaries
distroless~2 MB★★★★★CA certsNeed certificates
alpine~5 MB★★★★☆Shell, toolsDebugging, CGO
debian-slim~80 MB★★★☆☆Full distroComplex deps
dockerfile
FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /app/server /server

USER nonroot:nonroot

ENTRYPOINT ["/server"]

📦 Binary Optimization

Size Reduction Techniques

bash
# Before optimization: ~15MB
$ go build -o server ./cmd/server
$ ls -lh server
-rwxr-xr-x 1 user user 15M Jan 15 10:00 server

# With ldflags: ~10MB (-33%)
$ go build -ldflags="-w -s" -o server ./cmd/server
$ ls -lh server
-rwxr-xr-x 1 user user 10M Jan 15 10:00 server

# With UPX compression: ~4MB (-73%)
$ upx --best server
$ ls -lh server
-rwxr-xr-x 1 user user 4.0M Jan 15 10:00 server

🔥 UPX Tradeoffs

ProsCons
Smaller binarySlower startup (decompression)
Faster downloadsAnti-virus false positives
Less storageHarder to debug

Recommendation: Use UPX cho CLI tools, không cho services.

Version Injection

go
// main.go
var (
    version   = "dev"
    commit    = "unknown"
    buildTime = "unknown"
)

func main() {
    if len(os.Args) > 1 && os.Args[1] == "--version" {
        fmt.Printf("Version: %s\nCommit: %s\nBuilt: %s\n", 
            version, commit, buildTime)
        os.Exit(0)
    }
    // ...
}
bash
# Build with version info
go build -ldflags="-w -s \
    -X main.version=$(git describe --tags) \
    -X main.commit=$(git rev-parse --short HEAD) \
    -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
    -o server ./cmd/server

☸️ Kubernetes Deployment

Deployment Manifest

yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  labels:
    app: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/metrics"
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        fsGroup: 65534
      containers:
        - name: user-service
          image: myregistry/user-service:v1.0.0
          ports:
            - containerPort: 8080
              name: http
          
          # Resource limits
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          
          # Health probes
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 5
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          
          # Environment
          env:
            - name: PORT
              value: "8080"
            - name: LOG_LEVEL
              value: "info"
            - name: DB_HOST
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: host
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
          
          # Graceful shutdown
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]
      
      terminationGracePeriodSeconds: 60

Service & Ingress

yaml
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: user-service
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - api.example.com
      secretName: api-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api/users
            pathType: Prefix
            backend:
              service:
                name: user-service
                port:
                  number: 80

⚔️ Tradeoff: Helm vs Kustomize

ToolApproachProsConsWhen to Use
HelmTemplatingRich ecosystem, chartsComplex templatesStandard apps
KustomizeOverlaysNative K8s, simplerLess flexibleCustom apps

Kustomize Example

yaml
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml

# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
  - ../../base
patchesStrategicMerge:
  - deployment-patch.yaml
images:
  - name: myregistry/user-service
    newTag: v1.0.0

🌍 12-Factor App Configuration

Environment-Based Config

go
// internal/config/config.go
package config

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

type Config struct {
    Port            int
    DatabaseURL     string
    RedisURL        string
    LogLevel        string
    ShutdownTimeout time.Duration
}

func Load() *Config {
    return &Config{
        Port:            getEnvInt("PORT", 8080),
        DatabaseURL:     mustGetEnv("DATABASE_URL"),
        RedisURL:        getEnv("REDIS_URL", "localhost:6379"),
        LogLevel:        getEnv("LOG_LEVEL", "info"),
        ShutdownTimeout: getEnvDuration("SHUTDOWN_TIMEOUT", 30*time.Second),
    }
}

func getEnv(key, defaultVal string) string {
    if val := os.Getenv(key); val != "" {
        return val
    }
    return defaultVal
}

func mustGetEnv(key string) string {
    if val := os.Getenv(key); val != "" {
        return val
    }
    panic(fmt.Sprintf("required env var %s not set", key))
}

func getEnvInt(key string, defaultVal int) int {
    if val := os.Getenv(key); val != "" {
        if i, err := strconv.Atoi(val); err == nil {
            return i
        }
    }
    return defaultVal
}

func getEnvDuration(key string, defaultVal time.Duration) time.Duration {
    if val := os.Getenv(key); val != "" {
        if d, err := time.ParseDuration(val); err == nil {
            return d
        }
    }
    return defaultVal
}

🔄 CI/CD Pipeline (GitHub Actions)

yaml
# .github/workflows/deploy.yaml
name: Build and Deploy

on:
  push:
    branches: [main]
    tags: ['v*']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'
      
      - name: Run tests
        run: go test -race -coverprofile=coverage.out ./...
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3

  build:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      
      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,prefix=
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            VERSION=${{ github.ref_name }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to Kubernetes
        uses: azure/k8s-deploy@v4
        with:
          manifests: |
            k8s/deployment.yaml
            k8s/service.yaml
          images: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}

💻 Engineering Example: Production Makefile

makefile
# Makefile
.PHONY: build test docker deploy

# Variables
VERSION ?= $(shell git describe --tags --always --dirty)
COMMIT  ?= $(shell git rev-parse --short HEAD)
BUILD_TIME ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

LDFLAGS := -ldflags="-w -s \
    -X main.version=$(VERSION) \
    -X main.commit=$(COMMIT) \
    -X main.buildTime=$(BUILD_TIME)"

# Build
build:
	CGO_ENABLED=0 go build $(LDFLAGS) -o bin/server ./cmd/server

build-linux:
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/server-linux ./cmd/server

# Test
test:
	go test -race -cover ./...

test-integration:
	go test -race -tags=integration ./...

# Docker
docker-build:
	docker build -t myapp:$(VERSION) --build-arg VERSION=$(VERSION) .

docker-push:
	docker tag myapp:$(VERSION) myregistry/myapp:$(VERSION)
	docker push myregistry/myapp:$(VERSION)

# Deploy
deploy-staging:
	kubectl apply -k overlays/staging

deploy-production:
	kubectl apply -k overlays/production

# Local
run:
	go run ./cmd/server

dev:
	air  # Hot reload with air

Ship-to-Prod Checklist

Docker

  • [ ] Multi-stage build để minimize size
  • [ ] scratch/distroless base image
  • [ ] Non-root user trong container
  • [ ] Health check trong Dockerfile
  • [ ] Version injected tại build time

Kubernetes

  • [ ] Resource limits configured
  • [ ] Liveness/readiness probes configured
  • [ ] Secrets từ K8s Secrets hoặc Vault
  • [ ] Graceful shutdown với preStop hook
  • [ ] PodDisruptionBudget cho high availability

CI/CD

  • [ ] Tests pass trước khi build
  • [ ] Automated image tagging từ git tags
  • [ ] Vulnerability scanning trên images
  • [ ] Staged rollouts (staging → production)
  • [ ] Rollback procedure documented

12-Factor

  • [ ] Config từ environment (không hardcode)
  • [ ] Stateless processes
  • [ ] Logs to stdout
  • [ ] Dev/prod parity

📊 Summary

ComponentRecommendation
Base Imagescratch hoặc distroless
Build Flags-ldflags="-w -s"
K8s ManifestsKustomize với overlays
ConfigEnvironment variables
CI/CDGitHub Actions + ArgoCD

🎉 Production Section Complete!

Bạn đã master Go Production Patterns:

  • ✅ Project Structure
  • ✅ Testing & Benchmarks
  • ✅ HTTP Services
  • ✅ Database Patterns
  • ✅ Observability
  • ✅ Deployment

Tiếp theo: Advanced Topics - Generics, Reflection, CGO, và Performance.