Aller au contenu
Cloud medium

Haute Dispo avec EIP Flottante sur Outscale

46 min de lecture

logo 3ds outscale

Ce guide présente la mise en place d'une infrastructure haute disponibilité sur Outscale. L'objectif est de fournir un point d'entrée unique capable de basculer automatiquement entre deux nœuds situés dans des zones de disponibilité différentes. La solution combine Terraform pour le provisionnement, Ansible pour la configuration, et un cluster Corosync/Pacemaker en mode actif/passif avec une EIP flottante gérée dynamiquement.

L'architecture repose sur une approche multi-zones pour maximiser la résilience. Chaque zone (eu-west-2a et eu-west-2b) héberge un VPC distinct avec un nœud HAProxy et un serveur web Nginx. Cette séparation physique garantit qu'une défaillance d'une zone entière n'affecte pas le service.

Le cœur du système est l'EIP flottante qui constitue le point d'accès unique. Cette adresse IP publique statique peut être attachée et détachée dynamiquement, maintenant ainsi la continuité même en cas de panne. Cette solution utilise exclusivement des primitives Outscale sans dépendre de services managés, la rendant adaptée aux environnements souverains.

Un VPC Peering interconnecte les deux réseaux, permettant au cluster de communiquer et aux HAProxy d'accéder aux backends dans l'autre zone tout en préservant l'isolation réseau.

Architecture HA EIP flottante 2 zones — HAProxy actif/passif via VPC Peering, EIP unique pointant vers le nœud actif élu par Pacemaker

L'EIP pointe en permanence vers un seul HAProxy, celui désigné comme actif par le gestionnaire de cluster Pacemaker. Les backends Nginx sont répartis sur les deux zones et restent accessibles via le peering, garantissant ainsi que le HAProxy actif peut toujours distribuer la charge vers les serveurs disponibles, quelle que soit leur localisation.

Le système repose sur plusieurs composants détaillés dans le guide Corosync/Pacemaker. Corosync assure la communication inter-nœuds et détecte les pannes, tandis que Pacemaker orchestre les ressources et décide des actions correctives.

Pour un cluster à deux nœuds, l'option two_node: 1 dans Corosync adapte le comportement du quorum pour permettre au cluster de fonctionner avec un seul nœud actif en cas de perte de communication.

L'EIP est la ressource critique. Lorsque Pacemaker détecte une défaillance (nœud injoignable ou HAProxy défaillant), il détache automatiquement l'EIP du nœud problématique et la réattache au nœud secondaire avec un temps d'interruption minimal.

Provisionnement de l'infrastructure avec Terraform

Section intitulée « Provisionnement de l'infrastructure avec Terraform »

Le provisionnement suit une approche méthodique détaillée dans le guide Infrastructure as Code. La configuration du backend Terraform sur un bucket S3 Outscale garantit que l'état reste accessible et partageable, ce qui est essentiel en production.

terraform {
backend "s3" {
# Configuration du backend pour stocker l'état Terraform
bucket = "tf-state-ha"
key = "ha/terraform.tfstate"
region = "eu-west-2"
endpoints = {
s3 = "https://oos.eu-west-2.outscale.com"
}
skip_credentials_validation = true
skip_region_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
use_path_style = true
}
}

Chaque zone reçoit un VPC avec un bloc CIDR distinct : 10.0.0.0/24 pour la zone A et 10.0.1.0/24 pour la zone B, évitant tout conflit d'adressage.

resource "outscale_vpc" "vpc_a" {
# VPC pour la zone de disponibilité A
cidr_block = "10.0.0.0/24"
tags = {
Name = "vpc-ha-zone-a"
}
}
resource "outscale_vpc" "vpc_b" {
# VPC pour la zone de disponibilité B
cidr_block = "10.0.1.0/24"
tags = {
Name = "vpc-ha-zone-b"
}
}

Le VPC Peering crée une connexion bidirectionnelle mais ne configure pas automatiquement les routes. Il faut explicitement les ajouter dans les tables de routage de chaque VPC.

resource "outscale_net_peering" "peer" {
# Création du peering entre les deux VPC
source_net_id = outscale_vpc.vpc_a.net_id
acceptor_net_id = outscale_vpc.vpc_b.net_id
tags = {
Name = "peering-ha"
}
}
resource "outscale_route" "a_to_b" {
# Route du VPC A vers le VPC B via le peering
net_id = outscale_vpc.vpc_a.net_id
destination_ip_range = "10.0.1.0/24"
gateway_id = outscale_net_peering.peer.net_peering_id
}
resource "outscale_route" "b_to_a" {
# Route du VPC B vers le VPC A via le peering
net_id = outscale_vpc.vpc_b.net_id
destination_ip_range = "10.0.0.0/24"
gateway_id = outscale_net_peering.peer.net_peering_id
}

