DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    CI/CD PipelinesLesson 10 of 12
    Your progress · 0%
    On this page
    1. Push and pull deploys
    2. The config repository
    3. Argo CD
    4. Flux
    5. CI and GitOps together
    ↖ Course roadmap
    Lesson 10 · GitOps

    GitOps: Git says what runs, the cluster makes it so

    Deploy by changing Git, not by pushing to servers: push versus pull, a Kustomize config repo, Argo CD and Flux syncing it to Kubernetes, drift repair, and git revert rollbacks.

    40 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 6 complete

    Push and pull deploys

    In the deploys so far, CI pushes: it holds credentials for the target and runs commands against it. With GitOps, nobody pushes to production. A Git repository describes what should run, and an agent inside the cluster pulls it and makes the cluster match:

    Push: CI reaches into production. Pull: CI changes Git, and the cluster updates itself.© Diagram: DevOps Academy

    Push: CI reaches into production. Pull: CI changes Git, and the cluster updates itself.© Diagram: DevOps Academy
    Push (CI applies)Pull (GitOps)
    Who changes the clusterthe pipeline, with cluster credentialsan agent inside the cluster
    Credentials outside the clusteryes, in CI secretsnone: CI only writes to Git
    Audit trailpipeline logsGit history: who, what, when, approved by whom
    Manual changes in the clusterstay until the next deployundone automatically (self-heal)
    Rollbackre-run an old pipelinegit revert

    The OpenGitOps principles sum it up: the desired state is declarative, versioned in Git, pulled automatically, and continuously reconciled.

    The config repository

    GitOps needs a repository of manifests. A common layout keeps a shared base and one overlay per environment, with Kustomize (built into kubectl):

    shop-config/
    ├── apps/shop-api/
    │   ├── base/                  # what every environment shares
    │   │   ├── deployment.yaml
    │   │   ├── service.yaml
    │   │   └── kustomization.yaml
    │   └── production/
    │       └── kustomization.yaml # production's namespace and image tag
    └── clusters/production/
        └── argocd-shop-api.yaml   # tells Argo CD what to sync
    

    The base is an ordinary Deployment and Service:

    # apps/shop-api/base/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: shop-api
    spec:
      replicas: 2
      selector:
        matchLabels: { app: shop-api }
      template:
        metadata:
          labels: { app: shop-api }
        spec:
          containers:
            - name: shop-api
              image: ghcr.io/elonerajeev/shop-api:1.0.0
              ports:
                - containerPort: 3000
              readinessProbe:
                httpGet: { path: /health, port: 3000 }
              resources:
                requests: { cpu: 50m, memory: 64Mi }
                limits: { memory: 128Mi }
    
    # apps/shop-api/base/service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: shop-api
    spec:
      selector: { app: shop-api }
      ports:
        - port: 80
          targetPort: 3000
    
    # apps/shop-api/base/kustomization.yaml
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    resources: [deployment.yaml, service.yaml]
    

    The production overlay reuses the base and sets what's specific to production:

    # apps/shop-api/production/kustomization.yaml
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    namespace: shop
    resources: [../base]
    images:
      - name: ghcr.io/elonerajeev/shop-api
        newTag: 1.0.0
    

    A release is one line changing. CI does it with Kustomize:

    $ cd apps/shop-api/production
    $ kustomize edit set image ghcr.io/elonerajeev/shop-api:1.0.43-9f2a1bc
    $ grep -A2 '^images' kustomization.yaml
    images:
    - name: ghcr.io/elonerajeev/shop-api
      newTag: 1.0.43-9f2a1bc
    $ kustomize build . | kubeconform -strict -summary -
    Summary: 2 resources found parsing stdin - Valid: 2, Invalid: 0, Errors: 0, Skipped: 0
    
    cd apps/shop-api/production
    • cdchange directory
    grep -A2 '^images' kustomization.yaml
    • grepfind lines that match a pattern
    kustomize build . | kubeconform -strict -summary -
    • .this folder
    • |pipe: send output to the next command
    • -the previous directory

    kustomize build renders the final manifests, and kubeconform validates them against the Kubernetes schemas, so a typo fails in CI instead of in the cluster.

    Argo CD

    A commit changes the tag and Argo CD rolls it out. A manual kubectl change is drift, and self-heal undoes it.© Animation: DevOps Academy

    A commit changes the tag and Argo CD rolls it out. A manual kubectl change is drift, and self-heal undoes it.© Animation: DevOps Academy

    Argo CD runs in the cluster and syncs Applications: each one points at a path in a Git repository and a destination in the cluster. It has a web UI showing every resource, its health and its diff from Git.

    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: shop-api
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: https://github.com/elonerajeev/shop-config.git
        targetRevision: main
        path: apps/shop-api/production
      destination:
        server: https://kubernetes.default.svc     # the cluster Argo CD runs in
        namespace: shop
      syncPolicy:
        automated:
          prune: true           # delete what was removed from Git
          selfHeal: true        # undo manual changes in the cluster
        syncOptions:
          - CreateNamespace=true
    
    SettingWithout it
    automatedArgo CD shows OutOfSync and waits for someone to click Sync
    prunea manifest deleted from Git leaves its resource running
    selfHeala kubectl edit in production stays until the next commit

    The first Application is applied once with kubectl apply -f; after that, Argo CD can manage its own Applications from Git too (the "app of apps" pattern).

    Flux

    Flux does the same job as a set of small controllers with no central UI, configured entirely with Kubernetes resources: a source to watch, and what to apply from it:

    apiVersion: source.toolkit.fluxcd.io/v1
    kind: GitRepository
    metadata:
      name: shop-config
      namespace: flux-system
    spec:
      interval: 1m                # check Git every minute
      url: https://github.com/elonerajeev/shop-config
      ref:
        branch: main
    ---
    apiVersion: kustomize.toolkit.fluxcd.io/v1
    kind: Kustomization
    metadata:
      name: shop-api
      namespace: flux-system
    spec:
      interval: 5m                # re-apply and repair drift every 5 minutes
      sourceRef:
        kind: GitRepository
        name: shop-config
      path: ./apps/shop-api/production
      prune: true
      targetNamespace: shop
      wait: true                  # healthy only when the rollout finishes
      timeout: 2m
    
    Argo CDFlux
    Shapeone app with a rich web UIsmall controllers, CLI-first
    UnitApplication (and ApplicationSet for many)GitRepository + Kustomization or HelmRelease
    Multi-clusterone Argo CD can manage many clustersusually Flux in each cluster
    ExtrasUI, SSO, sync waves, rolloutsimage automation that commits new tags for you

    Both are CNCF graduated projects. Pick Argo CD when people want to see and click; pick Flux when everything, including Flux itself, should be plain YAML in Git. Both manifests above pass kubeconform with the CRD schemas from the community catalog, and a typo such as selfHeel is reported as additional properties 'selfHeel' not allowed.

    CI and GitOps together

    CI still builds, tests and publishes the image (lessons 5 and 6). Its last job is just a commit:

    update-config:
      runs-on: ubuntu-latest
      environment: production                 # approval before production (lesson 4)
      steps:
        - uses: actions/checkout@v7
          with:
            repository: elonerajeev/shop-config
            token: ${{ secrets.CONFIG_REPO_TOKEN }}   # can write to the config repo only
        - name: Set the new image tag
          working-directory: apps/shop-api/production
          env:
            VERSION: ${{ inputs.version }}
          run: |
            [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]{7}$ ]] || { echo "bad version: $VERSION"; exit 1; }
            kustomize edit set image "ghcr.io/elonerajeev/shop-api:$VERSION"
        - name: Commit and push
          env:
            VERSION: ${{ inputs.version }}
          run: |
            git config user.name "shop-api release bot"
            git config user.email "shop-api-bot@users.noreply.github.com"
            git commit -am "shop-api: deploy $VERSION to production"
            git push
    

    The version check refuses anything that isn't a real tag: x; echo pwned and 1.0.43 are both rejected. The token can write to one repository and nothing else; the cluster's credentials never leave the cluster.

    Rolling back

    $ git log --oneline -2
    f49d2b7 shop-api: deploy 1.0.43-9f2a1bc to production
    b2d9f32 shop-api: deploy 1.0.42-e3b0c44 to production
    $ git revert --no-edit f49d2b7 && git push     # Argo CD or Flux rolls 1.0.42 back out
    
    git log --oneline -2
    • gitversion control
    • loghistory
    git revert --no-edit f49d2b7 && git push
    • &&run the next command only if this one worked
    • pushupload commits
    In production

    Protect the config repository like production itself: required reviews, CODEOWNERS per environment, and no direct pushes except from the release bot. Never commit plain secrets to it; use Sealed Secrets, SOPS or the External Secrets Operator so Git holds only encrypted values or references. And alert on OutOfSync or failed syncs, so a broken commit doesn't sit unnoticed.

    ⌘
    Mini mission

    Build shop-api's config repository

    Create the shop-config repository with a base and a production overlay for shop-api, an Argo CD Application and the Flux equivalent. Validate everything with kustomize and kubeconform, change the image tag the way CI would, and practise a rollback with git revert.

    Input: kustomize and kubeconform (single binaries), GitOutput: A config repository whose every commit renders and validates
    Reveal one safe solution
    $ mkdir -p shop-config/apps/shop-api/{base,production} shop-config/clusters/production && cd shop-config && git init
    $ nano apps/shop-api/base/deployment.yaml apps/shop-api/base/service.yaml apps/shop-api/base/kustomization.yaml
    $ nano apps/shop-api/production/kustomization.yaml
    $ nano clusters/production/argocd-shop-api.yaml clusters/production/flux-shop-api.yaml
    $ kustomize build apps/shop-api/production | kubeconform -strict -summary -
    Summary: 2 resources found parsing stdin - Valid: 2, Invalid: 0, Errors: 0, Skipped: 0
    $ kubeconform -strict -summary -schema-location default \
        -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
        clusters/production/
    Summary: 3 resources found in 2 files - Valid: 3, Invalid: 0, Errors: 0, Skipped: 0
    $ git add . && git commit -m "shop-api: production at 1.0.0"
    $ (cd apps/shop-api/production && kustomize edit set image ghcr.io/elonerajeev/shop-api:1.0.43-9f2a1bc)
    $ git commit -am "shop-api: deploy 1.0.43-9f2a1bc to production"
    $ git revert --no-edit HEAD && grep newTag apps/shop-api/production/kustomization.yaml
      newTag: 1.0.0
    
    mkdir -p shop-config/apps/shop-api/{base,production} shop-config/clusters/production && cd shop-config && git init
    • mkdirmake a directory
    • -pcreate missing parents, no error if it exists
    • &&run the next command only if this one worked
    • cdchange directory
    • gitversion control
    • initcreate a repository here
    kustomize build apps/shop-api/production | kubeconform -strict -summary -
    • |pipe: send output to the next command
    • -the previous directory
    git add . && git commit -m "shop-api: production at 1.0.0"
    • addstage changes
    • .this folder
    • commitsave staged changes
    git revert --no-edit HEAD && grep newTag apps/shop-api/production/kustomization.yaml
    • grepfind lines that match a pattern

    The 3 resources are the Argo CD Application plus Flux's GitRepository and Kustomization (in practice you'd run one of the two tools; both are here to compare). Every file is in this lesson. With a local cluster (kind), install Argo CD from its getting-started guide and apply clusters/production/argocd-shop-api.yaml to see it sync.

    Try it yourself

    0 of 6 steps done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    What is the main difference between push and pull deployment?

    Go deeper

    Official reading for this lesson

    • ArticleOpenGitOps principlesOpenGitOps (CNCF) ↗
    • DocsArgo CD: getting startedArgo CD ↗
    • DocsArgo CD: automated sync policyArgo CD ↗
    • DocsFlux: get startedFlux ↗
    • DocsKustomize documentationKubernetes ↗
    • Toolkubeconform: Kubernetes manifest validatorkubeconform ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousOther CI tools: GitLab CI, CircleCI and BuildkiteNext lesson →Pipeline security: your CI is a production system