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:
