Skip to content
>_higan
This post has a translation:Read in Tiếng Việt

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/0 on 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 mv with 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

GoalDeclarative (block)CLI
Rename / move a resourcemoved blockterraform state mv
Import existing resourceimport blockterraform import
Unmanage without deletingremoved blockterraform 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 reality

Many 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):

  1. Lab 1moved block: rename a resource, prove nothing was destroyed by checking the ID didn't change.
  2. Lab 2removed block: remove from state while keeping the real file.
  3. 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 moved block to rename a resource without destroying it.
  • Can use a removed block 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

Share: