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.

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.