Les Security Groups définissent les règles pare-feu. Ports obligatoires : UDP 5404-5405 (Corosync), TCP 2224 et 3121 (Pacemaker), TCP 80 (HAProxy) et TCP 22 (SSH depuis bastion).

resource "outscale_security_group" "cluster_sg" {
description = "Security Group pour le cluster HA"
net_id = outscale_vpc.vpc_a.net_id
# Port SSH depuis le bastion
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
# Ports Corosync
ingress {
from_port = 5404
to_port = 5405
protocol = "udp"
cidr_blocks = ["10.0.0.0/16", "10.0.1.0/16"]
}
# Ports Pacemaker
ingress {
from_port = 2224
to_port = 2224
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16", "10.0.1.0/16"]
}
ingress {
from_port = 3121
to_port = 3121
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16", "10.0.1.0/16"]
}
# Port HTTP depuis Internet
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}

Les instances utilisent une image Packer personnalisée et reçoivent des tags (Role, Zone, Type) pour l'inventaire dynamique Ansible.

resource "outscale_vm" "haproxy_a" {
# Nœud HAProxy dans la zone A
keypair_name = outscale_keypair.main.keypair_name
image_id = var.rocky_image
instance_type = "tinav5.c2r4p2"
subnet_id = outscale_subnet.subnet_a.id
security_group_ids = [outscale_security_group.cluster_sg.id]
tags = {
Name = "haproxy-a"
Role = "haproxy"
Zone = "a"
Type = "cluster-node"
}
}
resource "outscale_vm" "haproxy_b" {
# Nœud HAProxy dans la zone B
keypair_name = outscale_keypair.main.keypair_name
image_id = var.rocky_image
instance_type = "tinav5.c2r4p2"
subnet_id = outscale_subnet.subnet_b.id
security_group_ids = [outscale_security_group.cluster_sg.id]
tags = {
Name = "haproxy-b"
Role = "haproxy"
Zone = "b"
Type = "cluster-node"
}
}

L'EIP est réservée mais non attachée lors du provisionnement. Pacemaker gérera l'attachement dynamique une fois le cluster configuré, évitant ainsi les conflits.

resource "outscale_public_ip" "eip" {
# Réservation de l'EIP sans attachement initial
# Pacemaker gérera l'attachement dynamique
tags = {
Name = "eip-ha-cluster"
}
}
output "elastic_ip" {
description = "Adresse IP publique pour l'accès au cluster"
value = outscale_public_ip.eip.public_ip
}

Ansible automatise la configuration du cluster de la préparation système à la déclaration des ressources Pacemaker.

L'inventaire dynamique utilise le plugin aws_ec2 compatible avec l'API Outscale. Point critique : utilisation des adresses IP privées pour éviter les incohérences liées à la bascule de l'EIP. Les connexions SSH passent par le bastion via ProxyJump.

# Inventaire dynamique aws_ec2.yml
plugin: amazon.aws.aws_ec2
aws_profile: outscale
regions:
- eu-west-2
# Utilisation des IP privées pour éviter les conflits avec l'EIP flottante
compose:
ansible_host: private_ip_address
# Organisation en groupes basée sur les tags
keyed_groups:
- key: tags.Role
separator: ''
- key: tags.Zone
separator: ''
- key: tags.Type
separator: ''
# Configuration du bastion comme jump host
ansible_ssh_common_args: '-o ProxyJump=bastion.example.com'

La préparation système installe les packages requis (Corosync, Pacemaker, pcs, HAProxy), désactive le firewall local (filtrage géré par Security Groups) et configure les hostnames avec mise à jour de /etc/hosts pour la résolution DNS locale.

- name: Préparation système pour le cluster
hosts: cluster-node
become: true
tasks:
- name: Installation des packages nécessaires
package:
name:
- corosync
- pacemaker
- pcs
- haproxy
state: present
- name: Désactivation du firewall
# Le firewall est désactivé car les Security Groups gèrent déjà le filtrage
systemd:
name: firewalld
enabled: false
state: stopped
- name: Configuration du hostname
hostname:
name: "{{ inventory_hostname }}.cluster.local"
- name: Mise à jour de /etc/hosts
# Ajout de tous les nœuds du cluster pour la résolution DNS locale
lineinfile:
dest: /etc/hosts
line: "{{ hostvars[item].ansible_host }} {{ item }}.cluster.local {{ item }}"
state: present
loop: "{{ groups['cluster-node'] }}"

Les routes statiques OS sont indispensables pour la communication Corosync via le peering. Important : Terraform configure les routes VPC mais elles ne sont pas automatiquement appliquées dans l'OS. Ansible crée des fichiers de route persistants.

