Introduction
Ansible is a natural fit for the deployment stage of CI/CD pipelines. After your code is built and tested, Ansible handles the infrastructure: provisioning servers, deploying artifacts, running migrations, and validating health. This guide shows integration patterns for the three most popular CI/CD platforms.
GitHub Actions
# .github/workflows/deploy.yml
name: Deploy with Ansible
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
default: "staging"
type: choice
options: [staging, production]
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment || 'staging' }}
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Ansible
run: |
pip install ansible-core==2.20.0 jmespath
ansible-galaxy collection install -r collections/requirements.yml
- name: Configure SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
- name: Create vault password file
run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > .vault_pass
- name: Run playbook
env:
ANSIBLE_HOST_KEY_CHECKING: "false"
run: |
ansible-playbook site.yml \
-i inventory/${{ github.event.inputs.environment || 'staging' }} \
--vault-password-file .vault_pass \
-e "app_version=${{ github.sha }}"
- name: Cleanup
if: always()
run: rm -f .vault_pass ~/.ssh/id_ed25519
With Execution Environments
- name: Run with ansible-navigator
run: |
pip install ansible-navigator
ansible-navigator run site.yml \
--eei ghcr.io/myorg/ansible-ee:latest \
-i inventory/staging \
-m stdout
GitLab CI
# .gitlab-ci.yml
stages:
- lint
- deploy-staging
- deploy-production
variables:
ANSIBLE_HOST_KEY_CHECKING: "false"
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
.ansible-base:
image: python:3.12-slim
before_script:
- pip install ansible-core==2.20.0 jmespath
- ansible-galaxy collection install -r collections/requirements.yml
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass
after_script:
- rm -f .vault_pass ~/.ssh/id_ed25519
lint:
stage: lint
image: python:3.12-slim
script:
- pip install ansible-lint
- ansible-lint site.yml
deploy-staging:
stage: deploy-staging
extends: .ansible-base
script:
- ansible-playbook site.yml
-i inventory/staging
--vault-password-file .vault_pass
-e "app_version=${CI_COMMIT_SHA}"
environment:
name: staging
url: https://app.example.com
only:
- main
deploy-production:
stage: deploy-production
extends: .ansible-base
script:
- ansible-playbook site.yml
-i inventory/production
--vault-password-file .vault_pass
-e "app_version=${CI_COMMIT_TAG}"
environment:
name: production
url: https://example.com
only:
- tags
when: manual # Require manual approval
Jenkins
// Jenkinsfile
pipeline {
agent any
parameters {
choice(name: 'ENVIRONMENT', choices: ['staging', 'production'])
string(name: 'APP_VERSION', defaultValue: '', description: 'Version to deploy')
}
environment {
ANSIBLE_HOST_KEY_CHECKING = 'false'
}
stages {
stage('Lint') {
steps {
sh '''
python3 -m venv venv
. venv/bin/activate
pip install ansible-lint
ansible-lint site.yml
'''
}
}
stage('Deploy') {
steps {
withCredentials([
sshUserPrivateKey(credentialsId: 'deploy-key', keyFileVariable: 'SSH_KEY'),
string(credentialsId: 'vault-password', variable: 'VAULT_PASS')
]) {
sh """
. venv/bin/activate
pip install ansible-core==2.20.0
ansible-galaxy collection install -r collections/requirements.yml
echo "\$VAULT_PASS" > .vault_pass
ansible-playbook site.yml \\
-i inventory/${params.ENVIRONMENT} \\
--vault-password-file .vault_pass \\
--private-key \$SSH_KEY \\
-e "app_version=${params.APP_VERSION ?: env.GIT_COMMIT}"
rm -f .vault_pass
"""
}
}
}
}
post {
failure {
slackSend(color: 'danger', message: "Deploy failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}")
}
success {
slackSend(color: 'good', message: "Deployed ${params.APP_VERSION} to ${params.ENVIRONMENT}")
}
}
}
Secrets Management
# Pattern 1: CI/CD secrets → vault password → encrypted vars
# Store ANSIBLE_VAULT_PASSWORD in CI/CD secrets
# All other secrets encrypted in vault files
# Pattern 2: CI/CD secrets → extra vars (simple projects)
ansible-playbook site.yml -e "db_password=$DB_PASSWORD"
# Pattern 3: External secret store
# HashiCorp Vault lookup
- name: Get secret from Vault
ansible.builtin.set_fact:
db_password: "{{ lookup('hashi_vault', 'secret/data/myapp:db_password') }}"
Pipeline Best Practices
# ✅ Always lint before deploy
# ✅ Deploy to staging before production
# ✅ Require manual approval for production
# ✅ Pin Ansible and collection versions
# ✅ Use --check --diff in PR pipelines
# ✅ Clean up secrets in post/after_script
# ✅ Pass commit SHA as app_version for traceability
# ✅ Use Execution Environments for reproducibility
Dry-Run in Pull Requests
# GitHub Actions — PR check
on:
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ansible-core ansible-lint
- run: ansible-lint site.yml
- run: ansible-playbook site.yml --check --diff -i inventory/staging
Common Mistakes
# ❌ Not pinning Ansible version
pip install ansible # Gets latest — may break!
# ✅ Pin versions
pip install ansible-core==2.20.0
# ❌ SSH key in repo
# .ssh/deploy_key committed to git
# ✅ SSH key in CI/CD secrets, injected at runtime
# ❌ Running against production on every push
on:
push:
branches: [main]
# Deploys to prod on every merge!
# ✅ Use environments + manual approval for production
Related Articles
- Ansible Execution Environments
- Ansible Playbook Best Practices
- Ansible check mode vs diff mode
- Ansible Vault Guide
Conclusion
GitHub Actions: Use actions/checkout + pip install + SSH key from secrets. GitLab CI: Use .extends for shared setup, when: manual for production gates. Jenkins: Use withCredentials for secrets, parameterized builds for environment selection. All three follow the same pattern: lint → dry-run (staging) → deploy (staging) → manual gate → deploy (production). Pin your Ansible version, encrypt secrets with Vault, and always clean up credentials in post-run steps.