SaveMyCert
Log in
5 of 5 free questions left today·for 30 a day
Terraform 004 practice

Terraform 004 sample questions — 25 answered and explained

25 Terraform 004 sample questions written to the official HashiCorp Certified: Terraform Associate (004) exam guide, grouped by exam domain and weighted like the real exam. Each shows the correct answer, why every other option is wrong, and the lesson that teaches it. None of them repeat the examples on the other Terraform 004 practice pages.

On this page
25
answers included
Full bank
397
on the practice page
Domains
8
weighted like the exam
Free, no account
5/day
a free account allows 30 a day

Domain 1: Infrastructure as Code (IaC) with Terraform

8% of the Terraform 004 exam, so 2 of the 25 questions on this page.

Question 1Infrastructure as Code (IaC) with Terraform

A platform team uses Terraform for AWS. It now wants GitHub repositories, Datadog monitors and Cloudflare DNS records tracked in state and shown in plan, exactly like its AWS resources. What makes this possible?

Choose one.

  • a
    Terraform core ships native resource types for popular SaaS platforms such as GitHub and Datadog

    Terraform core has no platform-specific resource types at all. Every platform, cloud or SaaS, is reached through a provider plugin.

  • b
    Each service's CLI is called from local-exec provisioners attached to resources in the configuration

    Provisioners run commands but do not create resources Terraform tracks in state or diffs in plan, so the requirement is not met.

  • c
    Each platform's objects are read through the external data source and refreshed on each run

    Data sources only read information; Terraform does not create, change or destroy what it reads, so the objects are not managed.

  • d
    Each service has a provider plugin that maps its API objects to Terraform resource types Correct

    Terraform is service-agnostic: any service with an API can have a provider, and its resources follow the same state, plan and apply lifecycle as cloud resources.

The concept

Terraform is service-agnostic: a provider is a bridge to any API, so SaaS platforms are managed with the same workflow as clouds.

Why that’s the answer

Providers for GitHub, Datadog and Cloudflare expose resource types (for example a repository or a monitor) that Terraform tracks in state and diffs in plan, exactly like aws_instance. Provisioners and the external data source are tempting because they can reach any CLI or script, but neither creates managed resources: provisioners run side-effect commands and data sources only read.

How to reason it out
  1. Note the requirement: objects tracked in state and shown in plan.
  2. Recall that only resources (from a provider) are managed through state and plan.
  3. Reject workarounds that run commands or read data without managing resources.

Exam tip: If a service has an API, a provider can make it a Terraform resource with the normal lifecycle.

Infrastructure as Code with Terraform: Concepts and Multi-Cloud Workflows — the lesson that teaches this.

Question 2Infrastructure as Code (IaC) with Terraform

A platform team publishes a reusable Terraform module for a standard application stack. Product teams now stand up their own environments by setting a few variables, with no tickets to the platform team. Which IaC advantage does this show?

Choose one.

  • a
    Self-service: teams get approved infrastructure on demand, within limits that the shared code defines Correct

    Reusable modules let teams provision for themselves while the platform team's code sets the guardrails.

  • b
    Immutable infrastructure: published module versions are replaced with new versions, not edited in place

    Versioned modules are good practice, but the scenario is about teams no longer waiting on another team, not about how modules are released.

  • c
    State locking: concurrent runs against one state are blocked so that team changes do not collide

    State locking prevents simultaneous writes to one state. It does nothing to remove the ticket queue.

  • d
    Drift detection: environments are compared with configuration to flag changes made outside it

    Drift detection finds out-of-band changes. It is unrelated to letting teams provision on their own.

The concept

Self-service is an IaC advantage: platform teams package approved patterns as reusable modules, and other teams provision from them on demand.

Why that’s the answer

The scenario's change is that product teams no longer file tickets: they call a module with a few variables. That is self-service within guardrails, because the module decides what can be built. State locking, drift detection and immutable releases are real Terraform concepts, but none of them removes the dependency on another team.

How to reason it out
  1. Identify what changed: teams no longer wait on the platform team.
  2. Match it to the advantage about who can provision, not how state or releases behave.
  3. Reject real features that solve a different problem.

Exam tip: Reusable modules + no ticket queue = self-service within guardrails.

Infrastructure as Code with Terraform: Concepts and Multi-Cloud Workflows — the lesson that teaches this.

Domain 2: Terraform fundamentals

11% of the Terraform 004 exam, so 3 of the 25 questions on this page.

Question 3Terraform fundamentals

The constraint is version = "~> 5.0" and .terraform.lock.hcl records the AWS provider at 5.31.0. Release 5.45.0 is now available. Which command moves the working directory to 5.45.0?

Choose one.

  • a
    terraform init -reconfigure

    -reconfigure reinitializes the backend configuration without migrating state; it keeps the provider selections in the lock file.

  • b
    terraform init -upgrade Correct

    -upgrade makes init disregard the locked selections and pick the newest version matching the constraint, then rewrite the lock file.

  • c
    terraform get -update

    get -update refreshes modules already downloaded. It does not touch providers or the lock file.

  • d
    terraform refresh

    refresh reconciles state with real infrastructure and has no effect on provider versions.

The concept

Once a provider version is recorded in the lock file, init keeps reinstalling it until you explicitly ask for an upgrade with terraform init -upgrade.

Why that’s the answer

Both 5.31.0 and 5.45.0 satisfy "~> 5.0", so a plain init keeps the locked 5.31.0; that stability is the point of the lock file. init -upgrade disregards the existing selections, chooses the newest version allowed by the constraint and rewrites the lock file, which you then commit. -reconfigure is the tempting init flag, but it concerns the backend, and get -update is the module equivalent of an upgrade.

How to reason it out
  1. Check the constraint: 5.45.0 is allowed, so no constraint edit is needed.
  2. Recall that init honours locked selections over newer matching releases.
  3. Run terraform init -upgrade and commit the updated lock file.

Exam tip: New matching provider releases are ignored until terraform init -upgrade re-selects them.

Terraform Providers, Version Constraints, and State Basics — the lesson that teaches this.

Question 4Terraform fundamentals

What role does a provider such as hashicorp/aws play when Terraform plans and applies changes?

Choose one.

  • a
    A hosted HashiCorp service that runs applies against AWS on your behalf

    A provider is a plugin binary that runs locally with Terraform; API calls go from your run to AWS, not through a HashiCorp-hosted execution service.

  • b
    A plugin that defines the AWS resource types and calls the AWS APIs for them Correct

    Providers declare the resource and data source types they support and implement the create, read, update and delete calls against the platform API.

  • c
    A registry module of prebuilt AWS architecture templates for common workloads

    Reusable bundles of resources are modules. Providers supply the resource types that modules are built from.

  • d
    The backend component that stores and encrypts state inside your AWS account

    Storing state is the backend's job. A provider neither stores nor encrypts state.

The concept

Terraform core has no platform knowledge; provider plugins define resource types and translate planned changes into API calls.

Why that’s the answer

Terraform core parses configuration, builds the dependency graph and manages state, then hands each resource operation to the provider that owns its type (aws_instance belongs to aws). The provider talks to the platform API. Modules package resources, backends store state, and HCP Terraform is the hosted run service; none of those is the provider.

How to reason it out
  1. Separate the roles: core (graph, state), provider (API calls), backend (state storage), module (reusable configuration).
  2. Map the resource type prefix to its provider.
  3. Pick the component that performs the API calls: the provider plugin.

Exam tip: Providers are plugins that define resource types and make the API calls; core never talks to the platform directly.

Terraform Providers, Version Constraints, and State Basics — the lesson that teaches this.

Question 5Terraform fundamentals

A team must deploy the same application into us-east-1 and eu-west-1 from a single configuration. How should they configure the AWS provider?

Choose one.

  • a
    One provider "aws" block with region set to a list of both Regions

    region takes a single string. A provider configuration does not fan resources out across several Regions.

  • b
    A second provider "aws" block with an alias, chosen via alias = "<alias>" on resources

    Near-twin: the second aliased block is right, but alias is an argument of provider blocks; a resource selects a configuration with the provider meta-argument.

  • c
    A second provider "aws" block with an alias, chosen via provider = aws.<alias> Correct

    alias creates an additional configuration of the same provider; resources that need it set the provider meta-argument to aws.<alias>.

  • d
    A second required_providers entry for aws, chosen via provider = "aws-eu-west-1"

    required_providers declares the plugin dependency once; extra Regions are extra provider configurations, and provider takes a reference, not a string.

The concept

Multiple configurations of the same provider are created with alias, and resources select a non-default configuration with the provider meta-argument.

Why that’s the answer

Keep the default provider "aws" block for us-east-1 and add a second block with alias = "europe" (any name) and region = "eu-west-1". Resources that belong in Europe set provider = aws.europe; the rest use the default. The near-twin gets the alias right but tries to choose it by a region argument on the resource, which is not how selection works, and required_providers declares the plugin only once.

How to reason it out
  1. Recognise the trigger: same provider, different settings.
  2. Add an aliased provider block with the second Region.
  3. Set provider = aws.<alias> on the resources (or module calls) that belong there.

Exam tip: Same provider, two Regions: add an alias and select it with provider = aws.<alias>.

Terraform Providers, Version Constraints, and State Basics — the lesson that teaches this.

Domain 3: Core Terraform workflow

19% of the Terraform 004 exam, so 5 of the 25 questions on this page.

Question 6Core Terraform workflow

A team has kept state on local disk and now adds a backend "s3" block to the configuration. After the switch, Terraform must keep tracking every resource it already manages, without importing anything again. Which command should they run?

Choose one.

  • a
    terraform init -reconfigure

    -reconfigure initializes the new backend while deliberately ignoring the previous configuration - it does NOT migrate existing state, which would leave the S3 backend empty.

  • b
    terraform init -backend=false

    -backend=false skips backend initialization entirely, so the S3 backend is never set up and no state is moved - useful for validate-only CI jobs, not for a migration.

  • c
    terraform init -migrate-state Correct

    -migrate-state tells init to detect the backend change and interactively copy the existing state into the newly configured backend.

  • d
    terraform init -upgrade

    -upgrade re-resolves provider and module versions. It does not handle backend changes or state migration.

The concept

Changing the backend configuration requires re-running terraform init, and init offers two distinct paths: -migrate-state copies existing state to the new backend, while -reconfigure abandons the old backend configuration without moving anything.

Why that’s the answer

The team's requirement is continuity: the state that currently describes their infrastructure must end up in S3. terraform init -migrate-state performs exactly that guided migration, prompting for confirmation before copying. Choosing -reconfigure here would be a classic mistake - the S3 backend would start empty, and the next plan would propose creating everything again because Terraform no longer knows about the existing resources.

How to reason it out
  1. Add or edit the backend block in the configuration.
  2. Run terraform init -migrate-state and confirm the prompt to copy state to the new backend.
  3. Verify with terraform plan that no unexpected changes are proposed.

Exam tip: -migrate-state moves existing state to a new backend; -reconfigure switches backends without moving state.

Terraform init, validate, and plan: the core workflow explained — the lesson that teaches this.

Question 7Core Terraform workflow

A team does not want to hard-code backend settings such as the bucket name and access credentials in their backend block. They leave those arguments out of the configuration. How can they supply the missing backend settings?

Choose one.

  • a
    Pass them with -backend-config when running terraform plan

    -backend-config is an init option. plan uses the backend that init already configured and does not accept backend settings, so the missing arguments would still be missing.

  • b
    Pass them with -backend-config when running terraform init Correct

    This is partial backend configuration: the backend block declares the backend type, and -backend-config supplies the remaining arguments during init.

  • c
    Export them as TF_VAR_ environment variables before init

    TF_VAR_ variables set input variable values, and a backend block cannot refer to input variables, locals or data sources, so these values never reach the backend.

  • d
    Set them in the provider block for the backend to inherit

    Provider configuration and backend configuration are separate. The backend does not inherit anything from provider blocks.

The concept

Terraform supports partial backend configuration: you may omit some backend arguments from the configuration file and provide them when running terraform init, using -backend-config="key=value" flags or -backend-config=path pointing at a configuration file.

Why that’s the answer

Backend blocks are processed during init, before input variables exist, so they cannot refer to variables, locals or data sources. Partial configuration is the supported way to keep secrets and environment-specific values out of committed code: the committed block names the backend type, and each environment passes its own bucket, key and credentials to terraform init with -backend-config (a file or key=value pairs). TF_VAR_ variables and provider settings never reach the backend, and plan does not take -backend-config at all.

How to reason it out
  1. Declare the backend block with only the non-sensitive, common settings.
  2. Run terraform init -backend-config=backend.hcl (or repeated key=value flags) to supply the rest.
  3. Keep per-environment backend config files out of version control if they contain secrets.

Exam tip: Backend blocks cannot use variables - supply dynamic backend settings with init -backend-config.

Terraform init, validate, and plan: the core workflow explained — the lesson that teaches this.

Question 8Core Terraform workflow

A CI job checks out a repository and immediately runs terraform validate, which fails with an error about missing provider schemas. The configuration itself is known to be correct. What is the most likely cause?

Choose one.

  • a
    The job never ran terraform init, so no provider plugins are installed Correct

    validate needs the provider schemas that init installs in order to check argument names and types. In a fresh checkout, init must run first.

  • b
    The job has no cloud credentials, and validate authenticates to the provider

    validate makes no remote API calls and needs no cloud credentials. Missing credentials would surface later, at plan or apply.

  • c
    The configuration was never applied, so validate finds no state to read

    validate has no dependency on state or on any prior apply; it works in a brand-new configuration that has never been applied.

  • d
    The runner cannot reach the remote state backend that validate reads

    validate does not read state at all, so state accessibility cannot be the cause.

The concept

Although terraform validate is an offline check, it is not dependency-free: it validates resource arguments against provider schemas, and those schemas come from the provider plugins that terraform init installs into the working directory.

Why that’s the answer

A CI runner starts from a clean checkout with no .terraform directory, so validate has no provider schemas to check against and fails. The fix is to run terraform init before validate in the pipeline (init -backend=false is a common variant when the job only validates and does not need state access). Credentials and state are red herrings - validate touches neither.

How to reason it out
  1. Add terraform init as the first Terraform step in the CI job.
  2. Run terraform validate after init succeeds.
  3. Fail the pipeline on a nonzero validate exit code to block broken configurations.

Exam tip: validate is offline but still requires an initialized directory for provider schemas.

Terraform init, validate, and plan: the core workflow explained — the lesson that teaches this.

Question 9Core Terraform workflow

A nightly job must report whether a configuration has changes waiting to be applied. The script branches on the command's result without parsing any output, and nothing may be changed. Which command should it run?

Choose one.

  • a
    terraform plan -detailed-exitcode Correct

    -detailed-exitcode makes plan exit 0 when there are no changes, 2 when changes are pending, and 1 on errors, so the script can branch on the status alone.

  • b
    terraform plan -out=nightly.tfplan

    -out saves the plan for a later apply, but the exit status is still 0 on success whether or not changes exist, so the script would have to parse the output to tell the cases apart.

  • c
    terraform plan -input=false

    -input=false stops plan from prompting for missing variable values, which suits unattended jobs, but the exit status is still 0 on success whether or not changes are pending.

  • d
    terraform plan -json

    -json streams machine-readable output, but the exit status is still 0 on success with or without changes, so the script would have to parse the output.

The concept

terraform plan -detailed-exitcode changes plan's exit-status contract into a three-way signal: 0 means success with an empty diff, 1 means an error occurred, and 2 means success with changes present.

Why that’s the answer

Only -detailed-exitcode changes what the exit status means: 0 for no changes, 2 for pending changes, 1 for errors. Every other plan variant exits 0 on success whether or not changes exist - saving the plan with -out, emitting JSON, or disabling interactive input - so a script would have to parse output to tell the cases apart. plan remains read-only, so nothing is changed.

How to reason it out
  1. Run terraform plan -detailed-exitcode in the scheduled job.
  2. Branch on the exit status: 0 = in sync, 2 = changes pending, 1 = error.
  3. Alert or trigger a review workflow when the exit status is 2.

Exam tip: plan -detailed-exitcode returns 0/1/2 for no-changes/error/changes-present - ideal for automation.

Terraform init, validate, and plan: the core workflow explained — the lesson that teaches this.

Question 10Core Terraform workflow

You suspect someone changed infrastructure manually in the cloud console. You want Terraform to show how real infrastructure differs from the state file, without proposing any changes to match the configuration. Which command fits best?

Choose one.

  • a
    terraform plan -refresh-only Correct

    A refresh-only plan queries real infrastructure and shows only how the state would be updated to match it, without planning any configuration-driven changes.

  • b
    terraform plan -target=aws_instance.web

    -target narrows a normal plan to specific resources; it still proposes changes to match the configuration rather than reporting pure drift.

  • c
    terraform validate

    validate never contacts real infrastructure or reads state, so it cannot detect drift.

  • d
    terraform plan -destroy

    A destroy plan proposes removing all managed infrastructure - the opposite of a drift report.

The concept

Refresh-only mode (terraform plan -refresh-only, and its counterpart apply -refresh-only) isolates the refresh step of the workflow: it compares the state file with real infrastructure and reports what state updates would be needed, without mixing in configuration changes.

Why that’s the answer

A normal plan conflates two comparisons - real infrastructure vs state, and state vs configuration - into one set of proposed actions. When investigating manual out-of-band changes, you want only the first comparison. plan -refresh-only presents exactly that drift, and if you then want the state file updated to accept the external changes, apply -refresh-only will do it without touching infrastructure.

How to reason it out
  1. Run terraform plan -refresh-only to see detected drift between state and reality.
  2. Review which attributes changed outside Terraform.
  3. Either run terraform apply -refresh-only to accept the drift into state, or run a normal plan/apply to revert the infrastructure to match configuration.

Exam tip: plan -refresh-only reports drift between state and real infrastructure without proposing config-driven changes.

Terraform init, validate, and plan: the core workflow explained — the lesson that teaches this.

Domain 4: Terraform configuration

22% of the Terraform 004 exam, so 5 of the 25 questions on this page.

Question 11Terraform configuration

A child module is instantiated with module "network" { source = "./modules/network" } and declares output "vpc_id". How does the root module reference that value?

Choose one.

  • a
    output.network.vpc_id

    There is no output. address prefix for reading values. Outputs of child modules are read through the module. prefix on the calling side.

  • b
    var.network.vpc_id

    var. addresses input variables declared in the current module, not values coming back from a child module.

  • c
    data.module.network.vpc_id

    data. addresses data sources. Module outputs are not data sources and use the module. prefix instead.

  • d
    module.network.vpc_id Correct

    Correct. A child module's outputs are exposed to its caller as module.MODULE_NAME.OUTPUT_NAME.

The concept

Outputs are the interface a child module exposes to its caller. The calling module reads them with the address form module.NAME.OUTPUT, where NAME is the label given in the module block.

Why that’s the answer

Since the module block is labeled network and the child declares output "vpc_id", the caller reads module.network.vpc_id. The distractors misuse other prefixes: output. is not a reference prefix at all, var. is for the module's own inputs, and data. is for data sources.

How to reason it out
  1. Declare output "vpc_id" with a value inside the child module.
  2. Instantiate the child with module "network" { source = ... } in the caller.
  3. Reference the value as module.network.vpc_id anywhere in the calling module.

Exam tip: Child module outputs are read as module.NAME.OUTPUT; outputs are the only values a module exposes upward.

Terraform Resources, Data Sources, Variables, and Outputs — the lesson that teaches this.

Question 12Terraform configuration

A variable must hold one structured value with exactly three named attributes: name (a string), instance_count (a number) and monitoring (a bool). Which type constraint fits?

Choose one.

  • a
    map(object({ name = string, instance_count = number, monitoring = bool }))

    This describes a map whose every element is such an object, keyed by arbitrary strings. The requirement is one value, not a collection of them.

  • b
    tuple([string, number, bool])

    A tuple fixes types by position, not by name, so callers would pass ["web", 2, true] and the attributes would have no names.

  • c
    object({ name = string, instance_count = number, monitoring = bool }) Correct

    Correct. An object type declares a fixed set of named attributes, each with its own type, which matches one structured value exactly.

  • d
    list(object({ name = string, instance_count = number, monitoring = bool }))

    This is a list of such objects. It would accept zero, one or many of them rather than exactly one structured value.

The concept

Structural types describe a fixed shape: object for named attributes of different types, tuple for positions of different types. Wrapping them in map() or list() makes a collection of that shape.

Why that’s the answer

One value with named attributes of mixed types is precisely an object type. The near-twins add an outer collection (map or list), which changes the variable into many such values, and the tuple keeps the types but loses the attribute names.

How to reason it out
  1. Ask whether the requirement is one value or a collection: here it is one.
  2. Ask whether elements are identified by name or by position: by name.
  3. Named attributes with mixed types means object({ ... }).

Exam tip: Named mixed attributes = object; positional mixed types = tuple; list()/map() around them makes a collection.

Terraform Resources, Data Sources, Variables, and Outputs — the lesson that teaches this.

Question 13Terraform configuration

A variable is declared with type = set(string). Which statement about its value is true?

Choose one.

  • a
    Elements keep their written order, and duplicate values are kept.

    That describes list(string). A set discards duplicates and has no defined order.

  • b
    Elements are key-value pairs, and each value is read by its key.

    That describes map(string). A set holds bare values with no keys.

  • c
    Elements are unordered and unique, and cannot be read by index. Correct

    Correct. A set is identified only by its values: duplicates collapse and there is no index, so var.x[0] fails (convert with tolist() if you need positions).

  • d
    Elements are unique and keep their order, so var.x[0] is valid.

    Uniqueness is right, but sets have no order and no index; indexing a set is an error.

The concept

Terraform's collection types differ in ordering and access: list is ordered and indexed, map is keyed, set is unordered and unique.

Why that’s the answer

A set(string) stores each distinct string once and keeps no order, so it cannot be indexed. That makes sets the natural input for for_each over strings but unsuitable when positions matter; tolist() converts a set when an index is needed. The near-twin that keeps uniqueness but adds ordering is the common misconception.

How to reason it out
  1. Recall the three collection types: list, map, set.
  2. Sets drop duplicates and have no defined order.
  3. No order means no index: element access needs tolist() or a for expression.

Exam tip: set = unique and unordered, no index; list = ordered and indexed; map = keyed.

Terraform Resources, Data Sources, Variables, and Outputs — the lesson that teaches this.

Question 14Terraform configuration

A module input must accept a value such as ["web", 2, true], where the first element is a string, the second a number and the third a bool. Which type constraint enforces exactly that shape?

Choose one.

  • a
    list(string)

    A list has one element type for every element, so Terraform converts 2 and true to the strings "2" and "true" instead of enforcing a type per position.

  • b
    object({ web = string, count = number, enabled = bool })

    An object type expects named attributes, written with braces. A bracketed positional value does not match an object type.

  • c
    list(tuple([string, number, bool]))

    This is a list whose elements are each a three-part tuple, so the caller would have to wrap the value in an extra set of brackets.

  • d
    tuple([string, number, bool]) Correct

    Correct. A tuple is a fixed-length sequence where each position has its own type, exactly matching string, number, bool.

The concept

A tuple type fixes both the length of a sequence and the type of each position; a list has one element type for every element.

Why that’s the answer

The value is positional (brackets) and mixes types, so the matching constraint is tuple([string, number, bool]). list(string) looks like it accepts the value, but only because Terraform converts the number and bool to strings: a list has one element type, so the per-position types are lost. The object option keys by name, and the list-of-tuples option adds an extra level of nesting.

How to reason it out
  1. The value is written with brackets, so it is a sequence, not an object.
  2. Each position has a different type, so a single-element-type list cannot express it.
  3. Fixed length with per-position types means tuple([...]).

Exam tip: Positional mixed types = tuple; a list converts its elements to one shared type.

Terraform Resources, Data Sources, Variables, and Outputs — the lesson that teaches this.

Question 15Terraform configuration

A variable "tags" is declared with type = map(string). Which expression reads the value stored under the key environment?

Choose one.

  • a
    var.tags[environment]

    Without quotes, environment is parsed as a reference to a named object, not as the string key, so Terraform reports an invalid reference.

  • b
    var.tags["environment"] Correct

    Correct. Map elements are read by key with a quoted string inside brackets.

  • c
    var.tags[0]

    Maps are keyed by strings, not by numeric position, so an index of 0 does not select the environment entry.

  • d
    element(var.tags, "environment")

    element() takes a list and a numeric index. It cannot look up a key in a map.

The concept

Map values are read by key: var.MAP["key"]. Lists are read by numeric index.

Why that’s the answer

A map(string) holds string values under string keys, so the value under environment is var.tags["environment"]. The unquoted form makes Terraform treat environment as a reference, numeric indexing is for lists and tuples, and element() works on lists only. (For keys that are valid identifiers, var.tags.environment also works, but it is not offered here.)

How to reason it out
  1. Identify the collection type: map(string), so access is by key.
  2. Keys are strings, so put the key in quotes inside brackets.
  3. Rule out list-style access: numeric indexes and element().

Exam tip: Read a map with var.name["key"]; numeric indexes are for lists and tuples.

Terraform Resources, Data Sources, Variables, and Outputs — the lesson that teaches this.

Domain 5: Terraform modules

11% of the Terraform 004 exam, so 3 of the 25 questions on this page.

Question 16Terraform modules

A Git repository at https://github.com/acme/infra.git holds several modules, and you need the one in the modules/vpc subdirectory, pinned to the tag v2.1.0. Which source string is correct?

Choose one.

  • a
    git::https://github.com/acme/infra.git?ref=v2.1.0//modules/vpc

    The subdirectory must come before the query string. Placing // after ?ref= makes it part of the ref value, which no tag matches.

  • b
    git::https://github.com/acme/infra.git/modules/vpc?ref=v2.1.0

    A single slash makes modules/vpc part of the repository URL, which points at a repository path that does not exist. The subdirectory needs the // delimiter.

  • c
    git::https://github.com/acme/infra.git//modules/vpc?ref=v2.1.0 Correct

    Correct. The // separates the repository from the subdirectory inside it, and ?ref=v2.1.0, placed after the subdirectory, selects the tag.

  • d
    git::https://github.com/acme/infra.git//modules/vpc plus version = "2.1.0"

    The subdirectory part is right, but version is accepted only for registry sources. The tag must be selected with ?ref= in the address.

The concept

Git module sources support two special pieces of syntax: a // delimiter that marks a subdirectory within the repository, and a ?ref= query parameter that selects a tag, branch or commit. The subdirectory belongs to the path and the ref to the query string.

Why that’s the answer

The correct string composes them in the required order: repository URL, then //subdirectory, then ?ref=tag. Putting the subdirectory after the query corrupts the ref value, a single slash loses the subdirectory delimiter, and the version argument is rejected for Git sources.

How to reason it out
  1. Start with the git:: prefix and the repository HTTPS URL ending in .git.
  2. Append // followed by the path of the module inside the repository.
  3. Append ?ref=v2.1.0 to check out the tagged release.
  4. Run terraform init to clone that ref and load only the subdirectory as the module.

Exam tip: Git sources: //subdir selects a folder inside the repo and ?ref= pins the revision, in that order.

Terraform Modules: Sourcing, Using, and Versioning Explained — the lesson that teaches this.

Question 17Terraform modules

The root module declares variable "region", and resources in a child module use var.region, but the child declares no variables. terraform validate reports a reference to an undeclared input variable inside the child. What fixes it?

Choose one.

  • a
    Move the root variable "region" block into a file named variables.tf at the root.

    File names do not matter to Terraform; all .tf files in a directory form one module. Moving the root declaration leaves the child without its own.

  • b
    Read the root value from inside the child with a terraform_remote_state data source.

    terraform_remote_state reads outputs from another configuration's state. It cannot see the calling module's variables during the same run.

  • c
    Reference the value inside the child as var.root.region instead of var.region.

    There is no var.root namespace. var. only refers to variables declared in the same module.

  • d
    Declare region in the child and set region = var.region on the module block. Correct

    Correct. The child must declare its own input variable, and the caller passes the root value in as an argument on the module block.

The concept

Variables are scoped to the module that declares them. A child module receives values only through input variables it declares, set as arguments on the calling module block.

Why that’s the answer

var.region inside the child refers to the child's own variables, and it has none, so validation fails. Declaring variable "region" in the child and passing region = var.region from the root is the only supported link. File names have no effect on scope, remote state reads another configuration's outputs, and there is no var.root syntax.

How to reason it out
  1. See that the reference is in the child, which declares no variables.
  2. Recall that child modules never inherit the caller's variables.
  3. Declare variable "region" inside the child module.
  4. Pass region = var.region as an argument on the module block in the root.

Exam tip: A child module sees only the variables it declares; the caller passes values in explicitly.

Terraform Modules: Sourcing, Using, and Versioning Explained — the lesson that teaches this.

Question 18Terraform modules

A child module called "app" creates aws_instance.web but declares no outputs. The root module needs that instance's ID for another resource. What must change?

Choose one.

  • a
    Reference module.app.aws_instance.web.id from the root module directly.

    The caller cannot reach resources inside a child by address. Only declared outputs are visible as module.app.<output>.

  • b
    Declare output "web_id" in the child and read module.app.web_id in the root. Correct

    Correct. The child exposes the value with an output block, and the root reads it as module.app.web_id.

  • c
    Declare output "web_id" in the root and read module.app.aws_instance.web.id.

    The output has to be declared in the module that owns the resource. A root output still cannot reach into the child's resources.

  • d
    Add a terraform_remote_state data source in the root that reads the child.

    A child module shares the root's state and is not a separate configuration. Its values reach the caller only through outputs.

The concept

A child module's resources are private to it. The only way to give the caller a value is an output block in the child, read as module.NAME.OUTPUT.

Why that’s the answer

Declaring output "web_id" with value = aws_instance.web.id inside the child exposes the ID, and the root reads module.app.web_id. Direct resource addresses across the module boundary are not allowed, an output declared in the root is on the wrong side of the boundary, and remote state is for reading a different configuration.

How to reason it out
  1. Note the child has the resource but no outputs.
  2. Recall that callers see only declared outputs of a child module.
  3. Add an output block in the child whose value is aws_instance.web.id.
  4. Reference module.app.web_id in the root.

Exam tip: To use a value from a child module, declare an output in the child and read module.NAME.OUTPUT.

Terraform Modules: Sourcing, Using, and Versioning Explained — the lesson that teaches this.

Domain 6: Terraform state management

11% of the Terraform 004 exam, so 3 of the 25 questions on this page.

Question 19Terraform state management

An engineer is blocked by a state lock error and considers running terraform apply -lock=false to get past it. What is the main risk of doing this?

Choose one.

  • a
    The apply also skips refresh, so recent drift goes undetected

    -lock=false only skips lock acquisition. Refresh is controlled separately by -refresh.

  • b
    The lock held by the other run is released, so that run fails mid-apply

    -lock=false does not touch the existing lock; it simply proceeds without acquiring one. The other run keeps its lock and carries on.

  • c
    Another run may still be writing, so two writers can corrupt the state Correct

    Correct. The lock exists because another operation may be writing state; bypassing it allows two concurrent writers and the lost updates or corruption locking prevents.

  • d
    The backend's lock record is deleted, disabling locking for later runs

    The flag affects only the current command. Later runs lock normally.

The concept

-lock=false tells one command not to acquire the state lock. It does not remove or alter anyone else's lock.

Why that’s the answer

If the lock is held, some operation probably is writing state. Proceeding without locking means two writers, exactly the corruption scenario locking exists to stop. The flag neither skips refresh, nor releases the other run's lock, nor changes behaviour for later runs.

How to reason it out
  1. Ask why the lock is held: another operation may be in progress.
  2. Note that -lock=false skips acquiring the lock but leaves the other lock in place.
  3. Conclude that both runs can now write state concurrently.

Exam tip: -lock=false reintroduces the concurrent-write risk locking prevents; it is almost never the right fix.

Terraform State: Backends, Locking, Remote State, and Drift — the lesson that teaches this.

Question 20Terraform state management

A networking configuration declares an output named subnet_id only inside a child module; the root module does not re-export it. An application configuration reads data.terraform_remote_state.network.outputs.subnet_id. What happens?

Choose one.

  • a
    It fails, because only the root module's outputs are exposed Correct

    Correct. terraform_remote_state exposes only output values declared in the root module of the source configuration. A child-module output must be re-exported by a root output to be readable.

  • b
    It works, because child module outputs are reachable by module path

    Module outputs are not addressable through terraform_remote_state; only root outputs appear under outputs.

  • c
    It fails, because the output must also be declared in the reading configuration

    The reading configuration declares no outputs for this; it only needs the data source. The real cause is that subnet_id is not a root module output of the network configuration.

  • d
    It works, because every output in the state is merged into outputs

    Only root module outputs are stored as the state's outputs; child-module outputs are not flattened into them.

The concept

terraform_remote_state reads another configuration's state and exposes that configuration's root module outputs only.

Why that’s the answer

The producing configuration controls what it publishes by declaring root outputs. A child-module output is internal until the root module re-exports it with its own output block (for example value = module.vpc.subnet_id). Sensitivity and module paths do not change this.

How to reason it out
  1. Locate where subnet_id is declared: a child module.
  2. Recall that terraform_remote_state exposes only root module outputs.
  3. Conclude that the reference fails until the root module re-exports the value.

Exam tip: terraform_remote_state sees only the source's root outputs; re-export child outputs at the root.

Terraform State: Backends, Locking, Remote State, and Drift — the lesson that teaches this.

Question 21Terraform state management

A platform team stores its network state in an S3 bucket and declares a root output named vpc_id. An application team declares data "terraform_remote_state" "network" with matching s3 backend settings. Which expression references the VPC ID?

Choose one.

  • a
    data.terraform_remote_state.network.vpc_id

    Remote outputs are nested under the outputs attribute; the output name is not a direct attribute of the data source.

  • b
    terraform_remote_state.network.outputs.vpc_id

    Without the data. prefix Terraform treats this as a managed resource of type terraform_remote_state, which does not exist.

  • c
    data.terraform_remote_state.network.outputs.vpc_id Correct

    Correct. Data sources are referenced with the data. prefix, and the source's root outputs are under the outputs attribute.

  • d
    data.terraform_remote_state.network.output.vpc_id

    The attribute is outputs (plural); output is not an attribute of terraform_remote_state.

The concept

terraform_remote_state is a data source, so it takes the data. prefix, and its root module outputs are exposed under the outputs attribute.

Why that’s the answer

The full path is data.<type>.<name>.outputs.<output_name>. Each distractor breaks one rule: missing outputs, missing data. or the singular output.

How to reason it out
  1. Recall that data sources are referenced with the data. prefix.
  2. Recall that remote outputs sit under the outputs attribute.
  3. Append the output name.

Exam tip: Remote outputs: data.terraform_remote_state.<name>.outputs.<output>.

Terraform State: Backends, Locking, Remote State, and Drift — the lesson that teaches this.

Domain 7: Maintain infrastructure with Terraform

8% of the Terraform 004 exam, so 2 of the 25 questions on this page.

Question 22Maintain infrastructure with Terraform

Before a risky refactor, an engineer wants a backup of a workspace's current state in raw state-file form, printed to stdout so it can be redirected to a file. Which command does this?

Choose one.

  • a
    terraform show -json

    terraform show -json emits Terraform's JSON representation of the state for tooling, a different document from the stored state file, so it is not a restorable copy.

  • b
    terraform state pull Correct

    terraform state pull downloads the current state from the configured backend and writes the raw state document to stdout.

  • c
    terraform output -json

    terraform output -json prints only the root output values, not the resources in state.

  • d
    terraform state push

    terraform state push goes the other way: it uploads a local state file to the backend.

The concept

terraform state pull reads the current state through whichever backend is configured and prints it to stdout in the raw state-file format (upgraded to the newest state-file version the installed Terraform supports, if needed).

Why that’s the answer

pull outputs the raw state file, which is why it is the standard way to back up or inspect remote state; if the stored file uses an older state-file version, the local copy is upgraded to the format the installed Terraform understands before it is printed. terraform show -json is the tempting near-twin: it is JSON too, but it is the machine-readable representation format for tools, not the state file itself. output -json covers only outputs, and push uploads rather than downloads.

How to reason it out
  1. Note the need: the current state in raw state-file form, on stdout.
  2. Recall that terraform state pull fetches and prints the raw state.
  3. Separate it from terraform show -json, which emits a different JSON representation for tooling.

Exam tip: terraform state pull = the raw stored state on stdout; terraform show -json = a JSON view for tools.

Terraform Import, State Inspection, and Debug Logging — the lesson that teaches this.

Question 23Maintain infrastructure with Terraform

A CI pipeline saves a plan with terraform plan -out=plan.tfplan. A later job must read that saved plan in machine-readable form so a policy tool can check it. Which command produces this?

Choose one.

  • a
    terraform state pull > plan.json

    state pull prints the current state, not the changes proposed in a saved plan.

  • b
    terraform plan -json

    plan -json streams machine-readable output for a brand-new plan run; it does not read the saved plan.tfplan artifact.

  • c
    terraform show plan.tfplan

    This reads the saved plan but renders it in human-readable form, which a policy tool cannot reliably parse.

  • d
    terraform show -json plan.tfplan Correct

    terraform show -json with a plan file renders that saved plan as a machine-readable JSON document for tooling.

The concept

A saved plan file is binary; terraform show reads it, and the -json flag turns the output into the documented machine-readable format.

Why that’s the answer

terraform show -json plan.tfplan is the standard way to hand a saved plan to policy checks. Two near-twins fail one requirement each: terraform show plan.tfplan reads the right artifact but only as human-readable text, and terraform plan -json is machine-readable but creates a new plan instead of reading the saved one. state pull covers current state, not proposed changes.

How to reason it out
  1. Identify the input: a saved plan file, not current state.
  2. Identify the output: machine-readable JSON.
  3. Combine both: terraform show (reads the file) with -json (format).

Exam tip: Read a saved plan for tooling with terraform show -json PLANFILE.

Terraform Import, State Inspection, and Debug Logging — the lesson that teaches this.

Domain 8: HCP Terraform

10% of the Terraform 004 exam, so 2 of the 25 questions on this page.

Question 24HCP Terraform

Remote runs in an HCP Terraform workspace fail because the AWS provider finds no credentials. A teammate had added AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to the workspace as Terraform variables marked sensitive. Which fix works without changing the configuration?

Choose one.

  • a
    Re-create both values in the environment variable category, keeping them sensitive Correct

    Correct. The AWS provider reads these names from the run's shell environment, which only environment-category variables populate. Sensitive values are still supplied to runs.

  • b
    Clear the sensitive flag on both Terraform variables so remote runs can read them

    Sensitive only hides the value from the UI and API; runs already receive it. The problem is the category, not the flag.

  • c
    Move both Terraform variables unchanged into a variable set applied to the workspace

    A variable set keeps each variable's category, so these would still be Terraform variables and never reach the provider's environment.

  • d
    Set access_key and secret_key in the provider block from matching input variables

    This could work but requires adding variable and provider arguments to the configuration, which the stem rules out.

The concept

Workspace variables come in two categories: Terraform variables (input variable values, like a .tfvars file) and environment variables (set in the run's shell). Either can be marked sensitive.

Why that’s the answer

Provider credentials such as AWS_ACCESS_KEY_ID are read from the environment, so they belong in the environment variable category. Two tempting fixes miss this: removing the sensitive flag changes nothing because sensitive values are still passed to runs, and a variable set preserves the variables' category. Wiring the values through input variables works in principle but changes the configuration.

How to reason it out
  1. Identify how the provider looks for credentials: environment variables.
  2. Check the variable category, not just its name.
  3. Recall that sensitive values are hidden from people, not from runs.
  4. Reject any fix that edits the configuration.

Exam tip: Provider credentials go in environment variables on the workspace (or a variable set), marked sensitive.

HCP Terraform: Workspaces, Collaboration, and Governance — the lesson that teaches this.

Question 25HCP Terraform

In HCP Terraform, what is the effect of marking a workspace variable as sensitive?

Choose one.

  • a
    Its value is encrypted at rest, while other workspace variables are kept in plain text

    HCP Terraform encrypts all variable values at rest. Sensitive changes who can read the value, not whether it is encrypted.

  • b
    Its value can be read only by organization owners in the UI and the Variables API

    No one, owners included, can read a sensitive value back. Authorised users can only overwrite it.

  • c
    Its value becomes write-only: hidden in the UI and API, yet still passed to runs Correct

    Correct. Sensitive variables are write-only for every user, but runs still receive them. Note that a configuration can still expose the value, for example through outputs or resource arguments.

  • d
    Its value is withheld from runs until a workspace admin confirms the apply step

    Sensitive values are supplied to plans and applies normally; the flag has nothing to do with run confirmation.

The concept

Sensitive workspace variables protect secrets from being read back by people while still letting runs use them.

Why that’s the answer

Marking a variable sensitive makes it write-only: it cannot be viewed in the UI or read through the Variables API by anyone, including owners. Runs still receive the value. It is not a log scrubber: a configuration that outputs the value or passes it to a resource argument can still expose it in logs or state.

How to reason it out
  1. Separate who can read the value from whether runs can use it.
  2. Sensitive = write-only for all users.
  3. Runs still receive the value.

Exam tip: Sensitive = write-only for people, still available to runs.

HCP Terraform: Workspaces, Collaboration, and Governance — the lesson that teaches this.

Keep going with Terraform 004

Terraform 004 practice by domain

Terraform 004 sample questions: your questions

No. Real exam questions are confidential. These are original questions written to the skills in the official Terraform 004 exam guide, so they test the same knowledge in the same style without being copies of the exam.

Source

The domains and weights on this page come from the official Terraform 004 exam guide.