I codified a hardened cloud database baseline in Terraform with private-subnet networking, KMS encryption at rest, and automated backups. Infrastructure-as-code made the secure configuration repeatable, reviewable, and drift-detectable across environments.

Objective & Context

Cloud database security is a configuration problem solved best as code. This lab provisions a managed database (AWS RDS / Azure SQL) into a private subnet with no public endpoint, enables encryption with a customer-managed key, and sets a backup retention window, aligning to CIS cloud benchmarks.

Learning Objectives

  • Declare a private, encrypted database in Terraform.
  • Place the database in private subnets with no public IP.
  • Enable KMS encryption at rest and automated backups.
  • Apply changes with plan/apply discipline.
  • Detect configuration drift with plan -detailed-exitcode.

Environment & Prerequisites

Prerequisites

  • Terraform 1.7 with a cloud provider authenticated
  • An existing VPC/VNet with private subnets
  • A KMS (or Key Vault) key for encryption at rest
  • Assumed skill: basic Terraform resources and SQL fundamentals

Step-by-Step Execution

1. Declare a private, encrypted database

resource "aws_db_instance" "app" {
  engine                  = "postgres"
  instance_class          = "db.t3.medium"
  storage_encrypted       = true
  kms_key_id              = aws_kms_key.db.arn
  publicly_accessible     = false
  backup_retention_period = 14
  db_subnet_group_name    = aws_db_subnet_group.private.name
}

Expected output:

(resource aws_db_instance.app declared)

What this teaches: Declaring encryption, private subnets, and backup retention in code makes the secure baseline the default, not a checklist item someone might skip. storage_encrypted can only be set at creation time; enabling it later requires restoring from a snapshot into a new encrypted instance, so getting it right in the initial declaration matters.

2. Plan and apply

terraform plan -out tf.plan && terraform apply tf.plan

Expected output:

aws_db_instance.app: Creation complete (encrypted, private)

What this teaches: plan -out plus apply tf.plan guarantees you apply exactly what you reviewed; the creation message confirms the security properties took effect.

3. Detect drift later

terraform plan -detailed-exitcode
$ terraform plan -detailed-exitcode
No changes. Infrastructure matches configuration.

Expected output:

No changes. Infrastructure matches configuration.

What this teaches: Exit code 0 from a detailed plan means no drift; exit code 2 would mean someone changed the database outside Terraform and needs investigation.

4. Verify encryption and endpoint from the CLI

aws rds describe-db-instances --db-instance-identifier app --query 'DBInstances[0].[StorageEncrypted,PubliclyAccessible,DBInstanceStatus]' --output text

Expected output:

True	False	available

What this teaches: Verification closes the loop between declared intent and actual state. Querying the live instance confirms storage encryption is on, no public endpoint exists, and the instance is available.

How It Works

How It Works

Infrastructure as code turns the database's security posture into a reviewable artifact. Private subnets, KMS encryption, and backup retention are declared once and applied identically in every environment, which eliminates the 'forgotten setting' class of misconfigurations.

Encryption at rest with a customer-managed KMS key protects data files, backups, and snapshots on disk. Combined with TLS in transit and private networking, the database is unreachable and unreadable outside your network and key policy.

Drift detection closes the loop: terraform plan compares desired state against reality, so manual console changes surface as diffs instead of silently eroding the baseline. In a pipeline, a non-zero detailed exit code can block or alert.

Validation & Testing

Confirm the database has no public endpoint, storage is encrypted with the CMK, and backups are retained. Pass criteria: connection only succeeds from inside the VPC, encryption is enabled, and terraform plan reports no drift after apply.

Advanced: Troubleshooting
  • Public exposure: set publicly_accessible=false and place in private subnets only.
  • Encryption can't be added later: enable at creation; retrofitting often requires a re-create.
  • State drift: remediate via Terraform, not console edits, to keep code authoritative.

Troubleshooting

SymptomLikely causeFix
apply fails: insufficient private subnet IPsthe subnet ran out of addresses for the instance and replicassize private subnets for growth or add subnets to the DB subnet group
database publicly reachable despite configpublicly_accessible was left true or a permissive security groupset publicly_accessible=false and restrict the security group to app subnets
KMS key policy denies the servicethe RDS service principal lacks kms:Encrypt/Decrypt on the keyadd the service principal to the key policy, not just IAM
plan shows perpetual diff on passwordthe provider cannot read back the passworduse ignore_changes or a secrets manager integration for the password field
drift detected but nothing looks wrongan automated process (backup windows, maintenance) updated a defaulted fieldpin the field explicitly in config or add it to ignore_changes after review

Key Results

  • Codified a private, encrypted database baseline as reusable Terraform.
  • Eliminated public database endpoints across environments.
  • Enabled KMS encryption at rest and 14-day backup retention.
  • Gained drift detection via terraform plan.

Key Takeaways

  • Codify the secure baseline: private, encrypted, backed up, by default.
  • Review plans before applying; apply exactly the reviewed plan file.
  • KMS encryption at rest plus private networking is the minimum viable posture.
  • Drift detection turns manual console changes into visible diffs.
  • Secrets belong in a secrets manager, not in Terraform state or code.

Practice Challenges

Challenge 1: Staging clone

Goal: Add a second Terraform workspace that provisions an identical staging database with a smaller instance class.

Hint: Use variables for instance_class and name; keep the security settings identical across workspaces.

Challenge 2: Drift alert

Goal: Change the backup retention in the console, then write a CI job that fails when plan -detailed-exitcode returns 2.

Hint: Schedule the check nightly; the exit code is the whole contract.