Aller au contenu
English
Cloud medium

PostgreSQL managé en réseau privé : sauvegarder, perdre, restaurer

Mesuré live le ·fr-par·scw 2.62.0

80 min de lecture

Une base managée Scaleway est joignable depuis Internet dès sa création, et la restaurer l'y remet. Ce sont les deux faits que cette leçon mesure, puis corrige. Vous allez créer une base PostgreSQL dont l'endpoint public n'existe pas, prendre un snapshot, provoquer la perte, restaurer, et constater que la base revenue porte une adresse publique et une ACL ouverte à tous. Vous en sortirez avec les deux seuls chiffres qu'un exploitant doit connaître, le RPO et le RTO, mesurés plutôt que promis.

  • Choisir une version de PostgreSQL sur le bon critère, sa fin de vie
  • Créer une base sans aucun endpoint public, et le prouver
  • Lire le réglage qui fixe votre RPO, actif par défaut et jamais annoncé
  • Mesurer un RTO de restauration, et savoir pourquoi ce n'est pas celui du service
  • Refermer le réseau qu'une restauration vient de rouvrir
  • Purger les artefacts qui survivent à la suppression de la base
  • scw 2.62.0 ou plus récent, profil dédié au lab
  • Un VPC compris, au moins dans ses grandes lignes (voir VPC et subnets)
  • jq et openssl sur le poste
  • Un projet dédié, avec alerte de budget

Le critère est la date de fin de vie, jamais la place dans la liste. L'API propose des moteurs que vous ne devriez pas prendre, et rien dans son affichage ne les distingue.

Fenêtre de terminal
scw rdb engine list region=fr-par -o json \
| jq -r '.[] | select(.engine_type=="PostgreSQL") | "\(.name)\tfin de vie \(.end_of_life[0:10])"'

Relevé le 2026-09-12 avec scw 2.62.0, la sortie contient 9 versions de PostgreSQL, dont 5 déjà périmées. La commande en filtre une dixième, MySQL-8, que le même appel renvoie :

VersionFin de vieÉtat au 2026-09-12
PostgreSQL-172029-11-08recommandée, 1153 jours devant elle
PostgreSQL-162028-11-09789 jours
PostgreSQL-152027-11-11425 jours
PostgreSQL-142026-11-1261 jours, à ne plus démarrer
PostgreSQL-13 et antérieures2025-11-13 et avantpérimées, toujours proposées

Se servir dans la liste sans lire end_of_life, c'est démarrer une base sur un moteur non supporté le jour même. C'est le même motif que bssd, que scw rdb node-type list continue de proposer parmi les available_volume_types alors que l'API le refuse à la création : la commande qui inventorie propose ce que la commande qui crée refuse.

Pourquoi une base créée par défaut est-elle exposée à Internet ?

Section intitulée « Pourquoi une base créée par défaut est-elle exposée à Internet ? »

Parce que Scaleway lui attribue un endpoint public sans le demander. La documentation officielle l'énonce sans détour : « When you create a Database Instance, a public endpoint is automatically attributed to it ». Ce n'est pas une erreur de manipulation, c'est le comportement par défaut.

Ce que cela produit, relevé sur une base créée sans aucun argument d'endpoint :

ÉlémentValeur par défaut constatée
Endpointpublic, via load balancer
ACL entrante0.0.0.0/0, action allow, description « Allow All »
Portnon standard, tiré à la création

La base répond donc au protocole PostgreSQL depuis n'importe où, et seule la robustesse du mot de passe la protège.

Le port mérite une mise en garde à part. Un premier test visant 5432 reçoit Connection refused, ce dont on conclut volontiers que l'isolation fonctionne. Le refus venait du mauvais port, pas d'une protection. Un tutoriel qui teste 5432 et annonce « c'est fermé » enseigne l'inverse de la réalité : c'est la différence entre un test qui passe et un test qui prouve.

On commence par le réseau, parce qu'il est la protection.

Fenêtre de terminal
VPC_ID=$(scw vpc vpc create name=donnees-vpc region=fr-par -o json | jq -r '.id')
PN_ID=$(scw vpc private-network create name=donnees-pn-db \
vpc-id="${VPC_ID}" region=fr-par -o json | jq -r '.id')

Vient le mot de passe, et il pose deux pièges qui coûtent chacun une session.

Fenêtre de terminal
MDP_DB="Aa1!$(openssl rand -base64 18 | tr -d '\n=+/')"

Le premier : l'API refuse un openssl rand -base64 nu environ une fois sur deux, avec le message password must be between 8 and 128 characters, contain at least one digit, one uppercase, one lowercase and one special character. Base64 n'emploie que [A-Za-z0-9+/=], et sur 24 caractères la probabilité qu'aucun + ni / n'apparaisse avoisine 47 % : le refus est fréquent, pas systématique, ce qui est pire, puisqu'un script peut passer plusieurs fois avant d'échouer. Le second est plus sournois : la parade naturelle tr -dc 'A-Z' </dev/urandom | head -c 4 tue le script sous set -e -o pipefail, parce que head ferme le tube dès qu'il a son compte, tr reçoit SIGPIPE, le pipeline rend 141 à cause de pipefail, et c'est set -e qui arrête tout. Le symptôme est trompeur, le script s'arrête sans message. Le préfixe fixe garantit les quatre classes sans refermer aucun tube.

Un troisième piège ne concerne que le copier-coller. Dans un shell interactif, !$ déclenche l'expansion d'historique et remplace ces deux caractères par le dernier argument de la commande précédente : le mot de passe obtenu n'est pas celui qu'on croit. En script, l'expansion est désactivée. Pour coller la ligne à la main, isolez le préfixe en apostrophes simples ou posez set +H au préalable.

La base, enfin.

Fenêtre de terminal
DB_ID=$(scw rdb instance create \
name=donnees-pg \
region=fr-par \
engine=PostgreSQL-17 \
node-type=DB-DEV-S \
is-ha-cluster=false \
user-name=labuser \
password="${MDP_DB}" \
volume-type=sbs_5k \
volume-size=10GB \
init-endpoints.0.private-network.private-network-id="${PN_ID}" \
init-endpoints.0.private-network.enable-ipam=true \
init-endpoints.0.load-balancer=false \
-o json | jq -r '.id')

Trois arguments portent toute la leçon, et le premier mérite d'être compris plutôt que recopié.

Ce n'est pas load-balancer=false qui retire l'endpoint public. C'est déjà la valeur par défaut du CLI. Ce qui l'évite, c'est de fournir un init-endpoints qui ne contient qu'un endpoint privé : l'API ne crée l'endpoint public « systématiquement » que lorsque init_endpoints est vide. L'écrire rend l'intention explicite, sans rien changer au résultat.

enable-ipam=true fait réserver l'adresse privée par le service d'adressage plutôt que de la choisir à la main. Et volume-type=sbs_5k choisit du Block, bssd étant refusé par l'API bien qu'encore listé.

Un dernier argument est déprécié et n'est conservé ici que parce que c'est lui qui a été exécuté : is-ha-cluster est remplacé par high-availability-mode (disabled, single_zone, multiple_zone) depuis le 12 août 2026. Sur une nouvelle base, employez le remplaçant.

On compte les endpoints, on ne les suppose pas.

Fenêtre de terminal
scw rdb instance get "${DB_ID}" region=fr-par -o json \
| jq '[.endpoints[] | {ip, port, prive: (.private_network != null), public: (.load_balancer != null)}]'

La sortie doit afficher un seul endpoint, privé, et aucun public :

[{"ip": "172.16.16.2", "port": 5432, "prive": true, "public": false}]

Deux choses à relever. Le port est 5432, le port standard : c'est l'endpoint public qui est tiré au hasard, pas le privé. Et la base a mis 264 secondes à passer ready, contre 184 secondes pour son équivalent public mesuré le même jour. L'isolation coûte 80 secondes, une fois, à la création.

Elle est active sans que vous l'ayez demandée, et elle fixe votre RPO à 24 heures.

Fenêtre de terminal
scw rdb instance get "${DB_ID}" region=fr-par -o json | jq '.backup_schedule'

Relevé le 2026-09-12 :

{"frequency": 24, "retention": 7, "disabled": false, "next_run_at": "2026-09-13T16:17:31Z"}

Le RPO, Recovery Point Objective, est ce que vous perdez : l'écart entre le dernier point récupérable et l'incident. Avec ce réglage, il vaut jusqu'à 24 heures. Ce n'est pas un défaut du produit, c'est une décision par défaut, et la plupart des tutoriels ne la mentionnent jamais.

Un détail de vocabulaire décide de la suite : sur un volume Block, l'autobackup produit des snapshots, pas des backups. La documentation prévient que « although they bear the same feature name, automated snapshots and automated backups are different in their design ». Conséquence pratique : la restauration ne remet pas les données dans la base existante, elle crée une base neuve.

Fenêtre de terminal
SNAP_ID=$(scw rdb snapshot create instance-id="${DB_ID}" region=fr-par \
name=donnees-pg-avant-incident -o json | jq -r '.id')
until [ "$(scw rdb snapshot get "${SNAP_ID}" region=fr-par -o json | jq -r '.status')" = "ready" ]; do
sleep 10
done

