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

  1. Separate Environments: Use different inventories for dev/staging/prod
  2. Secret Management: Use CI/CD secret stores, never commit credentials
  3. Idempotence Testing: Run playbooks twice to verify idempotence
  4. Rollback Strategy: Have automated rollback on failure
  5. Approval Gates: Require manual approval for production
  6. Version Pinning: Lock Ansible and role versions
  7. Fast Feedback: Run lint and syntax checks first
  8. 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