Skip to content

Ansible Post-provisioning

Introduction

Après le provisioning Terraform, Ansible configure les instances : installation de packages, déploiement d'applications, configuration des services. Cette approche sépare le provisioning (IaC) de la configuration (CaC).

Prérequis

Points à apprendre

Architecture Terraform + Ansible

graph TB
    DevOps["DevOps Engineer"]

    subgraph "Infrastructure as Code"
        Terraform["Terraform<br/>HCL<br/>Provision infrastructure<br/>(VMs, networks, volumes)"]
        Ansible["Ansible<br/>YAML<br/>Configure instances<br/>(packages, services, apps)"]
        Inventory["Dynamic Inventory<br/>JSON<br/>Généré par Terraform"]
    end

    subgraph "OpenStack Cloud"
        Instances["Instances<br/>VMs provisionnées"]
    end

    DevOps -->|terraform apply| Terraform
    Terraform -->|Create<br/>API| Instances
    Terraform -->|Generate<br/>terraform output| Inventory
    Inventory -->|Input| Ansible
    DevOps -->|ansible-playbook| Ansible
    Ansible -->|Configure<br/>SSH| Instances

Structure du projet

infrastructure/
├── terraform/
│   ├── environments/
│   │   └── production/
│   │       ├── main.tf
│   │       ├── outputs.tf
│   │       └── terraform.tfvars
│   └── modules/
├── ansible/
│   ├── inventory/
│   │   └── openstack.py       # Inventaire dynamique
│   ├── roles/
│   │   ├── common/
│   │   ├── webserver/
│   │   └── database/
│   ├── playbooks/
│   │   ├── site.yml
│   │   ├── webservers.yml
│   │   └── database.yml
│   ├── group_vars/
│   │   ├── all.yml
│   │   ├── webservers.yml
│   │   └── databases.yml
│   └── ansible.cfg
└── scripts/
    └── deploy.sh

Output Terraform pour Ansible

# terraform/environments/production/outputs.tf

output "ansible_inventory" {
  description = "Inventaire pour Ansible"
  value = {
    webservers = {
      hosts = {
        for idx, instance in module.web_servers.instance_names :
        instance => {
          ansible_host = module.web_servers.floating_ips[idx]
          private_ip   = module.web_servers.private_ips[idx]
          ansible_user = "ubuntu"
        }
      }
      vars = {
        role = "webserver"
      }
    }
    databases = {
      hosts = {
        for idx, instance in module.database.instance_names :
        instance => {
          ansible_host = module.database.private_ips[idx]
          ansible_user = "ubuntu"
        }
      }
      vars = {
        role = "database"
      }
    }
  }
  sensitive = false
}

# Génération du fichier d'inventaire
resource "local_file" "ansible_inventory" {
  content = templatefile("${path.module}/templates/inventory.tpl", {
    webservers = module.web_servers
    databases  = module.database
  })
  filename = "${path.module}/../../../ansible/inventory/hosts.ini"
}

Template d'inventaire

# terraform/environments/production/templates/inventory.tpl

[webservers]
%{ for idx, name in webservers.instance_names ~}
${name} ansible_host=${webservers.floating_ips[idx]} private_ip=${webservers.private_ips[idx]}
%{ endfor ~}

[databases]
%{ for idx, name in databases.instance_names ~}
${name} ansible_host=${databases.private_ips[idx]}
%{ endfor ~}

[all:vars]
ansible_user=ubuntu
ansible_python_interpreter=/usr/bin/python3

Inventaire dynamique OpenStack

#!/usr/bin/env python3
# ansible/inventory/openstack.py

import json
import subprocess
import sys

def get_terraform_output():
    """Récupère l'output Terraform"""
    result = subprocess.run(
        ['terraform', 'output', '-json', 'ansible_inventory'],
        capture_output=True,
        text=True,
        cwd='../terraform/environments/production'
    )
    return json.loads(result.stdout)

