Database infrastructure is the one place teams are often most hesitant to apply infrastructure-as-code — understandably, since a mistake in a Terraform plan for a database can mean data loss, not just a few minutes of downtime. That hesitation usually results in databases provisioned by hand in the console, configuration drifting silently from whatever the original setup was, and no record of why a parameter group setting exists or who changed it.
Terraform handles database infrastructure well when it's set up with the right guardrails: state management that doesn't corrupt under concurrent runs, secrets that never land in state files or version control, and explicit protection against the specific plan that destroys and recreates a resource instead of updating it in place. This guide covers the patterns that make Terraform trustworthy for RDS, Aurora, and Cloud SQL — not just how to write the resource block.
Prefer to skip the debugging and have an expert handle this? Book a 30-min diagnostic →
A Minimal RDS Module
A Terraform module for RDS should expose the parameters that actually vary between environments — instance class, allocated storage, engine version — while keeping safety-critical settings (encryption, deletion protection, backup retention) fixed or defaulted to safe values regardless of what a caller passes in. Hardcoding safety settings inside the module, rather than exposing them as overridable variables, prevents a rushed change in a calling module from accidentally disabling encryption or deletion protection on a production database. Multi-AZ, encrypted storage, and a real backup retention window should be the module's defaults, not opt-in flags a caller has to remember to set correctly every time.
resource "aws_db_instance" "main" {
identifier = var.identifier
engine = "postgres"
engine_version = var.engine_version
instance_class = var.instance_class
allocated_storage = var.allocated_storage
# Safety settings — fixed, not exposed as easily-misused overrides
storage_encrypted = true
multi_az = var.environment == "production"
backup_retention_period = var.environment == "production" ? 14 : 3
deletion_protection = var.environment == "production"
db_subnet_group_name = var.subnet_group_name
vpc_security_group_ids = var.security_group_ids
lifecycle {
prevent_destroy = true
}
}Remote State: Never Local, Always Locked
Local Terraform state for a database resource is a production incident waiting to happen — one engineer applies a change from their laptop, a second engineer applies a different change from theirs, and the two state files disagree about what actually exists, with no record of which is correct. Remote state (an S3 bucket with versioning enabled, or Terraform Cloud) combined with state locking (DynamoDB for S3 backends) is non-negotiable for anything touching a shared database. Locking prevents two concurrent applies from racing against each other and corrupting state; versioning on the state bucket means a corrupted or accidentally-modified state file can be rolled back rather than requiring a manual state reconstruction, which for a database resource is a genuinely dangerous exercise to do freehand.
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "database/production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}Preventing Accidental Destroy-and-Recreate
The most dangerous Terraform plan for a database is one that shows '-/+' — destroy and recreate — rather than a simple in-place update. Certain attribute changes force this behavior by AWS's own API constraints (changing an RDS instance's identifier, for example, cannot be done in place), and if that plan is approved without noticing the destroy step, the database and all its data are gone before the recreate step even begins. The lifecycle block's prevent_destroy = true stops Terraform from executing any plan that would destroy that resource, forcing a human to explicitly acknowledge and remove the guard before a destructive change can proceed — which is exactly the friction you want on a production database.
Equally important: always run terraform plan and actually read the output before terraform apply, specifically checking for any resource marked for destruction, not just skimming for a green checkmark.
Still Piecing This Together Yourself?
A senior engineer looks at your actual setup, not a generic checklist, and tells you exactly what's wrong and how to fix it.
Book a Diagnostic CallSecrets Management: Never in State, Never in Version Control
A database password passed as a plain Terraform variable ends up in the state file in plaintext by default — and Terraform state files are not encrypted at rest unless the backend explicitly enables it, meaning anyone with read access to the state bucket has the production database password. The correct pattern generates a random password with the random_password resource, stores it in a secrets manager (AWS Secrets Manager, GCP Secret Manager, or Vault), and has the application retrieve it at runtime — Terraform's role is provisioning the secret's existence and initial value, not being the system of record applications read from at request time. This also means rotating the password doesn't require a Terraform apply at all, just an update in the secrets manager and an application restart or credential refresh.
resource "random_password" "db_password" {
length = 32
special = true
}
resource "aws_secretsmanager_secret" "db_password" {
name = "prod/database/password"
}
resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string = random_password.db_password.result
}
resource "aws_db_instance" "main" {
# ...
password = random_password.db_password.result
}Get a Straight Answer, Not a Sales Pitch
Tell us what you're running into. We'll tell you directly what's causing it and what it takes to fix, before you sign anything.
Contact UsDetecting and Handling Drift
Drift happens the moment anyone changes a setting through the console instead of through Terraform — a parameter group value tweaked during an incident at 2 a.m., a security group rule added manually to unblock a deploy. Terraform doesn't know about that change until the next plan, at which point it will either show the drift as a change to revert, or worse, silently accept it as the new baseline if the state was refreshed without review. Running terraform plan on a schedule (nightly, in CI) against production — without applying — surfaces drift proactively instead of discovering it during the next legitimate change, when it's much harder to tell which parts of the diff are the intended change and which are unrelated drift from three weeks ago.
Terraform is trustworthy for database infrastructure once the guardrails are in place — prevent_destroy on anything holding production data, remote state with locking so concurrent applies can't corrupt each other, secrets that never touch state or version control, and scheduled drift detection so console changes don't accumulate invisibly. Skip those guardrails and Terraform becomes exactly as risky as manual console changes, just with an extra step before the mistake happens. Get them right, and database infrastructure changes go through the same review, plan, and audit trail as application code changes — which is the entire point of managing infrastructure as code in the first place.
Frequently Asked Questions
Is Terraform safe to use for production databases?
Yes, with the right guardrails: prevent_destroy on the database resource, remote state with locking, secrets kept out of state and version control via a secrets manager, and always reading the plan output before applying — specifically checking for any destroy or destroy-and-recreate action, not just approving on habit.
How do I stop Terraform from destroying my database by mistake?
Add a lifecycle block with prevent_destroy = true to the database resource. This makes Terraform refuse to execute any plan that would destroy that resource, requiring a human to explicitly remove the guard first — which forces a deliberate decision instead of an accidental one slipping through in a larger plan.
Should database passwords be stored in Terraform variables?
Not as plain variables — they end up in the state file in plaintext. Generate the password with the random_password resource, store it in a secrets manager (AWS Secrets Manager, GCP Secret Manager, Vault), and have applications retrieve it at runtime rather than reading it out of Terraform state.
What causes Terraform to want to destroy and recreate a database instead of updating it?
Certain attribute changes can't be applied in place due to the cloud provider's own API constraints — changing an RDS instance identifier is a common example. Terraform's plan will show this as a '-/+' destroy-and-recreate action rather than a simple update, which is exactly the kind of change prevent_destroy is meant to catch before it executes.
Get a Straight Answer on Your Setup
Tell us what you're running into. We read every message personally and reply within 24 hours with times for a free call.

