CI/CD Integration with Ansible
Ansible in CI/CD Pipelines
Integrating Ansible into Continuous Integration and Continuous Deployment (CI/CD) pipelines automates infrastructure provisioning, configuration management, and application deployment. This ensures consistent, repeatable deployments across all environments.
CI/CD Integration Benefits:
- Automated Testing: Run Ansible playbooks on every commit
- Consistent Deployments: Same code deploys to dev, staging, and production
- Fast Feedback: Catch configuration errors early
- Audit Trail: Track all infrastructure changes
GitHub Actions Integration
Basic Workflow
# .github/workflows/ansible.yml
name: Ansible CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
name: Ansible Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install ansible ansible-lint
- name: Run ansible-lint
run: ansible-lint playbooks/
test:
name: Test Playbooks
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Ansible
run: pip install ansible
- name: Syntax check
run: ansible-playbook playbooks/site.yml --syntax-check
- name: Dry run
run: ansible-playbook playbooks/site.yml --check
deploy:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/develop'
steps:
- uses: actions/checkout@v3
- name: Install Ansible
run: pip install ansible
- name: Add SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.STAGING_HOST }} >> ~/.ssh/known_hosts
- name: Deploy to staging
run: |
ansible-playbook playbooks/deploy.yml \
-i inventory/staging \
-e "deploy_version=${{ github.sha }}"
Using Ansible Galaxy Action
- name: Install Ansible roles
uses: nick-fields/retry@v2
with:
timeout_minutes: 5
max_attempts: 3
command: ansible-galaxy install -r requirements.yml
Matrix Testing
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ansible-version: ['2.13', '2.14', '2.15']
python-version: ['3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- run: pip install ansible==${{ matrix.ansible-version }}
- run: ansible-playbook playbooks/test.yml
GitLab CI Integration
# .gitlab-ci.yml
stages:
- lint
- test
- deploy
variables:
ANSIBLE_FORCE_COLOR: "true"
ANSIBLE_HOST_KEY_CHECKING: "false"
before_script:
- pip install ansible ansible-lint
lint:
stage: lint
image: python:3.10
script:
- ansible-lint playbooks/
- yamllint .
syntax-check:
stage: test
image: python:3.10
script:
- ansible-playbook playbooks/site.yml --syntax-check
dry-run:
stage: test
image: python:3.10
script:
- ansible-playbook playbooks/site.yml --check -i inventory/staging
deploy-staging:
stage: deploy
image: python:3.10
only:
- develop
before_script:
- pip install ansible
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
script:
- ansible-playbook playbooks/deploy.yml -i inventory/staging
environment:
name: staging
url: https://staging.example.com
deploy-production:
stage: deploy
image: python:3.10
only:
- main
when: manual
before_script:
- pip install ansible
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
script:
- ansible-playbook playbooks/deploy.yml -i inventory/production
environment:
name: production
url: https://www.example.com
Jenkins Integration
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
environment {
ANSIBLE_FORCE_COLOR = 'true'
ANSIBLE_HOST_KEY_CHECKING = 'false'
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'https://github.com/example/ansible-playbooks.git'
}
}
stage('Lint') {
steps {
sh 'pip install ansible-lint'
sh 'ansible-lint playbooks/'
}
}
stage('Syntax Check') {
steps {
sh 'ansible-playbook playbooks/site.yml --syntax-check'
}
}
stage('Test') {
steps {
sh '''
ansible-playbook playbooks/site.yml \
--check \
-i inventory/test
'''
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
withCredentials([sshUserPrivateKey(
credentialsId: 'ansible-ssh-key',
keyFileVariable: 'SSH_KEY'
)]) {
sh '''
ansible-playbook playbooks/deploy.yml \
-i inventory/staging \
--private-key=$SSH_KEY
'''
}
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
input {
message "Deploy to production?"
ok "Deploy"
}
steps {
withCredentials([sshUserPrivateKey(
credentialsId: 'ansible-ssh-key',
keyFileVariable: 'SSH_KEY'
)]) {
sh '''
ansible-playbook playbooks/deploy.yml \
-i inventory/production \
--private-key=$SSH_KEY
'''
}
}
}
}
post {
always {
cleanWs()
}
success {
echo 'Deployment successful!'
}
failure {
echo 'Deployment failed!'
mail to: 'ops@example.com',
subject: "Failed Pipeline: ${currentBuild.fullDisplayName}",
body: "Something went wrong with ${env.BUILD_URL}"
}
}
}
Azure Pipelines Integration
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
variables:
ANSIBLE_FORCE_COLOR: 'true'
stages:
- stage: Lint
jobs:
- job: AnsibleLint
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.10'
- script: |
pip install ansible ansible-lint
ansible-lint playbooks/
displayName: 'Run Ansible Lint'
- stage: Test
dependsOn: Lint
jobs:
- job: SyntaxCheck
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.10'
- script: pip install ansible
displayName: 'Install Ansible'
- script: ansible-playbook playbooks/site.yml --syntax-check
displayName: 'Syntax Check'
- job: DryRun
steps:
- script: |
pip install ansible
ansible-playbook playbooks/site.yml --check
displayName: 'Dry Run'
- stage: Deploy
dependsOn: Test
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: Production
environment: production
strategy:
runOnce:
deploy:
steps:
- script: pip install ansible
displayName: 'Install Ansible'
- task: DownloadSecureFile@1
name: sshKey
inputs:
secureFile: 'ansible_ssh_key'
- script: |
chmod 600 $(sshKey.secureFilePath)
ansible-playbook playbooks/deploy.yml \
-i inventory/production \
--private-key=$(sshKey.secureFilePath)
displayName: 'Deploy to Production'
CircleCI Integration
# .circleci/config.yml
version: 2.1
orbs:
python: circleci/python@2.1.1
jobs:
lint:
docker:
- image: cimg/python:3.10
steps:
- checkout
- python/install-packages:
pkg-manager: pip
pip-dependency-file: requirements.txt
- run:
name: Ansible Lint
command: ansible-lint playbooks/
test:
docker:
- image: cimg/python:3.10
steps:
- checkout
- python/install-packages:
pkg-manager: pip
- run:
name: Syntax Check
command: ansible-playbook playbooks/site.yml --syntax-check
- run:
name: Dry Run
command: ansible-playbook playbooks/site.yml --check
deploy:
docker:
- image: cimg/python:3.10
steps:
- checkout
- python/install-packages:
pkg-manager: pip
- add_ssh_keys:
fingerprints:
- "SO:ME:FIN:G:ER:PR:IN:T"
- run:
name: Deploy
command: |
ansible-playbook playbooks/deploy.yml \
-i inventory/production
workflows:
build-test-deploy:
jobs:
- lint
- test:
requires:
- lint
- deploy:
requires:
- test
filters:
branches:
only: main
Best Practices for CI/CD
- Separate Environments: Use different inventories for dev/staging/prod
- Secret Management: Use CI/CD secret stores, never commit credentials
- Idempotence Testing: Run playbooks twice to verify idempotence
- Rollback Strategy: Have automated rollback on failure
- Approval Gates: Require manual approval for production
- Version Pinning: Lock Ansible and role versions
- Fast Feedback: Run lint and syntax checks first
- Parallel Testing: Test multiple scenarios concurrently
Secret Management
Using Ansible Vault in CI/CD
# Store vault password in CI/CD secrets
# GitHub Actions
- name: Deploy with vault
env:
ANSIBLE_VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}
run: |
echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass
ansible-playbook playbooks/deploy.yml \
--vault-password-file .vault_pass
rm .vault_pass
# GitLab CI
deploy:
script:
- echo "$VAULT_PASSWORD" > .vault_pass
- ansible-playbook playbooks/deploy.yml --vault-password-file .vault_pass
after_script:
- rm -f .vault_pass
External Secret Management
# Using HashiCorp Vault
- name: Get secrets from Vault
env:
VAULT_ADDR: ${{ secrets.VAULT_ADDR }}
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
run: |
ansible-playbook playbooks/deploy.yml \
-e "vault_addr=$VAULT_ADDR" \
-e "vault_token=$VAULT_TOKEN"
Testing Strategies in CI/CD
Multi-Stage Testing
stages:
- lint # Static analysis
- syntax # YAML and playbook syntax
- unit # Molecule unit tests
- integration # Full playbook tests
- staging # Deploy to staging
- production # Deploy to production
# Each stage gates the next
# Failures stop the pipeline
Molecule in CI/CD
# .github/workflows/molecule.yml
name: Molecule Tests
on: [push, pull_request]
jobs:
molecule:
runs-on: ubuntu-latest
strategy:
matrix:
distro:
- ubuntu2004
- ubuntu2204
- centos8
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: pip install molecule molecule-plugins[docker]
- name: Run Molecule tests
run: molecule test
env:
MOLECULE_DISTRO: ${{ matrix.distro }}
Deployment Patterns
Blue-Green Deployment
- name: Blue-Green Deployment
hosts: load_balancers
tasks:
- name: Deploy to green environment
ansible.builtin.include_role:
name: deploy
vars:
environment: green
deploy_version: "{{ new_version }}"
- name: Run smoke tests on green
uri:
url: "http://green.internal.example.com/health"
status_code: 200
- name: Switch traffic to green
template:
src: lb_config.j2
dest: /etc/nginx/conf.d/upstream.conf
vars:
active_environment: green
- name: Reload load balancer
systemd:
name: nginx
state: reloaded
Canary Deployment
- name: Canary Deployment
hosts: webservers
serial:
- 1 # Deploy to 1 server (canary)
- 25% # Then 25% of remaining
- 100% # Then rest
max_fail_percentage: 0
tasks:
- name: Deploy new version
include_role:
name: deploy
- name: Monitor metrics
uri:
url: "http://localhost/health"
register: health_check
until: health_check.status == 200
retries: 5
delay: 10
Notifications and Reporting
- name: Send deployment notification
hosts: localhost
tasks:
- name: Notify Slack
slack:
token: "{{ slack_token }}"
msg: |
Deployment Status: {{ deployment_status }}
Environment: {{ environment }}
Version: {{ deploy_version }}
Duration: {{ deployment_duration }}
channel: '#deployments'
- name: Send email
mail:
host: smtp.example.com
to: ops@example.com
subject: "Deployment {{ deployment_status }}"
body: "{{ deployment_summary }}"
Quick Reference
# Common CI/CD commands
ansible-playbook playbook.yml --syntax-check # Syntax validation
ansible-playbook playbook.yml --check # Dry run
ansible-playbook playbook.yml --diff # Show changes
ansible-lint playbooks/ # Lint playbooks
yamllint . # Lint YAML
molecule test # Full Molecule test
# Environment variables for CI/CD
export ANSIBLE_FORCE_COLOR=true
export ANSIBLE_HOST_KEY_CHECKING=false
export ANSIBLE_STDOUT_CALLBACK=yaml
export ANSIBLE_LOAD_CALLBACK_PLUGINS=true
Next Steps
- Learn about Testing & Debugging for comprehensive testing
- Explore Best Practices for maintainable code
- Master Ansible Vault for secrets management
- Try the Playground to experiment with playbooks