Introduction

GitOps treats Git as the single source of truth for infrastructure and application state. ArgoCD watches Git repositories and automatically syncs Kubernetes resources when changes are pushed. Ansible extends this pattern beyond Kubernetes — managing servers, networks, cloud resources, and configurations. This guide covers deploying ArgoCD with Ansible, triggering Ansible runs from Git changes, and building a complete GitOps pipeline.

GitOps Principles

  1. Declarative — desired state described in Git
  2. Versioned — every change has a Git commit
  3. Automated — changes in Git trigger deployment
  4. Self-healing — drift is automatically corrected

Deploy ArgoCD with Ansible

---
- name: Deploy ArgoCD on Kubernetes
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Create ArgoCD namespace
      kubernetes.core.k8s:
        name: argocd
        kind: Namespace
        state: present

    - name: Install ArgoCD
      kubernetes.core.k8s:
        state: present
        src: https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
        namespace: argocd

    - name: Wait for ArgoCD server
      kubernetes.core.k8s_info:
        kind: Deployment
        name: argocd-server
        namespace: argocd
      register: argocd_deploy
      until: argocd_deploy.resources[0].status.readyReplicas | default(0) >= 1
      retries: 30
      delay: 10

    - name: Get initial admin password
      kubernetes.core.k8s_info:
        kind: Secret
        name: argocd-initial-admin-secret
        namespace: argocd
      register: argocd_secret

    - name: Display admin password
      ansible.builtin.debug:
        msg: "ArgoCD admin password: {{ argocd_secret.resources[0].data.password | b64decode }}"

    - name: Expose ArgoCD via Ingress
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: networking.k8s.io/v1
          kind: Ingress
          metadata:
            name: argocd-server
            namespace: argocd
            annotations:
              nginx.ingress.kubernetes.io/ssl-passthrough: "true"
              nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
          spec:
            rules:
              - host: argocd.example.com
                http:
                  paths:
                    - path: /
                      pathType: Prefix
                      backend:
                        service:
                          name: argocd-server
                          port:
                            number: 443

Register Applications in ArgoCD

- name: Register applications in ArgoCD
  kubernetes.core.k8s:
    state: present
    definition:
      apiVersion: argoproj.io/v1alpha1
      kind: Application
      metadata:
        name: "{{ item.name }}"
        namespace: argocd
      spec:
        project: default
        source:
          repoURL: "{{ item.repo }}"
          targetRevision: "{{ item.branch | default('main') }}"
          path: "{{ item.path }}"
        destination:
          server: https://kubernetes.default.svc
          namespace: "{{ item.namespace }}"
        syncPolicy:
          automated:
            prune: true
            selfHeal: true
          syncOptions:
            - CreateNamespace=true
  loop:
    - name: webapp
      repo: https://github.com/myorg/k8s-manifests.git
      path: apps/webapp
      namespace: production
    - name: monitoring
      repo: https://github.com/myorg/k8s-manifests.git
      path: apps/monitoring
      namespace: monitoring

GitOps for Ansible Playbooks

Pattern 1: Webhook Triggers

# GitHub webhook → Automation Controller → runs playbook
- name: Configure Automation Controller webhook
  awx.awx.job_template:
    name: "GitOps - Deploy Infrastructure"
    organization: "My Org"
    project: "Infrastructure"
    playbook: "site.yml"
    inventory: "Production"
    credential: "SSH Key"
    webhook_service: github
    webhook_credential: "GitHub Webhook Token"

Pattern 2: ansible-pull (Self-Healing)

# Servers pull from Git every 30 minutes
# cron job on every managed host:
- name: Set up GitOps pull
  ansible.builtin.cron:
    name: "GitOps ansible-pull"
    minute: "*/30"
    job: >-
      ansible-pull
      --url https://github.com/myorg/ansible-infra.git
      --checkout main
      --inventory localhost,
      --only-if-changed
      site.yml >> /var/log/ansible-pull.log 2>&1

Pattern 3: CI/CD Pipeline (GitHub Actions)

