Skip to main content

GitLab Ansible Pipeline: Run and Schedule Ansible Playbooks via GitLab

1304 words·
Ansible GitLab Schedule Pipeline
Table of Contents

GitLab Repository
#

File And Folder Structure
#

# ansible-pipeline
├── ansible.cfg  # Ansite configuration / settings
├── Dockerfiles
│   └── Dockerfile  # Dockerfile for Ansible container
├── .gitlab-ci.yml
├── inventory  # Ansible inventory
├── playbooks  # Some example Ansible playbooks
│   ├── example_playbook_2.yml
│   └── example_playbook.yml
├── README.md
└── roles  # Some example Ansible roles
    ├── example_playbook
    │   └── tasks
    │       └── main.yml
    └── example_playbook_2
        └── tasks
            └── main.yml

CI Pipeline Manifest
#

  • .gitlab-ci.yml
---
variables:
  # Dockerfile build args
  ANSIBLE_VERSION: "12.0.0"
  ANSIBLE_LINT_VERSION: "25.9.1"

  # Which playbook to run
  PLAYBOOK: "example_playbook"

  # Ansible configuration
  ANSIBLE_CONFIG: "${CI_PROJECT_DIR}/ansible.cfg"
  INVENTORY_PATH: "${CI_PROJECT_DIR}/inventory"
  ANSIBLE_HOST_KEY_CHECKING: "false"

  # Ansible CI logs output
  ANSIBLE_FORCE_COLOR: "true"
  ANSIBLE_STDOUT_CALLBACK: "yaml"

  # CI variables
  RUNNER_IMAGE_TAG: "${CI_REGISTRY_IMAGE}/ansible-runner:${CI_COMMIT_TAG}"
  RUNNER_IMAGE_LATEST: "${CI_REGISTRY_IMAGE}/ansible-runner:latest"


stages:
  - build
  - deploy


build:runner-image:
  stage: build
  image: docker:stable
  services:
    - docker:28.4-dind
  variables:
    DOCKER_TLS_CERTDIR: ""
  before_script:
    # Login to GitLab Container Registry using predefined CI/CD variables
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    # Try to pull the latest image for caching; ignore failure if it doesn't exist yet
    - docker pull "${RUNNER_IMAGE_LATEST}" || echo "No existing :latest image — building without cache."
    - >
      docker build
      --cache-from "${RUNNER_IMAGE_LATEST}"
      --pull
      -f Dockerfiles/Dockerfile
      --build-arg ANSIBLE_VERSION="${ANSIBLE_VERSION}"
      --build-arg ANSIBLE_LINT_VERSION="${ANSIBLE_LINT_VERSION}"
      -t "${RUNNER_IMAGE_TAG}"
      -t "${RUNNER_IMAGE_LATEST}"
      .
    - docker push "${RUNNER_IMAGE_TAG}"
    - docker push "${RUNNER_IMAGE_LATEST}"
  rules:
    - if: $CI_COMMIT_TAG


deploy:run-playbook:
  stage: deploy
  image: "${RUNNER_IMAGE_LATEST}"
  needs:
    - job: build:runner-image
      optional: true
  rules:
    # Only run when a PLAYBOOK is set
    - if: '$PLAYBOOK'
  before_script:
    - mkdir -p ~/.ssh
    - echo "$ANSIBLE_SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - ssh-keygen -y -f ~/.ssh/id_rsa >/dev/null
  script:
    - ansible --version
    - ansible-galaxy collection list || true
    # Resolve playbook path from the PLAYBOOK variable
    - export PLAYBOOK_PATH="${CI_PROJECT_DIR}/playbooks/${PLAYBOOK}.yml"
    - '[[ -f "$PLAYBOOK_PATH" ]] || { echo "Playbook not found: $PLAYBOOK_PATH"; exit 2; }'
    # Run Ansible Playbook
    - >
      ansible-playbook
      -i "${INVENTORY_PATH}"
      "${PLAYBOOK_PATH}"

Dockerfile
#

  • Dockerfiles/Dockerfile
FROM debian:12.12-slim

ENV TZ='Europe/Vienna'
# Disable interactive apt prompts
ENV DEBIAN_FRONTEND=noninteractive
# Set path for executables installed with pip
ENV PATH="/root/.local/bin:$PATH"
ENV SSH_DISABLE_HOST_CHECKING=true

# Fetch Ansible versionfrom CI pipeline
ARG ANSIBLE_VERSION
ARG ANSIBLE_LINT_VERSION

# Install dependencies
RUN apt update && \
    apt install --yes \
      apt-transport-https \
      build-essential \
      ca-certificates \
      curl \
      git \
      gnupg \
      gpg \
      jq \
      lsb-release \
      pipx \
      software-properties-common \
      sshpass \
      sudo \
      wget

