AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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

  1. Lint before deploy — ansible-lint and yamllint catch issues early
  2. Test with Molecule — Unit test roles before integration
  3. Environment gates — Manual approval for production deployments
  4. Rolling deploys with serial — Never deploy to 100% at once
  5. Health checks — Verify each batch before proceeding
  6. Vault for secrets — CI/CD secrets stored in platform's secret manager, injected at runtime
  7. Trigger AAP for production — CI handles build/test; AAP handles production deployment with RBAC and audit
  8. Pin ansible-core version — Same version in CI and production for consistency
  9. 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.

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home