courses

CI/CD, Secret Management, and GitOps

Getting code to production without handing out the keys

This page complements the comprehensive CI/CD reference at the repo root, which covers GitHub Actions, GitLab CI, and Jenkins in depth with worked examples. Read that page first for pipeline concepts, tool comparisons, and full workflow examples. This page adds the DevOps roadmap topics not covered there: secret management and GitOps.

ℹ️ The security thread tying these two topics together is credential exposure. Secret managers keep credentials out of source, logs, and process listings; GitOps’s pull model means the cluster reconciles itself from Git instead of handing deploy credentials to an external CI system. Both shrink the blast radius of a leaked secret.

CI/CD Overview

Continuous Integration and Continuous Deployment automate the path from committed code to running software. See the CI/CD reference for:

Quick reference: additional CI tools

The CI/CD reference covers GitHub Actions, GitLab CI, and Jenkins. The DevOps roadmap also lists:

Tool Type Key characteristic
Travis CI Cloud Pioneered .travis.yml; free tier deprecated
CircleCI Cloud Strong Docker/layer caching; orbs for reuse
Drone Self-hosted Lightweight; every step runs in a container
TeamCity Self-hosted JetBrains product; Kotlin DSL for pipelines

Secret Management

Secrets are credentials that must not appear in source code or CI logs: API keys, database passwords, TLS private keys, SSH keys, and service account tokens. Secret management systems store these encrypted and provide controlled access.

Why not environment variables or .env files?

Environment variables in CI systems are better than hardcoded values, but they have limits:

OpenBao

OpenBao is an open-source secrets management system — the OSI-licensed (MPL-2.0) Linux Foundation fork of HashiCorp Vault, adopted after Vault’s 2023 move to the BSL license (the same reasoning behind using OpenTofu over Terraform). It stores secrets encrypted, enforces access policies, issues short-lived dynamic credentials, and logs every access. The CLI is bao and the API is Vault-compatible, so these commands map one-to-one onto Vault.

# Install OpenBao (Debian/Ubuntu) — from the upstream .deb release asset
VER=2.5.5   # current release — see github.com/openbao/openbao/releases
curl -fsSLO "https://github.com/openbao/openbao/releases/download/v${VER}/bao_${VER}_linux_amd64.deb"
sudo apt install "./bao_${VER}_linux_amd64.deb"

bao version

Development server (local testing only)

bao server -dev                     # start in-memory dev server (NOT for production)
export BAO_ADDR='http://127.0.0.1:8200'
export BAO_TOKEN='root'             # dev server token shown in startup output
bao status

Basic secret operations

# Key-value secrets engine (v2 — versioned)
bao secrets enable -path=secret kv-v2

# Write secrets
bao kv put secret/myapp/db \
    username=appuser \
    password=s3cr3t

# Read secrets
bao kv get secret/myapp/db
bao kv get -field=password secret/myapp/db   # just the value

# List secrets
bao kv list secret/myapp

# Update a secret (creates a new version)
bao kv patch secret/myapp/db password=newpassword

# Delete (soft delete — versions retained)
bao kv delete secret/myapp/db

# Destroy a specific version permanently
bao kv destroy -versions=1 secret/myapp/db

Policies

# policy/myapp-read.hcl — allow read-only access to myapp secrets
path "secret/data/myapp/*" {
  capabilities = ["read", "list"]
}

path "secret/metadata/myapp/*" {
  capabilities = ["list"]
}
bao policy write myapp-read policy/myapp-read.hcl
bao policy list
bao policy read myapp-read

Dynamic credentials (database example)

Dynamic credentials are generated on demand and expire automatically — the database creates a temporary user with a time-limited password.

bao secrets enable database

bao write database/config/mydb \
    plugin_name=postgresql-database-plugin \
    allowed_roles="app-role" \
    connection_url="postgresql://:@db:5432/mydb" \
    username="bao-admin" \
    password="bao-admin-pass"

bao write database/roles/app-role \
    db_name=mydb \
    creation_statements="CREATE ROLE \"\" WITH LOGIN PASSWORD '' VALID UNTIL ''; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"\";" \
    default_ttl="1h" \
    max_ttl="24h"

# Application requests credentials
bao read database/creds/app-role
# Returns: username=v-app-role-xyz123, password=A1b2C3d4, lease_duration=1h

Python integration

import hvac      # the Vault API client — works unchanged against OpenBao
import os

client = hvac.Client(
    url=os.environ['BAO_ADDR'],
    token=os.environ['BAO_TOKEN'],
)

# Read a static secret
secret = client.secrets.kv.v2.read_secret_version(
    path='myapp/db',
    mount_point='secret',
)
db_password = secret['data']['data']['password']

# Request dynamic database credentials
creds = client.secrets.database.generate_credentials(name='app-role')
username = creds['data']['username']
password = creds['data']['password']

Sealed Secrets (Kubernetes)

Bitnami Sealed Secrets allows storing Kubernetes secrets as encrypted SealedSecret objects in Git. Only the controller running in the cluster can decrypt them, so the encrypted form is safe to commit.

# Install kubeseal CLI
curl -L https://github.com/bitnami-labs/sealed-secrets/releases/latest/download/kubeseal-linux-amd64 \
    -o /usr/local/bin/kubeseal
chmod +x /usr/local/bin/kubeseal

# Seal a secret
kubectl create secret generic db-credentials \
    --from-literal=username=appuser \
    --from-literal=password=s3cr3t \
    --dry-run=client -o yaml \
    | kubeseal --format yaml > sealed-db-credentials.yaml

# This file is safe to commit to Git
git add sealed-db-credentials.yaml
git commit -m "add sealed database credentials"

# Apply to cluster (controller decrypts and creates the real Secret)
kubectl apply -f sealed-db-credentials.yaml

SOPs (Secrets OPerationS)

SOPS is a file encryption tool from Mozilla. It encrypts individual values within YAML, JSON, and .env files using PGP keys, AWS KMS, GCP KMS, or Azure Key Vault — leaving keys visible but values encrypted.

# Install
curl -L https://github.com/getsops/sops/releases/latest/download/sops-v3.9.0.linux.amd64 \
    -o /usr/local/bin/sops
chmod +x /usr/local/bin/sops

# Create a .sops.yaml config
cat > .sops.yaml << 'EOF'
creation_rules:
  - path_regex: .*\.secret\.yaml$
    pgp: FINGERPRINT_HERE
  - path_regex: secrets/.*
    aws_kms: arn:aws:kms:us-east-1:123456789012:key/abc-123
EOF

# Encrypt a secrets file
sops -e secrets.yaml > secrets.enc.yaml

# Decrypt and view
sops -d secrets.enc.yaml

# Edit in place (decrypts, opens editor, re-encrypts on save)
sops secrets.enc.yaml

Cloud provider secret services

Provider Service Notes
AWS Secrets Manager Automatic rotation; charges per secret per month
AWS Parameter Store (SSM) Free tier; simpler; no automatic rotation
GCP Secret Manager Per-access pricing; IAM integration
Azure Key Vault Stores secrets, keys, and certificates
# Python: read from AWS Secrets Manager
import boto3
import json

def get_secret(secret_name, region='us-east-1'):
    client = boto3.client('secretsmanager', region_name=region)
    response = client.get_secret_value(SecretId=secret_name)
    return json.loads(response['SecretString'])

creds = get_secret('prod/myapp/db')
print(creds['username'])   # appuser

GitOps

GitOps is an operational model where the desired state of a system is stored entirely in Git, and an automated agent continuously reconciles the actual state to match it. Coined by Weaveworks in 2017, it applies the developer workflow (pull requests, code review, audit trail) to operations.

GitOps principles

  1. Declarative — the entire system state is described declaratively (Kubernetes manifests, Helm charts)
  2. Versioned and immutable — the desired state is stored in Git, providing history and rollback
  3. Pulled automatically — an agent inside the cluster pulls changes from Git (not pushed from CI)
  4. Continuously reconciled — the agent detects drift and corrects it

The pull-based model is more secure than push-based CI deployments: the cluster does not need external CI systems to have credentials that can modify it.

ArgoCD

ArgoCD is the most widely used GitOps controller for Kubernetes. It watches a Git repository and continuously reconciles the cluster state to match what is declared in that repo.

# Install ArgoCD into a Kubernetes cluster
kubectl create namespace 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=available --timeout=300s \
    deployment/argocd-server -n argocd

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

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

# Log in
argocd login localhost:8080 --username admin \
    --password $(kubectl -n argocd get secret argocd-initial-admin-secret \
    -o jsonpath="{.data.password}" | base64 -d)

Defining an application

# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/myapp-manifests.git
    targetRevision: main
    path: kubernetes/production
  destination:
    server: https://kubernetes.default.svc
    namespace: myapp
  syncPolicy:
    automated:
      prune: true       # delete resources removed from Git
      selfHeal: true    # revert manual changes in cluster
    syncOptions:
      - CreateNamespace=true
kubectl apply -f argocd-app.yaml

argocd app list                         # list applications
argocd app get myapp                    # show sync status
argocd app sync myapp                   # trigger manual sync
argocd app history myapp                # deployment history
argocd app rollback myapp 2             # roll back to revision 2

FluxCD

FluxCD (Flux v2) is an alternative GitOps toolkit. It is more modular than ArgoCD — composed of separate controllers for Git syncing, Helm releases, image automation, and notifications.

# Install Flux CLI
curl -s https://fluxcd.io/install.sh | sudo bash

# Bootstrap Flux into a cluster (connects to GitHub repo)
flux bootstrap github \
    --owner=myorg \
    --repository=fleet-infra \
    --branch=main \
    --path=clusters/production \
    --personal

# Check Flux components
flux check
flux get all                            # show all Flux resources

Flux GitRepository and Kustomization

# flux-source.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: myapp
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/myorg/myapp-manifests
  ref:
    branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: myapp
  namespace: flux-system
spec:
  interval: 5m
  path: ./kubernetes/production
  prune: true
  sourceRef:
    kind: GitRepository
    name: myapp
  targetNamespace: myapp
kubectl apply -f flux-source.yaml
flux get kustomizations                 # show reconciliation status
flux reconcile kustomization myapp      # force immediate reconciliation
flux logs                               # show Flux controller logs

ArgoCD vs FluxCD

Aspect ArgoCD FluxCD
UI Built-in web UI Third-party (Weave GitOps)
Architecture Single controller Modular toolkit
Helm support Built-in Helm controller
Multi-cluster ApplicationSet CRD Multi-tenancy via namespaces
Image auto-update Argo CD Image Updater Flux Image Automation
RBAC Fine-grained project RBAC Kubernetes RBAC only

Both are CNCF graduated projects. ArgoCD is often preferred when a visual UI matters to the team; FluxCD when a pure-Kubernetes, operator-composable approach is preferred.

GitOps repository structure

fleet-infra/                    # GitOps repo
├── clusters/
│   ├── production/
│   │   ├── flux-system/        # Flux bootstrap manifests
│   │   └── apps.yaml           # Kustomization pointing to apps/production
│   └── staging/
│       └── apps.yaml
└── apps/
    ├── base/                   # shared manifests (Deployment, Service)
    │   ├── deployment.yaml
    │   ├── service.yaml
    │   └── kustomization.yaml
    ├── production/
    │   ├── kustomization.yaml  # patches: replicas=5, image tag
    │   └── patches.yaml
    └── staging/
        ├── kustomization.yaml  # patches: replicas=1
        └── patches.yaml

Deployment workflow with GitOps:

# Developer: update image tag in the manifests repo
cd apps/production
sed -i 's/image: myapp:.*/image: myapp:v1.2.3/' patches.yaml
git add patches.yaml
git commit -m "deploy myapp v1.2.3 to production"
git push

# ArgoCD/Flux detects the commit and reconciles automatically
# No CI pipeline needs cluster credentials

Key takeaways

References


Related course pages: CI/CD reference · OpenBao · Infrastructure as Code

🛠️ Maintenance note: the install snippets pin versions (sops-v3.9.0, OpenBao 2.5.5, latest release URLs) that drift — re-verify each term and bump the OpenBao tag from its releases page. This course uses OpenBao (the OSI-licensed Linux Foundation fork) rather than HashiCorp Vault, which moved to the BSL license in 2023 — the bao CLI and API stay Vault-compatible. ArgoCD and Flux both reached CNCF graduated status; their bootstrap commands and CRD apiVersions evolve between releases.