- name: Configuration des routes pour le peering
hosts: cluster-node
become: true
vars:
# Gateway par défaut du sous-réseau
gateway_ip: "{{ ansible_default_ipv4.gateway }}"
tasks:
- name: Configuration de route statique vers l'autre zone
# Création d'un fichier de route persistant
copy:
dest: /etc/sysconfig/network-scripts/route-eth0
content: |
# Route vers la zone {{ 'B' if 'a' in inventory_hostname else 'A' }}
{{ '10.0.1.0/24' if 'a' in inventory_hostname else '10.0.0.0/24' }} via {{ gateway_ip }}
mode: '0644'
notify: Redémarrage réseau
handlers:
- name: Redémarrage réseau
systemd:
name: network
state: restarted

La CLI Outscale (osc-cli) est configurée pour l'agent OCF. Les credentials sont stockés dans Ansible Vault et déployés dans /root/.osc/config.json. Seul root nécessite cette configuration car Pacemaker exécute les agents sous cette identité.

- name: Configuration de la CLI Outscale
hosts: cluster-node
become: true
vars_files:
- vars/vault.yml
tasks:
- name: Création du répertoire de configuration
file:
path: /root/.osc
state: directory
mode: '0700'
- name: Déploiement du fichier de configuration
# Template contenant les credentials depuis le vault
template:
src: config.json.j2
dest: /root/.osc/config.json
mode: '0600'
no_log: true

La récupération dynamique de l'EIP via l'API Outscale évite le codage en dur et rend les playbooks réutilisables.

- name: Récupération de l'adresse EIP
hosts: localhost
connection: local
tasks:
- name: Interrogation de l'API pour obtenir l'EIP
shell: |
osc-cli api ReadPublicIps \
--profile default \
--Filters.Tags '[{"Key": "Name", "Values": ["eip-ha-cluster"]}]' \
--query 'PublicIps[0].PublicIp' \
--output text
register: eip_result
changed_when: false
- name: Sauvegarde de l'EIP comme fait
set_fact:
cluster_eip: "{{ eip_result.stdout }}"
cacheable: true

La configuration Corosync définit la topologie cluster. Le transport udpu (UDP unicast) est préféré en cloud. Chaque nœud a une IP privée et un nodeid unique. L'option two_node: 1 est fondamentale pour le quorum à deux nœuds.

# Template corosync.conf.j2
totem {
version: 2
cluster_name: ha-cluster
transport: udpu
interface {
ringnumber: 0
# Utilisation du réseau privé pour la communication cluster
bindnetaddr: {{ ansible_default_ipv4.address }}
broadcast: yes
mcastport: 5405
}
}
nodelist {
# Déclaration explicite de chaque nœud du cluster
node {
ring0_addr: 10.0.0.10
name: haproxy-a
nodeid: 1
}
node {
ring0_addr: 10.0.1.10
name: haproxy-b
nodeid: 2
}
}
quorum {
provider: corosync_votequorum
# Configuration spécifique pour un cluster à deux nœuds
two_node: 1
}
logging {
to_logfile: yes
logfile: /var/log/corosync/corosync.log
to_syslog: yes
timestamp: on
}

Ordre de démarrage : CorosyncPacemakerpcsd (daemon pour gestion via pcs). Ansible active aussi le démarrage automatique au boot.

- name: Démarrage des services cluster
hosts: cluster-node
become: true
tasks:
- name: Démarrage de Corosync
systemd:
name: corosync
enabled: true
state: started
- name: Démarrage de Pacemaker
# Pacemaker doit démarrer après Corosync
systemd:
name: pacemaker
enabled: true
state: started
- name: Démarrage de pcsd
# Nécessaire pour la gestion via pcs
systemd:
name: pcsd
enabled: true
state: started

L'authentification cluster utilise un mot de passe hacluster identique sur tous les nœuds. La commande pcs host auth établit la confiance mutuelle.

- name: Configuration de l'authentification cluster
hosts: cluster-node
become: true
vars_files:
- vars/vault.yml
tasks:
- name: Définition du mot de passe hacluster
user:
name: hacluster
password: "{{ hacluster_password | password_hash('sha512') }}"
- name: Authentification des nœuds
# Exécuté depuis un seul nœud pour configurer tout le cluster
command: >
pcs host auth
haproxy-a.cluster.local
haproxy-b.cluster.local
-u hacluster
-p {{ hacluster_password }}
run_once: true
delegate_to: "{{ groups['cluster-node'][0] }}"

Les ressources Pacemaker constituent le cœur de la configuration. HAProxy utilise l'agent systemd, tandis que l'EIP utilise un agent OCF personnalisé. Les contraintes garantissent la colocation (EIP + HAProxy sur même nœud) et l'ordre de démarrage.

- name: Configuration des ressources Pacemaker
hosts: cluster-node
become: true
run_once: true
delegate_to: "{{ groups['cluster-node'][0] }}"
tasks:
- name: Création de la ressource HAProxy
# Utilisation de l'agent systemd pour gérer HAProxy
command: >
pcs resource create haproxy systemd:haproxy
op monitor interval=30s timeout=20s
op start timeout=60s
op stop timeout=60s
args:
creates: /var/lib/pacemaker/cib/.haproxy_created
- name: Création de la ressource EIP
# Agent OCF personnalisé pour gérer l'Elastic IP
command: >
pcs resource create ElasticIP ocf:custom:osc-eip
ip="{{ cluster_eip }}"
op monitor interval=30s timeout=60s
op start timeout=120s
op stop timeout=60s
args:
creates: /var/lib/pacemaker/cib/.eip_created
- name: Contrainte de colocation
# L'EIP doit toujours être sur le même nœud que HAProxy
command: >
pcs constraint colocation add ElasticIP with haproxy INFINITY
- name: Contrainte d'ordre
# HAProxy doit démarrer avant l'attachement de l'EIP
command: >
pcs constraint order haproxy then ElasticIP

La configuration HAProxy utilise un template Jinja2 qui génère dynamiquement la liste des backends depuis l'inventaire Ansible, incluant tous les serveurs web multi-zones.

# Template haproxy.cfg.j2
global
log /dev/log local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
frontend http-in
bind *:80
default_backend webservers
backend webservers
balance roundrobin
# Génération dynamique des backends depuis l'inventaire
{% for host in groups['webservers'] %}
server {{ host }} {{ hostvars[host].ansible_host }}:80 check inter 2000 rise 2 fall 3
{% endfor %}

L'agent OCF personnalisé fait le pont entre Pacemaker et l'API Outscale. Il implémente les opérations start, stop, monitor et meta-data selon les conventions OCF.

Déployé dans /usr/lib/ocf/resource.d/custom/osc-eip, il utilise des fonctions bash avec des codes de retour OCF stricts pour que Pacemaker interprète correctement l'état de la ressource.

Stratégie IMDS-first — minimiser la pression API

Section intitulée « Stratégie IMDS-first — minimiser la pression API »

Le point dur d'un agent OCF cloud est la disponibilité de l'API : un monitor qui s'exécute toutes les 30 secondes et qui interroge ReadPublicIps à chaque cycle représente 2 880 appels par jour et par cluster, sur un endpoint qui peut tomber pour des raisons hors de votre contrôle. Si l'API est indisponible 60 secondes, le monitor échoue, Pacemaker considère la ressource malade et tente une bascule alors que la ressource est en réalité saine — faux positif coûteux.

Le contournement est d'utiliser au maximum le service de métadonnées d'instance (IMDS) accessible localement à http://169.254.169.254 : c'est un endpoint local à l'hyperviseur, sub-millisecond, qui survit aux pannes de l'API publique. L'IMDS expose meta-data/public-ipv4 qui reflète l'EIP réellement attachée à la VM courante — exactement l'information dont on a besoin pour le monitor.

Action OCFSource primaireAppel API ?
monitor (toutes les 30 s)IMDS public-ipv4Aucun en chemin nominal
startIMDS-first puis LinkPublicIp si nécessaire0 si déjà attachée, 1 sinon
stopIMDS-first puis UnlinkPublicIp si nécessaire0 si déjà détachée, 1 sinon
meta-dataXML statiqueAucun
validate-allIMDS instance-idAucun

Sur un cluster sain, le monitor ne touche jamais à l'API — la pression tombe de 2 880 à 0 appel par jour. Les appels API ne surviennent qu'aux moments où l'on change l'état (bascule, réparation), avec un wrapper de retry à backoff exponentiel pour absorber les drops transitoires.

