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¶
- Terraform Provider OpenStack
- Module Compute
- Ansible installé (v2.14+)
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/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