RDS Terraform : base managée sans fuite
À la fin de ce tutoriel, vous saurez comparer RDS et une base self-managed sur EC2, déclarer un
aws_db_subnet_group, un Security Group et unaws_db_instance(PostgreSQL ou MySQL), activer le stockage chiffré, générer le mot de passe avecrandom_password+ outputsensitive(jamais de MDP en clair dans le HCL), choisirskip_final_snapshotselon lab vs prod, et exécuter un lab optionnel / plan-only enca-central-1— Terraform ≥ 1.9, AWS provider ~> 5.0. Message clé : RDS coûte cher — privilégiezplan,db.t4g.micro(Free Tier selon compte), et destroy immédiat.Niveau : Intermédiaire · Temps estimé : 40–50 min (lecture) / +15–25 min si apply · Versions testées : Terraform ≥ 1.9, AWS provider ~> 5.0 · Dernière vérification : 2026-09-10
Slug proposé :
terraform-rds· Série : Terraform · Remplace / fusionne : #1689 (RDS Terraform quasi vide)
Prérequis
- Avoir suivi IAM (
terraform-iam) : rôles, least privilege, auth sans clés longues - Idéalement State S3 (
terraform-state-backend-s3) et Modules (terraform-modules-bases) - Notions VPC / sous-réseaux : prochain tuto Terraform VPC (
terraform-vpc) - Terraform ≥ 1.9, AWS CLI v2, profil de lab (
aws sts get-caller-identityOK) - Provider random pour
random_password
Coût estimé : élevé si apply (instance + stockage gp3 à l’heure). Préférez terraform plan. Si apply : db.t4g.micro, 20 Gio, destroy immédiat. Aucun secret en clair dans le HCL / tfvars.
Auth sans secrets.
AWS_PROFILE/ SSO uniquement. Mot de passe RDS :random_password+sensitive/ Secrets Manager — jamais hardcodé.
Ce que nous allons construire
Lab terraform rds (ca-central-1) — plan-only privilégié
├── versions.tf / providers.tf → TF ≥ 1.9, aws ~> 5.0, random, region ca-central-1
├── Étapes 1–4 → RDS vs EC2, subnet group, SG, aws_db_instance
├── Secrets → random_password + sensitive (pas de MDP en HCL)
├── apply OPTIONNEL → db.t4g.micro → destroy IMMÉDIAT
└── Checklist + erreurs + quiz
(Alt : « Terraform RDS : aws_db_instance, subnet group, chiffrement, random_password — ca-central-1 ».)
Étape 1 — RDS vs self-managed sur EC2
Choisir RDS, c’est déléguer les patches du moteur, les sauvegardes automatiques et (optionnellement) le failover Multi-AZ. En contrepartie, vous perdez l’accès root OS et certaines extensions. Pour 80 % des applications web et APIs internes, ce compromis est gagnant : moins d’astreintes « disque plein à 3 h », plus de focus sur le schéma et les requêtes.
Cas réel : une équipe héberge Postgres sur EC2 « pour économiser ». Six mois plus tard, les snapshots sont manuels, le chiffrement disque est partiel, et personne n’a testé un restore. Migrer vers aws_db_instance avec storage_encrypted = true et une rétention de backup ≥ 7 jours réduit ce risque opérationnel — à condition de soigner le réseau (subnets privés, SG least privilege).
Piège coût : un db.r6g.large Multi-AZ oublié après un POC facture jour et nuit. Le lab P2 impose db.t4g.micro, plan-only prioritaire, destroy immédiat.
| Critère | Amazon RDS | Base self-managed (EC2) |
|---|---|---|
| Patches OS / moteur | Gérés AWS | À votre charge |
| Backups / snapshots | Automatiques + manuels | À scripter |
| Multi-AZ / failover | Option native (coût ++) | À concevoir |
| Contrôle kernel / extensions | Limité | Total |
| Terraform | aws_db_instance + subnet group + SG |
aws_instance + EBS + user_data |
Pour un lab DevOps et la plupart des applis standard, RDS gagne : backups et chiffrement en un attribut. Le self-managed reste pour extensions non supportées ou tuning extrême. terraform rds exige quand même un réseau propre : sous-réseaux (idéalement privés), SG qui n’ouvre le port qu’aux apps.
Étape 2 — Subnet group + Security Group
Le DB subnet group est un prérequis API : RDS refuse de démarrer sans au moins deux subnets dans des AZ distinctes. En lab, le VPC default convient pour un plan ; en prod, placez RDS dans des subnets privés sans route IGW (terraform-vpc). Ouvrir 0.0.0.0/0 sur 5432 est une erreur de sécurité classique — préférez le SG applicatif ou une IP /32 de bastion/SSM.
RDS exige un DB subnet group (≥ 2 AZ) et un SG. Lab : VPC default ca-central-1 ; prod : subnets privés (voir terraform-vpc).
# versions.tf
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
# providers.tf
provider "aws" {
region = "ca-central-1"
# Auth : AWS_PROFILE / SSO — jamais de clés ici
}
# variables.tf
variable "project" {
type = string
description = "Préfixe lab unique (sans secret)."
}
variable "vpc_id" {
type = string
description = "VPC du lab (souvent le VPC default)."
}
variable "subnet_ids" {
type = list(string)
description = "≥ 2 subnet IDs dans des AZ distinctes."
}
variable "allowed_cidr" {
type = string
description = "CIDR autorisé en entrée Postgres (IP /32 ou plage app)."
default = "10.0.0.0/16"
}
# network.tf
resource "aws_db_subnet_group" "lab" {
name = "${var.project}-db-subnets"
subnet_ids = var.subnet_ids
tags = {
Name = "${var.project}-db-subnets"
ManagedBy = "terraform"
Lab = "terraform-rds"
Region = "ca-central-1"
}
}
resource "aws_security_group" "rds" {
name = "${var.project}-rds-sg"
description = "Lab P2 : Postgres depuis CIDR / SG app uniquement"
vpc_id = var.vpc_id
ingress {
description = "Postgres depuis plage lab"
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = [var.allowed_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project}-rds-sg"
Lab = "terraform-rds"
}
}
Points durs : ≥ 2 AZ dans le subnet group ; évitez 0.0.0.0/0 sur 5432 (préférez IP /32 ou SG app) ; MySQL = port 3306, Postgres = 5432.
Étape 3 — aws_db_instance : engine, storage, encryption
storage_encrypted = true doit être la norme dès le lab : le repasser à true plus tard peut imposer un recreate. publicly_accessible = false évite d’exposer l’endpoint sur Internet. backup_retention_period = 0 accélère le destroy en lab mais est inacceptable en prod. Documentez ces écarts lab/prod dans le README du module pour qu’un copier-coller ne recrée pas un lab en production.
Cœur de terraform rds : PostgreSQL, classe Free Tier friendly, stockage gp3 chiffré — sans MDP en clair.
# secrets.tf — JAMAIS de mot de passe littéral dans le HCL
resource "random_password" "db" {
length = 24
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
}
# main.tf
resource "aws_db_instance" "lab" {
identifier = "${var.project}-pg"
engine = "postgres"
engine_version = "16"
instance_class = "db.t4g.micro" # Free Tier selon compte / Region
allocated_storage = 20
max_allocated_storage = 20
storage_type = "gp3"
storage_encrypted = true
db_name = "appdb"
username = "appadmin"
password = random_password.db.result
db_subnet_group_name = aws_db_subnet_group.lab.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = false
multi_az = false
backup_retention_period = 0
skip_final_snapshot = true # LAB UNIQUEMENT
deletion_protection = false
apply_immediately = true
tags = {
Name = "${var.project}-pg"
ManagedBy = "terraform"
Lab = "terraform-rds"
Region = "ca-central-1"
}
}
output "db_endpoint" {
description = "Endpoint host:port (pas de secret)."
value = aws_db_instance.lab.endpoint
}
output "db_password" {
description = "MDP généré — sensible."
value = random_password.db.result
sensitive = true
}
| Attribut | Lab | Prod (indicatif) |
|---|---|---|
instance_class |
db.t4g.micro |
Selon charge ; Multi-AZ |
storage_encrypted |
true |
true (+ CMK si politique) |
publicly_accessible |
false |
false |
backup_retention_period |
0 |
≥ 7 |
skip_final_snapshot |
true |
false + final_snapshot_identifier |
deletion_protection |
false |
true |
MySQL : engine = "mysql", SG port 3306. Même discipline secrets.
Étape 4 — Secrets : random_password, sensitive, Secrets Manager
Le mot de passe généré vit dans le state Terraform : protégez le backend (S3 + chiffrement + verrouillage, voir terraform-state-backend-s3). L’output sensitive réduit les fuites dans les logs, mais un terraform output -raw en CI public reste dangereux. En prod, poussez le secret vers Secrets Manager et faites lire l’application via IAM — pas via un output collé dans Slack.
random_passwordgénère au apply ; la valeur vit dans le state (backend S3 chiffré —terraform-state-backend-s3).- Output
sensitive = truepour limiter les fuites dans les logs CI. - En prod : Secrets Manager ou SSM
SecureString+ lecture IAM — pas d’output collé dans Slack. - Jamais
-var='db_password=…'dans un historique partagé.
# CONCEPTUEL — prod / module (hors lab minimal)
# resource "aws_secretsmanager_secret" "db" {
# name = "${var.project}/rds/master"
# }
# resource "aws_secretsmanager_secret_version" "db" {
# secret_id = aws_secretsmanager_secret.db.id
# secret_string = jsonencode({
# username = aws_db_instance.lab.username
# password = random_password.db.result
# host = aws_db_instance.lab.address
# port = aws_db_instance.lab.port
# dbname = aws_db_instance.lab.db_name
# })
# }
Factorisez via Modules (terraform-modules-bases) pour réutiliser subnet group + instance + secret sans dupliquer le HCL.
Étape 5 — skip_final_snapshot, coûts, destroy
skip_final_snapshot = true est un raccourci de lab. En prod, un destroy sans snapshot final peut effacer la dernière copie récupérable si les backups automatiques ont expiré. Activez deletion_protection dès que la base contient des données non reproductibles. Surveillez aussi les snapshots manuels orphelins : ils facturent du stockage après la suppression de l’instance.
| Contexte | skip_final_snapshot |
Destroy |
|---|---|---|
| Lab P2 | true |
Rapide, pas de snapshot final (évite coût / oubli) |
| Prod | false |
final_snapshot_identifier obligatoire |
deletion_protection = true en prod bloque un destroy accidentel ; en lab, false. RDS facture instance + stockage + I/O (+ Multi-AZ) : plan-only ou destroy immédiat après apply. Note Free Tier : db.t4g.micro / 20 Gio peuvent être éligibles (750 h/mois selon compte) — vérifiez Billing.
Étape 6 — Lab optionnel : plan-only puis destroy
Le plan suffit souvent pour valider subnet group, SG et attributs aws_db_instance. Un apply RDS prend plusieurs minutes : prévoyez le créneau et le destroy dans la même session. Vérifiez ensuite describe-db-instances filtré sur votre préfixe projet — une instance « available » oubliée est l’un des gaspillages les plus fréquents des comptes lab.
Mode A — Recommandé : validate / plan (0 €)
mkdir -p ~/terraform-rds-lab && cd ~/terraform-rds-lab
# Collez versions.tf, providers.tf, variables.tf, network.tf, secrets.tf, main.tf
export AWS_PROFILE=lab
export AWS_REGION=ca-central-1
PROJECT="votreprenom20260910"
VPC_ID=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true
--query 'Vpcs[0].VpcId' --output text --region ca-central-1)
# Deux subnet IDs d’AZ distinctes → ["subnet-aaa","subnet-bbb"]
terraform init && terraform fmt && terraform validate
terraform plan
-var="project=${PROJECT}"
-var="vpc_id=${VPC_ID}"
-var='subnet_ids=["subnet-AAA","subnet-BBB"]'
-var="allowed_cidr=203.0.113.10/32"
# STOP — RDS coûte cher à l’heure
Mode B — Apply minimal puis destroy immédiat
terraform apply -auto-approve
-var="project=${PROJECT}"
-var="vpc_id=${VPC_ID}"
-var='subnet_ids=["subnet-AAA","subnet-BBB"]'
-var="allowed_cidr=203.0.113.10/32"
terraform output db_endpoint
terraform destroy -auto-approve
-var="project=${PROJECT}"
-var="vpc_id=${VPC_ID}"
-var='subnet_ids=["subnet-AAA","subnet-BBB"]'
-var="allowed_cidr=203.0.113.10/32"
cd ~ && rm -rf ~/terraform-rds-lab
aws rds describe-db-instances --region ca-central-1
--query "DBInstances[?contains(DBInstanceIdentifier, '${PROJECT}')]"
# Attendu : []
Étape 7 — Checklist
- Distinguer RDS managé vs base self-managed EC2.
- Déclarer
aws_db_subnet_group(≥ 2 AZ) + SG port engine. - Créer
aws_db_instance: Postgres/MySQL,gp3,storage_encrypted = true. - Mot de passe via
random_password+ outputsensitive— pas de MDP en HCL. - Lab :
skip_final_snapshot = true; prod :false+ identifier. - Region
ca-central-1, TF ≥ 1.9, provider~> 5.0,db.t4g.micro. - Plan ou destroy immédiat — pas de RDS idle.
Nettoyage
cd ~/terraform-rds-lab
terraform destroy -auto-approve
-var="project=${PROJECT}"
-var="vpc_id=${VPC_ID}"
-var='subnet_ids=["subnet-AAA","subnet-BBB"]'
-var="allowed_cidr=203.0.113.10/32"
cd ~ && rm -rf ~/terraform-rds-lab
# Filet : describe-db-instances + snapshots orphelins + SG Lab=terraform-rds
Erreurs fréquentes
| Symptôme | Cause | Correction |
|---|---|---|
| Subnet group / AZ | 1 subnet ou 2 dans même AZ | ≥ 2 subnets AZ distinctes |
| Password invalide | Trop court / caractères interdits | random_password + override_special RDS-safe |
| Timeout / unreachable | SG trop fermé | Ouvrir 5432 depuis SG app ou IP /32 |
| Destroy bloqué | deletion_protection ou snapshot final |
Lab : protection false, skip_final_snapshot = true |
| Facture après destroy | Snapshots restants | Supprimer snapshots tagués lab |
| Secret dans Git | password = "…" en HCL |
random_password + Secrets Manager |
engine_version invalide |
Version retirée en Region | describe-db-engine-versions --engine postgres |
| State fuité | Backend local + MDP en CI | S3 chiffré ; sensitive ; pas de -raw en pipeline |
| Apply très long / timeout CI | Création RDS > timeout pipeline | Plan-only en CI ; apply manuel chronométré |
|---|---|---|
| Connexion SSL refusée | Paramètre moteur / client | Aligner sslmode client et parameter group |
Nom identifier invalide |
Caractères / longueur | Minuscules, tirets, règles RDS |
Quiz (3 questions)
1. Où placer le mot de passe master RDS dans un lab Terraform propre ?
- A. En clair dans
main.tf - B.
random_password+ outputsensitive(Secrets Manager en prod) - C. Dans un commentaire Git « temporaire »
2. Pour un lab terraform rds économique en ca-central-1 ?
- A.
db.r6g.2xlargeMulti-AZ - B.
db.t4g.micro+ plan-only / destroy immédiat - C. EC2
c7i.metalself-managed obligatoire
3. En production, skip_final_snapshot ?
- A. Toujours
truepour aller vite - B.
false+final_snapshot_identifier - C. Ignorer : les snapshots sont gratuits
4. Pourquoi storage_encrypted = true dès le lab ?
- A. Gratuit et sans effet
- B. Bonne pratique ; activer plus tard peut forcer recreate
- C. Obligatoire seulement avec Multi-AZ
5. Que risque publicly_accessible = true sur RDS ?
- A. Rien si le SG est ouvert
- B. Exposition réseau élargie — à éviter hors besoin justifié
- C. Désactive le chiffrement
Réponses : 1‑B · 2‑B · 3‑B · 4‑B · 5‑B
FAQ
Postgres ou MySQL pour ce lab ? Les deux suivent le même schéma Terraform. Ce guide illustre Postgres (5432) ; pour MySQL, changez engine et le port SG (3306).
Le Free Tier couvre-t-il toujours db.t4g.micro ? Cela dépend du compte et de la Region. Vérifiez Billing / Free Tier avant un apply prolongé. Ne supposez jamais « micro = gratuit ».
Où stocker le mot de passe en prod ? Secrets Manager ou SSM SecureString + IAM de l’app. Pas de littéral HCL, pas de tfvars Git, pas d’output non protégé dans les logs CI.
Pour aller plus loin
- Doc :
aws_db_instance,aws_db_subnet_group,random_password, RDS pricing - Sur ce site : RDS MySQL / Postgres (
aws-rds-mysql-postgres) ; suite Terraform VPC (terraform-vpc)
Maillage série Terraform (P2)
| ← Précédent | IAM (terraform-iam) |
| → Suivant | VPC (terraform-vpc) |
| Aussi | State S3 (terraform-state-backend-s3) · Modules (terraform-modules-bases) · RDS AWS (aws-rds-mysql-postgres) |
| Carte | Overview · Install · Workflow · Blocks · Providers · Variables · Locals · State · Modules · Workspaces · Labs AWS |
Retour parcours Terraform — hub de la série et leçons sœurs.