# Install Ansible with pipx (into /root/.local/bin directory)
RUN pipx install --include-deps ansible==$ANSIBLE_VERSION
RUN pipx install --include-deps ansible-lint==$ANSIBLE_LINT_VERSION
RUN pipx inject ansible netaddr
RUN pipx inject ansible hvac
RUN pipx inject ansible "mitogen==0.3.29"

# Install Ansible Collections
RUN ansible-galaxy collection install community.general && \
    ansible-galaxy collection install ansible.utils && \
    ansible-galaxy collection install serverscom.mitogen

# Disable SSH host-checking
RUN echo "SSH_DISABLE_HOST_CHECKING=$SSH_DISABLE_HOST_CHECKING" >> /etc/environment && \
    echo "export SSH_DISABLE_HOST_CHECKING=$SSH_DISABLE_HOST_CHECKING" >> /root/.bashrc

# Remove downloaded .deb packages cache and metadata
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

Ansible Files
#

Inventory
#

  • inventory
[example_hosts]
192.168.70.105 ansible_user=debian

Ansible Configuration
#

  • ansible.cfg
[defaults]
roles_path = ./roles

Example Playbooks
#

  • playbooks/example_playbook.yml
---
- name: Example playbook
  hosts: example_hosts
  become: true
  gather_facts: false

  roles:
    - example_playbook
  • playbooks/example_playbook_2.yml
---
- name: Example playbook
  hosts: example_hosts
  become: true
  gather_facts: false

  roles:
    - example_playbook_2

Example Roles
#

  • roles/example_playbook/tasks/main.yml
---
- name: Ensure file exists
  ansible.builtin.copy:
    dest: /tmp/example-file-1
    content: |
      some text   
    owner: debian
    group: debian
  • roles/example_playbook_2/tasks/main.yml
---
- name: Ensure file exists
  ansible.builtin.copy:
    dest: /tmp/example-file-2
    content: |
      some more text   
    owner: debian
    group: debian



Setup
#

Ansible and Ansible-Lint Version
#

Find the latest or available Anible and Ansible-Lint versions:

# List available Ansible verions
pip index versions ansible

# Shell output:
Available versions: 12.0.0, 11.10.0, 11.9.0, 11.8.0, 11.7.0, 11.6.0, 11.5.0, 11.4.0, 11.3.0, 11.2.0, 11.1.0, 11.0.0, 10.7.0, 10.6.0, 10.5.0, 10.4.0, 10.3.0, 10.2.0, 10.1.0, 10.0.1, 9.13.0, 9.12.0, 9.11.0, 9.10.0, 9.9.0, 9.8.0, 9.7.0, 9.6.1, 9.5.1, 9.4.0, 9.3.0, 9.2.0, 9.1.0, 9.0.1, 8.7.0, 8.6.1, 8.6.0, 8.5.0, 8.4.0, 8.3.0, 8.2.0, 8.1.0, 8.0.0, 7.7.0, 7.6.0, 7.5.0, 7.4.0, 7.3.0, 7.2.0, 7.1.0, 7.0.0, 6.7.0, 6.6.0, 6.5.0, 6.4.0, 6.3.0, 6.2.0, 6.1.0, 6.0.0, 5.10.0, 5.9.0, 5.8.0, 5.7.1, 5.7.0, 5.6.0, 5.5.0, 5.4.0, 5.3.0, 5.2.0, 5.1.0, 5.0.1, 4.10.0, 4.9.0, 4.8.0, 4.7.0, 4.6.0, 4.5.0, 4.4.0, 4.3.0, 4.2.0, 4.1.0, 4.0.0, 3.4.0, 3.3.0, 3.2.0, 3.1.0, 3.0.0, 2.10.7, 2.10.6, 2.10.5, 2.10.4, 2.10.3, 2.10.2, 2.10.1, 2.10.0, 2.9.27, 2.9.26, 2.9.25, 2.9.24, 2.9.23, 2.9.22, 2.9.21, 2.9.20, 2.9.19, 2.9.18, 2.9.17, 2.9.16, 2.9.15, 2.9.14, 2.9.13, 2.9.12, 2.9.11, 2.9.10, 2.9.9, 2.9.8, 2.9.7, 2.9.6, 2.9.5, 2.9.4, 2.9.3, 2.9.2, 2.9.1, 2.9.0, 2.8.20, 2.8.19, 2.8.18, 2.8.17, 2.8.16, 2.8.15, 2.8.14, 2.8.13, 2.8.12, 2.8.11, 2.8.10, 2.8.9, 2.8.8, 2.8.7, 2.8.6, 2.8.5, 2.8.4, 2.8.3, 2.8.2, 2.8.1, 2.8.0, 2.7.18, 2.7.17, 2.7.16, 2.7.15, 2.7.14, 2.7.13, 2.7.12, 2.7.11, 2.7.10, 2.7.9, 2.7.8, 2.7.7, 2.7.6, 2.7.5, 2.7.4, 2.7.3, 2.7.2, 2.7.1, 2.7.0, 2.6.20, 2.6.19, 2.6.18, 2.6.17, 2.6.16, 2.6.15, 2.6.14, 2.6.13, 2.6.12, 2.6.11, 2.6.10, 2.6.9, 2.6.8, 2.6.7, 2.6.6, 2.6.5, 2.6.4, 2.6.3, 2.6.2, 2.6.1, 2.6.0, 2.5.15, 2.5.14, 2.5.13, 2.5.12, 2.5.11, 2.5.10, 2.5.9, 2.5.8, 2.5.7, 2.5.6, 2.5.5, 2.5.4, 2.5.3, 2.5.2, 2.5.1, 2.5.0, 2.4.6.0, 2.4.5.0, 2.4.4.0, 2.4.3.0, 2.4.2.0, 2.4.1.0, 2.4.0.0, 2.3.3.0, 2.3.2.0, 2.3.1.0, 2.3.0.0, 2.2.3.0, 2.2.2.0, 2.2.1.0, 2.2.0.0, 2.1.6.0, 2.1.5.0, 2.1.4.0, 2.1.3.0, 2.1.2.0, 2.1.1.0, 2.1.0.0, 2.0.2.0, 2.0.1.0, 2.0.0.2, 2.0.0.1, 2.0.0.0, 1.9.6, 1.9.5, 1.9.4, 1.9.3, 1.9.2, 1.9.1, 1.9.0.1, 1.8.4, 1.8.3, 1.8.2, 1.8.1, 1.8, 1.7.2, 1.7.1, 1.7, 1.6.10, 1.6.9, 1.6.8, 1.6.7, 1.6.6, 1.6.5, 1.6.4, 1.6.3, 1.6.2, 1.6.1, 1.6, 1.5.5, 1.5.4, 1.5.3, 1.5.2, 1.5.1, 1.5, 1.4.5, 1.4.4, 1.4.3, 1.4.2, 1.4.1, 1.4, 1.3.4, 1.3.3, 1.3.2, 1.3.1, 1.3.0, 1.2.3, 1.2.2, 1.2.1, 1.2, 1.1, 1.0
# List available Ansible verions
pip index versions ansible-lint

# Shell output:
Available versions: 25.9.1, 25.9.0, 25.8.2, 25.8.1, 25.8.0, 25.7.0, 25.6.1, 25.6.0, 25.5.0, 25.4.0, 25.2.1, 25.2.0, 25.1.3, 25.1.2, 25.1.1, 25.1.0, 24.12.2, 24.10.0, 24.9.2, 24.9.1, 24.9.0, 24.7.0, 24.6.1, 24.6.0, 24.5.0, 24.2.3, 24.2.2, 24.2.1, 24.2.0, 6.22.2, 6.22.1, 6.22.0, 6.21.1, 6.21.0, 6.20.3, 6.20.2, 6.20.1, 6.20.0, 6.19.0, 6.18.0, 6.17.2, 6.17.1, 6.17.0, 6.16.2, 6.16.1, 6.16.0, 6.15.0, 6.14.6, 6.14.4, 6.14.3, 6.14.2, 6.14.1, 6.14.0, 6.13.1, 6.13.0, 6.12.2, 6.12.1, 6.12.0, 6.11.0, 6.10.2, 6.10.1, 6.10.0, 6.9.1, 6.9.0, 6.8.7, 6.8.6, 6.8.5, 6.8.4, 6.8.3, 6.8.2, 6.8.1, 6.8.0, 6.7.0, 6.6.1, 6.6.0, 6.5.2, 6.5.1, 6.5.0, 6.4.0, 6.3.0, 6.2.2, 6.2.1, 6.2.0, 6.1.0, 6.0.2, 6.0.1, 6.0.0, 5.4.0, 5.3.2, 5.3.1, 5.3.0, 5.2.1, 5.2.0, 5.1.3, 5.1.2, 5.1.1, 5.0.12, 5.0.11, 5.0.10, 5.0.9, 5.0.8, 5.0.7, 5.0.6, 5.0.5, 5.0.4, 5.0.3, 5.0.2, 5.0.1, 5.0.0, 4.3.7, 4.3.6, 4.3.5, 4.3.4, 4.3.3, 4.3.2, 4.3.1, 4.3.0, 4.2.0, 4.1.0, 4.0.1, 4.0.0, 3.5.1, 3.5.0, 3.4.23, 3.4.22, 3.4.21, 3.4.20, 3.4.19, 3.4.18, 3.4.17, 3.4.16, 3.4.15, 3.4.13, 3.4.12, 3.4.11, 3.4.10, 3.4.9, 3.4.8, 3.4.7, 3.4.6, 3.4.5, 3.4.4, 3.4.3, 3.4.1, 3.4.0, 3.3.3, 3.3.2, 3.3.0, 3.2.5, 3.2.4, 3.2.3, 3.2.2, 3.2.1, 3.2.0, 3.1.3, 3.1.2, 3.1.1, 3.1.0, 3.0.1, 3.0.0, 2.7.1, 2.7.0, 2.6.2, 2.6.1, 2.5.0, 2.4.2, 2.4.1, 2.4.0, 2.3.9, 2.3.8, 2.3.6, 2.3.5, 2.3.3, 2.3.2, 2.3.1, 2.3.0, 2.2.0, 2.1.3, 2.1.0, 2.0.3, 2.0.1, 1.0.4, 1.0.3, 1.0.2, 1.0.1, 1.0.0