def main():
    if len(sys.argv) == 2 and sys.argv[1] == '--list':
        inventory = get_terraform_output()
        # Ajouter le groupe "all"
        inventory['all'] = {
            'children': list(inventory.keys())
        }
        print(json.dumps(inventory, indent=2))
    elif len(sys.argv) == 3 and sys.argv[1] == '--host':
        # Host vars (vide car déjà dans le groupe)
        print(json.dumps({}))
    else:
        print("Usage: --list or --host <hostname>", file=sys.stderr)
        sys.exit(1)

if __name__ == '__main__':
    main()

Configuration Ansible

# ansible/ansible.cfg

[defaults]
inventory = ./inventory/hosts.ini
remote_user = ubuntu
private_key_file = ~/.ssh/id_rsa

host_key_checking = False
retry_files_enabled = False

roles_path = ./roles
collections_paths = ./collections

stdout_callback = yaml
callback_enabled = timer, profile_tasks

[privilege_escalation]
become = True
become_method = sudo
become_user = root

[ssh_connection]
pipelining = True
control_path = /tmp/ansible-%%r@%%h:%%p

[inventory]
enable_plugins = ini, script, auto

Rôle common

# ansible/roles/common/tasks/main.yml

---
- name: Update apt cache
  apt:
    update_cache: yes
    cache_valid_time: 3600

- name: Install common packages
  apt:
    name:
      - curl
      - wget
      - vim
      - htop
      - net-tools
      - python3-pip
      - unzip
      - jq
    state: present

- name: Configure timezone
  timezone:
    name: Europe/Paris

- name: Configure NTP
  apt:
    name: chrony
    state: present
  notify: restart chrony

- name: Create deploy user
  user:
    name: deploy
    groups: sudo
    shell: /bin/bash
    create_home: yes

- name: Add SSH key for deploy user
  authorized_key:
    user: deploy
    key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
    state: present

- name: Configure sysctl parameters
  sysctl:
    name: "{{ item.name }}"
    value: "{{ item.value }}"
    state: present
    reload: yes
  loop:
    - { name: 'net.core.somaxconn', value: '65535' }
    - { name: 'vm.swappiness', value: '10' }
# ansible/roles/common/handlers/main.yml

---
- name: restart chrony
  service:
    name: chrony
    state: restarted

Rôle webserver

# ansible/roles/webserver/tasks/main.yml

---
- name: Install Nginx
  apt:
    name:
      - nginx
      - certbot
      - python3-certbot-nginx
    state: present

- name: Create web root directory
  file:
    path: /var/www/{{ app_name }}
    state: directory
    owner: www-data
    group: www-data
    mode: '0755'

- name: Deploy Nginx configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/{{ app_name }}
  notify: reload nginx

- name: Enable site
  file:
    src: /etc/nginx/sites-available/{{ app_name }}
    dest: /etc/nginx/sites-enabled/{{ app_name }}
    state: link
  notify: reload nginx

- name: Remove default site
  file:
    path: /etc/nginx/sites-enabled/default
    state: absent
  notify: reload nginx

- name: Deploy application
  copy:
    src: "{{ app_artifact }}"
    dest: /var/www/{{ app_name }}/
    owner: www-data
    group: www-data
  when: app_artifact is defined
  notify: reload nginx

- name: Ensure Nginx is running
  service:
    name: nginx
    state: started
    enabled: yes
# ansible/roles/webserver/templates/nginx.conf.j2

upstream backend {
{% for host in groups['webservers'] %}
    server {{ hostvars[host]['private_ip'] }}:{{ app_port | default(8080) }};
{% endfor %}
}

server {
    listen 80;
    server_name {{ server_name | default('_') }};

    root /var/www/{{ app_name }};
    index index.html;

    location / {
        try_files $uri $uri/ @backend;
    }

    location @backend {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /health {
        return 200 'OK';
        add_header Content-Type text/plain;
    }
}

Rôle database

# ansible/roles/database/tasks/main.yml

---
- name: Install PostgreSQL
  apt:
    name:
      - postgresql
      - postgresql-contrib
      - python3-psycopg2
    state: present

- name: Configure PostgreSQL to listen on all interfaces
  lineinfile:
    path: /etc/postgresql/14/main/postgresql.conf
    regexp: "^#?listen_addresses"
    line: "listen_addresses = '*'"
  notify: restart postgresql

- name: Configure pg_hba.conf
  template:
    src: pg_hba.conf.j2
    dest: /etc/postgresql/14/main/pg_hba.conf
  notify: restart postgresql

- name: Ensure PostgreSQL is running
  service:
    name: postgresql
    state: started
    enabled: yes

- name: Create database
  become_user: postgres
  postgresql_db:
    name: "{{ db_name }}"
    state: present

- name: Create database user
  become_user: postgres
  postgresql_user:
    name: "{{ db_user }}"
    password: "{{ db_password }}"
    db: "{{ db_name }}"
    priv: "ALL"
    state: present

Playbook principal

# ansible/playbooks/site.yml

---
- name: Configure all hosts
  hosts: all
  become: yes
  roles:
    - common
  tags:
    - common

- name: Configure webservers
  hosts: webservers
  become: yes
  roles:
    - webserver
  tags:
    - webservers

- name: Configure databases
  hosts: databases
  become: yes
  roles:
    - database
  tags:
    - databases

Variables de groupe

# ansible/group_vars/all.yml

---
ansible_python_interpreter: /usr/bin/python3
app_name: myapp
environment: production
# ansible/group_vars/webservers.yml

---
app_port: 8080
server_name: app.example.com
# ansible/group_vars/databases.yml

---
db_name: myapp
db_user: myapp
db_password: "{{ vault_db_password }}"

Script de déploiement complet

#!/bin/bash
# scripts/deploy.sh

set -e

ENVIRONMENT=${1:-production}
ACTION=${2:-all}

cd "$(dirname "$0")/.."

echo "=== Deploying $ENVIRONMENT ==="

case $ACTION in
    terraform|all)
        echo "=== Running Terraform ==="
        cd terraform/environments/$ENVIRONMENT
        terraform init
        terraform apply -auto-approve
        cd ../../..
        ;;&

    ansible|all)
        echo "=== Running Ansible ==="
        cd ansible

        # Attendre que les instances soient accessibles
        echo "Waiting for instances to be ready..."
        sleep 30

        # Exécuter Ansible
        ansible-playbook playbooks/site.yml \
            --extra-vars "environment=$ENVIRONMENT"

        cd ..
        ;;
esac

echo "=== Deployment complete ==="

Diagramme de flux

flowchart TD
    Start([Start]) --> TFInit[terraform init]

    subgraph "Terraform"
        TFInit --> TFPlan[terraform plan]
        TFPlan --> TFApply[terraform apply]
        TFApply --> GenInv[Generate inventory file]
    end

    GenInv --> Wait[Wait for instances SSH ready]

    subgraph "Ansible"
        Wait --> LoadInv[Load inventory]
        LoadInv --> CommonRole[Apply common role<br/>all hosts]
        CommonRole --> WebRole[Apply webserver role]
        CommonRole --> DBRole[Apply database role]
    end

    WebRole --> Complete[Deployment complete]
    DBRole --> Complete
    Complete --> Stop([Stop])

Exemples pratiques

Exécution étape par étape

# 1. Provisionner l'infrastructure
cd terraform/environments/production
terraform apply

# 2. Vérifier l'inventaire généré
cat ../../../ansible/inventory/hosts.ini

# 3. Tester la connectivité Ansible
cd ../../../ansible
ansible all -m ping

# 4. Exécuter le playbook complet
ansible-playbook playbooks/site.yml

# 5. Ou par tags
ansible-playbook playbooks/site.yml --tags webservers
ansible-playbook playbooks/site.yml --tags databases

Vérification post-déploiement

# Vérifier les webservers
ansible webservers -m shell -a "systemctl status nginx"

# Vérifier la database
ansible databases -m shell -a "systemctl status postgresql"

# Test de connectivité
ansible webservers -m uri -a "url=http://localhost/health"

Ressources

Checkpoint

  • Inventaire généré depuis Terraform
  • Rôles Ansible créés (common, webserver, database)
  • Playbook site.yml fonctionnel
  • Variables de groupe configurées
  • Script de déploiement automatisé
  • Configuration vérifiée post-déploiement