/usr/lib/ocf/resource.d/custom/osc-eip
#!/bin/bash
# Agent OCF pour la gestion de l'Elastic IP Outscale
#
# Stratégie IMDS-first :
# - monitor : 0 appel API (curl IMDS local uniquement)
# - start/stop : IMDS d'abord, API uniquement si l'état doit changer
# - retry à backoff exponentiel sur les appels API mutants
# En-têtes OCF obligatoires
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs
# Codes de retour OCF standards
OCF_SUCCESS=0
OCF_ERR_GENERIC=1
OCF_ERR_ARGS=2
OCF_ERR_UNIMPLEMENTED=3
OCF_ERR_PERM=4
OCF_ERR_INSTALLED=5
OCF_ERR_CONFIGURED=6
OCF_NOT_RUNNING=7
# Paramètres de la ressource (passés par pcs resource create ip=...)
EIP="${OCF_RESKEY_ip}"
PROFILE="${OCF_RESKEY_profile:-default}"
IMDS_URL="${OCF_RESKEY_imds_url:-http://169.254.169.254/latest/meta-data}"
API_RETRIES="${OCF_RESKEY_api_retries:-3}"
API_TIMEOUT="${OCF_RESKEY_api_timeout:-10}"
# ---------------------------------------------------------------------------
# Helpers IMDS — sub-ms, locaux à l'hyperviseur, pas d'appel API
# ---------------------------------------------------------------------------
# Lit un champ IMDS avec timeout court (--max-time 2). Stdout = valeur, ou vide.
imds_get() {
curl -s --max-time 2 --fail "${IMDS_URL}/$1" 2>/dev/null
}
# ID de la VM courante. Mis en cache au premier appel pour éviter le re-curl.
INSTANCE_ID_CACHE=""
get_instance_id() {
if [ -z "${INSTANCE_ID_CACHE}" ]; then
INSTANCE_ID_CACHE=$(imds_get "instance-id")
fi
printf '%s' "${INSTANCE_ID_CACHE}"
}
# IP publique réellement attachée à cette VM (vide si aucune EIP attachée).
get_local_public_ip() {
imds_get "public-ipv4"
}
# Vrai si la VM courante porte déjà l'EIP cible — pure IMDS, pas d'API.
have_floating_eip() {
local current
current=$(get_local_public_ip)
[ "${current}" = "${EIP}" ]
}
# ---------------------------------------------------------------------------
# Helper API — wrapper retry à backoff exponentiel (1s, 2s, 4s...)
# ---------------------------------------------------------------------------
osc_call_with_retry() {
local attempt=0
local sleep_time=1
local rc=0
while [ ${attempt} -lt ${API_RETRIES} ]; do
if osc-cli api "$@" --profile "${PROFILE}" >/dev/null 2>&1; then
return 0
fi
rc=$?
ocf_log warn "osc-cli api $1 attempt $((attempt + 1))/${API_RETRIES} failed (rc=${rc}), retry in ${sleep_time}s"
sleep ${sleep_time}
sleep_time=$((sleep_time * 2))
attempt=$((attempt + 1))
done
ocf_log err "osc-cli api $1 failed after ${API_RETRIES} attempts"
return 1
}
# ---------------------------------------------------------------------------
# Actions OCF
# ---------------------------------------------------------------------------
# monitor : pure IMDS, pas d'appel API. C'est le hot path.
eip_monitor() {
if have_floating_eip; then
ocf_log debug "EIP ${EIP} attachée à cette VM (IMDS confirme)"
return ${OCF_SUCCESS}
fi
# Si l'IMDS répond une autre IP (ou aucune), l'EIP n'est pas chez nous.
ocf_log debug "EIP ${EIP} non attachée à cette VM"
return ${OCF_NOT_RUNNING}
}
# start : IMDS-first. N'appelle l'API que si on ne porte pas déjà l'EIP.
eip_start() {
if have_floating_eip; then
ocf_log info "EIP ${EIP} déjà attachée (IMDS) — no-op"
return ${OCF_SUCCESS}
fi
local instance_id
instance_id=$(get_instance_id)
if [ -z "${instance_id}" ]; then
ocf_log err "Impossible de lire instance-id via IMDS — réseau métadonnées HS ?"
return ${OCF_ERR_GENERIC}
fi
ocf_log info "Attachement de l'EIP ${EIP} à ${instance_id}"
if ! osc_call_with_retry LinkPublicIp \
--PublicIp "${EIP}" \
--VmId "${instance_id}"; then
return ${OCF_ERR_GENERIC}
fi
# Confirmation IMDS — l'API a accepté, on attend la propagation jusqu'à
# 30 secondes en lisant l'IMDS local (pas de re-call API).
local i=0
while [ ${i} -lt 30 ]; do
sleep 1
if have_floating_eip; then
ocf_log info "EIP ${EIP} confirmée par IMDS sur ${instance_id}"
return ${OCF_SUCCESS}
fi
i=$((i + 1))
done
ocf_log err "API LinkPublicIp OK mais IMDS ne confirme pas après 30s"
return ${OCF_ERR_GENERIC}
}
# stop : IMDS-first. N'appelle l'API que si on porte effectivement l'EIP.
eip_stop() {
if ! have_floating_eip; then
ocf_log info "EIP ${EIP} déjà détachée de cette VM (IMDS) — no-op"
return ${OCF_SUCCESS}
fi
ocf_log info "Détachement de l'EIP ${EIP}"
if ! osc_call_with_retry UnlinkPublicIp --PublicIp "${EIP}"; then
# Pacemaker considère stop comme « tout sauf échec dur ». Si l'API
# est down, on log un warn et on retourne SUCCESS — l'autre nœud
# qui prend la main fera lui-même un Link qui désassocie.
ocf_log warn "UnlinkPublicIp a échoué — l'EIP sera ré-attachée par le futur start"
return ${OCF_SUCCESS}
fi
return ${OCF_SUCCESS}
}
# Fonction meta-data: décrit la ressource pour Pacemaker
eip_meta_data() {
cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="osc-eip">
<version>1.0</version>
<longdesc lang="fr">
Agent OCF pour gérer une Elastic IP Outscale dans un cluster Pacemaker.
Cet agent permet d'attacher/détacher dynamiquement une EIP en fonction
de l'état du cluster.
</longdesc>
<shortdesc lang="fr">Gestion d'une Elastic IP Outscale</shortdesc>
<parameters>
<parameter name="ip" unique="1" required="1">
<longdesc lang="fr">
Adresse IP publique élastique à gérer
</longdesc>
<shortdesc lang="fr">Elastic IP</shortdesc>
<content type="string" />
</parameter>
<parameter name="profile" unique="0" required="0">
<longdesc lang="fr">
Profil de configuration osc-cli à utiliser (par défaut: default)
</longdesc>
<shortdesc lang="fr">Profil osc-cli</shortdesc>
<content type="string" default="default" />
</parameter>
</parameters>
<actions>
<action name="start" timeout="120s" />
<action name="stop" timeout="60s" />
<action name="monitor" timeout="60s" interval="30s" depth="0" />
<action name="meta-data" timeout="5s" />
</actions>
</resource-agent>
END
}
# Point d'entrée principal
case "${1}" in
start)
eip_start
;;
stop)
eip_stop
;;
monitor)
eip_monitor
;;
meta-data)
eip_meta_data
;;
validate-all)
# Validation de la configuration
if [ -z "${EIP}" ]; then
ocf_log err "Paramètre 'ip' obligatoire manquant"
exit ${OCF_ERR_CONFIGURED}
fi
exit ${OCF_SUCCESS}
;;
*)
echo "Usage: ${0} {start|stop|monitor|meta-data|validate-all}"
exit ${OCF_ERR_UNIMPLEMENTED}
;;
esac
exit $?