Commande acceptée en 1 seconde, snapshot ready en 10 secondes sur un volume de 10 Go.

Le snapshot survit-il à la base qu'il sauvegarde ?

Section intitulée « Le snapshot survit-il à la base qu'il sauvegarde ? »

On provoque la perte, plutôt que de l'imaginer.

Fenêtre de terminal
scw rdb instance delete "${DB_ID}" region=fr-par
scw rdb snapshot get "${SNAP_ID}" region=fr-par -o json \
| jq '{name, status, instance_id, instance_name}'

La sortie doit afficher un snapshot ready qui référence encore l'identifiant et le nom d'une base qui n'existe plus. La documentation l'annonce pour les snapshots manuels, ceux de ce lab : « When you delete an Instance, its associated snapshots are not deleted. Snapshots can be manually deleted or will be deleted after the retention period defined ». Elle le dit séparément pour les artefacts d'autobackup : « when you delete a Database Instance, the backups and snapshots created with the autobackup feature are not automatically deleted ».

Bonne nouvelle pour la reprise, mauvaise pour la facture : le snapshot pèse 10 Go et apparaît sur la note en ligne Database Storage SBS 5K Snapshot. Détruire la base ne suffit donc pas à arrêter le compteur.

Fenêtre de terminal
NEW_ID=$(scw rdb snapshot restore "${SNAP_ID}" region=fr-par \
instance-name=donnees-pg-restauree node-type=DB-DEV-S -o json | jq -r '.id')
until [ "$(scw rdb instance get "${NEW_ID}" region=fr-par -o json | jq -r '.status')" = "ready" ]; do
sleep 20
done

225 secondes entre la décision de restaurer et une base de nouveau ready. C'est le RTO, Recovery Time Objective, dans sa version la plus favorable.

Ce n'est pas le RTO de votre service. La base restaurée est une base neuve, avec un nouvel endpoint : rien ne rebranche l'application toute seule. Le temps de rétablissement réel ajoute donc la mise à jour de la configuration applicative, son redéploiement, et la vérification. Restaurer des données n'est pas rétablir un service, et confondre les deux fait promettre un RTO qu'on ne tiendra pas.

Restaurer une base privée la rend-elle publique ?

Section intitulée « Restaurer une base privée la rend-elle publique ? »

Oui, et c'est le fait le plus important de cette leçon.

Fenêtre de terminal
scw rdb instance get "${NEW_ID}" region=fr-par -o json \
| jq '[.endpoints[] | {ip, port, prive: (.private_network != null)}]'
scw rdb acl list instance-id="${NEW_ID}" region=fr-par -o json

La base d'origine n'avait aucun endpoint public. La base restaurée, relevée le 2026-09-12 :

[{"ip": "203.0.113.84", "port": 9263, "prive": false}]
[{"ip": "0.0.0.0/0", "protocol": "tcp", "direction": "inbound",
"action": "allow", "description": "Allow All", "port": 9263}]

Adresse publique, port aléatoire, aucun endpoint privé, et une ACL ouverte à tous. La restauration ne restaure pas la topologie réseau : elle applique les valeurs par défaut du produit.

Une procédure de reprise après incident réexpose donc la base à Internet, en silence. Le moment est mal choisi : on restaure sous pression, on vérifie que les données sont là, on rebranche l'application, et personne ne relit la configuration réseau. Aucune page de documentation ne signale ce comportement.

La conséquence tient en une phrase : un plan de reprise ne s'arrête pas aux données, il referme le réseau.

Fenêtre de terminal
EP_PUBLIC=$(scw rdb instance get "${NEW_ID}" region=fr-par -o json \
| jq -r '.endpoints[] | select(.load_balancer != null) | .id')
scw rdb endpoint delete "${EP_PUBLIC}" instance-id="${NEW_ID}" region=fr-par

Relancez ensuite la vérification d'isolation : la sortie doit afficher zéro endpoint public. Sans cette étape, l'exercice de reprise a laissé le système moins sûr qu'avant l'incident.

Trois familles, dans cet ordre, et les snapshots ne partent pas tout seuls.

Fenêtre de terminal
scw rdb instance delete "${NEW_ID}" region=fr-par
# On attend la disparition RÉELLE : `rdb instance get` sort en erreur pendant
# tout état transitoire, y compris `deleting`. Un échec ne prouve donc rien.
until [ "$(scw rdb instance list region=fr-par -o json | jq 'length')" = "0" ]; do
sleep 10
done
scw rdb snapshot list region=fr-par -o json | jq -r '.[].id' | while read -r SNAP; do
scw rdb snapshot delete "${SNAP}" region=fr-par
done
scw vpc private-network delete "${PN_ID}" region=fr-par
scw vpc vpc delete "${VPC_ID}" region=fr-par

Le commentaire du milieu vaut un avertissement. scw rdb instance get sort en erreur pendant un état transitoire, avec le message transient state error for resource 'instance' suivi, en sortie -o json, de current_state: 'deleting' ; en sortie humaine, la même erreur s'écrit Resource instance with ID … is in a transient state 'deleting'.

Le mécanisme mérite d'être nommé, parce qu'il explique pourquoi le symptôme surprend : ce n'est pas la lecture de l'instance qui échoue, c'est l'appel secondaire que le CLI enchaîne derrière elle pour lister les règles d'ACL. Conclure « la base est détruite » depuis l'échec de cette commande donne donc un verdict faux : la base facture encore. On compte les objets qui restent, ce qui est une identité observable.

La vérification de sortie ferme la boucle.

Fenêtre de terminal
scw rdb instance list region=fr-par -o json | jq 'length'
scw rdb snapshot list region=fr-par -o json | jq 'length'
scw vpc private-network list region=fr-par -o json | jq 'length'

Les trois sorties doivent afficher 0. Coût de la session complète, relevé dans scw billing consumption list : 0,06 EUR.

Toutes les bases ne méritent pas le même isolement, et le tableau ci-dessous part du besoin plutôt que du réglage. Il se lit de gauche à droite : la colonne du milieu dit ce qu'on pose, la dernière ce qu'on accepte en échange.

BesoinChoixCe qu'il coûte
Une base servie uniquement par des applications du même VPCendpoint privé seul, init-endpoints sans load balancer80 s de plus à la création, et plus aucun accès depuis votre poste
Un accès d'administration ponctuel depuis l'extérieurendpoint privé plus un bastion sur Public Gatewayune ressource de plus à exploiter et à payer
Un prototype jetable, détruit dans l'heureendpoint public avec ACL restreinte à votre adresseune ACL à tenir à jour, et un port non standard à retenir
Une base de démonstration laissée en ligneaucun de ces choix ne convientle défaut ouvre 0.0.0.0/0 : c'est le cas à ne jamais reproduire

La première ligne est celle du lab, et c'est le défaut que devrait avoir toute base de production. Les deux suivantes sont des compromis assumés. La dernière est celle qu'on obtient sans rien décider.

Trois choses ont marché, une a résisté, et une a démenti la documentation.

Ce qui a été prouvéMesure
Une base peut naître sans aucun endpoint public0 public, 1 privé sur 172.16.16.2:5432
L'isolation a un coût en temps, pas en argent264 s contre 184 s, soit 80 s
Un snapshot survit à la base qu'il sauvegardesnapshot ready référençant une base disparue, 10 Go facturés
La restauration rouvre le réseaubase revenue publique, ACL 0.0.0.0/0, port aléatoire
Le RTO annoncé n'est pas celui du service225 s jusqu'à ready, endpoint à rebrancher ensuite

Ce qui a résisté : l'aide du CLI, qui annonce l'endpoint public « systematically present » alors que l'API accepte de s'en passer. Croire l'aide aurait fait conclure que l'isolation était impossible.

Ce qui a démenti la documentation : rien n'annonce que la restauration produit une base publique. Le fait ne s'obtient qu'en jouant la reprise.

Dépendances de destruction, validées en lab et non devinées :

ObjetBloqueConséquence
Baserienmais sa suppression est asynchrone
Snapshotriensurvit à la base, à purger séparément
Private Networksa suppressiontant que la base y est rattachée
VPCsa suppressiontant que le Private Network existe

Coût de la session : 0,06 EUR, relevé dans scw billing consumption list.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ces valeurs décident de l'architecture avant d'écrire la moindre commande, et aucune ne se devine.

  • Un seul Private Network par base. La FAQ officielle : « currently, you can connect one Private Network to each Database Instance ». Un projet qui sépare ses environnements par réseau sépare donc aussi ses bases.
  • Sauvegarde manuelle sur Block : 585 Go maximum. La documentation réserve les backups manuels aux bases dont le stockage est « inferior or equal to 585 GB » sur Block Storage.
  • Rétention par défaut : 7 jours, fréquence 24 h. Relevé dans backup_schedule le 2026-09-12.
  • IPv6 non supporté sur les Database Instances (FAQ).
  • L'autobackup ignore les quotas : « if a Database Instance has autobackup set up, even if the quota is exceeded, the backup or snapshot will still be automatically created ». Une base sauvegardée continue donc de produire des artefacts facturés au delà du quota.

Les deux premières lignes se ressemblent et ne se diagnostiquent pas pareil : un message de politique de mot de passe se lit, un script qui s'arrête sans rien dire se traque.

SymptômeCause probableSolution
password must be between 8 and 128 characters, contain at least one digit, one uppercase, one lowercase and one special charactermot de passe issu de base64, sans caractère spécialpréfixer les quatre classes, comme dans le bloc ci-dessus
Le script s'arrête sans message après la génération du mot de passehead a fermé un tube, tr a reçu SIGPIPE, le pipeline rend 141 sous pipefailne pas refermer de tube prématurément
bssd volume type is deprecated, this action is no longer supported. Please use sbs volume typebssd est proposé par node-type list et refusé par l'APIemployer sbs_5k ou sbs_15k
transient state error for resource 'instance' sur un getla commande échoue pendant provisioning, initializing et deletingcompter les objets restants dans un list, jamais interpréter le code de retour
Connection refused sur le port 5432 d'une base publiquel'endpoint public est sur un port aléatoirelire le port dans .endpoints[].port avant de tester

Ces quatre erreurs ont un point commun : elles font confiance à un défaut, à une liste ou à un code de retour plutôt qu'à une observation.

AntipatternConséquenceDiscipline
Créer la base sans arguments d'endpointbase joignable depuis Internet, ACL « Allow All », protégée par le seul mot de passeinit-endpoints et load-balancer=false dès la création
Choisir la version du moteur dans la listedémarrage sur un moteur déjà en fin de vietrier sur end_of_life, jamais sur l'ordre d'affichage
Considérer la restauration comme terminée quand la base est readybase de reprise publique, et RTO du service sous-estimérefermer l'endpoint public, puis rebrancher, puis vérifier
Supprimer la base et passer à autre choseun snapshot manuel sans expires-at facture jusqu'à sa suppression, invisible six mois plus tardpurger les snapshots comme une étape à part entière, ou poser expires-at à la création

Question clé : qui peut ouvrir une connexion à cette base, à cet instant ? La réponse change sans prévenir, puisqu'une restauration recrée un endpoint public avec une ACL ouverte. La discipline consiste à faire du contrôle d'endpoint une étape du plan de reprise, vérifiée par la même commande que celle du jour de la création, et non une propriété acquise à la création.

Question clé : combien de données acceptez-vous de perdre, et combien de temps d'indisponibilité ? Les deux se lisent, backup_schedule.frequency pour le premier, un essai chronométré pour le second. La discipline est de jouer la restauration au moins une fois : un plan de reprise jamais exécuté n'est pas un plan, et c'est cet essai qui a révélé l'endpoint public. Rappel utile : en mode single_zone, le seul documenté au 2026-09-12, la haute disponibilité place les deux nœuds dans le même datacenter, sur deux baies, et ne remplace donc pas une sauvegarde. L'API expose un mode multiple_zone depuis le 11 août 2026, sans page produit ni entrée de changelog à cette date : à ne pas considérer comme un choix de production tant qu'il n'est pas documenté.

Question clé : qu'est-ce qui continue de facturer après la suppression de la base ? Les snapshots. La politique est posée dès la création, mais le premier snapshot automatique n'arrive que 24 heures plus tard, la date de création servant de base ; la rétention vaut 7 jours par défaut. La discipline est une vérification de sortie qui reliste chaque famille, snapshots compris, plutôt que de conclure depuis la disparition de l'instance.

  • Un endpoint public est attribué par défaut à toute base créée sans init-endpoints, avec une ACL 0.0.0.0/0 décrite « Allow All ».
  • init-endpoints.0.load-balancer=false supprime cet endpoint, malgré une aide du CLI qui le dit « systematically present ».
  • L'endpoint privé écoute sur 5432 ; c'est le public qui est tiré sur un port aléatoire.
  • L'isolation coûte 80 secondes à la création : 264 s contre 184 s, mesuré le 2026-09-12.
  • Le RPO par défaut est de 24 heures, fixé par backup_schedule.frequency, actif sans demande.
  • Le RTO mesuré est de 225 secondes, et ce n'est qu'un plancher : la base restaurée porte un nouvel endpoint.
  • Restaurer une base privée la rend publique, ACL ouverte comprise. Le plan de reprise doit refermer le réseau.
  • Les snapshots survivent à la base et facturent : leur purge est une étape, pas une conséquence.

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn