Si vous avez déjà “corrigé à la main” un serveur WordPress à 2h du matin après une migration ratée, vous savez pourquoi l’Infrastructure as Code (IaC) change la donne : vous voulez pouvoir reconstruire proprement, pas “réparer”.

Objectif : déployer WordPress 6.9.4 (avril 2026) de façon reproductible avec Terraform (provisionnement) et Ansible (configuration), en incluant Nginx, PHP-FPM (8.1+ ; ici 8.3), MariaDB, Redis, SSL, headers, cache et un workflow staging/migration basé sur WP-CLI.

Le besoin / Le problème serveur

Le problème vient rarement de WordPress lui-même. Il vient de l’infrastructure : versions PHP divergentes entre staging et prod, extensions manquantes, droits fichiers incohérents, config Nginx “unique” introuvable, base de données restaurée sans réécrire les URLs, cache Redis qui sert des sessions obsolètes, etc.

Ce que vous saurez mettre en place à la fin :

  • Une VM (ou plusieurs) provisionnée(s) par Terraform, avec firewall et IP publique.
  • Un playbook Ansible idempotent qui configure l’OS, installe Nginx/PHP-FPM/MariaDB/Redis, et déploie WordPress 6.9.4 via WP-CLI.
  • Un staging clonable (DB + uploads + rewrite URLs) et une restauration automatisable.
  • Un set de fichiers de config complets (Nginx, PHP, wp-config, systemd timers optionnels).

Dans mon expérience, la meilleure valeur de l’IaC sur WordPress n’est pas “aller plus vite”. C’est de rendre les incidents prévisibles : vous pouvez reproduire un serveur à l’identique pour diagnostiquer.

Résumé rapide

  • Terraform crée l’infra (exemple : Hetzner Cloud). Adaptez le provider (AWS/GCP/OVH) sans changer votre logique Ansible.
  • Ansible configure Debian 12, durcit SSH, installe Nginx + PHP-FPM 8.3, MariaDB, Redis, Fail2ban.
  • WP-CLI installe WordPress 6.9.4 de manière idempotente (si déjà installé, on ne casse rien).
  • Cache serveur : FastCGI cache (optionnel) + Redis object cache (optionnel) + purge contrôlée.
  • Staging : export/import DB + rsync uploads + search-replace WP-CLI, sans casser la sérialisation.
  • Vérification : curl, nginx -t, php-fpm, mysqladmin, wp core version, wp option get home.

Avant de commencer (prérequis)

Accès requis :

  • Un poste local avec Terraform et Ansible (et SSH).
  • Clé SSH chargée (agent) et accès root initial sur la VM (ou user cloud).
  • WP-CLI disponible sur le serveur (installé via Ansible ci-dessous).

Sauvegarde obligatoire (si vous migrez un site existant) :

  • Dump SQL + archive uploads (et idéalement le répertoire wp-content complet).
  • Test de restauration sur staging avant toute action en production.

Versions cible (avril 2026) :

  • WordPress : 6.9.4 (cible de ce guide).
  • PHP : 8.1+ recommandé ; je déploie 8.3 car c’est courant et performant en 2026.
  • Base : MariaDB 10.11+ (LTS) ou MySQL 8.0+.
  • Serveur web : Nginx.

Sources officielles utiles :

Étape 1 : Provisionner l’infra avec Terraform (réseau, VM, firewall)

Je prends Hetzner Cloud comme exemple car le provider est simple. Le pattern est identique ailleurs : une ressource “server”, une IP, un firewall, et des outputs pour Ansible.

Arborescence recommandée

infra/
  terraform/
    main.tf
    variables.tf
    outputs.tf
  ansible/
    inventory.ini
    site.yml
    group_vars/
      all.yml
    roles/
      common/
      web/
      db/
      wordpress/

Terraform : variables

# terraform/variables.tf
variable "hcloud_token" {
  type      = string
  sensitive = true
}

variable "ssh_public_key_path" {
  type    = string
  default = "~/.ssh/id_ed25519.pub"
}

variable "server_name" {
  type    = string
  default = "wp-prod-01"
}

variable "server_type" {
  type    = string
  default = "cx22"
}

variable "location" {
  type    = string
  default = "fsn1"
}

variable "image" {
  type    = string
  default = "debian-12"
}

Terraform : ressources (VM + firewall + SSH key)

# terraform/main.tf
terraform {
  required_version = ">= 1.7.0"
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = ">= 1.49.0"
    }
  }
}

provider "hcloud" {
  token = var.hcloud_token
}

resource "hcloud_ssh_key" "deployer" {
  name       = "deployer-key"
  public_key = file(var.ssh_public_key_path)
}

resource "hcloud_firewall" "wp_fw" {
  name = "wp-fw"

  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "22"
    source_ips = ["0.0.0.0/0", "::/0"]
    # Commentaire : en production, restreignez à votre IP/bastion
  }

  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "80"
    source_ips = ["0.0.0.0/0", "::/0"]
  }

  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "443"
    source_ips = ["0.0.0.0/0", "::/0"]
  }
}

resource "hcloud_server" "wp" {
  name        = var.server_name
  server_type = var.server_type
  location    = var.location
  image       = var.image
  ssh_keys    = [hcloud_ssh_key.deployer.id]

  firewall_ids = [hcloud_firewall.wp_fw.id]

  labels = {
    role = "wordpress"
    env  = "prod"
  }
}

Terraform : outputs pour Ansible

# terraform/outputs.tf
output "server_ipv4" {
  value = hcloud_server.wp.ipv4_address
}

output "server_name" {
  value = hcloud_server.wp.name
}

Exécution Terraform

cd infra/terraform

export TF_VAR_hcloud_token="xxx"
terraform init
terraform fmt
terraform validate
terraform apply

# Récupérer l'IP pour l'inventaire Ansible
terraform output -raw server_ipv4

Edge case fréquent : vous copiez l’IP dans l’inventaire mais vous oubliez que votre DNS pointe encore vers l’ancien serveur. Résultat : Let’s Encrypt échoue (HTTP-01). Corrigez le DNS avant l’étape SSL.

Étape 2 : Préparer l’OS avec Ansible (packages, users, SSH, UFW)

Je sépare en rôles. Ça évite le playbook monolithique “impossible à relire”. Et surtout, vous pouvez réutiliser common sur d’autres projets.

Inventaire

# ansible/inventory.ini
[wordpress]
wp-prod-01 ansible_host=203.0.113.10 ansible_user=root

Variables globales

# ansible/group_vars/all.yml
wp_domain: "example.com"
wp_site_title: "Site WordPress"
wp_admin_user: "admin"
wp_admin_email: "[email protected]"

# Commentaire : ne stockez pas le mot de passe en clair en vrai.
# Utilisez Ansible Vault ou un secret manager.
wp_admin_password: "ChangezMoi-Long-Unique"

wp_db_name: "wordpress"
wp_db_user: "wp_user"
wp_db_password: "ChangezMoi-DB-Long-Unique"

wp_root: "/var/www/{{ wp_domain }}/public"
wp_shared: "/var/www/{{ wp_domain }}/shared"

php_version: "8.3"

Playbook principal

# ansible/site.yml
- name: Déploiement WordPress (Nginx + PHP-FPM + MariaDB + Redis)
  hosts: wordpress
  become: true

  roles:
    - role: common
    - role: db
    - role: web
    - role: wordpress

Rôle common (durcissement minimal, packages, user deploy)

# ansible/roles/common/tasks/main.yml
- name: Mettre à jour l'index APT
  ansible.builtin.apt:
    update_cache: true
    cache_valid_time: 3600

- name: Installer les paquets de base
  ansible.builtin.apt:
    name:
      - curl
      - ca-certificates
      - gnupg
      - unzip
      - rsync
      - git
      - ufw
      - fail2ban
      - logrotate
    state: present

- name: Créer un utilisateur deploy (sans mot de passe)
  ansible.builtin.user:
    name: deploy
    shell: /bin/bash
    create_home: true

- name: Autoriser la clé SSH de deploy
  ansible.builtin.authorized_key:
    user: deploy
    key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/id_ed25519.pub') }}"
  # Commentaire : adaptez la clé si vous utilisez une autre.

- name: Activer UFW et autoriser SSH/HTTP/HTTPS
  ansible.builtin.ufw:
    rule: allow
    port: "{{ item }}"
    proto: tcp
  loop:
    - "22"
    - "80"
    - "443"

- name: Activer UFW
  ansible.builtin.ufw:
    state: enabled
    policy: deny

Race condition classique : vous activez UFW avant d’avoir autorisé le port 22, et vous vous enfermez dehors. Ici, l’ordre est volontaire.

Étape 3 : Installer Nginx + PHP-FPM 8.3 + MariaDB + Redis

Rôle db : MariaDB + base + user

# ansible/roles/db/tasks/main.yml
- name: Installer MariaDB
  ansible.builtin.apt:
    name:
      - mariadb-server
      - mariadb-client
    state: present

- name: S'assurer que MariaDB est démarré
  ansible.builtin.service:
    name: mariadb
    state: started
    enabled: true

- name: Créer la base WordPress
  community.mysql.mysql_db:
    name: "{{ wp_db_name }}"
    state: present
    encoding: utf8mb4
    collation: utf8mb4_unicode_ci

- name: Créer l'utilisateur DB WordPress et lui donner les droits
  community.mysql.mysql_user:
    name: "{{ wp_db_user }}"
    password: "{{ wp_db_password }}"
    priv: "{{ wp_db_name }}.*:ALL"
    host: "localhost"
    state: present

Note pratique : les modules community.mysql nécessitent souvent python3-pymysql. Si Ansible vous renvoie une erreur du type “Python MySQLdb not found”, installez-le.

# Ajoutez dans common si nécessaire
- name: Installer le driver PyMySQL pour Ansible
  ansible.builtin.apt:
    name: python3-pymysql
    state: present

Rôle web : Nginx + PHP-FPM + extensions + Redis

# ansible/roles/web/tasks/main.yml
- name: Installer Nginx
  ansible.builtin.apt:
    name: nginx
    state: present

- name: Installer PHP-FPM et extensions requises
  ansible.builtin.apt:
    name:
      - "php{{ php_version }}-fpm"
      - "php{{ php_version }}-cli"
      - "php{{ php_version }}-mysql"
      - "php{{ php_version }}-curl"
      - "php{{ php_version }}-gd"
      - "php{{ php_version }}-intl"
      - "php{{ php_version }}-mbstring"
      - "php{{ php_version }}-xml"
      - "php{{ php_version }}-zip"
      - "php{{ php_version }}-imagick"
      - "php{{ php_version }}-redis"
      - "php{{ php_version }}-opcache"
    state: present

- name: Installer Redis server
  ansible.builtin.apt:
    name: redis-server
    state: present

- name: Activer services Nginx, PHP-FPM, Redis
  ansible.builtin.service:
    name: "{{ item }}"
    state: started
    enabled: true
  loop:
    - nginx
    - "php{{ php_version }}-fpm"
    - redis-server

Compatibilité Divi 5 / Elementor / Avada : côté serveur, c’est surtout PHP memory_limit, max_input_vars et les timeouts qui font la différence. Je fournis une config PHP dédiée plus bas, avec des valeurs réalistes pour des builders lourds.

Étape 4 : Déployer WordPress 6.9.4 avec WP-CLI (idempotent)

Je préfère WP-CLI à un zip “manuellement décompressé” : c’est scriptable, et vous pouvez contrôler précisément la version. La doc WP-CLI est ici : wp core download.

Rôle wordpress : arborescence, permissions, WP-CLI, core install

# ansible/roles/wordpress/tasks/main.yml
- name: Créer les répertoires du site
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: www-data
    group: www-data
    mode: "0755"
  loop:
    - "/var/www/{{ wp_domain }}"
    - "{{ wp_root }}"
    - "{{ wp_shared }}"
    - "{{ wp_shared }}/uploads"

- name: Télécharger WP-CLI
  ansible.builtin.get_url:
    url: "https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar"
    dest: /usr/local/bin/wp
    mode: "0755"
  # Commentaire : vous pouvez pinner une version WP-CLI en interne si vous voulez du 100% reproductible.

- name: Vérifier que WP-CLI fonctionne
  ansible.builtin.command: wp --info
  register: wp_info
  changed_when: false

- name: Télécharger WordPress 6.9.4 (si non présent)
  ansible.builtin.command: >
    wp core download
    --path={{ wp_root }}
    --version=6.9.4
    --locale=fr_FR
    --skip-content
  args:
    creates: "{{ wp_root }}/wp-settings.php"
  become_user: www-data

- name: Générer wp-config.php si absent
  ansible.builtin.command: >
    wp config create
    --path={{ wp_root }}
    --dbname={{ wp_db_name }}
    --dbuser={{ wp_db_user }}
    --dbpass={{ wp_db_password }}
    --dbhost=localhost
    --dbcharset=utf8mb4
    --skip-check
  args:
    creates: "{{ wp_root }}/wp-config.php"
  become_user: www-data

- name: Ajouter des constantes utiles (Redis, debug log) dans wp-config.php
  ansible.builtin.blockinfile:
    path: "{{ wp_root }}/wp-config.php"
    marker: "/* {mark} ANSIBLE MANAGED BLOCK */"
    insertafter: "define\( 'DB_COLLATE'.*"
    block: |
      // Commentaire : cache objet Redis (plugin requis côté WordPress)
      define( 'WP_REDIS_HOST', '127.0.0.1' );
      define( 'WP_REDIS_PORT', 6379 );
      define( 'WP_CACHE', true );

      // Commentaire : logs PHP/WordPress côté serveur (ne pas laisser en prod si vous logguez des données sensibles)
      define( 'WP_DEBUG', false );
      define( 'WP_DEBUG_LOG', true );
      define( 'WP_DEBUG_DISPLAY', false );
  notify: Reload php-fpm

- name: Installer la base WordPress (idempotent : si déjà installé, ne pas échouer)
  ansible.builtin.shell: |
    set -e
    if wp core is-installed --path={{ wp_root }}; then
      echo "WordPress déjà installé, on ne touche pas."
    else
      wp core install 
        --path={{ wp_root }} 
        --url="https://{{ wp_domain }}" 
        --title="{{ wp_site_title }}" 
        --admin_user="{{ wp_admin_user }}" 
        --admin_password="{{ wp_admin_password }}" 
        --admin_email="{{ wp_admin_email }}"
    fi
  args:
    executable: /bin/bash
  become_user: www-data

- name: Fixer les permissions (wp-content en écriture)
  ansible.builtin.file:
    path: "{{ wp_root }}/wp-content"
    state: directory
    owner: www-data
    group: www-data
    mode: "0775"

handlers:
  - name: Reload php-fpm
    ansible.builtin.service:
      name: "php{{ php_version }}-fpm"
      state: reloaded

Erreur réaliste : copier/coller un snippet WP-CLI dans le mauvais répertoire. WP-CLI se base sur --path (ou le répertoire courant). Ici, je force systématiquement --path={{ wp_root }} pour éviter le “wp: error: This does not seem to be a WordPress installation”.

Installer un plugin de cache Redis (optionnel mais courant)

Si vous utilisez Redis object cache, installez un plugin maintenu. Exemple via WP-CLI :

sudo -u www-data wp plugin install redis-cache --activate --path=/var/www/example.com/public
sudo -u www-data wp redis enable --path=/var/www/example.com/public

Je vois souvent un piège : activer Redis côté serveur mais oublier d’installer/activer le plugin. Vous avez Redis qui tourne… et aucun gain.

Étape 5 : SSL, headers HTTP, cache serveur et purge

Je préfère certbot en mode Nginx, mais uniquement si votre DNS est correct. Si vous avez un proxy (Cloudflare), vous devrez gérer le challenge DNS ou basculer temporairement.

Certbot + Nginx

sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx

# Remplacez example.com
sudo certbot --nginx -d example.com -d www.example.com --redirect --agree-tos -m [email protected] --no-eff-email

Cache FastCGI (optionnel) + purge contrôlée

FastCGI cache sur WordPress demande de la discipline (purge sur publish/update). Sur des sites avec Elementor/Divi/Avada, j’ai souvent vu des “pages fantômes” si vous cachez trop agressivement. Je le garde optionnel : Redis object cache + OPcache suffisent souvent.

Si vous activez FastCGI cache, prévoyez une purge (hook WordPress côté plugin ou endpoint sécurisé). Ne faites pas un endpoint de purge sans auth : c’est une porte ouverte au DoS.

Étape 6 : Staging, migration et restauration (Ansible + WP-CLI)

Le workflow le plus fiable que j’ai trouvé : “staging = copie de prod” (DB + uploads), puis search-replace via WP-CLI. WP-CLI gère la sérialisation, ce que sed ne fera jamais correctement.

Dump DB + rsync uploads (prod → local ou prod → staging)

# Sur le serveur source (prod)
sudo -u www-data wp db export /tmp/prod.sql --path=/var/www/example.com/public

# Copier le dump vers staging
rsync -avz -e "ssh -p 22" root@PROD_IP:/tmp/prod.sql ./prod.sql

# Copier les uploads (attention au volume)
rsync -avz -e "ssh -p 22" root@PROD_IP:/var/www/example.com/public/wp-content/uploads/ 
  root@STAGING_IP:/var/www/staging.example.com/public/wp-content/uploads/

Import + réécriture URLs + flush permaliens

# Sur staging
sudo -u www-data wp db import /tmp/prod.sql --path=/var/www/staging.example.com/public

# Réécrire les URLs (gère la sérialisation)
sudo -u www-data wp search-replace 'https://example.com' 'https://staging.example.com' 
  --all-tables --precise --recurse-objects 
  --path=/var/www/staging.example.com/public

# Régénérer les permaliens (évite les 404 post-migration)
sudo -u www-data wp rewrite flush --hard --path=/var/www/staging.example.com/public

Erreur fréquente : oublier wp rewrite flush après migration, surtout quand Nginx a été modifié. Vous obtenez des 404 sur les pages, et vous perdez 30 minutes à suspecter la DB.

Fichiers de configuration complets

Nginx : vhost WordPress (HTTP→HTTPS, PHP-FPM, sécurité de base)

# /etc/nginx/sites-available/example.com
# Commentaire : vhost Nginx WordPress avec PHP-FPM et règles classiques.

server {
  listen 80;
  listen [::]:80;
  server_name example.com www.example.com;

  root /var/www/example.com/public;
  index index.php index.html;

  # Commentaire : Let’s Encrypt gère souvent le redirect lui-même si --redirect est utilisé.
  location /.well-known/acme-challenge/ {
    allow all;
  }

  location / {
    return 301 https://$host$request_uri;
  }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name example.com www.example.com;

  root /var/www/example.com/public;
  index index.php index.html;

  # Commentaire : certbot injecte généralement les chemins de certificats ici.
  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  # Headers sécurité (ajustez selon vos besoins)
  add_header X-Frame-Options "SAMEORIGIN" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
  add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

  # Commentaire : HSTS uniquement si vous êtes sûr du HTTPS partout (y compris sous-domaines si includeSubDomains)
  add_header Strict-Transport-Security "max-age=15552000" always;

  # Taille upload : builders + médias
  client_max_body_size 128m;

  # WordPress permaliens
  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  # Bloquer l'accès à des fichiers sensibles
  location ~* /(wp-config.php|readme.html|license.txt) {
    deny all;
  }

  # PHP
  location ~ .php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;

    # Commentaire : évite certaines attaques PATH_INFO
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # Timeouts utiles pour imports lourds (Elementor/Divi/Avada)
    fastcgi_read_timeout 120s;
  }

  # Cache statique basique
  location ~* .(css|js|jpg|jpeg|png|gif|svg|webp|ico|woff2?)$ {
    expires 30d;
    add_header Cache-Control "public, max-age=2592000, immutable";
    try_files $uri =404;
  }

  # Interdire l'exécution PHP dans uploads
  location ~* /wp-content/uploads/.*.php$ {
    deny all;
  }

  access_log /var/log/nginx/example.com.access.log;
  error_log  /var/log/nginx/example.com.error.log;
}

Activer le site

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
sudo nginx -t
sudo systemctl reload nginx

php.ini (FPM) : valeurs réalistes pour WordPress + page builders

; /etc/php/8.3/fpm/conf.d/99-wordpress.ini
; Commentaire : réglages PHP adaptés à WordPress 6.9.4 et aux builders (Divi 5, Elementor, Avada).

memory_limit = 512M
max_execution_time = 120
max_input_time = 120
max_input_vars = 5000

upload_max_filesize = 128M
post_max_size = 128M

; Logs (à adapter)
log_errors = On
error_log = /var/log/php8.3-fpm-wordpress.log

; OPcache (gains réels)
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=60000
opcache.validate_timestamps=1
opcache.revalidate_freq=2

wp-config.php : exemple minimal + bloc Ansible

<?php
/**
 * Commentaire : exemple wp-config.php compatible WordPress 6.9.4.
 * Généré initialement via WP-CLI, puis enrichi (Redis, debug log).
 */

define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wp_user' );
define( 'DB_PASSWORD', 'ChangezMoi-DB-Long-Unique' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );

/* BEGIN ANSIBLE MANAGED BLOCK */
// Commentaire : cache objet Redis (plugin requis côté WordPress)
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
define( 'WP_CACHE', true );

// Commentaire : logs PHP/WordPress côté serveur
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
/* END ANSIBLE MANAGED BLOCK */

define( 'AUTH_KEY',         'mettez-des-cles-uniques' );
define( 'SECURE_AUTH_KEY',  'mettez-des-cles-uniques' );
define( 'LOGGED_IN_KEY',    'mettez-des-cles-uniques' );
define( 'NONCE_KEY',        'mettez-des-cles-uniques' );
define( 'AUTH_SALT',        'mettez-des-cles-uniques' );
define( 'SECURE_AUTH_SALT', 'mettez-des-cles-uniques' );
define( 'LOGGED_IN_SALT',   'mettez-des-cles-uniques' );
define( 'NONCE_SALT',       'mettez-des-cles-uniques' );

$table_prefix = 'wp_';

if ( ! defined( 'ABSPATH' ) ) {
  define( 'ABSPATH', __DIR__ . '/' );
}

require_once ABSPATH . 'wp-settings.php';

Vérification

Vous voulez des tests qui disent vite “où ça casse”. Voici une séquence que j’utilise presque systématiquement.

Tests services

# Nginx
sudo nginx -t
systemctl status nginx --no-pager

# PHP-FPM
systemctl status php8.3-fpm --no-pager
php -v

# MariaDB
systemctl status mariadb --no-pager
mysqladmin ping

# Redis
systemctl status redis-server --no-pager
redis-cli ping

Tests WordPress via WP-CLI

sudo -u www-data wp core version --path=/var/www/example.com/public
sudo -u www-data wp option get home --path=/var/www/example.com/public
sudo -u www-data wp plugin status --path=/var/www/example.com/public

Tests HTTP

curl -I http://example.com
curl -I https://example.com

# Vérifier une page (doit renvoyer 200)
curl -s -o /dev/null -w "%{http_code}n" https://example.com/

Si vous avez un 301 en boucle, c’est presque toujours une double redirection : Nginx force HTTPS + WordPress “Home/Site URL” incohérents (http vs https) ou proxy mal déclaré.

Si ça ne marche pas

Je pars du principe que vous avez exécuté Terraform puis Ansible, et que le site ne répond pas correctement.

Checklist logs

# Nginx logs
sudo tail -n 200 /var/log/nginx/example.com.error.log
sudo tail -n 200 /var/log/nginx/example.com.access.log

# PHP-FPM logs (si vous avez défini error_log)
sudo tail -n 200 /var/log/php8.3-fpm-wordpress.log

# Systemd
sudo journalctl -u nginx -n 200 --no-pager
sudo journalctl -u php8.3-fpm -n 200 --no-pager
sudo journalctl -u mariadb -n 200 --no-pager

Diagnostic permissions (très fréquent)

# Vérifier owner/group
sudo stat -c "%U:%G %a %n" /var/www/example.com/public
sudo stat -c "%U:%G %a %n" /var/www/example.com/public/wp-content

# Corriger rapidement (à affiner selon votre politique)
sudo chown -R www-data:www-data /var/www/example.com/public
sudo find /var/www/example.com/public -type d -exec chmod 0755 {} ;
sudo find /var/www/example.com/public -type f -exec chmod 0644 {} ;
sudo chmod -R 0775 /var/www/example.com/public/wp-content

Tableau de diagnostic (symptômes réalistes)

Symptôme Cause probable Vérification Solution
502 Bad Gateway Socket PHP-FPM incorrect / service down systemctl status php8.3-fpm, Nginx error log Corriger fastcgi_pass vers le bon socket, redémarrer PHP-FPM
White Screen / 500 Extension PHP manquante, memory_limit trop bas Logs PHP-FPM, php -m Installer extensions, augmenter memory_limit
Erreur “Error establishing a database connection” Credentials DB mauvais, user sans droits mysql -u wp_user -p, vérifier wp-config Recréer user/priv, corriger wp-config
SSL Let’s Encrypt échoue DNS pas à jour, port 80 filtré dig example.com, firewall provider Corriger DNS, ouvrir 80, relancer certbot
Pages en cache “bizarres” après update Cache FastCGI/Redis non purgé Headers de cache, plugin Redis Purge ciblée, ajuster règles de cache

Pièges et erreurs courantes

Erreur Cause Solution
Copier le code au mauvais endroit Vous modifiez un vhost différent de celui réellement servi nginx -T | grep -R "server_name example.com" puis corriger le bon fichier
Oublier un point-virgule / parenthèse Édition manuelle de wp-config.php Évitez l’édition manuelle : utilisez wp config set ou Ansible blockinfile
Tester sur production sans sauvegarde Pression / “petit changement” Automatisez dump SQL + archive uploads avant toute action
Conflit cache / oubli de purge navigateur Headers cache statiques trop agressifs Ajoutez versioning assets, purgez CDN, invalidez cache serveur
Permaliens non régénérés Migration, changement de règles Nginx wp rewrite flush --hard
Erreur liée à PHP trop ancien VM image obsolète ou dépôt figé Vérifiez php -v, migrez vers PHP 8.1+ (idéalement 8.3)
Code d’ancien tutoriel incompatible Snippets Apache/.htaccess sur Nginx, directives obsolètes Utilisez une conf Nginx native (ci-dessus) et des pratiques 2026

Sécurité serveur

L’IaC rend la sécurité plus simple, parce que vous pouvez appliquer des règles au lieu de les “documenter”. Points que je mets presque toujours en place :

  • SSH : désactiver l’auth par mot de passe, limiter les IP sources (bastion/VPN), changer le user root (ou au minimum interdire root login).
  • Secrets : Ansible Vault pour wp_admin_password, wp_db_password. Évitez les variables en clair dans Git.
  • Permissions : pas de 777, exécution PHP interdite dans uploads, owner cohérent.
  • Headers HTTP : au minimum X-Content-Type-Options, Referrer-Policy, HSTS (avec prudence).
  • Mises à jour : WP-CLI + cron/CI pour patcher (core/plugins) après test staging.

Pour les aspects WordPress applicatifs (nonces, capacités, etc.), la référence reste Security APIs. Côté serveur, souvenez-vous qu’un endpoint de purge cache non protégé est une vulnérabilité pratique : on peut le marteler et saturer PHP.

Ressources

FAQ

Pourquoi séparer Terraform et Ansible ?

Terraform gère bien le cycle de vie des ressources (VM, firewall, IP). Ansible gère bien l’état logiciel (packages, fichiers, services). Mélanger les deux finit souvent en scripts non idempotents.

Pourquoi pinner WordPress à 6.9.4 au lieu de “latest” ?

Pour la reproductibilité. En incident, vous voulez pouvoir reconstruire exactement la même version. Ensuite, vous planifiez l’upgrade (staging → prod). WP-CLI permet les deux.

PHP 8.1 minimum, pourquoi déployer 8.3 ?

8.1 est le minimum recommandé ici, mais 8.3 est souvent un meilleur compromis perf/support en 2026. Si un plugin impose 8.1/8.2, adaptez php_version dans vos variables.

MariaDB ou MySQL ?

Les deux fonctionnent très bien avec WordPress. MariaDB 10.11 LTS est un choix courant sur Debian. Si vous avez des contraintes d’entreprise (MySQL 8), remplacez le rôle db.

Redis est-il obligatoire ?

Non. Sur un blog simple, OPcache + cache navigateur suffisent. Redis devient intéressant avec beaucoup d’admin-ajax, de transients, ou des builders lourds.

FastCGI cache : je l’active ?

Si vous avez du trafic anonyme élevé, oui, mais avec une stratégie de purge solide. Sinon, vous allez courir après des incohérences (surtout pendant les mises à jour de contenu).

Comment gérer les secrets proprement ?

Ansible Vault est la solution la plus simple. Alternative : un secret manager (Vault, AWS SSM, etc.) et des lookups Ansible. Évitez de commiter des mots de passe dans group_vars.

Comment intégrer un staging automatique dans ce setup ?

Ajoutez un second serveur Terraform (labels env=staging), un inventaire Ansible distinct, puis un playbook “clone” (dump/import + rsync + search-replace). Gardez des URLs séparées et des robots noindex côté staging.

Elementor/Divi/Avada : des réglages serveur spécifiques ?

Oui : memory_limit, max_input_vars, post_max_size, timeouts Nginx/PHP. Le fichier 99-wordpress.ini plus haut est un socle réaliste.

Quelle est l’erreur la plus fréquente en IaC WordPress ?

Penser que “déployer = installer WordPress”. En pratique, c’est la migration (DB + uploads + URLs + cache) qui casse. Automatisez cette partie tôt, et testez-la régulièrement.