Cloud Database Configuration with Terraform and Encryption
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.7with 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
no public IP)] KMS[KMS key] -->|encrypt at rest| DB DB --> BK[Automated backups
retention 14d]
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.planExpected 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 textExpected 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.
private subnets")] DB --> KMS["KMS encryption
at rest"] DB --> BK["automated backups
+ retention"] DB --> DR["drift detection
plan -detailed-exitcode"]
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=falseand 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
| Symptom | Likely cause | Fix |
|---|---|---|
| apply fails: insufficient private subnet IPs | the subnet ran out of addresses for the instance and replicas | size private subnets for growth or add subnets to the DB subnet group |
| database publicly reachable despite config | publicly_accessible was left true or a permissive security group | set publicly_accessible=false and restrict the security group to app subnets |
| KMS key policy denies the service | the RDS service principal lacks kms:Encrypt/Decrypt on the key | add the service principal to the key policy, not just IAM |
| plan shows perpetual diff on password | the provider cannot read back the password | use ignore_changes or a secrets manager integration for the password field |
| drift detected but nothing looks wrong | an automated process (backup windows, maintenance) updated a defaulted field | pin 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.