50 Ansible in CI/CD-Pipelines

50.1 Grundlagen von CI/CD im Kontext von Ansible

50.1.1 Zielsetzung: Automatisierung von Tests, Validierung und Rollout von Playbooks

Ansible-basierte Infrastrukturen erfordern systematische Validierung und kontrollierte Deployment-Prozesse. CI/CD-Pipelines automatisieren die Qualitätssicherung von Playbooks, Rollen und Collections durch statische Analyse, Syntax-Prüfung und funktionale Tests. Die kontinuierliche Integration verhindert fehlerhafte Konfigurationen in Produktivumgebungen und standardisiert Deployment-Workflows.

50.1.2 Typische CI/CD-Tools

GitLab CI: Integrierte Pipeline-Funktionalität mit YAML-basierter Konfiguration und nativer Container-Unterstützung.

GitHub Actions: Workflow-Engine mit umfangreichem Marketplace für vorgefertigte Actions und matrix-basierte Teststrategien.

Jenkins: Flexible Pipeline-Orchestrierung mit umfangreichen Plugin-Ökosystem und Pipeline-as-Code über Jenkinsfile.

Alle Plattformen unterstützen parallele Job-Ausführung, Artefakt-Management und Integration mit externen Tools für Ansible-Validierung.

50.2 Integration von Ansible in CI/CD-Workflows

50.2.1 Platzierung von Ansible-Aufgaben im Build-/Deploy-Prozess

Ansible-Tasks werden typischerweise in mehreren Pipeline-Phasen eingesetzt:

  1. Validate: Syntax-Checks und Linting
  2. Test: Funktionale Tests mit Molecule oder Test-Umgebungen
  3. Build: Packaging von Rollen und Collections
  4. Deploy: Automatisierte Rollouts in definierte Umgebungen

50.2.2 Beispiel: Deployment einer Rolle über GitLab CI

# .gitlab-ci.yml
stages:
  - validate
  - test
  - deploy

variables:
  ANSIBLE_HOST_KEY_CHECKING: "false"
  ANSIBLE_STDOUT_CALLBACK: yaml

before_script:
  - apk add --no-cache python3 py3-pip openssh-client
  - pip3 install ansible ansible-lint yamllint molecule[docker]
  - eval $(ssh-agent -s)
  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -

validate:syntax:
  stage: validate
  script:
    - ansible-playbook --syntax-check site.yml
    - ansible-lint .
    - yamllint .
  only:
    - merge_requests
    - main

test:molecule:
  stage: test
  services:
    - docker:dind
  variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - cd roles/webserver
    - molecule test
  only:
    - merge_requests
    - main

deploy:staging:
  stage: deploy
  script:
    - ansible-playbook -i inventories/staging site.yml --limit staging
  environment:
    name: staging
    url: https://staging.company.com
  only:
    - main

deploy:production:
  stage: deploy
  script:
    - ansible-playbook -i inventories/production site.yml --limit production --check --diff
    - ansible-playbook -i inventories/production site.yml --limit production
  environment:
    name: production
    url: https://production.company.com
  when: manual
  only:
    - tags

50.3 Testing und Linting in der Pipeline

50.3.1 Nutzung von ansible-lint, yamllint, ansible-playbook –syntax-check

Statische Code-Analyse identifiziert Qualitätsprobleme vor der Ausführung:

# Syntax-Validierung
ansible-playbook --syntax-check playbooks/*.yml

# Ansible-spezifisches Linting
ansible-lint --parseable --quiet playbooks/ roles/

# YAML-Syntax und -Stil
yamllint --format parseable .

50.3.2 Beispiel für ein CI-Job-Fragment zur Validierung

# GitHub Actions Workflow
name: Ansible Quality Gates

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.9'

      - name: Install dependencies
        run: |
          pip install ansible ansible-lint yamllint

      - name: Run yamllint
        run: yamllint .

      - name: Run ansible-lint
        run: ansible-lint

      - name: Run syntax check
        run: |
          for playbook in playbooks/*.yml; do
            ansible-playbook --syntax-check "$playbook"
          done

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Run ansible-lint security rules
        run: |
          ansible-lint --parseable --quiet \
            --enable-list no-hard-coded-passwords,risky-shell-pipe

Jenkins Pipeline-Beispiel:

// Jenkinsfile
pipeline {
    agent any

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Lint') {
            parallel {
                stage('YAML Lint') {
                    steps {
                        sh 'yamllint --format parseable . > yamllint.log || true'
                        publishHTML([
                                allowMissing: false,
                                alwaysLinkToLastBuild: true,
                                keepAll: true,
                                reportDir: '.',
                                reportFiles: 'yamllint.log',
                                reportName: 'YAML Lint Report'
                        ])
                    }
                }
                stage('Ansible Lint') {
                    steps {
                        sh 'ansible-lint --parseable . > ansible-lint.log || true'
                        archiveArtifacts artifacts: 'ansible-lint.log'
                    }
                }
            }
        }

        stage('Syntax Check') {
            steps {
                script {
                    def playbooks = sh(
                            script: "find . -name '*.yml' -path './playbooks/*'",
                            returnStdout: true
                    ).trim().split('\n')

                    playbooks.each { playbook ->
                        sh "ansible-playbook --syntax-check ${playbook}"
                    }
                }
            }
        }
    }
}

50.4 Einsatz von Molecule für Rollen-Tests

50.4.1 Kurzeinführung Molecule

Molecule automatisiert das Testen von Ansible-Rollen durch Bereitstellung isolierter Test-Umgebungen. Das Framework unterstützt verschiedene Treiber (Docker, Vagrant, Podman) und Testframeworks (Testinfra, pytest).

50.4.2 Automatisierte Tests für Rollen

Molecule-Konfiguration für eine typische Webserver-Rolle:

# molecule/default/molecule.yml
---
dependency:
  name: galaxy
  options:
    requirements-file: requirements.yml
driver:
  name: docker
platforms:
  - name: instance-ubuntu
    image: ubuntu:20.04
    pre_build_image: true
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    command: /lib/systemd/systemd
  - name: instance-centos
    image: centos:8
    pre_build_image: true
    privileged: true
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    command: /usr/sbin/init
provisioner:
  name: ansible
  config_options:
    defaults:
      stdout_callback: yaml
verifier:
  name: testinfra

Testdefinition:

# molecule/default/tests/test_default.py
import os
import testinfra.utils.ansible_runner

testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')

def test_nginx_installed(host):
    nginx = host.package("nginx")
    assert nginx.is_installed

def test_nginx_running(host):
    nginx = host.service("nginx")
    assert nginx.is_running
    assert nginx.is_enabled

def test_nginx_config_valid(host):
    cmd = host.run("nginx -t")
    assert cmd.rc == 0

def test_website_accessible(host):
    cmd = host.run("curl -s -o /dev/null -w '%{http_code}' http://localhost")
    assert "200" in cmd.stdout

50.4.3 Anbindung an Container (Docker) oder Vagrant

Pipeline-Integration mit Matrix-Testing:

# .gitlab-ci.yml - Molecule mit Matrix
test:molecule:
  stage: test
  services:
    - docker:dind
  variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
  parallel:
    matrix:
      - MOLECULE_SCENARIO: [default, ubuntu-18, centos-7]
  script:
    - cd roles/$ROLE_NAME
    - molecule test --scenario-name $MOLECULE_SCENARIO
  artifacts:
    reports:
      junit: roles/*/molecule/*/junit.xml

50.5 Umgang mit Secrets in der Pipeline

50.5.1 Ansible Vault in Kombination mit CI/CD

Vault-Passwörter werden über CI-Umgebungsvariablen bereitgestellt:

# Verschlüsseltes Inventory
ansible-vault encrypt_string 'production_db_password' --name 'db_password'

# Pipeline-Nutzung
echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass
ansible-playbook -i inventories/production site.yml --vault-password-file .vault_pass
rm .vault_pass

50.5.2 Secure Variable Handling über CI-Umgebungsvariablen

GitLab CI Vault-Integration:

# .gitlab-ci.yml
deploy:production:
  stage: deploy
  variables:
    ANSIBLE_VAULT_PASSWORD_FILE: /tmp/vault_pass
  before_script:
    - echo "$VAULT_PASSWORD" > /tmp/vault_pass
    - chmod 600 /tmp/vault_pass
  script:
    - ansible-playbook -i inventories/production site.yml
  after_script:
    - rm -f /tmp/vault_pass
  environment:
    name: production

GitHub Actions mit verschlüsselten Secrets:

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  release:
    types: [published]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Create vault password file
        run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > .vault_pass

      - name: Deploy with Ansible
        run: |
          ansible-playbook \
            -i inventories/production \
            --vault-password-file .vault_pass \
            --extra-vars "release_version=${{ github.event.release.tag_name }}" \
            site.yml
        env:
          ANSIBLE_HOST_KEY_CHECKING: false

Jenkins Credentials Plugin:

// Jenkinsfile
pipeline {
    agent any

    environment {
        ANSIBLE_VAULT_PASSWORD_FILE = credentials('ansible-vault-password')
    }

    stages {
        stage('Deploy') {
            steps {
                ansiblePlaybook(
                        playbook: 'site.yml',
                        inventory: 'inventories/production',
                        vaultCredentialsId: 'ansible-vault-password',
                        extras: '--limit production'
                )
            }
        }
    }
}

50.6 Versionierung und Playbook-Freigabe

50.6.1 Git-Strategien (Branches, Tags)

Feature-Branch-Workflow mit Ansible:

# Feature-Entwicklung
git checkout -b feature/nginx-ssl-config
# Entwicklung und Tests
git commit -m "Add SSL configuration for nginx role"
git push origin feature/nginx-ssl-config

# Merge Request/Pull Request für Review

# Release-Tagging
git checkout main
git tag -a v2.1.0 -m "Release version 2.1.0 - SSL support"
git push origin v2.1.0

GitFlow-basierte Pipeline:

# .gitlab-ci.yml - GitFlow Integration
variables:
  DEPLOY_STAGING: "develop"
  DEPLOY_PRODUCTION: "main"

deploy:staging:
  script:
    - ansible-playbook -i inventories/staging site.yml
  only:
    - develop

deploy:production:
  script:
    - ansible-playbook -i inventories/production site.yml
  only:
    - main
    - tags

hotfix:production:
  script:
    - ansible-playbook -i inventories/production site.yml --tags hotfix
  only:
    - /^hotfix\/.*$/
  when: manual

50.6.2 Integration von Playbooks in Release-Zyklen

Semantic Versioning für Ansible-Projekte:

# galaxy.yml - Collection Versioning
namespace: company
name: infrastructure
version: "${CI_COMMIT_TAG:-0.0.0-dev}"
description: Company infrastructure automation

# .gitlab-ci.yml - Automated Versioning
build:collection:
  stage: build
  script:
    - |
      if [[ $CI_COMMIT_TAG ]]; then
        export VERSION=$CI_COMMIT_TAG
      else
        export VERSION="0.0.0-${CI_COMMIT_SHORT_SHA}"
      fi
    - sed -i "s/version: .*/version: $VERSION/" galaxy.yml
    - ansible-galaxy collection build
  artifacts:
    paths:
      - "*.tar.gz"
    expire_in: 1 week

