Terraform #09 — Security, Secrets & Safe Refactoring
Managing secrets the right way, least-privilege infrastructure, and refactoring running production code safely using moved/import/removed blocks.
Goal: Secure your infrastructure and secrets properly, and refactor running production code without causing downtime or corrupting state.
PART A — SECURITY & SECRETS
9.1. Managing secrets — what not to do
Never do this:
# Hardcoded secret in code
resource "aws_db_instance" "db" {
password = "SuperSecret123" # permanently in git history after the first commit!
}
# Putting secrets in tfvars and committing them
# Echoing secrets to output without marking them sensitive
# Thinking sensitive=true is enough (state still stores plaintext!)Do this instead — pull secrets at runtime from a secret manager:
# Read from AWS Secrets Manager (nothing in the code itself)
data "aws_secretsmanager_secret_version" "db" {
secret_id = "prod/db/password"
}
resource "aws_db_instance" "db" {
password = data.aws_secretsmanager_secret_version.db.secret_string
}Or using HashiCorp Vault:
data "vault_generic_secret" "db" {
path = "secret/prod/db"
}
# use: data.vault_generic_secret.db.data["password"]⚠️ Even when pulling from Vault or Secrets Manager, the value still gets written to state. That's why securing state (Module 02) is non-negotiable. The ideal: let the service manage its own password so Terraform never sees it at all (e.g. RDS
manage_master_user_password = true, which stores it directly in Secrets Manager).
The "Terraform never touches the secret" pattern
A few ways to keep secrets completely out of state:
- RDS
manage_master_user_password = true→ AWS manages the password in Secrets Manager automatically. - Create an empty secret with Terraform, let the application write the value at runtime.
- Use IAM roles / Workload Identity instead of passwords wherever possible.
9.2. Infrastructure security
Least privilege in practice
# ❌ Too broad
resource "aws_security_group_rule" "bad" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # SSH open to the world
}
# ✅ Restricted
resource "aws_security_group_rule" "good" {
type = "ingress"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.office_cidr]
}Common security issues that checkov/tfsec will flag
- S3 bucket: block public access, enable encryption + versioning.
- RDS/EBS: encryption at rest.
- Security groups: no
0.0.0.0/0on sensitive ports (22, 3389, 3306...). - IAM: no
"*"for Action/Resource when it can be avoided. - Logging enabled (CloudTrail, VPC flow logs, ALB access logs).
- IMDSv2 required for EC2.
- No public IPs when not needed.
IAM for Terraform itself
- Use IAM role + OIDC in CI (no static access keys).
- The Terraform role only has permissions needed for the stack it manages.
- Separate roles per environment (prod role ≠ dev role).
9.3. check blocks — post-apply assertions (Terraform 1.5+)
Unlike validation (for inputs) or precondition/postcondition (attached to a resource), check blocks are independent — they don't block apply, they just warn.
check "health_check" {
data "http" "app" {
url = "https://${aws_lb.main.dns_name}/health"
}
assert {
condition = data.http.app.status_code == 200
error_message = "Health check failed after deployment."
}
}PART B — SAFE REFACTORING
Renaming or moving a resource in production the wrong way means destroy + recreate — downtime, data loss, bad day. Terraform gives you proper tools for this.
9.4. moved block — rename or move without destroying
Available since Terraform 1.1+. Replaces manual
terraform state mvwith version-controlled, reviewable declarations.
Renaming a resource
# Before: resource "aws_instance" "web" {...}
# You want to rename it to "app_server"
moved {
from = aws_instance.web
to = aws_instance.app_server
}
resource "aws_instance" "app_server" { # new name
# ... same config
}terraform plan reports "moved", not "destroyed." After it stabilizes, you can delete the moved block.
Moving a resource into a module
moved {
from = aws_instance.web
to = module.compute.aws_instance.web
}Changing from count to for_each
moved {
from = aws_instance.web[0]
to = aws_instance.web["primary"]
}9.5. import block — take ownership of existing resources (revisited from Module 02)
import {
to = aws_s3_bucket.legacy
id = "my-existing-bucket"
}Combine with terraform plan -generate-config-out=gen.tf to auto-generate the resource block. Ideal when inheriting ClickOps infrastructure.
9.6. removed block — stop managing without deleting (Terraform 1.7+)
removed {
from = aws_instance.legacy
lifecycle {
destroy = false # false = only remove from state, DON'T delete the real resource
}
}Useful when you want to hand off a resource to another stack, or just stop managing it without destroying it.
9.7. Refactoring tools at a glance
| Goal | Declarative (block) | CLI |
|---|---|---|
| Rename / move a resource | moved block | terraform state mv |
| Import existing resource | import block | terraform import |
| Unmanage without deleting | removed block | terraform state rm |
Prefer blocks over CLI commands — they're reviewable in PRs, version-controlled, repeatable, and safer. Use the CLI for emergency or one-off situations.
9.8. Drift and reconciliation (revisited from Module 02)
terraform plan -refresh-only # detect drift without changing anything
terraform apply -refresh-only # update state to match realityMany teams run scheduled drift detection (cron job) to get alerted when someone modifies infrastructure outside Terraform.
9.9. Hands-on Labs
In hands-on/ (uses the local + time providers — no cloud needed):
- Lab 1 —
movedblock: rename a resource, prove nothing was destroyed by checking the ID didn't change. - Lab 2 —
removedblock: remove from state while keeping the real file. - Lab 3 — Read through the security checklist + review correct vs incorrect secret handling examples.
Follow LAB.md.
✅ Module 09 completion criteria
- Know at least two ways to handle secrets without hardcoding them.
- Understand why secrets still end up in state even when using Vault.
- Can use a
movedblock to rename a resource without destroying it. - Can use a
removedblock to unmanage a resource without deleting it. - Can list 5+ items from the security checklist.
- Know when to use blocks vs CLI commands for state operations.
➡️ Next: Module 10 — Capstone Project