Ansible CI/CD Pipeline Integration

Manual playbook execution — running ansible-playbook from your laptop — works for learning and one-off tasks, but it is not how professional teams operate. In production, playbooks run automatically: triggered by code commits, pull request merges, scheduled maintenance windows, or monitoring alerts. This lesson teaches you to integrate Ansible into two of the most widely used CI/CD platforms: GitHub Actions and Jenkins.

Why Automate Playbook Execution

Automating Ansible execution through a CI/CD pipeline delivers several critical benefits:

  • Consistency: Every deployment follows the exact same process regardless of which engineer triggered it
  • Auditability: The CI/CD system logs every deployment with timestamps, triggered-by identity, and full output
  • Gate enforcement: Linting, syntax checks, and tests run automatically before any playbook touches production
  • Reduced human error: No one can accidentally skip the vault password, forget to pull latest changes, or run against the wrong inventory

GitHub Actions: Deploying with Ansible on Push

GitHub Actions workflows are defined in .github/workflows/ YAML files in your repository.

Create .github/workflows/deploy.yml:

name: Deploy LAMP Stack

on:
  push:
    branches: [main]
  workflow_dispatch:    # Allow manual triggers from GitHub UI

jobs:
  lint:
    name: Lint and Syntax Check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install Ansible and tools
        run: |
          pip install ansible ansible-lint yamllint

      - name: Run yamllint
        run: yamllint .

      - name: Run ansible-lint
        run: ansible-lint

      - name: Syntax check all playbooks
        run: ansible-playbook site.yml --syntax-check -i inventory/

  deploy:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: lint    # Only deploy if lint passes
    environment: production    # GitHub environment with required reviewers
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python and Ansible
        run: pip install ansible boto3

      - name: Install Galaxy requirements
        run: ansible-galaxy install -r requirements.yml

      - name: Write Vault password
        run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > .vault_pass
        # File is created only in the runner's ephemeral environment

      - name: Write SSH private key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/ansible_ed25519
          chmod 600 ~/.ssh/ansible_ed25519

      - name: Run deployment playbook
        run: |
          ansible-playbook site.yml \
            -i inventory/ \
            --vault-password-file .vault_pass \
            --private-key ~/.ssh/ansible_ed25519 \
            -v

      - name: Clean up secrets
        if: always()    # Run even if deployment fails
        run: |
          rm -f .vault_pass ~/.ssh/ansible_ed25519

Storing Secrets in GitHub Actions

Never put passwords or private keys in your workflow file or repository. Use GitHub's encrypted Secrets:

  1. Go to your GitHub repository → Settings → Secrets and variables → Actions
  2. Create ANSIBLE_VAULT_PASSWORD with your Vault password value
  3. Create SSH_PRIVATE_KEY with the contents of your private key file

These secrets are injected as environment variables during the workflow run and are never exposed in logs.

Jenkins Pipeline: Declarative Jenkinsfile

Create a Jenkinsfile in your repository root:

pipeline {
    agent {
        docker {
            image 'python:3.11-slim'
            args '-v /home/jenkins/.ssh:/root/.ssh:ro'
        }
    }

    environment {
        ANSIBLE_HOST_KEY_CHECKING = 'False'
        ANSIBLE_FORCE_COLOR = 'true'
    }

    stages {
        stage('Setup') {
            steps {
                sh 'pip install ansible ansible-lint'
                sh 'ansible-galaxy install -r requirements.yml'
            }
        }

        stage('Lint') {
            steps {
                sh 'ansible-lint'
                sh 'ansible-playbook site.yml --syntax-check -i inventory/'
            }
        }

        stage('Deploy to Staging') {
            steps {
                withCredentials([string(credentialsId: 'vault-password', variable: 'VAULT_PASS')]) {
                    sh '''
                        echo "$VAULT_PASS" > .vault_pass
                        ansible-playbook site.yml \
                            -i inventory/staging/ \
                            --vault-password-file .vault_pass
                        rm -f .vault_pass
                    '''
                }
            }
        }

        stage('Smoke Test') {
            steps {
                sh 'ansible-playbook tests/smoke-test.yml -i inventory/staging/'
            }
        }

        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            input {
                message "Deploy to production?"
                ok "Yes, deploy"
                submitter "admin,devlead"
            }
            steps {
                withCredentials([string(credentialsId: 'vault-password-prod', variable: 'VAULT_PASS')]) {
                    sh '''
                        echo "$VAULT_PASS" > .vault_pass
                        ansible-playbook site.yml \
                            -i inventory/production/ \
                            --vault-password-file .vault_pass \
                            -v
                        rm -f .vault_pass
                    '''
                }
            }
        }
    }

    post {
        always {
            sh 'rm -f .vault_pass'
            cleanWs()
        }
        failure {
            emailext(
                subject: "Deployment FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                body: "Check Jenkins: ${env.BUILD_URL}",
                to: 'devops@example.com'
            )
        }
    }
}

Key CI/CD Pipeline Patterns

Lint before deploy. Always run ansible-lint and --syntax-check as the first stage. Failed linting blocks the deployment, preventing obviously broken playbooks from touching infrastructure.

Staging before production. Deploy to a staging environment first, run automated tests, and only proceed to production after staging passes. The Jenkins example above uses an input step to require human approval before production — appropriate for sensitive deployments.

Ephemeral secrets. Write secrets to temporary files only during the pipeline run and explicitly delete them in a post: always block or cleanup step. This limits the window during which secrets exist on the CI runner's filesystem.

Immutable artifacts. Tag your Ansible code at the same time as your application code. Deploy the same tagged version through staging and production to guarantee consistency.

Try This: Add Ansible to a GitHub Repository

Push your LAMP stack project from Topic 28 to a GitHub repository. Create a GitHub Actions workflow that runs ansible-lint and --syntax-check on every pull request. Add your Vault password as a GitHub Secret. Make a deliberate syntax error in a playbook, open a pull request, and confirm the workflow fails and blocks the merge. Then fix the error and watch the workflow pass. This end-to-end experience with CI/CD-gated Ansible deployments is a marketable skill.

Summary

Integrating Ansible with CI/CD platforms converts manual playbook execution into a consistent, auditable, gated automation workflow. GitHub Actions workflows defined in YAML run on code pushes and use encrypted Secrets for credentials. Jenkins Declarative Pipelines support multi-stage deployments with approval gates. Both platforms follow the same core pattern: lint, syntax-check, deploy to staging, test, deploy to production. Ephemeral secret handling is critical for security in all CI/CD Ansible integrations.

Leave a Comment