Skip to content

GitOps — Recap

What it is

A pattern, not a tool. Git is the single source of truth for everything — infrastructure, application topology, application code. Every change goes through Git. Automation reconciles what's in Git with what's actually running.

No manual SSH to deploy. No "I ran a command on the server." If it's not in Git, it didn't happen.


The three repo structure

terraform-ansible-repo    → infrastructure layer
                            VMs, networking, Azure services,
                            Docker install, Swarm setup, secrets
                            owner: you, manually, rarely

stack-repo                → application topology layer
                            stack.yml, service definitions
                            CI does docker stack deploy
                            owner: you or CI, when services change

code-repo                 → application source
                            source code, Dockerfile
                            CI builds and pushes image
                            owner: CI, every push

Dependency flows one way:

terraform-ansible  →  produces the cluster
stack-repo         →  deploys onto that cluster
code-repo          →  produces the images stack-repo references

Nothing flows backwards.


The full end to end flow

1. terraform apply
      VMs provisioned, VNet, NSG, Blob Storage, Key Vault, Function
      Service Principal created, role assignments done

2. ansible-playbook playbook.yml
      Docker installed on manager and worker
      Swarm initialised on manager
      Worker joined to Swarm
      Azure SP credentials loaded as Docker secrets

3. git push to code-repo
      GitHub Actions builds image
      Tags with :latest and :<git-sha>
      Updates image tag in stack-repo stack.yml
      Pushes to stack-repo master

4. stack-repo CI triggers on push
      SSHes into manager
      docker stack deploy
      app is running

Steps 1 and 2 are one-time. Steps 3 and 4 happen on every code change.


Cadence of each repo

terraform-ansible-repo     provision once, touch rarely
                            new VM, new Azure service, NSG rule change

stack-repo                 touch when services change
                            new service, new env var, new volume, image tag update

code-repo                  every feature, every fix
                            fully automated, no manual steps

How the image tag flows between repos

In code-repo GitHub Actions:

SHA=$(git rev-parse --short HEAD)

# build and push
docker build -t ghcr.io/abhishek052off/averazure:$SHA .
docker push ghcr.io/abhishek052off/averazure:$SHA
docker push ghcr.io/abhishek052off/averazure:latest

# update stack-repo
sed -i 's|ghcr.io/abhishek052off/averazure:.*|ghcr.io/abhishek052off/averazure:'$SHA'|' stack.yml
git commit -am "update image tag to $SHA"
git push stack-repo master

stack-repo always reflects exactly what is deployed. Rollback is reverting a commit.


GitOps in Kubernetes world

In K8s, tools like ArgoCD and Flux do what your GitHub Actions does — but continuously. They poll the repo and sync the cluster state to match. If someone manually changes something on the cluster, ArgoCD reverts it back to what Git says.

Your GitHub Actions only syncs on push. ArgoCD syncs constantly. Same pattern, different implementation.


Full runnable code — GitHub Actions pipelines

code-repo pipeline

.github/workflows/build.yml

name: Build and Deploy

on:
  push:
    branches: [master]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Login to GHCR
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_TOKEN }}

      - name: Build and push image
        run: |
          SHA=$(git rev-parse --short HEAD)
          docker build -t ghcr.io/abhishek052off/averazure:$SHA .
          docker build -t ghcr.io/abhishek052off/averazure:latest .
          docker push ghcr.io/abhishek052off/averazure:$SHA
          docker push ghcr.io/abhishek052off/averazure:latest
          echo "SHA=$SHA" >> $GITHUB_ENV

      - name: Checkout stack-repo
        uses: actions/checkout@v3
        with:
          repository: abhishek052off/stack-repo
          token: ${{ secrets.GHCR_TOKEN }}
          path: stack-repo

      - name: Update image tag in stack.yml
        run: |
          sed -i 's|ghcr.io/abhishek052off/averazure:.*|ghcr.io/abhishek052off/averazure:${{ env.SHA }}|' stack-repo/stack.yml
          cd stack-repo
          git config user.email "ci@averazure.com"
          git config user.name "CI Bot"
          git commit -am "update image tag to ${{ env.SHA }}"
          git push

GitHub Secrets needed in code-repo:

  • GHCR_TOKEN — PAT with read/write packages and repo access

stack-repo pipeline

.github/workflows/deploy.yml

name: Deploy Stack

on:
  push:
    branches: [master]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Login to GHCR
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GHCR_TOKEN }}

      - name: Deploy to Swarm
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.MANAGER_IP }}
          username: azureuser
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u abhishek052off --password-stdin
            docker stack deploy -c /home/azureuser/stack.yml aver --with-registry-auth

      - name: Copy updated stack.yml to manager
        uses: appleboy/scp-action@v0.1.4
        with:
          host: ${{ secrets.MANAGER_IP }}
          username: azureuser
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: stack.yml
          target: /home/azureuser/

GitHub Secrets needed in stack-repo:

  • GHCR_TOKEN — PAT with read packages
  • MANAGER_IP — public IP of aver-manager
  • SSH_PRIVATE_KEY — your private key contents

stack.yml (lives in stack-repo)

version: "3.8"

services:
  aver_api:
    image: ghcr.io/abhishek052off/averazure:latest
    environment:
      - ASPNETCORE_URLS=http://0.0.0.0:8080
    secrets:
      - azure_tenant_id
      - azure_client_id
      - azure_client_secret
    ports:
      - "8080:8080"
    networks:
      - aver_net
    deploy:
      replicas: 1
      update_config:
        order: start-first

  aver_rabbitmq:
    image: rabbitmq:3-management
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    ports:
      - "15672:15672"
    networks:
      - aver_net
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.hostname == aver-manager

  aver_seq:
    image: datalust/seq:latest
    environment:
      - ACCEPT_EULA=Y
    volumes:
      - seq_data:/data
    ports:
      - "8081:80"
    networks:
      - aver_net
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.hostname == aver-manager

secrets:
  azure_tenant_id:
    external: true
  azure_client_id:
    external: true
  azure_client_secret:
    external: true

volumes:
  rabbitmq_data:
  seq_data:

networks:
  aver_net:
    driver: overlay

The one liner for interviews

"Infrastructure, application topology, and application code live in separate repos with separate cadences. Git is the source of truth. A push to code-repo triggers image build and tag update in the stack repo, which triggers deploy. Nothing is done manually after the initial cluster setup."