Ansible + GitLab CI/CD — Automate Deployments with Pipelines
Introduction
GitLab CI/CD is one of the most popular platforms for automating software delivery. Combining GitLab pipelines with Ansible creates a powerful deployment workflow: Git push triggers a pipeline that runs Ansible playbooks to configure infrastructure, deploy applications, and verify results.
This guide covers the complete integration — from basic pipeline setup to advanced patterns with Ansible Vault secrets, rolling deployments, and multi-environment promotion.
Basic Pipeline Setup
.gitlab-ci.yml
```yaml
stages:
- lint
- test
- deploy-staging
- verify-staging
- deploy-production
variables: ANSIBLE_HOST_KEY_CHECKING: "False" ANSIBLE_FORCE_COLOR: "True" PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
cache: paths: - .pip-cache/ - venv/
Base job template
.ansible-base: image: python:3.12-slim before_script: - python -m venv venv - source venv/bin/activate - pip install ansible-core ansible-lint pytest-testinfra - ansible --version - ansible-galaxy install -r requirements.yml
lint: extends: .ansible-base stage: lint script: - ansible-lint playbooks/ - ansible-playbook playbooks/site.yml --syntax-check
test: extends: .ansible-base stage: test script: - ansible-playbook playbooks/site.yml --check --diff -i inventories/staging/hosts
deploy-staging: extends: .ansible-base stage: deploy-staging script: - ansible-playbook playbooks/site.yml -i inventories/staging/hosts environment: name: staging url: https://app.example.com only: - main
verify-staging: extends: .ansible-base stage: verify-staging script: - pytest tests/test_staging.py --hosts='ansible://staging' -v only: - main
deploy-production: extends: .ansible-base stage: deploy-production script: - ansible-playbook playbooks/site.yml -i inventories/production/hosts environment: name: production url: https://www.example.com when: manual only: - main ```
Managing SSH Keys
Using GitLab CI/CD Variables
```yaml
Store SSH private key as CI/CD variable (type: File)
Settings → CI/CD → Variables → SSH_PRIVATE_KEY
deploy-staging: extends: .ansible-base before_script: - !reference [.ansible-base, before_script] - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh - chmod 700 ~/.ssh - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts script: - ansible-playbook playbooks/site.yml -i inventories/staging/hosts ```
Ansible Vault Integration
Using GitLab Variables for Vault Password
```yaml
Store vault password as CI/CD variable: ANSIBLE_VAULT_PASSWORD
Settings → CI/CD → Variables → ANSIBLE_VAULT_PASSWORD (masked)
.ansible-base: before_script: - python -m venv venv - source venv/bin/activate - pip install ansible-core - echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass - chmod 600 .vault_pass - export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass after_script: - rm -f .vault_pass ```
Rolling Deployment Pattern
```yaml deploy-production: extends: .ansible-base stage: deploy-production script: # Deploy in batches with health checks - > ansible-playbook playbooks/rolling-deploy.yml -i inventories/production/hosts -e "app_version=$CI_COMMIT_SHORT_SHA" -e "serial_count=2" --vault-password-file .vault_pass environment: name: production when: manual allow_failure: false ```
```yaml
playbooks/rolling-deploy.yml
-
name: Rolling deployment hosts: webservers serial: "{{ serial_count | default(1) }}" max_fail_percentage: 0 pre_tasks:
-
name: Remove from load balancer ansible.builtin.uri: url: "http://{{ lb_host }}/api/remove/{{ inventory_hostname }}" method: POST
-
name: Wait for connections to drain ansible.builtin.wait_for: timeout: 30
roles:
- deploy-app
post_tasks:
-
name: Health check ansible.builtin.uri: url: "http://{{ inventory_hostname }}:{{ app_port }}/health" status_code: 200 retries: 10 delay: 5
-
name: Add back to load balancer ansible.builtin.uri: url: "http://{{ lb_host }}/api/add/{{ inventory_hostname }}" method: POST ```
-
Multi-Environment Promotion
```yaml stages:
- build
- deploy-dev
- deploy-staging
- deploy-production
deploy-dev: stage: deploy-dev script: - ansible-playbook site.yml -i inventories/dev/hosts -e "env=dev" environment: name: development only: - merge_requests
deploy-staging: stage: deploy-staging script: - ansible-playbook site.yml -i inventories/staging/hosts -e "env=staging" environment: name: staging only: - main
deploy-production: stage: deploy-production script: - ansible-playbook site.yml -i inventories/production/hosts -e "env=production" environment: name: production when: manual only: - tags ```
Project Structure
``` my-infrastructure/ ├── .gitlab-ci.yml ├── ansible.cfg ├── requirements.yml ├── inventories/ │ ├── dev/ │ │ ├── hosts │ │ └── group_vars/ │ ├── staging/ │ │ ├── hosts │ │ └── group_vars/ │ └── production/ │ ├── hosts │ └── group_vars/ ├── playbooks/ │ ├── site.yml │ ├── deploy.yml │ └── rollback.yml ├── roles/ │ ├── common/ │ ├── webserver/ │ └── database/ └── tests/ ├── test_webserver.py └── test_database.py ```
Notifications
```yaml deploy-production: stage: deploy-production script: - ansible-playbook site.yml -i inventories/production/hosts - > curl -X POST "$SLACK_WEBHOOK_URL" -H 'Content-Type: application/json' -d "{"text":"✅ Production deployed: $CI_COMMIT_SHORT_SHA by $GITLAB_USER_NAME"}" after_script: - > if [ "$CI_JOB_STATUS" == "failed" ]; then curl -X POST "$SLACK_WEBHOOK_URL" -H 'Content-Type: application/json' -d "{"text":"❌ Production deploy FAILED: $CI_COMMIT_SHORT_SHA"}" fi ```
Troubleshooting
"Host key verification failed": ```yaml variables: ANSIBLE_HOST_KEY_CHECKING: "False" ```
"Permission denied (publickey)": Make sure SSH_PRIVATE_KEY CI/CD variable has the correct key and is of type "File".
Slow pip installs: Use pip caching and a pre-built Docker image with Ansible installed.
Related Articles
- Ansible CI/CD with GitHub Actions
- Ansible Vault Encrypt Decrypt
- Ansible Lint Best Practices
- Ansible Rolling Updates
- Ansible Molecule Testing
Conclusion
GitLab CI/CD + Ansible creates a complete infrastructure delivery pipeline. Use stages for lint → test → deploy → verify, manage secrets with Vault + CI/CD variables, and implement rolling deployments with health checks. The manual gate for production ensures human approval while automating everything else.