50.7 Best Practices

50.7.1 Trennung von Test-/Staging-/Produktivumgebungen

Environment-spezifische Konfiguration:

# inventories/staging/group_vars/all.yml
environment: staging
database_host: staging-db.company.com
api_replicas: 1
debug_mode: true

# inventories/production/group_vars/all.yml
environment: production
database_host: prod-db.company.com
api_replicas: 3
debug_mode: false

Pipeline mit Environment-Gates:

# .gitlab-ci.yml - Environment Progression
deploy:staging:
  stage: deploy
  script:
    - ansible-playbook -i inventories/staging site.yml
  environment:
    name: staging
    url: https://staging.company.com
  only:
    - develop

deploy:production:
  stage: deploy
  script:
    - ansible-playbook -i inventories/production site.yml --check --diff
    - read -p "Continue with deployment? (y/N): " confirm
    - [[ $confirm == [yY] ]] || exit 1
        - ansible-playbook -i inventories/production site.yml
        environment:
        name: production
        url: https://production.company.com
        when: manual
        only:
        - main

50.7.2 Tag-basierte Ausführung

Granulare Deployment-Kontrolle:

# Playbook mit strategischen Tags
- name: Configure application servers
  hosts: app_servers
  tasks:
    - name: Install packages
      package:
        name: "{{ item }}"
        state: present
      loop: "{{ required_packages }}"
      tags: [packages, setup]

    - name: Configure application
      template:
        src: app.conf.j2
        dest: /etc/app/app.conf
      notify: restart application
      tags: [config, app]

    - name: Deploy application code
      git:
        repo: "{{ app_repository }}"
        dest: "{{ app_directory }}"
        version: "{{ app_version | default('main') }}"
      notify: restart application
      tags: [deploy, app]

    - name: Update database schema
      command: "{{ app_directory }}/bin/migrate.py"
      tags: [database, migrate]
      when: run_migrations | default(false)

Pipeline mit selektiver Tag-Ausführung:

# .gitlab-ci.yml - Tag-basierte Deployments
deploy:config-only:
  stage: deploy
  script:
    - ansible-playbook -i inventories/production site.yml --tags config
  when: manual
  only:
    - main

deploy:hotfix:
  stage: deploy
  script:
    - ansible-playbook -i inventories/production site.yml --tags hotfix,config
  when: manual
  only:
    - /^hotfix\/.*$/

deploy:full:
  stage: deploy
  script:
    - ansible-playbook -i inventories/production site.yml
  when: manual
  only:
    - tags

50.7.3 Rollback-Strategien

Automatisierte Rollback-Mechanismen:

# Playbook mit Rollback-Funktionalität
- name: Deploy application with rollback capability
  hosts: app_servers
  vars:
    app_releases_path: /opt/app/releases
    app_current_path: /opt/app/current
    keep_releases: 5
  
  tasks:
    - name: Create releases directory
      file:
        path: "{{ app_releases_path }}"
        state: directory
      tags: [deploy]

    - name: Deploy new release
      git:
        repo: "{{ app_repository }}"
        dest: "{{ app_releases_path }}/{{ ansible_date_time.epoch }}"
        version: "{{ app_version }}"
      register: new_release
      tags: [deploy]

    - name: Update symlink to new release
      file:
        src: "{{ new_release.dest }}"
        dest: "{{ app_current_path }}"
        state: link
        force: yes
      notify: restart application
      tags: [deploy]

    - name: Cleanup old releases
      shell: |
        cd {{ app_releases_path }} && \
        ls -1t | tail -n +{{ keep_releases + 1 }} | xargs rm -rf
      tags: [deploy, cleanup]

# Rollback-Playbook
- name: Rollback application
  hosts: app_servers
  tasks:
    - name: Get previous release
      shell: |
        cd {{ app_releases_path }} && \
        ls -1t | head -n 2 | tail -n 1
      register: previous_release
      tags: [rollback]

    - name: Rollback to previous release
      file:
        src: "{{ app_releases_path }}/{{ previous_release.stdout }}"
        dest: "{{ app_current_path }}"
        state: link
        force: yes
      notify: restart application
      tags: [rollback]

50.7.4 Monitoring und Alerting

Integration von Deployment-Monitoring:

# Pipeline mit Health Checks
deploy:production:
  stage: deploy
  script:
    - ansible-playbook -i inventories/production site.yml
    - sleep 30  # Warten auf Service-Start
    - ansible-playbook -i inventories/production health-check.yml
  after_script:
    - |
      if [ $CI_JOB_STATUS = "failed" ]; then
        ansible-playbook -i inventories/production rollback.yml
        curl -X POST "$SLACK_WEBHOOK" -d '{"text":"Deployment failed and rolled back!"}'
      else
        curl -X POST "$SLACK_WEBHOOK" -d '{"text":"Deployment successful!"}'
      fi
  environment:
    name: production

Health-Check Playbook:

# health-check.yml
- name: Verify deployment health
  hosts: app_servers
  tasks:
    - name: Check application endpoint
      uri:
        url: "http://{{ inventory_hostname }}:8080/health"
        method: GET
        status_code: 200
        timeout: 10
      retries: 3
      delay: 10

    - name: Verify database connectivity
      command: "{{ app_directory }}/bin/db-check.py"
      register: db_check
      failed_when: db_check.rc != 0

    - name: Check disk space
      shell: df -h / | awk 'NR==2 {print $5}' | sed 's/%//'
      register: disk_usage
      failed_when: disk_usage.stdout | int > 90