SSH Key Pair
#

Create SSH Key Pair
#

Create a SSH key pair, the private key is used within the Ansible container, the public key is used for the Ansible hosts:

# Create SSH key pair
ssh-keygen -t rsa -b 4096 -C "ansible" -f ~/.ssh/ansible

Public Key
#

# Copy the publich SSH key to the Ansible hosts: Manually add it to the .ssh/authorized_keys file
cat ~/.ssh/ansible.pub

# Copy the publich SSH key to the Ansible hosts
ssh-copy-id -i ~/.ssh/ansible.pub debian@192.168.70.105

Private Key
#

# Encode private SSH key: For GitLab variables type file
base64 -w0 ~/.ssh/ansible

# Shell output:
LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUNGd0FBQUFkemMyZ3RjbgpOaEFBQUFBd0VBQVFBQUFnRUF1VHZYTTZZOGk5Z2x4b3Z6clNhRXg3RjdGVHUyOGlGMm92SnZnbzNsWkM5eUhjdmVSVlFvCitDN0xkbWxvRXU4UEh1ZUpDOTV4NHB5K2t3QnArSmRtbllMbkZSNFJkTmJmTGg1a015NFdPVUZWV1hNWFovSVN3b1A2b1AKZjdTaDlQZHI5K1kvVUlTMC9YMHFZKytlN0UzdmtERUpMQ25VS3RJdUE4Z1hyYlhJejZ2M08rN2NEUFRjOXNGeVJTb2k1RQpWSklxYWpDOEFhQnQyQXU5aGY1bU5zbkRaelltOHcrcUZCSlduNkFsZ3FiUG9XcGEwMXRlUWFlRWZ1Vm43ai9STWZ6aFRHClQrVUJPOTcybW1ScllPSUN2TVplbjF1SVRQU0xRNE9KT2V1TW9KZjJUSHgvNm1KNWg3MVNjMFRUQ3dCWll6a3orcXVwSnQKeXVQTjUzMnVaUHdBVmRndk5yak44N01KRHFPNzNJMC9BNVo4Q0VwalNacFJJWUVjUXhNMldtVGJ3MkQ1WDI2RzNZVUlq...

Add the encoded private key to the GitLab CI variables:

  • Go to: “Settings” > “CI/CD > “Variables”

  • Click “Add Variable”

  • Select “Type”: “Variable”

  • Select “Visibility”: “Masked”

  • Define “Key”: “ANSIBLE_SSH_PRIVATE_KEY”

  • Define “Value”


Test SSH Connection
#

# Test SSH connection to Ansible host
ssh -i ~/.ssh/ansible debian@192.168.70.105



Run Ansible Playbook via GitLab
#

Push Tag
#

# Create local tag
git tag 0.1.0

# Push tag to remote repository
git push origin 0.1.0

Run Playbook with specific Ansible Playbook
#

  • Go to: “Build” > “Pipelines” > “New pipeline”

  • Select “Run for branch name or tag”: “main”

Define the following variable:

# Input variable key: 
PLAYBOOK

# Input variable value:
example_playbook_2
  • Click “Run pipeline”

Schedule Pipeline for specific Ansible Playbook
#

  • Go to: “Build” > “Pipeline schedules” > “Create a new pipeline schedule”
  • Once scheduled, the pipeline can be triggered manually: