Ansible vs GitHub Actions: Key Differences & When to Use Each (2026)
By Luca Berton · Published 2024-01-01 · Category: installation
Ansible vs GitHub Actions comparison. When to use each, CI/CD pipeline integration with Jenkins and GitLab, and how to combine them for DevOps automation.
Introduction
Modern software delivery demands automated pipelines that build, test, and deploy without manual intervention. Ansible fits naturally into CI/CD pipelines — handling infrastructure provisioning, application deployment, configuration management, and post-deployment verification. This guide shows how to integrate Ansible with the three most popular CI/CD platforms.
See also: AAP 2.6 CI/CD Pipeline Integration: GitOps Workflows with Jenkins, GitLab, and GitHub Actions
GitHub Actions
Basic Ansible Playbook Runner
# .github/workflows/deploy.yml
name: Deploy with Ansible
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible
run: |
pip install ansible-core ansible-lint
ansible-galaxy collection install -r collections/requirements.yml
- name: Configure SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
- name: Run Ansible Playbook
run: |
ansible-playbook playbooks/deploy.yml \
-i "${{ secrets.DEPLOY_HOST }}," \
--private-key ~/.ssh/deploy_key \
-e "app_version=${{ github.sha }}"
env:
ANSIBLE_HOST_KEY_CHECKING: "false"
Full Pipeline with Testing
name: Full CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install tools
run: pip install ansible-core ansible-lint yamllint
- name: YAML Lint
run: yamllint -c .yamllint .
- name: Ansible Lint
run: ansible-lint playbooks/
- name: Syntax Check
run: ansible-playbook playbooks/site.yml --syntax-check
test:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Molecule
run: pip install ansible-core molecule molecule-docker
- name: Run Molecule tests
run: |
cd roles/webserver
molecule test
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
deploy-staging:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: |
pip install ansible-core
ansible-playbook playbooks/deploy.yml \
-i inventories/staging/hosts.yml \
--private-key <(echo "${{ secrets.SSH_KEY }}") \
-e "app_version=${{ github.sha }}"
deploy-production:
needs: deploy-staging
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
pip install ansible-core
ansible-playbook playbooks/deploy.yml \
-i inventories/production/hosts.yml \
--private-key <(echo "${{ secrets.SSH_KEY }}") \
-e "app_version=${{ github.sha }}"
GitLab CI
Basic Pipeline
# .gitlab-ci.yml
stages:
- lint
- test
- deploy-staging
- deploy-production
variables:
ANSIBLE_HOST_KEY_CHECKING: "false"
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
paths:
- .cache/pip
lint:
stage: lint
image: python:3.12
script:
- pip install ansible-core ansible-lint yamllint
- yamllint .
- ansible-lint playbooks/
- ansible-playbook playbooks/site.yml --syntax-check
molecule-test:
stage: test
image: docker:latest
services:
- docker:dind
before_script:
- apk add python3 py3-pip
- pip install ansible-core molecule molecule-docker --break-system-packages
script:
- cd roles/webserver && molecule test
deploy-staging:
stage: deploy-staging
image: python:3.12
environment:
name: staging
url: https://staging.example.com
only:
- main
before_script:
- pip install ansible-core
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
script:
- ansible-playbook playbooks/deploy.yml
-i inventories/staging/hosts.yml
-e "app_version=${CI_COMMIT_SHA}"
deploy-production:
stage: deploy-production
image: python:3.12
environment:
name: production
url: https://app.example.com
only:
- main
when: manual # Require manual approval
before_script:
- pip install ansible-core
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
script:
- ansible-playbook playbooks/deploy.yml
-i inventories/production/hosts.yml
-e "app_version=${CI_COMMIT_SHA}"
See also: Automate Ansible Collection Testing with GitHub Actions
Jenkins
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
environment {
ANSIBLE_HOST_KEY_CHECKING = 'false'
}
stages {
stage('Lint') {
steps {
sh '''
pip install ansible-core ansible-lint yamllint
yamllint .
ansible-lint playbooks/
ansible-playbook playbooks/site.yml --syntax-check
'''
}
}
stage('Test') {
steps {
sh '''
pip install molecule molecule-docker
cd roles/webserver
molecule test
'''
}
}
stage('Deploy Staging') {
when {
branch 'main'
}
steps {
withCredentials([sshUserPrivateKey(
credentialsId: 'deploy-key',
keyFileVariable: 'SSH_KEY'
)]) {
sh """
ansible-playbook playbooks/deploy.yml \
-i inventories/staging/hosts.yml \
--private-key \$SSH_KEY \
-e 'app_version=${env.GIT_COMMIT}'
"""
}
}
}
stage('Approval') {
when {
branch 'main'
}
steps {
input message: 'Deploy to production?',
ok: 'Deploy',
submitter: 'platform-team'
}
}
stage('Deploy Production') {
when {
branch 'main'
}
steps {
withCredentials([sshUserPrivateKey(
credentialsId: 'deploy-key',
keyFileVariable: 'SSH_KEY'
)]) {
sh """
ansible-playbook playbooks/deploy.yml \
-i inventories/production/hosts.yml \
--private-key \$SSH_KEY \
-e 'app_version=${env.GIT_COMMIT}'
"""
}
}
}
}
post {
failure {
slackSend channel: '#deployments',
color: 'danger',
message: "Deployment FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
}
success {
slackSend channel: '#deployments',
color: 'good',
message: "Deployment SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
}
}
}
Jenkins Ansible Plugin
// Using the Ansible plugin for Jenkins
stage('Deploy') {
steps {
ansiblePlaybook(
playbook: 'playbooks/deploy.yml',
inventory: 'inventories/production/hosts.yml',
credentialsId: 'ssh-deploy-key',
extras: "-e app_version=${env.GIT_COMMIT}",
colorized: true
)
}
}
Trigger AAP from CI/CD
GitHub Actions → AAP
- name: Trigger AAP Job Template
run: |
curl -X POST \
"${{ secrets.AAP_URL }}/api/v2/job_templates/${{ secrets.AAP_TEMPLATE_ID }}/launch/" \
-H "Authorization: Bearer ${{ secrets.AAP_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"extra_vars": {
"app_version": "${{ github.sha }}",
"environment": "production",
"deployer": "${{ github.actor }}"
}
}'
Wait for AAP Job Completion
- name: Wait for AAP job
run: |
JOB_URL=$(curl -s -X POST \
"${{ secrets.AAP_URL }}/api/v2/job_templates/${{ secrets.AAP_TEMPLATE_ID }}/launch/" \
-H "Authorization: Bearer ${{ secrets.AAP_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"extra_vars": {"version": "${{ github.sha }}"}}' \
| jq -r '.url')
while true; do
STATUS=$(curl -s \
"${{ secrets.AAP_URL }}${JOB_URL}" \
-H "Authorization: Bearer ${{ secrets.AAP_TOKEN }}" \
| jq -r '.status')
case $STATUS in
successful) echo "Job succeeded"; exit 0 ;;
failed|error|canceled) echo "Job $STATUS"; exit 1 ;;
*) echo "Status: $STATUS, waiting..."; sleep 15 ;;
esac
done
See also: Automate Ansible Galaxy Roles with GitHub Actions
Molecule Testing in CI
# roles/webserver/molecule/default/molecule.yml
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu
image: ubuntu:24.04
pre_build_image: true
- name: rocky
image: rockylinux:9
pre_build_image: true
provisioner:
name: ansible
verifier:
name: ansible
# roles/webserver/molecule/default/verify.yml
- name: Verify
hosts: all
tasks:
- name: Check nginx is running
ansible.builtin.service_facts:
- name: Assert nginx service
ansible.builtin.assert:
that:
- "'nginx.service' in services"
- "services['nginx.service'].state == 'running'"
- name: Check nginx responds
ansible.builtin.uri:
url: http://localhost:80
status_code: 200
Deployment Playbook Pattern
# playbooks/deploy.yml
---
- name: Deploy application
hosts: webservers
become: true
serial: "25%"
max_fail_percentage: 10
vars:
app_version: "{{ lookup('env', 'APP_VERSION') | default('latest') }}"
pre_tasks:
- name: Remove from load balancer
ansible.builtin.uri:
url: "{{ lb_api }}/servers/{{ inventory_hostname }}/disable"
method: POST
delegate_to: localhost
- name: Wait for connections to drain
ansible.builtin.wait_for:
timeout: 30
roles:
- role: deploy_app
vars:
version: "{{ app_version }}"
post_tasks:
- name: Health check
ansible.builtin.uri:
url: "http://{{ inventory_hostname }}:8080/health"
status_code: 200
retries: 10
delay: 5
register: health
until: health.status == 200
- name: Re-enable in load balancer
ansible.builtin.uri:
url: "{{ lb_api }}/servers/{{ inventory_hostname }}/enable"
method: POST
delegate_to: localhost
Best Practices
Lint before deploy — ansible-lint and yamllint catch issues early Test with Molecule — Unit test roles before integration Environment gates — Manual approval for production deployments Rolling deploys withserial — Never deploy to 100% at once
Health checks — Verify each batch before proceeding
Vault for secrets — CI/CD secrets stored in platform's secret manager, injected at runtime
Trigger AAP for production — CI handles build/test; AAP handles production deployment with RBAC and audit
Pin ansible-core version — Same version in CI and production for consistency
Cache pip packages — Speed up pipeline execution
FAQ
CI/CD direct Ansible vs triggering AAP?
Use direct Ansible for simple deployments and dev/staging. Use AAP for production — it adds RBAC, audit logging, credential isolation, and a web UI for visibility.
How to handle Ansible Vault passwords in CI?
Store the vault password as a CI/CD secret variable. Pass it with --vault-password-file <(echo "$VAULT_PASSWORD").
Docker-in-Docker for Molecule in CI?
GitLab CI needs services: [docker:dind]. GitHub Actions has Docker available by default. Jenkins needs the Docker Pipeline plugin.
Conclusion
Integrating Ansible into CI/CD pipelines automates the full delivery lifecycle — from code commit through testing to production deployment. Whether using GitHub Actions, GitLab CI, or Jenkins, Ansible provides consistent infrastructure automation that fits naturally into modern DevOps workflows.
Related Articles
• Ansible GitOps with AAP • Ansible Execution Environments • Ansible Automation Platform RBAC • Ansible vs Jenkins: Can Ansible Replace CI/CD? • Ansible Collection CI Requirement 2026 • ACTION REQUIRED: Collections Must Add CI Test Runs • Automate Collection Testing with GitHub Actions • Install and Configure Molecule for Role TestingCategory: installation