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
- Declarative — desired state described in Git
- Versioned — every change has a Git commit
- Automated — changes in Git trigger deployment
- 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
- Single repo per concern — separate repos for K8s manifests, Ansible playbooks, application code
- Branch protection on main — require reviews before merge
- Automated testing — lint + Molecule + dry-run before merge
- Immutable artifacts — pin versions, tags, and image digests
- Drift detection — scheduled check-mode runs to catch manual changes
- Secrets never in Git — use Sealed Secrets, External Secrets, or Ansible Vault
- Rollback = revert — just revert the Git commit
Related Articles
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.