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

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

Related Articles

Ansible GitOps with AAPAnsible Execution EnvironmentsAnsible Automation Platform RBACAnsible vs Jenkins: Can Ansible Replace CI/CD?Ansible Collection CI Requirement 2026ACTION REQUIRED: Collections Must Add CI Test RunsAutomate Collection Testing with GitHub ActionsInstall and Configure Molecule for Role Testing

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home