High-yield Terraform review for the Associate (003) exam: core workflow, state/backends/locking, variables & expressions, modules & versioning, providers & auth, import/replace/refresh-only, workspaces, policy & best practices, and common CLI.
Use this for last-mile review. Skim top-to-bottom, star weak rows, and pair with targeted drills.
Standard loop: init → fmt/validate → plan → apply → (plan -destroy) → destroy
1terraform init # download providers/modules, set backend
2terraform fmt -recursive # canonical formatting
3terraform validate # static checks
4terraform plan # create execution plan
5terraform apply # apply plan (auto-approve optional)
6terraform plan -destroy # plan a destroy
7terraform destroy # tear down
Key files
main.tf
/ *.tf
— resources, data, providers, modulesvariables.tf
— input vars + types/validationoutputs.tf
— outputs (can be sensitive = true
)providers.tf
— required_providers
, auth/configversions.tf
— required_version
, provider constraints.terraform.lock.hcl
— provider dependency lockterraform.tfvars
/ *.auto.tfvars
— default var values.tfvars.json
— JSON var files.terraform/
— working dir (providers/modules cache)Provider & CLI version constraints
1terraform {
2 required_version = "~> 1.8" # allow patch/minor within 1.8.x
3 required_providers {
4 aws = { source = "hashicorp/aws", version = "~> 5.0" }
5 }
6}
AWS
AWS_ACCESS_KEY_ID
, AWS_SECRET_ACCESS_KEY
, AWS_SESSION_TOKEN
, AWS_PROFILE
1provider "aws" {
2 region = var.region
3 profile = var.aws_profile
4}
Azure
az login
/ Service principal (ARM_CLIENT_ID
, ARM_CLIENT_SECRET
, ARM_TENANT_ID
, ARM_SUBSCRIPTION_ID
)1provider "azurerm" { features {} }
GCP
GOOGLE_APPLICATION_CREDENTIALS
(JSON), or ADC via gcloud auth application-default login
1provider "google" { project = var.project region = var.region }
Multiple providers / aliases
1provider "aws" { region = "us-east-1" }
2provider "aws" { alias = "west" region = "us-west-2" }
3
4resource "aws_s3_bucket" "logs" {
5 provider = aws.west
6 bucket = "my-logs-west"
7}
Input variables (types + validation + sensitive)
1variable "instance_type" {
2 type = string
3 default = "t3.micro"
4 validation {
5 condition = contains(["t3.micro","t3.small"], var.instance_type)
6 error_message = "Use t3.micro or t3.small."
7 }
8}
9
10variable "db_password" {
11 type = string
12 sensitive = true
13}
Var precedence (highest → lowest)
-var
and later -var-file
on CLI*.auto.tfvars
(alphabetical)terraform.tfvars
/ .tfvars.json
TF_VAR_name
default
Locals (computed values)
1locals {
2 common_tags = { app = "shop", env = var.env }
3}
Outputs
1output "alb_dns" {
2 value = aws_lb.app.dns_name
3 sensitive = false
4}
Count vs for_each
count
→ index-based, good for homogeneous listsfor_each
→ key-based (map/set), stable addressing, preferred1resource "aws_iam_user" "basic" {
2 for_each = toset(["alice","bob"])
3 name = each.key
4}
for / dynamic blocks
1tags = { for k, v in local.common_tags : k => v if v != "" }
2
3dynamic "ingress" {
4 for_each = var.ingress_rules
5 content {
6 from_port = ingress.value.port
7 to_port = ingress.value.port
8 cidr_blocks = ingress.value.cidr_blocks
9 }
10}
Useful functions (memorize)
coalesce()
, try()
, can()
merge()
, concat()
, zipmap()
, toset()
, tomap()
regex()
, regexall()
, replace()
cidrsubnet()
, cidrhost()
jsonencode()
, yamldecode()
file()
, templatefile()
Implicit deps via references: resource A
uses resource B
’s attr → DAG edge.
Explicit deps (use sparingly)
1resource "aws_instance" "app" {
2 depends_on = [aws_iam_role.app]
3}
Lifecycle meta-arguments
1resource "aws_launch_template" "lt" {
2 lifecycle {
3 create_before_destroy = true # minimize downtime for replace
4 prevent_destroy = true # guard rails
5 ignore_changes = [tags] # drift tolerance for fields
6 replace_triggered_by = [var.ami_id] # re-create when input changes
7 }
8}
Provisioners — last resort only; prefer cloud-init/user-data or config mgmt.
Layout
1modules/
2 vpc/
3 main.tf variables.tf outputs.tf README.md
4envs/
5 prod/ main.tf
Call modules
1module "vpc" {
2 source = "terraform-aws-modules/vpc/aws"
3 version = "~> 5.0"
4 name = "core-vpc"
5 cidr = "10.0.0.0/16"
6}
Pin versions for modules & providers. Keep modules small, composable, documented. Prefer input validation, typed variables, and outputs
for wiring.
State facts
Remote backends (common)
1terraform {
2 backend "s3" {
3 bucket = "tf-state-prod"
4 key = "network/terraform.tfstate"
5 region = "us-east-1"
6 dynamodb_table = "tf-locks"
7 encrypt = true
8 }
9}
Init with partial config
1terraform init \
2 -backend-config="bucket=tf-state-prod" \
3 -backend-config="key=apps/prod.tfstate"
State CLI (surgical fixes)
1terraform state list
2terraform state show aws_s3_bucket.logs
3terraform state mv aws_s3_bucket.old aws_s3_bucket.new
4terraform state rm aws_s3_bucket.orphan
5terraform providers # see dependencies
6terraform providers mirror ./providers # air-gapped
Lock file: .terraform.lock.hcl
pin provider versions; commit it.
Import (two options)
1# In config:
2resource "aws_s3_bucket" "logs" { bucket = "my-logs" }
3
4import {
5 to = aws_s3_bucket.logs
6 id = "my-logs"
7}
Then:
1terraform plan
2terraform apply
1terraform import aws_s3_bucket.logs my-logs
Replace resource intentionally (instead of taint)
1terraform plan -replace=aws_instance.app
2terraform apply -replace=aws_instance.app
Refresh-only change detection
1terraform plan -refresh-only
2terraform apply -refresh-only
default
, dev
, stage
, prod
)1terraform workspace list
2terraform workspace new stage
3terraform workspace select prod
Use in config
1locals {
2 name_suffix = terraform.workspace == "prod" ? "" : "-${terraform.workspace}"
3}
Pitfall: Workspaces do not branch provider credentials or networking per se; you must still parameterize those.
Data source (read-only view of existing infra)
1data "aws_ami" "latest" {
2 most_recent = true
3 owners = ["amazon"]
4 filter {
5 name = "name"
6 values = ["amzn2-ami-hvm-*-x86_64-gp2"]
7 }
8}
Expose values from modules
1output "subnet_ids" {
2 value = module.vpc.private_subnet_ids
3}
count
: aws_instance.web[0]
for_each
: aws_security_group.sg["admin"]
module.vpc.aws_subnet.this[each.key]
For loops for maps/lists
1variable "names" { type = list(string) }
2locals {
3 name_tags = { for n in var.names : n => { Name = n } }
4}
Golden rules
terraform plan
artifacts)sensitive
+ providers’ secrets managers)plan -refresh-only
to reconcile; then decide remediation.lifecycle.create_before_destroy
(or replace_triggered_by
) instead of manual ordering.-replace=addr
(do not hand-edit state).terraform import
), then plan
/apply
.for_each
(avoid index churn from count
).lifecycle.ignore_changes = [field]
.lifecycle.prevent_destroy = true
(with break-glass procedure).1terraform plan -var-file=prod.tfvars -out=plan.bin
2terraform apply plan.bin
3terraform plan -target=module.network # surgical, use sparingly
4terraform plan -replace=aws_lb.app
5TF_LOG=INFO terraform apply # debug logging (temporary)
Use
-target
sparingly (can violate the DAG intent). Prefer architectural fixes or staged applies.
required_providers
, network/proxy, and env creds.terraform state mv old new
.ignore_changes
, external controllers, or missing inputs; ensure provider versions match across machines/CI (lock file!).terraform workspace show
and parameterize names/regions.versions.tf
, providers.tf
, main.tf
, variables.tf
.outputs.tf
(expose IDs/ARNs).init → fmt → validate → plan → apply
.-replace
, import block, plan -refresh-only
, and workspaces.