Chapter 9: CI/CD and GitOps

Haiyue
23min

Chapter 9: CI/CD and GitOps

Learning Objectives
  • Master Kubernetes integration with CI/CD pipelines
  • Learn to implement GitOps using ArgoCD
  • Understand continuous deployment strategies for containerized applications
  • Become proficient in automated releases and rollbacks

Key Concepts

CI/CD Overview

🔄 正在渲染 Mermaid 图表...

GitOps Principles

🔄 正在渲染 Mermaid 图表...

Traditional CI/CD vs GitOps

🔄 正在渲染 Mermaid 图表...

GitHub Actions CI

Basic CI Pipeline

# .github/workflows/ci.yaml
name: CI Pipeline

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

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.21'

    - name: Run tests
      run: |
        go test -v -race -coverprofile=coverage.out ./...

    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage.out

  lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Run golangci-lint
      uses: golangci/golangci-lint-action@v3
      with:
        version: latest

  build:
    needs: [test, lint]
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    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.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=ref,event=pr
          type=sha,prefix=
          type=raw,value=latest,enable={{is_default_branch}}

    - name: Build and push
      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

  security-scan:
    needs: build
    runs-on: ubuntu-latest
    steps:
    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
        format: 'sarif'
        output: 'trivy-results.sarif'

    - name: Upload Trivy scan results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'

Multi-stage Dockerfile

# Dockerfile
# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Dependency caching
COPY go.mod go.sum ./
RUN go mod download

# Build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./cmd/server

# Runtime stage
FROM gcr.io/distroless/static-debian11

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

USER nonroot:nonroot

EXPOSE 8080

ENTRYPOINT ["/server"]

ArgoCD GitOps

ArgoCD Architecture

🔄 正在渲染 Mermaid 图表...

Installing ArgoCD

# Create namespace
kubectl create namespace argocd

# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Wait for Pods to be ready
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=300s

# Get initial password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Port forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Install CLI
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd && sudo mv argocd /usr/local/bin/

# Login
argocd login localhost:8080

Creating Application

# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
  finalizers:
  - resources-finalizer.argocd.argoproj.io
spec:
  project: default

  source:
    repoURL: https://github.com/myorg/myapp-manifests.git
    targetRevision: HEAD
    path: overlays/production

  destination:
    server: https://kubernetes.default.svc
    namespace: production

  syncPolicy:
    automated:
      prune: true           # Delete resources not in Git
      selfHeal: true        # Auto heal drift
      allowEmpty: false     # Don't allow empty applications
    syncOptions:
    - CreateNamespace=true  # Auto create namespace
    - PrunePropagationPolicy=foreground
    - PruneLast=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

  # Health check
  ignoreDifferences:
  - group: apps
    kind: Deployment
    jsonPointers:
    - /spec/replicas  # Ignore HPA-managed replicas

  # Notification
  info:
  - name: url
    value: https://myapp.example.com

Kustomize Structure

myapp-manifests/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   └── kustomization.yaml
├── overlays/
│   ├── development/
│   │   ├── kustomization.yaml
│   │   └── patches/
│   │       └── replicas.yaml
│   ├── staging/
│   │   ├── kustomization.yaml
│   │   └── patches/
│   │       ├── replicas.yaml
│   │       └── resources.yaml
│   └── production/
│       ├── kustomization.yaml
│       └── patches/
│           ├── replicas.yaml
│           ├── resources.yaml
│           └── ingress.yaml
└── components/
    ├── monitoring/
    │   └── kustomization.yaml
    └── security/
        └── kustomization.yaml
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- deployment.yaml
- service.yaml
- configmap.yaml

commonLabels:
  app.kubernetes.io/name: myapp

---
# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 200m
            memory: 256Mi

---
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: production

resources:
- ../../base

components:
- ../../components/monitoring

images:
- name: myapp
  newName: ghcr.io/myorg/myapp
  newTag: v1.2.3

patches:
- path: patches/replicas.yaml
- path: patches/resources.yaml
- path: patches/ingress.yaml

configMapGenerator:
- name: myapp-config
  behavior: merge
  literals:
  - LOG_LEVEL=info
  - ENVIRONMENT=production

---
# overlays/production/patches/replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 5

ArgoCD ApplicationSet

# Auto-generate multi-environment Applications
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: myapp-environments
  namespace: argocd
spec:
  generators:
  # List generator
  - list:
      elements:
      - env: development
        namespace: dev
        replicas: "1"
      - env: staging
        namespace: staging
        replicas: "2"
      - env: production
        namespace: production
        replicas: "5"

  template:
    metadata:
      name: 'myapp-{{env}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/myapp-manifests.git
        targetRevision: HEAD
        path: 'overlays/{{env}}'
      destination:
        server: https://kubernetes.default.svc
        namespace: '{{namespace}}'
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
        - CreateNamespace=true

