Shipping infrastructure changes without validation is a fast way to break environments. If you’re using Ansible, you already treat infrastructure as code—so it deserves the same CI/CD rigor as application code.
Let’s build a practical CI/CD pipeline for Ansible using GitHub Actions that checks syntax, enforces best practices, and optionally deploys changes safely.
What we’re aiming for
A solid Ansible CI/CD pipeline should:
- Validate YAML syntax
- Run ansible-lint for best practices
- Check playbook execution in a safe environment
- Optionally deploy to target servers
We’ll wire all of this into a GitHub Actions workflow.
Repository layout (example)
Your project might look something like this:
1
2.
3├── inventory/
4│ └── hosts.yml
5├── playbooks/
6│ └── site.yml
7├── roles/
8├── group_vars/
9├── host_vars/
10└── .github/
11 └── workflows/
12 └── ansible-ci.yml
13Start with the workflow file
Create .github/workflows/ansible-ci.yml. Here’s a working pipeline you can adapt:
1
2name: Ansible CI/CD Pipeline
3
4on:
5 push:
6 branches: [ "main" ]
7 pull_request:
8 branches: [ "main" ]
9
10jobs:
11 ansible-ci:
12 runs-on: ubuntu-latest
13
14 steps:
15 - name: Checkout repository
16 uses: actions/checkout@v4
17
18 - name: Set up Python
19 uses: actions/setup-python@v5
20 with:
21 python-version: '3.11'
22
23 - name: Install dependencies
24 run: |
25 pip install ansible ansible-lint
26
27 - name: Validate YAML syntax
28 run: |
29 ansible-playbook playbooks/site.yml --syntax-check
30
31 - name: Run ansible-lint
32 run: |
33 ansible-lint playbooks/site.yml
34This already gives you a basic CI pipeline for Ansible with GitHub Actions. Every push or pull request will be validated.
Here’s where things get interesting: testing execution
Syntax checks don’t guarantee your playbook works. To go further, you can simulate execution using a local inventory or a containerized environment.
Option: Run against localhost
1
2- name: Run playbook locally
3 run: |
4 ansible-playbook -i "localhost," -c local playbooks/site.yml
5This is useful for roles that don’t depend on external infrastructure.
Option: Use Molecule (advanced)
If you want proper testing, Molecule lets you spin up test instances (Docker, Vagrant, etc.). That’s a deeper setup but worth it for production-grade pipelines.
Adding deployment to the pipeline
Once validation passes, you may want to deploy automatically. This is where many teams get into trouble—so keep it controlled.
Use GitHub secrets for SSH
Store sensitive data securely:
- SSH_PRIVATE_KEY
- ANSIBLE_HOST
Deployment step example
1
2- name: Setup SSH
3 run: |
4 mkdir -p ~/.ssh
5 echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
6 chmod 600 ~/.ssh/id_rsa
7
8- name: Run Ansible playbook on remote host
9 run: |
10 ansible-playbook -i inventory/hosts.yml playbooks/site.yml
11A common mistake developers make is deploying on every push. Instead, restrict deployment to specific conditions.
Deploy only on main branch
1
2if: github.ref == 'refs/heads/main'
3Adding environments for safer releases
GitHub Actions supports environments like staging and production. You can require approvals before deployment.
Example:
1
2environment:
3 name: production
4This adds a manual checkpoint before your Ansible deployment runs.
Speeding things up
CI pipelines can get slow, especially when installing dependencies repeatedly.
Cache Python packages
1
2- name: Cache pip dependencies
3 uses: actions/cache@v4
4 with:
5 path: ~/.cache/pip
6 key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
7This can significantly reduce pipeline runtime.
Common pitfalls (worth avoiding)
- Skipping linting: ansible-lint catches subtle issues early
- Hardcoding secrets: always use GitHub Secrets
- No dry runs: use
--checkmode when possible - Blind deployments: add environment approvals or conditions
A slightly more complete pipeline
Here’s a refined version combining everything:
1
2name: Ansible CI/CD
3
4on:
5 push:
6 branches: [ "main" ]
7
8jobs:
9 deploy:
10 runs-on: ubuntu-latest
11
12 steps:
13 - uses: actions/checkout@v4
14
15 - uses: actions/setup-python@v5
16 with:
17 python-version: '3.11'
18
19 - run: pip install ansible ansible-lint
20
21 - run: ansible-playbook playbooks/site.yml --syntax-check
22
23 - run: ansible-lint playbooks/site.yml
24
25 - name: Dry run
26 run: |
27 ansible-playbook -i inventory/hosts.yml playbooks/site.yml --check
28
29 - name: Deploy
30 if: github.ref == 'refs/heads/main'
31 run: |
32 ansible-playbook -i inventory/hosts.yml playbooks/site.yml
33Why this setup works well
You get fast feedback on every change, consistent validation across teams, and safer deployments. More importantly, your infrastructure changes become predictable.
And that’s the real win: fewer surprises in production.
Where to go next
If you want to push this further:
- Integrate Molecule for role testing
- Add Slack or email notifications
- Use dynamic inventories (AWS, Azure, etc.)
- Split workflows for CI and CD stages
Once this pipeline is in place, your Ansible workflows stop being “scripts you hope work” and become a reliable, repeatable delivery system.