DevOps Elastic Hayway
Document

SUBSCRIBE TO GET FULL ACCESS TO THE E-BOOKS FOR FREE 🎁SUBSCRIBE NOW

Professional Dropdown with Icon

SUBSCRIBE NOW TO GET FREE ACCESS TO EBOOKS

TerraformLesson 20 / 2413 min readUpdated September 13, 2026

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 un aws_db_instance (PostgreSQL ou MySQL), activer le stockage chiffré, générer le mot de passe avec random_password + output sensitive (jamais de MDP en clair dans le HCL), choisir skip_final_snapshot selon lab vs prod, et exécuter un lab optionnel / plan-only en ca-central-1 — Terraform ≥ 1.9, AWS provider ~> 5.0. Message clé : RDS coûte cher — privilégiez plan, 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-identity OK)
  • 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.

  1. random_password génère au apply ; la valeur vit dans le state (backend S3 chiffré — terraform-state-backend-s3).
  2. Output sensitive = true pour limiter les fuites dans les logs CI.
  3. En prod : Secrets Manager ou SSM SecureString + lecture IAM — pas d’output collé dans Slack.
  4. 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

  1. Distinguer RDS managé vs base self-managed EC2.
  2. Déclarer aws_db_subnet_group (≥ 2 AZ) + SG port engine.
  3. Créer aws_db_instance : Postgres/MySQL, gp3, storage_encrypted = true.
  4. Mot de passe via random_password + output sensitive — pas de MDP en HCL.
  5. Lab : skip_final_snapshot = true ; prod : false + identifier.
  6. Region ca-central-1, TF ≥ 1.9, provider ~> 5.0, db.t4g.micro.
  7. 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 + output sensitive (Secrets Manager en prod)
  • C. Dans un commentaire Git « temporaire »

2. Pour un lab terraform rds économique en ca-central-1 ?

  • A. db.r6g.2xlarge Multi-AZ
  • B. db.t4g.micro + plan-only / destroy immédiat
  • C. EC2 c7i.metal self-managed obligatoire

3. En production, skip_final_snapshot ?

  • A. Toujours true pour 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

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.

Share your love

Leave a Reply

Your email address will not be published. Required fields are marked *