L'agent nécessite les permissions API : LinkPublicIp, UnlinkPublicIp et ReadPublicIps. La bonne pratique — non négociable en production — est de créer un utilisateur EIM dédié avec une policy scopée à ces 3 actions seulement.

Création du compte EIM scopé — pourquoi via SDK Python

Section intitulée « Création du compte EIM scopé — pourquoi via SDK Python »
#!/usr/bin/env python3
"""Bootstrap idempotent du compte EIM dédié au cluster Pacemaker."""
import json, os
from osc_sdk_python import Gateway
USERNAME = "capstone-haproxy-cluster"
POLICY_NAME = "capstone-haproxy-cluster-eip-only"
# Format EIM OUTSCALE — Version 2012-10-17 (legacy AWS), Resource string "*".
POLICY_DOCUMENT = json.dumps({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["api:LinkPublicIp", "api:UnlinkPublicIp", "api:ReadPublicIps"],
"Resource": "*",
}],
})
with Gateway(
access_key=os.environ["OUTSCALE_ACCESSKEYID"],
secret_key=os.environ["OUTSCALE_SECRETKEYID"],
region=os.environ.get("OUTSCALE_REGION", "eu-west-2"),
) as gw:
# CreateUser, CreatePolicy, LinkPolicy, CreateAccessKey, écrire ~/.osc/config.json
...

Le script complet (scripts/bootstrap-eim-haproxy-cluster.py) :

  1. Crée le user EIM (idempotent — skip si existe).
  2. Crée la policy avec le Document scopé (idempotent).
  3. Attache la policy au user (idempotent).
  4. Crée une access key (une seule fois — la secret key n'est lisible qu'à la création).
  5. Écrit le profil dans ~/.osc/config.json côté contrôleur (mode 0600).

Le playbook Ansible déploie ensuite ce profil sur les nœuds du cluster en /root/.osc/config.json (mode 0600, no_log: true).

Le piège du deadlock EIP unique — solution NIC secondaire

Section intitulée « Le piège du deadlock EIP unique — solution NIC secondaire »

Découverte critique en test sur le compte : si l'EIP flottante est attachée à la NIC primaire de la VM et que pacemaker bascule la ressource ailleurs, la VM abandonnée perd toute IP publique. Conséquence : elle ne peut plus joindre api.eu-west-2.outscale.com → la prochaine élection Pacemaker qui veut lui ré-attacher la flottante timeout sur LinkPublicIp (ConnectTimeout 60 s).

Cascade deadlock NIC primaire — t=0 état initial, t=1 bascule a→b avec EIP egress écrasée, t=2 deadlock haproxy-a sans IP publique → API injoignable → fallback haproxy-c. Solution = NIC secondaire dédiée à la flottante

Solution propre — NIC secondaire dédiée à la flottante :

# NIC secondaire sur chaque HAProxy, dans le même subnet, dédiée à l'EIP flottante.
resource "outscale_nic" "haproxy_secondary" {
for_each = local.haproxy_nodes
subnet_id = each.value.subnet_id
description = "Secondary NIC for floating EIP failover"
security_group_ids = [outscale_security_group.haproxy[each.key].security_group_id]
dynamic "tags" {
for_each = merge(local.base_tags, { Name = "${var.project}-haproxy-${each.key}-eth1" })
content { key = tags.key; value = tags.value }
}
}
resource "outscale_nic_link" "haproxy_secondary" {
for_each = local.haproxy_nodes
vm_id = outscale_vm.haproxy[each.key].vm_id
nic_id = outscale_nic.haproxy_secondary[each.key].nic_id
device_number = 1
}
# L'EIP egress reste attachée en permanence à la NIC primaire (eth0).
# L'agent OCF attache la flottante à la NIC SECONDAIRE (eth1) :
# osc-cli api LinkPublicIp --PublicIp 5.x.x.x --NicId <secondary-nic-id>

L'agent OCF doit alors interroger l'IMDS pour récupérer l'ID de la NIC secondaire, pas le vmId. La NIC primaire garde son EIP egress qui ne bouge jamais — accès API permanent garanti.

Pré-requis bastion — LoginGraceTime adapté au ProxyJump

Section intitulée « Pré-requis bastion — LoginGraceTime adapté au ProxyJump »

Si Ansible orchestre le cluster via un bastion en ProxyJump (cas typique du capstone), un apt install pacemaker long peut faire timeout la session SSH preauth côté bastion sshd. Le hardening du Chap 3 met LoginGraceTime 30 par défaut — trop court pour un install qui dépasse souvent la minute via tunnel ProxyJump.

Override mandatoire dans le drop-in du bastion /etc/ssh/sshd_config.d/10-capstone-hardening.conf :

LoginGraceTime 120

Côté Ansible (ansible.cfg) :

[defaults]
timeout = 60 # default 10s — trop court pour ProxyJump
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=600s -o ServerAliveInterval=30 -o ServerAliveCountMax=10

ControlPersist=600s (10 min) garde le master SSH en vie pour toute la durée d'un run. ServerAliveInterval=30 + CountMax=10 tolère 5 minutes sans I/O sans tuer la connexion (cas d'un apt long qui ne pipe rien sur stdout).

Paquet PyPI osc-sdk — fournit le binaire osc-cli

Section intitulée « Paquet PyPI osc-sdk — fournit le binaire osc-cli »

Le binaire osc-cli est packagé sur PyPI sous le nom osc-sdk (et non osc-cli). Pour l'installer via pip dans le playbook :

- name: Installer osc-cli via pip (paquet PyPI s'appelle osc-sdk)
ansible.builtin.pip:
name: osc-sdk # ← pas "osc-cli" !
state: present
executable: pip3
extra_args: "--break-system-packages"

L'erreur classique : pip install osc-cli retourne Could not find a version that satisfies the requirement osc-cli.

pcs cluster setup --force — idempotence en cas de run partiel

Section intitulée « pcs cluster setup --force — idempotence en cas de run partiel »

Si un run précédent du playbook a partiellement créé le cluster sur un nœud (par exemple un pcs cluster setup qui a réussi sur 2 nœuds sur 3), le second run échoue avec The host seems to be in a cluster already. La résolution propre :

Fenêtre de terminal
pcs cluster setup capstone-haproxy --start --wait=60 --force \
10.10.0.233 10.11.0.29 10.12.0.153

--force écrase la conf existante sur les nœuds qui en ont déjà une. C'est acceptable en lab et au déploiement initial — pas en exploitation continue où on préférera pcs cluster destroy + pcs cluster setup pour avoir un audit propre.

Le déploiement Ansible copie le script, le rend exécutable (chmod 755) et vérifie son fonctionnement.

- name: Déploiement de l'agent OCF EIP
hosts: cluster-node
become: true
tasks:
- name: Création du répertoire pour les agents personnalisés
file:
path: /usr/lib/ocf/resource.d/custom
state: directory
mode: '0755'
- name: Copie de l'agent OCF
copy:
src: files/osc-eip
dest: /usr/lib/ocf/resource.d/custom/osc-eip
mode: '0755'
- name: Vérification de l'agent
# Test que l'agent répond correctement à meta-data
command: /usr/lib/ocf/resource.d/custom/osc-eip meta-data
register: agent_test
failed_when: agent_test.rc != 0
changed_when: false

La surveillance est essentielle. La commande pcs status fournit une vue d'ensemble : état des nœuds, ressources et contraintes. C'est le premier réflexe en cas de problème.

Fenêtre de terminal
# Vérification de l'état global du cluster
pcs status
Cluster name: ha-cluster
Cluster Summary:
* Stack: corosync
* Current DC: haproxy-a (version 2.0.5-9) - partition with quorum
* Last updated: Sun Jan 15 10:30:45 2024
* Last change: Sun Jan 15 09:15:20 2024
* 2 nodes configured
* 2 resource instances configured
Node List:
* Online: [ haproxy-a haproxy-b ]
Full List of Resources:
* haproxy (systemd:haproxy): Started haproxy-a
* ElasticIP (ocf::custom:osc-eip): Started haproxy-a
Daemon Status:
corosync: active/enabled
pacemaker: active/enabled
pcsd: active/enabled

La commande crm_mon offre une surveillance temps réel (option -1 pour vue instantanée). Les logs sont essentiels au diagnostic :

  • Corosync : /var/log/corosync/corosync.log (communication, détection pannes)
  • Pacemaker : journalctl -u pacemaker (décisions ressources, basculements)
  • HAProxy : journalctl -u haproxy (trafic)
Fenêtre de terminal
# Surveillance des logs Pacemaker en temps réel
journalctl -u pacemaker -f
# Logs Corosync pour déboguer la communication cluster
tail -f /var/log/corosync/corosync.log
# Logs HAProxy pour analyser le trafic
journalctl -u haproxy -f

Les fail-counts et migration-threshold (détaillés dans le guide Corosync/Pacemaker) gèrent les échecs. Pacemaker incrémente le compteur à chaque échec. Atteinte du seuil = basculement automatique. Réinitialisation via pcs resource cleanup.

Fenêtre de terminal
# Consultation des compteurs d'échecs
pcs resource failcount show
# Réinitialisation du compteur pour une ressource spécifique
pcs resource cleanup ElasticIP
# Réinitialisation globale de tous les compteurs
pcs resource cleanup

Pour la maintenance, mettre un nœud en standby via pcs node standby déplace les ressources sans alarmes. Le rechargement HAProxy (systemctl reload) se fait sans interruption ni détection d'échec par Pacemaker.

Fenêtre de terminal
# Rechargement de la configuration HAProxy sans interruption
systemctl reload haproxy
# Vérification que Pacemaker voit toujours la ressource comme active
pcs resource show haproxy --full

Cause la plus fréquente de dysfonctionnement. Vérifier les routes réseau OS avec ip route show et ping. Contrôler l'ouverture des ports UDP 5404-5405.

Fenêtre de terminal
# Vérification des routes configurées
ip route show
# Test de connectivité vers l'autre nœud
ping -c 3 10.0.1.10
# Vérification que les ports Corosync sont ouverts
nc -zu 10.0.1.10 5404
nc -zu 10.0.1.10 5405

Généralement : authentification API ou erreurs agent OCF. Tester avec pcs resource debug-monitor ElasticIP et vérifier les credentials osc-cli.

Fenêtre de terminal
# Test manuel de l'agent OCF
/usr/lib/ocf/resource.d/custom/osc-eip monitor
# Vérification des logs de la ressource
pcs resource debug-monitor ElasticIP
# Test de l'API Outscale avec osc-cli
osc-cli api ReadPublicIps --profile default

Corosync est sensible aux décalages d'horloge. Utiliser NTP ou chrony est indispensable (timedatectl status pour vérifier).

Fenêtre de terminal
# Vérification de l'heure système
timedatectl status
# Synchronisation immédiate avec NTP
chronyc makestep
# Activation de la synchronisation automatique
systemctl enable --now chronyd

Vérifier : connectivité réseau, Security Groups, santé services Nginx. Utiliser socat pour consulter les stats HAProxy.

Fenêtre de terminal
# Consultation des statistiques HAProxy
echo "show stat" | socat stdio /run/haproxy/admin.sock
# Test de connectivité vers un backend
curl -v http://10.0.0.20:80
# Vérification des health checks
pcs resource show haproxy --full | grep monitor

Situation rare avec bonne config. Solution : arrêter un nœud, vérifier l'autre, puis redémarrer pour resynchronisation.

Fenêtre de terminal
# Arrêt complet d'un nœud
pcs cluster stop haproxy-b
# Vérification sur l'autre nœud
pcs status
# Redémarrage du nœud arrêté
pcs cluster start haproxy-b

Améliore drastiquement la stabilité avec quorum natif. Retirer two_node: 1 et le cluster tolère l'isolation d'un nœud sans split-brain.

Amélioration majeure : isolation complète du nœud défaillant avant basculement, éliminant les corruptions de données. Nécessite un agent fencing pour API Outscale.

  • TLS : termination sur HAProxy ou passthrough, automatisable avec Let's Encrypt
  • Monitoring : Prometheus + Grafana pour métriques HAProxy, Corosync et Pacemaker
  • Multi-applications : frontends HAProxy multiples avec contraintes colocation
  • Persistance : DRBD pour réplication volumes entre nœuds, géré par Pacemaker

Cette architecture démontre qu'une infrastructure haute disponibilité robuste et souveraine est possible sur Outscale CloudGouv sans dépendre de services managés. L'approche multi-AZ avec VPC Peering offre une excellente résilience, tandis que le cluster Corosync/Pacemaker garantit des basculements automatiques rapides.

L'automatisation complète (Terraform + Ansible) rend cette infrastructure reproductible et maintenable, réduisant considérablement les erreurs humaines.

L'EIP flottante gérée par un agent OCF personnalisé illustre la flexibilité de l'open source : un mécanisme de basculement en quelques centaines de lignes, parfaitement intégré aux primitives Outscale.

Cette solution constitue une base solide pour des environnements de production critiques nécessitant un contrôle complet de la pile, extensible selon les besoins (fencing, TLS, nœuds supplémentaires, multi-services).

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn