Day 24: Final Exam Preparation: Terraform Associate (004) Practice and Tips

Day 24: Final Exam Preparation: Terraform Associate (004) Practice and Tips

Day 23 built the reference cheat sheet. Day 24 is the simulation layer: exam-style practice questions with explained answers, troubleshooting scenarios, test-taking strategies, and a final prep checklist. Everything here targets the current **HashiCorp Certified: Terraform Associate (004)** exam.

004 vs older study material. Older posts and practice sets target the 003 revision, which had 9 domains and assumed (unofficial) per-domain percentages. The 004 exam content list publishes 8 domains and no percentages or passing score. Practice questions and study advice that lean on the old 9-domain weightings are not wrong, but they predate the 004 additions: check blocks, moved/removed blocks, the Vault provider for secrets, and the expanded HCP Terraform domain (Projects, Change Requests, Dynamic Provider Credentials, OPA, Health). The questions below cover all of those.


Overview

Section What You'll Find
Exam Tips Test-taking strategies specific to this exam format
Practice Questions — Core Workflow The highest-weighted domain (20%)
Practice Questions — State Frequently tested, easy to confuse
Practice Questions — Modules Sources, versioning, providers
Practice Questions — Configuration count, for_each, functions, lifecycle
Practice Questions — HCP Terraform Workspaces, Sentinel, execution modes
Troubleshooting Scenarios Symptom → diagnosis → fix
Final Day Checklist What to do the day before the exam

Exam Tips

Format specifics

The exam is delivered through PSI's online proctoring platform. Before reading any question:

  • Check whether the question is single-answer ('Which of the following...') or multi-answer ('Select TWO that...'). Multi-answer questions require all correct options selected — partial credit is not given.
  • Budget roughly 60 seconds per question (~57 questions in 60 minutes ≈ 63 seconds each). If you do not know an answer, flag it and move on. Return at the end.

How to read the questions

Many wrong answers are technically true statements that do not answer the specific question asked. The most common traps:

'Which of the following is NOT...' — scan all options before selecting. The wrong answer here is often something that sounds correct in isolation.

'Which of the following will ALWAYS...' — absolutes are usually wrong. An option that says Terraform 'always' does something is often the distractor.

'What happens FIRST...' — sequencing questions are common for the init → validate → plan → apply workflow and for the destroy order.

What to expect across the 8 domains

HashiCorp does not publish per-domain weightings for 004, so plan to be tested on every objective. The 004 content list groups objectives into 8 domains — expect a mix from all of them, with the largest objective surfaces being:

  • Domain 4 (Terraform configuration) — variables, outputs, expressions, complex types, custom conditions and check blocks, sensitive data and Vault provider. The most sub-objectives of any 004 domain.
  • Domain 8 (HCP Terraform) — substantially expanded in 004: workspaces, Projects, Change Requests, Dynamic Provider Credentials, OPA, Health/Drift Detection, Variable Sets, Run Triggers.
  • Domain 6 (State management) — backends, locking, drift, and the moved and removed blocks (new emphasis in 004).
  • Domain 3 (Core workflow) — fundamentals you cannot skip: init, validate, plan, apply, destroy, fmt.

Do not skip HCP Terraform — it has the most net-new 004 content and the workspace/project distinction appears repeatedly.

The concepts most frequently tested

From review of the 004 exam objectives and recent candidate reports, these appear most often:

  1. terraform init flags and when each is required
  2. The variable precedence order (7 sources)
  3. count vs for_each — the renumbering problem
  4. sensitive = true — what it does and does not protect (and the same caveat for Vault data sources)
  5. The backend block — no variables allowed
  6. CLI workspaces vs HCP Terraform workspaces vs HCP Terraform Projects
  7. Sentinel and OPA enforcement levels (Advisory / Soft Mandatory / Hard Mandatory)
  8. terraform taint — deprecated, replaced by -replace
  9. terraform refresh — deprecated as standalone; plan -refresh-only (inspect) and apply -refresh-only (persist)
  10. Module source format — the // separator between repo URL and subdirectory
  11. import {} block (1.5+) and the -generate-config-out flag
  12. moved block (1.1+) vs terraform state mv; removed block (1.7+) vs terraform state rm
  13. precondition / postcondition (abort) vs check block (warn-only)
  14. Dynamic Provider Credentials in HCP Terraform (OIDC → AssumeRoleWithWebIdentity) — replaces static workspace env vars

Practice Questions — Core Workflow


Q1. An engineer runs terraform plan -out=tfplan and reviews the output. Twenty minutes later, a colleague applies a change to the same environment. The engineer then runs terraform apply tfplan. What happens?

A) Terraform re-runs the plan before applying
B) Terraform applies the saved plan without re-planning
C) Terraform detects the state change and refreshes before applying
D) Terraform fails because the saved plan is stale

Answer: B

terraform apply <planfile> applies the exact saved plan without generating a new one. Terraform does NOT detect that another change occurred. This is why teams use CI pipelines with short plan-to-apply windows, and why HCP Terraform locks the workspace during a run. If the saved plan conflicts with the current state, some operations may fail at the provider level — but Terraform does not check first.


Q2. Which command formats Terraform configuration files AND fails with a non-zero exit code if any file needs changes (for use in CI)?

A) terraform fmt
B) terraform fmt -recursive
C) terraform fmt -check
D) terraform fmt -diff

Answer: C

terraform fmt -check exits with code 1 if any file needs formatting, without modifying the files. This is the CI-safe form. -recursive applies formatting to subdirectories but still modifies files and exits 0. -diff shows what would change without writing. Running terraform fmt alone reformats files and exits 0.


Q3. Which of the following is true about terraform destroy -target=aws_db_instance.main? (Select TWO)

A) It destroys only aws_db_instance.main and its dependencies
B) It destroys only aws_db_instance.main, leaving dependencies untouched
C) It destroys resources that depend on aws_db_instance.main
D) After destroy, resources that depended on aws_db_instance.main may be in an inconsistent state
E) It produces a plan that must be approved before destruction

Answer: B and D

-target destroys only the targeted resource. Dependencies (resources it depends on) are left running. Dependents (resources that depend on it) are also left running but may now be broken — a security group reference that no longer exists, for example. This is why -target is for surgical corrections, not routine operations. (E is tempting — terraform destroy does prompt for approval by default, but the question asks what is true specifically about the -target behavior, not the approval flow.)


Q4. A terraform plan output shows -/+ next to an aws_autoscaling_group resource. What does this mean?

A) The ASG will be updated in place
B) The ASG will be created (it does not yet exist)
C) The ASG will be destroyed and a new one created
D) The ASG configuration has drifted from the state file

Answer: C

-/+ means destroy-then-create (replacement). This is the most dangerous change in Terraform because all instances in the ASG are terminated during the replacement. A ~ symbol indicates an in-place update. A + indicates creation. Drift is displayed differently — it shows up in the plan as a change but does not always use -/+.


Q5. An engineer wants to force-replace a specific EC2 instance on the next apply without changing any configuration. Which command should they use?

A) terraform taint aws_instance.web
B) terraform apply -replace=aws_instance.web
C) terraform state rm aws_instance.web && terraform apply
D) terraform destroy -target=aws_instance.web && terraform apply

Answer: B

terraform apply -replace=<address> is the current way to force replacement since Terraform 0.15.2. terraform taint is deprecated — it still works but generates a deprecation warning. state rm followed by apply would recreate the resource but with potential state inconsistencies. destroy then apply would leave the resource absent between the two commands.


Q6. What is the correct order of the Terraform core workflow?

A) validate → init → plan → apply
B) init → plan → validate → apply
C) init → validate → plan → apply
D) plan → init → validate → apply

Answer: C

init must run first (downloads providers and modules). validate requires a valid provider install but makes no API calls. plan makes API calls to compare desired vs current state. apply executes the changes. In practice, many engineers skip explicit validate since plan includes validation — but the conceptual order for the exam is init → validate → plan → apply.


Practice Questions — State


Q7. A developer ran terraform apply and the S3 bucket was created successfully. The team then manually deleted the S3 bucket from the AWS console. What happens on the next terraform plan?

A) Terraform detects the deletion and shows no changes
B) Terraform detects the deletion and plans to recreate the bucket
C) Terraform raises an error because the state is inconsistent
D) The next plan succeeds only if terraform refresh is run first

Answer: B

terraform plan always refreshes the state by default (reads current real-world state from the provider) before showing what would change. Since the bucket is gone, Terraform plans to recreate it. No manual terraform refresh is required — the refresh is built into the plan. (D was true in older Terraform versions when plan did not auto-refresh, but is not true for current versions.)


Q8. Which of the following statements about sensitive values and Terraform state is correct?

A) Variables marked sensitive = true are encrypted in the state file
B) Outputs marked sensitive = true are not stored in the state file
C) Sensitive values are stored in plaintext in the state file regardless of the sensitive flag
D) The sensitive flag encrypts the value when using a remote backend

Answer: C

sensitive = true only redacts the value from terminal output — the plan, apply, and output commands will show (sensitive value) instead of the actual value. The value is always stored in the state file in plaintext. Protecting sensitive data requires encrypting the state file itself (S3 with SSE, HCP Terraform's built-in encryption) — not the sensitive flag.


Q9. An engineer needs to rename a resource from aws_s3_bucket.logs to aws_s3_bucket.application_logs in the Terraform configuration. Which approach correctly renames the resource in state without destroying and recreating the S3 bucket?

A) Delete the old resource block, add the new one, run terraform apply
B) Run terraform state mv aws_s3_bucket.logs aws_s3_bucket.application_logs then update the config
C) Run terraform import aws_s3_bucket.application_logs <bucket-name> then remove the old resource
D) Add moved {} block to the configuration then run terraform apply

Answer: B and D (both are correct, but B is the classic CLI approach and D is the declarative moved block introduced in Terraform 1.1)

terraform state mv renames the resource in state before the config change is applied, so Terraform sees the new name in state and does not recreate the bucket. The moved block in the config (moved { from = aws_s3_bucket.logs; to = aws_s3_bucket.application_logs }) achieves the same result declaratively — Terraform processes the move during the next apply. The moved block is the preferred modern approach for team workflows because it is committed to the repo and applies for every collaborator, whereas state mv is a one-off local operation. 004 emphasis: know the difference and prefer moved for shared codebases.


Q10. Which of the following is a requirement for using DynamoDB for Terraform state locking with an S3 backend?

A) The DynamoDB table must be in the same region as the S3 bucket
B) The DynamoDB table must have a partition key named LockID of type String
C) The DynamoDB table must have billing mode set to PROVISIONED
D) The DynamoDB table must have a sort key named Digest

Answer: B

The table must have a partition key (hash key) named exactly LockID with type String. The region does not need to match the S3 bucket (though it usually does for latency). Billing mode can be PAY_PER_REQUEST (on-demand) or PROVISIONED — both work. No sort key is required.


Q11. What does terraform state rm aws_instance.web do?

A) Terminates the EC2 instance in AWS and removes it from state
B) Removes the EC2 instance from state but the instance continues running in AWS
C) Marks the instance for deletion on the next terraform apply
D) Moves the instance to a separate state file

Answer: B

state rm removes the resource from Terraform's knowledge without touching the real resource. After state rm, Terraform has no record of aws_instance.web — the instance still runs in AWS but is "unmanaged" from Terraform's perspective. A subsequent terraform apply would attempt to create a new instance (because the config still has it but the state does not).


Practice Questions — Modules


Q12. Which of the following module source strings correctly references the networking subdirectory within a GitHub repository at tag v2.0.0?

A) "github.com/mohamednourdine/terraform-modules/modules/networking?ref=v2.0.0"
B) "git::https://github.com/mohamednourdine/terraform-modules.git//modules/networking?ref=v2.0.0"
C) "https://github.com/mohamednourdine/terraform-modules/modules/networking@v2.0.0"
D) "github.com/mohamednourdine/terraform-modules.git//modules/networking#v2.0.0"

Answer: B

The // double-slash separates the repository URL from the subdirectory path within the repository. The ?ref= query parameter specifies the git tag, branch, or commit SHA. Option A is missing the git:: prefix and the //. Option C uses HTTPS format but not the Terraform-recognized syntax. Option D uses the wrong ref delimiter.


Q13. A root module calls a child module and passes providers = { aws = aws.eu }. The child module has no required_providers block in its versions.tf. What happens?

A) Terraform ignores the providers argument and uses the default AWS provider
B) Terraform raises an error because the module does not declare the provider
C) The child module uses aws.eu for all its aws_* resources
D) Terraform downloads the default AWS provider for the child module

Answer: A

Without a required_providers block in the child module, Terraform cannot associate the passed provider with the module's resources. The providers argument in the module call is silently ignored and the module uses the default (non-aliased) provider. This means resources in the child module end up in us-east-1 instead of eu-west-1 with no error — a silent misconfiguration. This is one of the most dangerous provider alias gotchas.


Q14. What version of a module does the constraint version = "~> 1.5.2" allow?

A) Any version >= 1.5.2 and < 2.0.0
B) Any version >= 1.5.2 and < 1.6.0
C) Any version >= 1.5.0 and < 2.0.0
D) Exactly version 1.5.2

Answer: B

When ~> is used with a version that has three components (1.5.2), it locks the leftmost non-fixed digit — allowing only patch-level updates within 1.5.x. ~> 1.5 (two components) allows minor + patch updates within 1.x. ~> 1 (one component) would allow any 1.x.x version. This is the pessimistic constraint operator — it protects against breaking changes in the next minor or major version.


Q15. A module output is needed by another module in the same root configuration. How does the second module reference the first module's output?

A) data.module.first.output_name
B) module.first.output_name
C) var.first_module_output_name
D) output.first.output_name

Answer: B

Module outputs are referenced as module.<module_label>.<output_name>. The module label is the name given in the module block: module "first" { ... } → referenced as module.first.<output>. Data sources use data.<type>.<name>.<attribute>. Variables use var.<name>. There is no output. reference syntax for cross-module access.


Practice Questions — Configuration


Q16. An engineer has a count = 3 resource creating three EC2 instances. They need to remove the second instance (index 1) from Terraform management without destroying it. What happens to the third instance (index 2) after terraform state rm aws_instance.web[1]?

A) The third instance is unaffected and remains as aws_instance.web[2]
B) The third instance is renamed to aws_instance.web[1] in state
C) The next terraform apply recreates index 1 and leaves index 2 unchanged
D) The next terraform plan shows a change to destroy index 2 and create a new index 1

Answer: A

state rm only removes aws_instance.web[1] from state — web[2] remains registered. However, on the next terraform plan, Terraform sees a three-instance config (count = 3) with only two instances in state (0 and 2). It plans to create a new web[1] because index 1 is missing. Index 2 is still in state and remains unchanged. This illustrates why count is problematic for non-uniform lists — use for_each for named instances.


Q17. Which of the following for_each inputs are valid? (Select TWO)

A) for_each = ["us-east-1a", "us-east-1b"]
B) for_each = toset(["us-east-1a", "us-east-1b"])
C) for_each = { az_a = "us-east-1a", az_b = "us-east-1b" }
D) for_each = 3
E) for_each = var.count

Answer: B and C

for_each accepts a map or a set(string). A plain list (A) is not valid — it must be converted with toset(). A number (D) is not valid. A variable that evaluates to a number (E) is not valid. Sets and maps are the only accepted types.


Q18. What does the following lifecycle block prevent?

lifecycle {
  ignore_changes = [ami]
}

A) The resource is destroyed if the AMI ID changes
B) A new resource is created if the AMI ID changes
C) Terraform does not plan any changes when only the AMI ID has changed
D) The AMI ID attribute is not stored in the state file

Answer: C

ignore_changes = [ami] tells Terraform to ignore differences in the ami attribute when comparing the state to the real infrastructure. If the AMI in state differs from the AMI declared in the config, Terraform does not include it in the plan. This is commonly used with Auto Scaling Groups and EC2 instances where the AMI might be updated outside Terraform (e.g., by a patching process) and you do not want Terraform to force a replacement.


Q19. What does the try() function do?

A) Attempts to apply a resource and catches errors
B) Evaluates an expression and returns a fallback value if the expression raises an error
C) Tests whether a value is null and returns an alternative
D) Retries a failed provider API call

Answer: B

try(expression, fallback) evaluates the first argument. If it raises any error (type mismatch, index out of range, attribute not found), it silently returns the fallback value. It does not catch runtime infrastructure errors — only expression evaluation errors. coalesce() is different: it returns the first non-null, non-empty value from a list of values.


Q20. Which meta-argument explicitly declares that a resource depends on another, even though there is no attribute reference linking them?

A) provider
B) lifecycle
C) depends_on
D) connection

Answer: C

depends_on creates an explicit dependency when Terraform cannot infer the relationship from attribute references. A common use case: an IAM role policy attachment must complete before an EC2 instance launches, but the launch template does not directly reference the policy ARN. Without depends_on, Terraform might create the instance before the IAM role has its permissions. With depends_on = [aws_iam_role_policy_attachment.ec2], Terraform waits for the attachment before creating the instance.


Q20a (004). A team needs to stop managing an existing S3 bucket with Terraform without deleting the bucket from AWS. The change must be reviewed in a pull request and applied identically by every team member. Which approach satisfies all requirements?

A) Run terraform state rm aws_s3_bucket.legacy locally on each developer's machine
B) Add a removed { from = aws_s3_bucket.legacy; lifecycle { destroy = false } } block, delete the resource block, and merge
C) Set lifecycle { prevent_destroy = true } on the resource and delete it from state
D) Comment out the resource block and run terraform apply

Answer: B

The removed block (Terraform 1.7+) is the declarative equivalent of terraform state rm. With lifecycle { destroy = false }, the resource is removed from state but the real S3 bucket is left alone. Because the block lives in the config, it is reviewed in the PR and applied uniformly by everyone — unlike state rm, which is a per-developer local operation. Option C does not actually remove the bucket from state. Option D would cause Terraform to plan a destroy of the bucket on the next apply.


Q20b (004). A module declares the following block on a data source:

lifecycle {
  postcondition {
    condition     = self.architecture == "x86_64"
    error_message = "AMI must be x86_64."
  }
}

The AMI returned by the data source is arm64. What happens?

A) The plan succeeds with a warning
B) The plan succeeds; the postcondition is checked only after apply
C) The plan fails with the configured error message
D) Terraform retries with a different AMI

Answer: C

precondition and postcondition failures abort the plan or apply with the configured error message. They are designed to enforce invariants that must hold for the configuration to be valid. The contrast with the check block matters for the exam: a failed check produces a warning, not an error, because check is intended for ongoing health monitoring (the value can fail later without breaking the apply that created it). 004 explicitly tests this distinction.


Q20c (004). A team uses the Vault provider to fetch a database password at apply time:

data "vault_kv_secret_v2" "db" {
  mount = "kv"
  name  = "prod/database"
}

Which of the following statements is correct?

A) The password is encrypted in the Terraform state file
B) The password is never stored in the Terraform state file
C) The password is stored in plaintext in the state file and must still be protected by state encryption
D) The password is fetched on every plan but discarded after apply

Answer: C

The Vault provider keeps the secret out of the configuration (no plaintext in .tf files or VCS), but the value fetched by the data source is still written into the state file like any other data source attribute. State encryption (S3 SSE / HCP Terraform's at-rest encryption) is still required. 004 explicitly tests this caveat because the natural assumption is that 'using Vault' protects state, and it does not. Pair the Vault provider with state encryption, or use Vault's dynamic secrets engines so a leaked state exposes only an already-rotated credential.


Practice Questions — HCP Terraform


Q21. An engineer creates a workspace in HCP Terraform and marks AWS_SECRET_ACCESS_KEY as a sensitive environment variable. Which of the following is true? (Select TWO)

A) The value is encrypted at rest in HCP Terraform's storage
B) The value is visible to workspace admins in the HCP Terraform UI
C) The value is redacted in run logs
D) The value cannot be updated after it is set
E) The value is accessible via the HCP Terraform API with admin credentials

Answer: A and C

Sensitive variables in HCP Terraform are encrypted at rest and redacted from all run logs — the log shows [sensitive] where the value would appear. They are NOT visible in the UI to anyone (A is correct). They can be updated (overwritten) at any time but cannot be read back — it is write-only (D is wrong). The API also cannot retrieve the value — only overwrite it (E is wrong).


Q22. A team uses the cloud block to connect a workspace to HCP Terraform with the remote execution mode. An engineer runs terraform plan locally. Where does the plan actually execute?

A) On the engineer's local machine using local AWS credentials
B) In HCP Terraform's managed environment using workspace variables
C) On a Terraform agent in the engineer's private network
D) It fails because plans must be triggered by a VCS commit

Answer: B

In remote execution mode, terraform plan is a client-side command that streams output from the remote run. The actual execution happens in HCP Terraform's managed environment, using the workspace's stored variables (including AWS credentials). The engineer's local machine needs only the HCP Terraform API token (from terraform login), not AWS credentials. VCS integration is optional — manual CLI-triggered runs work in remote mode.


Q23. A Sentinel policy with enforcement level soft-mandatory blocks a terraform apply. Who can override it?

A) Nobody — soft-mandatory cannot be overridden
B) Any workspace member
C) Organization owners and workspace admins
D) Only HashiCorp support

Answer: C

Soft Mandatory allows organization owners and workspace admins to override the policy block with a written justification. This override is logged in HCP Terraform's audit trail. Hard Mandatory cannot be overridden by anyone. Advisory never blocks — it only warns.


Q24. A workspace in HCP Terraform has auto-apply disabled. A VCS push to the tracked branch triggers a run. The plan completes with 2 changes. What happens next?

A) The apply runs automatically after the plan
B) The run pauses and waits for a user to confirm and apply in the HCP Terraform UI
C) The run fails because auto-apply is required for VCS-triggered runs
D) The plan is discarded after 24 hours if no action is taken

Answer: B

With auto-apply disabled, every run (whether VCS-triggered or CLI-triggered) pauses after the plan phase and waits for a user to click 'Confirm & Apply' in the HCP Terraform UI. The run does not expire quickly — it remains pending until actioned or discarded manually. VCS integration does not require or imply auto-apply.


Q24a (004). An organization has 30 workspaces grouped under an HCP Terraform Project called fastapi-platform. They want to grant a new SRE team Read access to every workspace in the project, and have AWS region/role variables auto-applied to all of them. Which approach minimizes ongoing maintenance?

A) Add the team to each workspace individually and copy the AWS variables into each one
B) Grant team permissions and attach a Variable Set at the project level
C) Use a single VCS-connected workspace with for_each over the 30 environments
D) Use Sentinel to enforce variable values across all workspaces

Answer: B

HCP Terraform Projects are the organizational layer above workspaces precisely for this case. Team permissions, Variable Sets, and policy sets can all be attached at the project level and apply to every workspace within. Adding 30 individual permissions and 30 copies of the same variables is the anti-pattern this feature was designed to eliminate. Sentinel enforces policies but is not a mechanism for distributing variable values.


Q24b (004). An HCP Terraform workspace is configured with Dynamic Provider Credentials for AWS instead of the legacy AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY workspace env vars. How does the workspace authenticate to AWS at run time?

A) HCP Terraform retrieves the IAM user's credentials from AWS Secrets Manager
B) HCP Terraform issues a short-lived OIDC token; the AWS provider exchanges it for a role via AssumeRoleWithWebIdentity
C) HCP Terraform uses the Vault provider to fetch a stored access key
D) The workspace falls back to the local AWS CLI credentials chain

Answer: B

Dynamic Provider Credentials in HCP Terraform issues a workload identity OIDC token to each run. AWS is configured with an IAM identity provider for app.terraform.io and an IAM role whose trust policy permits AssumeRoleWithWebIdentity from that issuer, scoped to the specific HCP Terraform organization/workspace. Result: no long-lived AWS credentials are stored in HCP Terraform. The same primitive (OIDC → federated trust) underpins Azure federated credentials, GCP Workload Identity Federation, and Vault.


Q24c (004). Which two HCP Terraform features are grouped under Health and require the Plus edition? (Select TWO)

A) Drift Detection
B) Variable Sets
C) Continuous Validation
D) Run Triggers
E) Sentinel policy sets

Answer: A and C

Drift Detection runs a periodic refresh-only run and surfaces an alert when state diverges from real infrastructure. Continuous Validation periodically re-evaluates check block assertions and surfaces a warning when one starts failing. Both are grouped under 'Health' in the HCP Terraform UI and both require the Plus edition. Variable Sets and Run Triggers are available on the Standard tier. Sentinel policy sets are also Plus-tier but are listed under Governance, not Health.


Troubleshooting Scenarios

These are the errors most commonly encountered in real Terraform work. Each maps to an exam-style troubleshooting question.


Scenario 1: Backend block uses a variable

terraform {
  backend "s3" {
    bucket = var.state_bucket   # ERROR
    key    = "prod/terraform.tfstate"
  }
}

Error: Variables may not be used here.
Cause: The backend block is evaluated before variables are resolved. Terraform does not support dynamic backend configuration from variables or locals.
Fix: Use a literal string, or use partial backend configuration with -backend-config:

terraform init -backend-config="bucket=mnourdine-tf-state"

Or use a separate -backend-config file:

# backend.hcl
bucket = "mnourdine-tf-state"
terraform init -backend-config=backend.hcl

Scenario 2: Resource exists in AWS but not in state

Symptom: terraform apply attempts to create a resource that already exists in AWS. The apply fails with a "resource already exists" error from the provider.

Cause: The resource was created manually, by another Terraform config, or the state was lost/corrupted.

Fix:

# 1. Write the resource block in the config to match the existing resource
# 2. Import the existing resource into state
terraform import aws_s3_bucket.logs my-existing-bucket-name

# 3. Run plan — should show no changes if config matches real resource
terraform plan

Scenario 3: State lock is stuck

Symptom: terraform apply prints "Error acquiring the state lock" and hangs or fails immediately. A previous apply was killed mid-run.

Cause: The DynamoDB lock entry was not cleaned up when the previous process was terminated.

Fix:

# Get the lock ID from the error message or DynamoDB console
terraform force-unlock <lock-id>

Only use force-unlock when you are certain no other process holds the lock. If two processes hold locks simultaneously and you force-unlock, you risk state corruption.


Scenario 4: Module not updating after version change

Symptom: Updated version = "~> 1.5" in the module block, but terraform plan still shows the old module behavior.

Cause: terraform init has not been re-run after the version constraint change. The old module version is cached in .terraform/modules/.

Fix:

terraform init -upgrade
# Re-resolves module versions within the new constraint

Scenario 5: for_each on a list

# ERROR
resource "aws_subnet" "private" {
  for_each = var.subnet_cidrs   # var.subnet_cidrs = list(string)
}

Error: The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type list of string.

Fix:

resource "aws_subnet" "private" {
  for_each = toset(var.subnet_cidrs)
  cidr_block = each.value
}

Or use a map for clearer keys:

for_each = { for cidr in var.subnet_cidrs : cidr => cidr }

Scenario 6: Provider alias not working in module

Symptom: Module resources are created in us-east-1 even though the module call passes providers = { aws = aws.eu }.

Cause: The module has no required_providers block. Without it, Terraform ignores the providers argument.

Fix — add to modules/web-app/versions.tf:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Scenario 7: Sensitive output appears in state

Symptom: terraform output db_password shows (sensitive value), but the actual password is visible in the state file as plaintext.

Cause: sensitive = true on an output only redacts terminal output. It has no effect on state storage.

Fix: Encrypt the state backend:

  • S3: encrypt = true in the backend block (SSE-S3)
  • S3 with KMS: add kms_key_id to the backend block for customer-managed encryption
  • HCP Terraform: state is automatically encrypted at rest

Scenario 8: terraform validate passes but terraform plan fails

Symptom: validate exits 0, but plan fails with InvalidClientTokenId: The security token included in the request is invalid.

Cause: validate does not make provider API calls — it only checks HCL syntax and the provider schema. The plan actually calls the AWS API and discovers the credentials are expired or misconfigured.

Fix: Check AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN (if using temporary credentials), and AWS_PROFILE environment variables. Run aws sts get-caller-identity to verify the credentials work before running terraform plan.


Scenario 9 (004): import {} block plan-only fails

Symptom: Added an import {} block targeting an existing S3 bucket and ran terraform plan -generate-config-out=imported.tf. The plan fails with Resource already managed by Terraform or Cannot generate configuration: file already exists.

Cause: Either the import {} block points at a resource address that already exists in state (a duplicate), or the -generate-config-out target file already exists — the flag refuses to overwrite.

Fix:

# If the resource is already in state, you do not need to re-import. Remove the
# import block.
# If -generate-config-out fails because the file exists, point it at a new file:
terraform plan -generate-config-out=imported-new.tf
# Then merge the generated resource block into your real config and delete
# both the scratch file and the import block.

The import {} block is one-shot — keep it in the repo for one apply cycle, then remove it. Leaving import blocks in the config indefinitely is harmless but noisy.


Scenario 10 (004): check block warning blocks the apply (it shouldn't)

Symptom: A check block fails its assertion and the engineer thinks the apply was blocked. They cannot find a way to override it.

Cause: This is a misdiagnosis — check block failures do not block apply. They emit a warning only. If the apply was actually blocked, the failure was elsewhere (a precondition, postcondition, Sentinel/OPA policy, or a real provider error).

Fix: Re-read the run output to find the actual blocker. If you see Warning: Check block assertion failed next to the check block, that did not stop the apply. The 004 exam tests this distinction directly:

Construct On failure
variable.validation Aborts plan
lifecycle.precondition Aborts plan or apply (whichever is running)
lifecycle.postcondition Aborts plan or apply
check { assert {...} } Warning only — apply continues
Sentinel/OPA policy (mandatory) Blocks apply

Scenario 11 (004): Vault data source still leaks the secret

Symptom: Switched from a hardcoded password = "..." to a Vault data source to keep the secret out of git. A subsequent state file inspection shows the password in plaintext anyway.

Cause: The Vault provider keeps secrets out of the configuration, not out of state. Every data source attribute is recorded in state, including secret values fetched at apply time.

Fix: Combine Vault with state encryption — they are complementary, not alternatives:

  • S3 backend: encrypt = true (SSE-S3) or kms_key_id = "..." (SSE-KMS)
  • HCP Terraform: state is encrypted at rest by default
  • Restrict who can read state: S3 bucket policies, HCP Terraform team RBAC

For maximum protection, use Vault's dynamic secrets engines (e.g., vault_database_secret_backend_role) so the credential in state is short-lived and is rotated by Vault before a leaked state file is useful to an attacker.


Final Day Checklist

The day before the exam

During the exam

  • Read each question fully before looking at options
  • Note single-answer vs multi-answer before selecting
  • Flag uncertain questions and return at the end
  • For troubleshooting questions: identify the symptom, narrow to a cause, then pick the fix
  • For 'which is NOT correct' questions: verify every option independently
  • Use process of elimination on anything with absolutes ('always', 'never', 'only')

High-confidence areas to confirm before starting

Run through these mentally — if you hesitate on any, review before the exam:

  1. The seven variable input precedence sources (command-line -var wins; auto-tfvars files load together in lexical order)
  2. The ~> constraint with two-part (~> 1.5) vs three-part (~> 1.5.2) versions
  3. CLI workspace commands: new, select, list, show, delete
  4. terraform state mv vs the moved block — same effect, different workflow (CLI vs declarative); prefer moved for teams
  5. terraform state rm vs the removed block (with lifecycle { destroy = false }) — same effect, prefer removed for teams
  6. sensitive = true — what it does to state (nothing). The same caveat applies to Vault data sources.
  7. The backend block — cannot reference var.* or local.*
  8. CLI workspace vs HCP Terraform workspace vs HCP Terraform Project — know all three
  9. Sentinel and OPA: Advisory / Soft Mandatory / Hard Mandatory — who can override each
  10. Dynamic Provider Credentials in HCP Terraform — OIDC → AssumeRoleWithWebIdentity replaces static workspace env vars
  11. terraform taint status (deprecated; use apply -replace)
  12. terraform refresh status (deprecated standalone; plan -refresh-only to inspect, apply -refresh-only to persist)
  13. precondition / postcondition (abort) vs check block (warn-only)
  14. import CLI command vs import {} block (1.5+, with -generate-config-out)

Where I Am At

Twenty-four days of Terraform, one 004 exam ready to sit. The practice questions above hit the concepts that appear most frequently on the current revision — not because they are the hardest, but because they are the ones where a small misunderstanding produces a wrong answer on an otherwise well-prepared candidate. The 004-tagged questions (Q20a/b/c, Q24a/b/c) and Scenarios 9–11 cover the topics most likely to differ from older 003-era practice material still circulating online: moved/removed blocks, check blocks, the Vault provider state caveat, HCP Terraform Projects, Dynamic Provider Credentials, and the Health features (Drift Detection + Continuous Validation).

The troubleshooting scenarios are the other half of preparation. The exam uses scenario-based questions that describe a symptom and ask for the root cause or fix. Having seen these patterns in the lab — the backend variable error, the stuck state lock, the silent alias misconfiguration, the check block warning that didn't actually block apply — makes the exam scenario immediately recognizable.

The remaining days of the challenge shift back to hands-on infrastructure: the private networking module, advanced patterns, and real-world project work. The certification validates the foundation; the hands-on work is where the depth actually develops.


This post is part of a 30-day Terraform learning journey.

Share This Article

Did you find this helpful?

💬 Comments

No comments yet. Be the first to share your thoughts!

Leave a Comment

Get In Touch

I'm always open to discussing new projects and opportunities.

Location Yassa/Douala, Cameroon
Availability Open for opportunities

Connect With Me

Send a Message

Have a project in mind? Let's talk about it.