GitOps and Progressive Delivery: Deploying with Confidence

February 8, 2021

GitOps has matured from buzzword to standard practice. Combined with progressive delivery—canary deployments, feature flags, and automated rollbacks—it enables confident, frequent deployments.

Here’s how to implement GitOps with progressive delivery.

GitOps Fundamentals

Core Principles

gitops_principles:
  declarative:
    - Desired state described in git
    - No imperative commands
    - Complete system description

  versioned:
    - Git is source of truth
    - Full history of changes
    - Easy rollback (git revert)

  pulled:
    - Cluster pulls from git
    - No external access to cluster
    - More secure than push

  reconciled:
    - Continuous reconciliation
    - Drift detection and correction
    - Self-healing

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                          Git Repository                          │
│                    (source of truth)                             │
└───────────────────────────┬─────────────────────────────────────┘
                            │ pull
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│                       GitOps Operator                            │
│                    (Argo CD / Flux)                              │
├─────────────────────────────────────────────────────────────────┤
│  - Watch git for changes                                         │
│  - Compare desired vs actual                                     │
│  - Sync (reconcile)                                              │
│  - Report status                                                 │
└───────────────────────────┬─────────────────────────────────────┘
                            │ apply
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│                    Kubernetes Cluster                            │
└─────────────────────────────────────────────────────────────────┘

Argo CD Setup

Installation

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

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

# Access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

Application Definition

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/my-app-config
    targetRevision: HEAD
    path: overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Multi-Environment Setup

# Application per environment
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-app
spec:
  generators:
    - list:
        elements:
          - env: staging
            cluster: staging-cluster
          - env: production
            cluster: production-cluster
  template:
    metadata:
      name: 'my-app-{{env}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/my-app-config
        targetRevision: HEAD
        path: 'overlays/{{env}}'
      destination:
        server: '{{cluster}}'
        namespace: my-app

Progressive Delivery

Argo Rollouts

Enhanced deployment strategies:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  replicas: 10
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: myregistry/my-app:v2
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: { duration: 5m }
        - setWeight: 30
        - pause: { duration: 5m }
        - setWeight: 50
        - pause: { duration: 5m }
        - setWeight: 100

Canary with Analysis

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: { duration: 2m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 50
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 100
      canaryService: my-app-canary
      stableService: my-app-stable
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  metrics:
    - name: success-rate
      interval: 1m
      count: 5
      successCondition: result[0] >= 0.95
      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]))

Blue-Green Deployment

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app
spec:
  replicas: 3
  strategy:
    blueGreen:
      activeService: my-app-active
      previewService: my-app-preview
      autoPromotionEnabled: false
      prePromotionAnalysis:
        templates:
          - templateName: smoke-tests
      postPromotionAnalysis:
        templates:
          - templateName: success-rate

Feature Flags Integration

With LaunchDarkly/Flagsmith

# Deployment with feature flag sidecar
spec:
  containers:
    - name: app
      env:
        - name: FEATURE_FLAG_SERVER
          value: http://localhost:8080
    - name: flag-relay
      image: launchdarkly/ld-relay
      env:
        - name: LD_SDK_KEY
          valueFrom:
            secretKeyRef:
              name: launchdarkly
              key: sdk-key

Progressive Rollout with Flags

# Application code checks flag
feature_flags:
  new_checkout_flow:
    default: false
    rules:
      - percentage: 10  # 10% of users
        value: true
      - percentage: 50  # After validation
        value: true
      - percentage: 100 # Full rollout
        value: true

Automated Rollbacks

On Analysis Failure

apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
  strategy:
    canary:
      steps:
        - setWeight: 20
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: error-rate
            args:
              - name: threshold
                value: "0.01"  # 1% error rate
      # Automatic rollback on analysis failure
      abortScaleDownDelaySeconds: 30

Manual Rollback

# Argo Rollouts CLI
kubectl argo rollouts abort my-app
kubectl argo rollouts undo my-app

# Or through Argo CD
argocd app sync my-app --revision <previous-commit>

Repository Structure

Monorepo Pattern

config-repo/
├── apps/
│   ├── my-app/
│   │   ├── base/
│   │   │   ├── deployment.yaml
│   │   │   ├── service.yaml
│   │   │   └── kustomization.yaml
│   │   └── overlays/
│   │       ├── staging/
│   │       │   ├── kustomization.yaml
│   │       │   └── patch.yaml
│   │       └── production/
│   │           ├── kustomization.yaml
│   │           └── patch.yaml
│   └── other-app/
├── infrastructure/
│   ├── cert-manager/
│   ├── ingress-nginx/
│   └── monitoring/
└── argocd/
    ├── projects.yaml
    └── applications.yaml

App-of-Apps Pattern

# Root application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
spec:
  source:
    path: argocd/applications
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true

CI/CD Integration

Image Updater

# Argo CD Image Updater
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  annotations:
    argocd-image-updater.argoproj.io/image-list: app=myregistry/my-app
    argocd-image-updater.argoproj.io/app.update-strategy: semver
    argocd-image-updater.argoproj.io/write-back-method: git

PR-Based Updates

# CI pipeline updates config repo
steps:
  - name: Update image tag
    run: |
      cd config-repo
      kustomize edit set image myregistry/my-app:${{ github.sha }}
      git commit -am "Update my-app to ${{ github.sha }}"
      git push

Monitoring and Notifications

Argo CD Notifications

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
data:
  trigger.on-sync-succeeded: |
    - description: Application synced successfully
      send:
        - app-sync-succeeded
      when: app.status.operationState.phase in ['Succeeded']

  template.app-sync-succeeded: |
    message: |
      {{.app.metadata.name}} synced successfully.
      Revision: {{.app.status.sync.revision}}

  service.slack: |
    token: $slack-token

Metrics

# Key metrics to monitor
gitops_metrics:
  - argocd_app_sync_status
  - argocd_app_health_status
  - rollout_phase
  - analysis_run_metric_result
  - deployment_update_total

Key Takeaways

GitOps provides the workflow. Progressive delivery provides the safety. Together, they enable frequent, confident deployments.