Multi-cluster Deployment

# Add cluster
# argocd cluster add <context-name>

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: myapp-multi-cluster
  namespace: argocd
spec:
  generators:
  - clusters:
      selector:
        matchLabels:
          env: production

  template:
    metadata:
      name: 'myapp-{{name}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/myapp-manifests.git
        targetRevision: HEAD
        path: overlays/production
      destination:
        server: '{{server}}'
        namespace: production
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

Deployment Strategies

Rolling Update

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%        # Maximum 25% extra Pods
      maxUnavailable: 25%  # Maximum 25% Pods unavailable
  template:
    spec:
      containers:
      - name: myapp
        image: myapp:v2
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

Blue-Green Deployment

# Blue environment (current version)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-blue
  labels:
    app: myapp
    version: blue
spec:
  replicas: 5
  selector:
    matchLabels:
      app: myapp
      version: blue
  template:
    metadata:
      labels:
        app: myapp
        version: blue
    spec:
      containers:
      - name: myapp
        image: myapp:v1

---
# Green environment (new version)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-green
  labels:
    app: myapp
    version: green
spec:
  replicas: 5
  selector:
    matchLabels:
      app: myapp
      version: green
  template:
    metadata:
      labels:
        app: myapp
        version: green
    spec:
      containers:
      - name: myapp
        image: myapp:v2

---
# Service switching
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
    version: green  # Switch to green environment
  ports:
  - port: 80
    targetPort: 8080

Canary Deployment (Argo Rollouts)

# Install Argo Rollouts
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml

# Install kubectl plugin
curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64 && sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: myapp
spec:
  replicas: 10
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:v2
        ports:
        - containerPort: 8080
  strategy:
    canary:
      # Canary steps
      steps:
      - setWeight: 10        # 10% traffic to new version
      - pause: {duration: 5m} # Pause 5 minutes for observation
      - setWeight: 30
      - pause: {duration: 5m}
      - setWeight: 50
      - pause: {duration: 5m}
      - setWeight: 80
      - pause: {duration: 5m}

      # Traffic management
      canaryService: myapp-canary
      stableService: myapp-stable

      # Analysis template
      analysis:
        templates:
        - templateName: success-rate
        startingStep: 1
        args:
        - name: service-name
          value: myapp-canary

      # Auto rollback
      maxSurge: "25%"
      maxUnavailable: 0

---
# Analysis template
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  args:
  - name: service-name
  metrics:
  - name: success-rate
    interval: 1m
    count: 5
    successCondition: result[0] >= 0.95
    failureCondition: result[0] < 0.90
    provider:
      prometheus:
        address: http://prometheus:9090
        query: |
          sum(rate(http_requests_total{service="{{args.service-name}}",status=~"2.."}[5m]))
          /
          sum(rate(http_requests_total{service="{{args.service-name}}"}[5m]))
# Monitor Rollout status
kubectl argo rollouts get rollout myapp -w

# Manually promote
kubectl argo rollouts promote myapp

# Abort rollback
kubectl argo rollouts abort myapp

# Rollback to previous version
kubectl argo rollouts undo myapp

Complete CI/CD Example

Complete Pipeline

# .github/workflows/cicd.yaml
name: CI/CD Pipeline

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

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
  MANIFEST_REPO: myorg/myapp-manifests

jobs:
  ci:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}

    steps:
    - uses: actions/checkout@v4

    - name: Run tests
      run: make test

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

    - 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=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 }}

    - name: Scan image
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}'
        exit-code: '1'
        severity: 'CRITICAL,HIGH'

  cd-staging:
    needs: ci
    runs-on: ubuntu-latest
    environment: staging

    steps:
    - name: Checkout manifests repo
      uses: actions/checkout@v4
      with:
        repository: ${{ env.MANIFEST_REPO }}
        token: ${{ secrets.MANIFEST_REPO_TOKEN }}
        path: manifests

    - name: Update image tag
      run: |
        cd manifests/overlays/staging
        kustomize edit set image myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.ci.outputs.image-tag }}

    - name: Commit and push
      run: |
        cd manifests
        git config user.name "GitHub Actions"
        git config user.email "actions@github.com"
        git add .
        git commit -m "chore: update staging to ${{ needs.ci.outputs.image-tag }}"
        git push

    - name: Wait for ArgoCD sync
      run: |
        # Wait for ArgoCD to complete sync
        sleep 60

    - name: Verify deployment
      run: |
        # Run integration tests
        curl -f https://staging.myapp.example.com/health

  cd-production:
    needs: [ci, cd-staging]
    runs-on: ubuntu-latest
    environment: production

    steps:
    - name: Checkout manifests repo
      uses: actions/checkout@v4
      with:
        repository: ${{ env.MANIFEST_REPO }}
        token: ${{ secrets.MANIFEST_REPO_TOKEN }}
        path: manifests

    - name: Update image tag
      run: |
        cd manifests/overlays/production
        kustomize edit set image myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.ci.outputs.image-tag }}

    - name: Create Pull Request
      uses: peter-evans/create-pull-request@v5
      with:
        path: manifests
        token: ${{ secrets.MANIFEST_REPO_TOKEN }}
        commit-message: 'chore: update production to ${{ needs.ci.outputs.image-tag }}'
        title: 'Deploy ${{ needs.ci.outputs.image-tag }} to production'
        body: |
          Automated PR to deploy version ${{ needs.ci.outputs.image-tag }} to production.

          ## Changes
          - Image: `${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.ci.outputs.image-tag }}`

          ## Checklist
          - [ ] Staging verification passed
          - [ ] Reviewed changes
        branch: deploy/production-${{ needs.ci.outputs.image-tag }}

Notifications

# ArgoCD notification configuration
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
  namespace: argocd
data:
  service.slack: |
    token: $slack-token
  template.app-deployed: |
    message: |
      Application {{.app.metadata.name}} is now {{.app.status.sync.status}}.
      Revision: {{.app.status.sync.revision}}
  template.app-health-degraded: |
    message: |
      Application {{.app.metadata.name}} health has degraded.
      Health Status: {{.app.status.health.status}}
  trigger.on-deployed: |
    - when: app.status.sync.status == 'Synced' and app.status.health.status == 'Healthy'
      send: [app-deployed]
  trigger.on-health-degraded: |
    - when: app.status.health.status == 'Degraded'
      send: [app-health-degraded]

---
# Subscribe to notifications
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  annotations:
    notifications.argoproj.io/subscribe.on-deployed.slack: deployments
    notifications.argoproj.io/subscribe.on-health-degraded.slack: alerts

Rollback Strategies

ArgoCD Rollback

# View history
argocd app history myapp

# Rollback to specific revision
argocd app rollback myapp <revision>

# Rollback to previous revision
argocd app rollback myapp

# Rollback via Git
git revert HEAD
git push origin main
# ArgoCD will auto sync

Helm Rollback

# View Release history
helm history myapp -n production

# Rollback to specific version
helm rollback myapp 3 -n production

# Rollback to previous version
helm rollback myapp -n production

Argo Rollouts Rollback

# Abort current release and rollback
kubectl argo rollouts abort myapp

# Rollback to previous version
kubectl argo rollouts undo myapp

# Rollback to specific version
kubectl argo rollouts undo myapp --to-revision=3

Practical Exercise

Complete GitOps Workflow

Project structure:
├── application/              # Application code repository
│   ├── src/
│   ├── Dockerfile
│   └── .github/workflows/
│       └── ci.yaml

├── manifests/                # Manifests repository
│   ├── base/
│   ├── overlays/
│   │   ├── development/
│   │   ├── staging/
│   │   └── production/
│   └── argocd/
│       ├── applications/
│       └── projects/

└── infrastructure/           # Infrastructure repository
    ├── terraform/
    └── clusters/
# manifests/argocd/projects/default.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: myapp
  namespace: argocd
spec:
  description: My Application Project

  # Allowed source repositories
  sourceRepos:
  - https://github.com/myorg/myapp-manifests.git

  # Allowed destination clusters and namespaces
  destinations:
  - namespace: '*'
    server: https://kubernetes.default.svc

  # Allowed cluster resources
  clusterResourceWhitelist:
  - group: ''
    kind: Namespace

  # Namespace resource blacklist
  namespaceResourceBlacklist:
  - group: ''
    kind: ResourceQuota
  - group: ''
    kind: LimitRange

  # Roles
  roles:
  - name: developer
    policies:
    - p, proj:myapp:developer, applications, get, myapp/*, allow
    - p, proj:myapp:developer, applications, sync, myapp/*, allow
    groups:
    - dev-team
CI/CD Best Practices
  1. Single responsibility: CI handles build/test, CD handles deployment
  2. Immutable images: Each build generates unique tag
  3. Declarative configuration: Use Git to manage all configurations
  4. Progressive delivery: Use canary or blue-green deployment
  5. Auto rollback: Automatic rollback on failure detection
  6. Environment isolation: Development/test/production use separate configurations
  7. Security scanning: Integrate security scanning in CI

Summary

Through this chapter, you should have mastered:

  • CI pipeline: Build, test, scan, push images
  • GitOps principles: Declarative, versioned, automated
  • ArgoCD: Application, ApplicationSet, sync policies
  • Deployment strategies: Rolling update, blue-green, canary
  • Rollback mechanisms: ArgoCD, Helm, Argo Rollouts rollback

In the next chapter, we will learn about production environment best practices, mastering enterprise-grade Kubernetes cluster planning and operations.