# .github/workflows/gitops-ansible.yml
name: GitOps Ansible Deploy
on:
  push:
    branches: [main]
    paths:
      - 'playbooks/**'
      - 'roles/**'
      - 'inventory/**'

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

      - name: Install Ansible
        run: pip install ansible

      - name: Run playbook
        run: ansible-playbook -i inventory/production site.yml
        env:
          ANSIBLE_HOST_KEY_CHECKING: "false"

Complete GitOps Pipeline

┌──────────┐    push    ┌──────────┐    webhook    ┌──────────────┐
│ Developer │──────────►│   Git     │─────────────►│  Automation   │
│           │           │  (main)   │              │  Controller   │
└──────────┘           └──────────┘              └──────┬────────┘
                            │                            │
                            │ ArgoCD                     │ Ansible
                            │ watches                    │ runs
                            ▼                            ▼
                       ┌──────────┐              ┌──────────────┐
                       │ ArgoCD   │              │  Servers,     │
                       │ syncs    │              │  Network,     │
                       │ K8s      │              │  Cloud        │
                       └──────────┘              └──────────────┘

Implementation

---
# site.yml — top-level GitOps playbook
- name: GitOps Infrastructure
  hosts: all
  become: true
  roles:
    - base
    - security
    - monitoring

- name: Web Servers
  hosts: webservers
  become: true
  roles:
    - nginx
    - certbot
    - app_deploy

- name: Database Servers
  hosts: databases
  become: true
  roles:
    - postgresql
    - backup

Drift Detection

---
- name: Detect configuration drift
  hosts: all
  become: true
  tasks:
    - name: Run playbook in check mode
      ansible.builtin.include_role:
        name: "{{ item }}"
      loop:
        - base
        - security
      check_mode: true
      register: drift_results

    - name: Report drift
      ansible.builtin.debug:
        msg: "DRIFT DETECTED on {{ inventory_hostname }}"
      when: drift_results.changed

    - name: Send drift alert
      ansible.builtin.uri:
        url: "{{ slack_webhook_url }}"
        method: POST
        body_format: json
        body:
          text: "⚠️ Configuration drift detected on {{ inventory_hostname }}"
      when: drift_results.changed
      delegate_to: localhost

Schedule drift detection every hour:

- name: Schedule drift detection
  ansible.builtin.cron:
    name: "Drift detection"
    minute: "0"
    job: >-
      ansible-playbook -i inventory/production drift-check.yml
      --check >> /var/log/drift-check.log 2>&1

ArgoCD + Ansible Operator

For running Ansible from ArgoCD directly:

# Deploy Ansible Operator-managed resources via ArgoCD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: ansible-infra
  namespace: argocd
spec:
  source:
    repoURL: https://github.com/myorg/ansible-operator-resources.git
    path: config/
  destination:
    server: https://kubernetes.default.svc
    namespace: ansible-system
  syncPolicy:
    automated:
      selfHeal: true

Secrets Management

# Use Sealed Secrets or External Secrets with ArgoCD
- name: Install Sealed Secrets controller
  kubernetes.core.k8s:
    state: present
    src: https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.0/controller.yaml

# For Ansible: use Vault for secrets
- name: Deploy with Vault secrets
  ansible.builtin.include_role:
    name: deploy_app
  vars:
    db_password: "{{ lookup('hashi_vault', 'secret/data/myapp:db_password') }}"

Best Practices

  1. Single repo per concern — separate repos for K8s manifests, Ansible playbooks, application code
  2. Branch protection on main — require reviews before merge
  3. Automated testing — lint + Molecule + dry-run before merge
  4. Immutable artifacts — pin versions, tags, and image digests
  5. Drift detection — scheduled check-mode runs to catch manual changes
  6. Secrets never in Git — use Sealed Secrets, External Secrets, or Ansible Vault
  7. Rollback = revert — just revert the Git commit

Conclusion

GitOps with Ansible and ArgoCD gives you the best of both worlds — ArgoCD handles Kubernetes resources with automatic sync and self-healing, while Ansible manages everything else (servers, networks, cloud resources) triggered by Git webhooks or scheduled pulls. Git becomes the single source of truth: every infrastructure change is a commit, every rollback is a revert, and drift is automatically corrected.