CI/CD with GitLab CI¶
- In this lab we integrate Ansible with GitLab CI/CD to run playbooks automatically when code is pushed or merged.
- GitLab CI offers tight integration with GitLab repositories, including environment management and protected branches.
What will we learn?¶
- Creating
.gitlab-ci.ymlpipelines for Ansible - Storing secrets as GitLab CI/CD variables
- Multi-stage pipelines (lint → test → deploy)
- Using GitLab Environments for deployment tracking
Prerequisites¶
- Complete Lab 024 in order to have working lint knowledge and a linted playbook.
01. Basic .gitlab-ci.yml¶
# .gitlab-ci.yml
image: python:3.12-slim
variables:
ANSIBLE_FORCE_COLOR: "1"
ANSIBLE_HOST_KEY_CHECKING: "false"
before_script:
- pip install ansible ansible-lint
- ansible-galaxy collection install -r requirements.yml
- ansible --version
stages:
- lint
- test
- deploy
lint:
stage: lint
script:
- ansible-lint site.yml
- ansible-playbook site.yml --syntax-check -i "localhost,"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
test:
stage: test
script:
- ansible-playbook site.yml --check -i inventory/staging/
environment:
name: staging
rules:
- if: $CI_COMMIT_BRANCH == "develop"
deploy-staging:
stage: deploy
script:
- echo "$VAULT_PASSWORD" > /tmp/vault_pass
- chmod 600 /tmp/vault_pass
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ansible-playbook site.yml
-i inventory/staging/
--vault-password-file /tmp/vault_pass
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == "develop"
after_script:
- rm -f /tmp/vault_pass ~/.ssh/id_rsa
deploy-production:
stage: deploy
script:
- echo "$VAULT_PASSWORD_PROD" > /tmp/vault_pass
- chmod 600 /tmp/vault_pass
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY_PROD" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ansible-playbook site.yml
-i inventory/production/
--vault-password-file /tmp/vault_pass
environment:
name: production
url: https://example.com
when: manual # Require manual approval
rules:
- if: $CI_COMMIT_BRANCH == "main"
after_script:
- rm -f /tmp/vault_pass ~/.ssh/id_rsa
02. Using a Custom Docker Image¶
# Dockerfile.ansible
FROM python:3.12-slim
RUN pip install ansible ansible-lint \
&& ansible-galaxy collection install community.general community.docker
WORKDIR /ansible
# .gitlab-ci.yml with custom image
image: registry.gitlab.com/myorg/ansible-runner:latest
# Or build it in CI
build-runner:
stage: .pre
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE/ansible-runner:$CI_COMMIT_SHA -f Dockerfile.ansible .
- docker push $CI_REGISTRY_IMAGE/ansible-runner:$CI_COMMIT_SHA
rules:
- changes:
- Dockerfile.ansible
- requirements.yml
03. Multi-Stage Pipeline with Molecule¶
# .gitlab-ci.yml with Molecule testing
stages:
- lint
- molecule-test
- integration-test
- deploy
variables:
DOCKER_DRIVER: overlay2
lint:
stage: lint
image: python:3.12-slim
script:
- pip install ansible-lint
- ansible-lint
rules:
- if: $CI_MERGE_REQUEST_ID
molecule-default:
stage: molecule-test
image: python:3.12-slim
services:
- docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2375
before_script:
- pip install molecule[docker] ansible
script:
- cd roles/nginx
- molecule test
rules:
- if: $CI_COMMIT_BRANCH
changes:
- roles/nginx/**/*
integration-test:
stage: integration-test
script:
- ansible-playbook tests/integration.yml -i inventory/test/
environment:
name: test
rules:
- if: $CI_COMMIT_BRANCH == "develop"
deploy:
stage: deploy
script:
- ansible-playbook site.yml -i inventory/production/
environment:
name: production
when: manual
rules:
- if: $CI_COMMIT_BRANCH == "main"
04. GitLab CI/CD Variables¶
Set these in Project → Settings → CI/CD → Variables:
| Variable | Type | Description |
|---|---|---|
VAULT_PASSWORD | Variable | Ansible Vault password (masked) |
SSH_PRIVATE_KEY | Variable | SSH private key (masked, base64) |
STAGING_HOST | Variable | Staging server hostname/IP |
PROD_HOST | Variable | Production server hostname/IP |
# Use in CI scripts
before_script:
- echo "$VAULT_PASSWORD" > /tmp/.vault_pass
- chmod 600 /tmp/.vault_pass
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
05. GitLab Environments¶
# Track deployments in GitLab's Environments UI
deploy-staging:
script:
- ansible-playbook site.yml -i inventory/staging/
environment:
name: staging
url: https://staging.example.com
on_stop: stop-staging # Reference the stop job
stop-staging:
script:
- ansible-playbook teardown.yml -i inventory/staging/
environment:
name: staging
action: stop
when: manual
06. ansible-runner for Better Integration¶
# Using ansible-runner for better GitLab integration
deploy-with-runner:
image: quay.io/ansible/ansible-runner:latest
script:
- ansible-runner run /runner --playbook site.yml
artifacts:
paths:
- /runner/artifacts/
reports:
junit: /runner/artifacts/*/job_events/*-runner_on_ok.json
expire_in: 1 week

07. Hands-on¶
- Create a
.gitlab-ci.ymlwith four stages:lint,syntax-check,dry-run, anddeploy.
??? success “Solution”
cat > .gitlab-ci.yml << 'EOF'
image: python:3.12-slim
variables:
ANSIBLE_FORCE_COLOR: "1"
ANSIBLE_HOST_KEY_CHECKING: "false"
stages:
- lint
- syntax-check
- dry-run
- deploy
before_script:
- pip install ansible ansible-lint --quiet
- ansible --version
lint:
stage: lint
script:
- ansible-lint site.yml
syntax-check:
stage: syntax-check
script:
- ansible-playbook site.yml --syntax-check -i "localhost,"
dry-run:
stage: dry-run
script:
- ansible-playbook site.yml --check -i inventory/
rules:
- if: $CI_COMMIT_BRANCH != "main"
deploy:
stage: deploy
script:
- ansible-playbook site.yml -i inventory/
environment:
name: production
when: manual
rules:
- if: $CI_COMMIT_BRANCH == "main"
EOF
- Simulate each pipeline stage locally before pushing to GitLab.
??? success “Solution”
# Install tools
pip install ansible ansible-lint
# Lint stage
ansible-lint site.yml
# Syntax-check stage
ansible-playbook site.yml --syntax-check -i "localhost,"
# Dry-run stage
ansible-playbook site.yml --check -i inventory/
- Commit and push the pipeline file to trigger it in GitLab.
??? success “Solution”
git add .gitlab-ci.yml
git commit -m "Add GitLab CI pipeline for Ansible"
git push origin develop
### Output
# Open GitLab → CI/CD → Pipelines to view the running pipeline
- Add a GitLab CI job that runs
ansible-lintand fails the pipeline if violations are found.
??? success “Solution”
docker exec ansible-controller sh -c "cd /labs-scripts && cat >> .gitlab-ci.yml << 'EOF'
lint:
stage: test
image: pipelinecomponents/ansible-lint:latest
script:
- ansible-lint --profile production *.yml
rules:
- if: '\$CI_PIPELINE_SOURCE == \"merge_request_event\"'
- if: '\$CI_COMMIT_BRANCH == \$CI_DEFAULT_BRANCH'
EOF"
Expected: The lint job runs on MR and main branch, rejecting playbooks that violate best practices.
- Create a multi-environment GitLab CI pipeline with
staging→productiongates using manual approval.
??? success “Solution”
docker exec ansible-controller sh -c "cd /labs-scripts && cat > .gitlab-ci-multi-env.yml << 'EOF'
stages:
- lint
- deploy-staging
- deploy-production
variables:
ANSIBLE_FORCE_COLOR: \"true\"
lint:
stage: lint
image: cytopia/ansible:latest
script:
- ansible-playbook site.yml --syntax-check -i inventory/staging/
deploy-staging:
stage: deploy-staging
image: cytopia/ansible:latest
environment:
name: staging
url: https://staging.example.com
before_script:
- ansible-galaxy install -r requirements.yml || true
script:
- ansible-playbook site.yml -i inventory/staging/ --diff
rules:
- if: '\$CI_COMMIT_BRANCH == \$CI_DEFAULT_BRANCH'
deploy-production:
stage: deploy-production
image: cytopia/ansible:latest
environment:
name: production
url: https://example.com
script:
- ansible-playbook site.yml -i inventory/production/ --diff
rules:
- if: '\$CI_COMMIT_BRANCH == \$CI_DEFAULT_BRANCH'
when: manual # Requires manual approval
allow_failure: false
EOF"
08. Summary¶
.gitlab-ci.ymldefines stages: lint → test → deploy in a GitLab pipeline- Store secrets in GitLab CI/CD Variables (masked and protected)
- Use
when: manualfor deployment stages to require human approval - GitLab Environments track deployment history and provide rollback options
- Use a custom Docker image with Ansible pre-installed for faster pipelines
ansible-runnerprovides better artifact management and reporting