Objective 6 of the Terraform Associate (004) exam is called Terraform state management, and three of its four sub-objectives are about backends: describe the local backend, describe state locking, and configure remote state using the backend block. Our state management guide covers the why: what state contains, why a remote backend with locking matters, and the terraform state subcommands. It gives the S3 backend example and two one-line rules (no variables in the block, re-run init after a change).
This article is about the mechanics behind those two lines. What exactly is Terraform storing when you run init against a backend, and how does it know the backend changed? Which flag copies state, which one refuses to, and which one skips the backend entirely? What does “native S3 locking” mean now that the DynamoDB table is deprecated? Where does each backend put a non-default workspace, and which backends can lock at all? The exam asks these as scenario questions (“the team wants to move state from local to S3 without losing it”, “init fails with a backend configuration changed error in CI”, “a developer’s apply died and the next run says the state is locked”), and the answers all live in the backend block, the .terraform directory and a handful of init flags.
A version note before we start: the 004 exam page states that it tests on Terraform 1.12. The current stable release at the time of writing is 1.17.1, and the backend behaviour below is the same across that range except where a version is called out.
What a Backend Is (and Is Not)
A backend is the component that decides where state is stored and how it is locked. In Terraform’s current architecture that is all it does; execution of plan and apply always happens on the machine running the CLI, except for the remote backend and the cloud block, which hand operations to HCP Terraform or Terraform Enterprise.
terraform {
required_version = ">= 1.12.0"
backend "s3" {
bucket = "acme-terraform-state"
key = "networking/prod/terraform.tfstate"
region = "eu-west-1"
use_lockfile = true
encrypt = true
}
}
The documented rules for that block are short and every one of them is a question:
| Rule | Exact behaviour |
|---|---|
| One backend per configuration | ”A configuration can only provide one backend block.” A second one is a configuration error |
| Only in the root module | Backend blocks in child modules are ignored; the root module decides where state lives |
| No named values | ”A backend block cannot refer to named values (like input variables, locals, or data source attributes).” The error is Variables not allowed |
| Not referenceable | Nothing in the configuration can read the backend’s arguments |
Mutually exclusive with cloud | ”If your configuration includes a cloud block, it cannot include a backend block.” |
Default is local | Omit the block and Terraform stores terraform.tfstate next to the configuration |
| Built in, not pluggable | ”You cannot load additional backends as plugins.” The set is fixed per CLI version |
The “no variables” rule exists because the backend must be resolved before Terraform can evaluate anything: variables, locals and data sources all require a state and a provider, and both come after backend initialization. The sanctioned escape hatch is partial configuration, covered next.
The current built-in list is local, remote, azurerm, consul, cos, gcs, http, kubernetes, oci, oss, pg and s3. Terraform 1.3 removed artifactory, etcd, etcdv3, manta and swift; a configuration naming one of those fails init on any supported release. If a question lists “Artifactory backend” as an option, it is a distractor on current Terraform.
Partial Configuration: Keeping Secrets and Environments Out of the Block
Because the block cannot read variables, Terraform lets you leave arguments out and supply them at init time. The docs call this partial configuration, and there are three sources, which can be combined:
- A file passed with
-backend-config=PATH. The file holds top-level key = value attributes (not abackendblock), and the recommended naming is*.backendname.tfbackend, for exampleprod.s3.tfbackend. - Key/value pairs passed as
-backend-config="KEY=VALUE", repeatable. The docs warn these land in shell history, so they are for non-secret values such as bucket names. - Interactive prompts for any required argument still missing, unless input is disabled with
-input=false, in which caseiniterrors. Terraform “will not prompt for optional values”.
The minimal block is just the type:
terraform {
backend "s3" {}
}
terraform init \
-backend-config=envs/prod.s3.tfbackend \
-backend-config="key=networking/prod/terraform.tfstate"
Two consequences follow, and both are exam fodder.
The merged configuration is written to disk. “The final, merged configuration is stored on disk in the .terraform directory.” Anything you pass through -backend-config, including an access key or a Postgres connection string, is stored in plain text there and in any saved plan file. The documented recommendation for credentials is therefore environment variables, not -backend-config, and .terraform must never be committed.
Partial configuration is how one configuration serves many environments. The same main.tf with an empty backend "s3" {} block is initialized against dev.s3.tfbackend in one pipeline and prod.s3.tfbackend in another. That is also the recommended alternative to CLI workspaces for strongly isolated environments, as our workspaces guide explains.
What terraform init Stores, and How It Detects a Changed Backend
After a successful init, the working directory contains .terraform/terraform.tfstate. Despite the name, that file is not your infrastructure state; it is the backend state: the backend type, the merged configuration, and a hash of it. Terraform also records which workspace is selected in .terraform/environment. The whole directory is relocatable with TF_DATA_DIR, which is how CI systems keep it out of the repository checkout.
On every later init, Terraform compares the backend block (plus any -backend-config input) with the stored configuration. If they differ, the docs say Terraform “will automatically detect any changes in your configuration and request a reinitialization”, and since Terraform 0.15 that request is an error rather than an automatic migration:
Error: Backend configuration changed
A change in the backend configuration has been detected, which may require
migrating existing state.
If you wish to attempt automatic migration of the state, use "terraform init -migrate-state".
If you wish to store the current configuration with no changes to the state, use "terraform init -reconfigure".
The CLI reference states the rule in one line: “Either -reconfigure or -migrate-state must be supplied to update the backend configuration.” Which one you pick is the single most tested backend decision.
The four init flags that control backends
| Flag | What it does | When it is the answer |
|---|---|---|
-migrate-state | ”Attempt to copy existing state to the new backend.” Prompts to confirm, and with multiple workspaces asks whether to copy all of them | Moving state from local to S3, from S3 to GCS, between buckets or keys, or into HCP Terraform |
-reconfigure | ”Disregards any existing configuration, preventing migration of any existing state.” The new backend is adopted as if the directory were fresh | The state already exists at the new location (another engineer migrated it) or you intentionally want to start pointing at a different, already-populated state |
-force-copy | ”Suppresses these prompts and answers yes to the migration questions.” Implies -migrate-state | Non-interactive pipelines that must migrate |
-backend=false | ”To skip backend configuration.” Providers and modules still install | terraform validate or fmt jobs in CI with no backend credentials; the docs advise using it only in a directory already initialized for that backend |
Two traps hide in that table. -reconfigure does not delete or move anything; it simply stops Terraform from looking at the old backend, so if the new location is empty your next plan proposes creating everything again. And -migrate-state copies state between backends, not between workspaces; the destination keeps its own workspace layout, which is why Terraform asks about workspaces during migration when the old backend had several.
Removing a backend
Deleting the backend block and running init is a backend change like any other. Terraform prompts to migrate the state to the local backend; with -migrate-state you end up with terraform.tfstate on disk. That is the documented way to “go back to local”, and it is also why an accidental deletion of the block in a pull request is dangerous: a reviewer who approves it and runs init -migrate-state moves production state onto a laptop.
Backing up before a migration
The docs are blunt: “Before migrating to a new backend, we strongly recommend manually backing up your state.” The practical command is terraform state pull > backup.tfstate against the old backend before changing the block. Migration itself is a read-from-old, write-to-new copy; it does not delete the source, which is why old S3 keys linger after a move.
The Local Backend in Detail
Objective 6a is “describe the local backend”, so know its defaults rather than just its existence.
| Setting | Default | Notes |
|---|---|---|
path | terraform.tfstate relative to the root module | Overridable in the block |
workspace_dir | terraform.tfstate.d | Non-default workspaces live at terraform.tfstate.d/<name>/terraform.tfstate |
| Backup | terraform.tfstate.backup written before each state change | -backup=PATH overrides, -backup=- disables |
| Locking | Yes, “using system APIs” (an OS-level file lock) | Protects against two processes on the same machine, nothing more |
Two facts matter in scenarios. The local backend does lock, so “state locking is only available with remote backends” is false. And an explicit backend "local" { path = "../shared/terraform.tfstate" } is legal; it is the one backend that terraform_remote_state can read without any remote service, which the data source docs demonstrate with a path under config.
State Locking: What Locks, What Doesn’t, and How to Recover
The language docs define the contract: “If supported by your backend, Terraform will lock your state for all operations that could write state.” Locking is automatic, silent on success, and fatal on failure: “If acquiring the lock is taking longer than expected, Terraform will output a status message”, and if it cannot acquire the lock at all it stops.
| Control | Behaviour |
|---|---|
-lock=false | Disables locking for that command. The docs say “we do not recommend it” |
-lock-timeout=<duration> | How long to wait for a held lock. Default 0s, so by default Terraform fails immediately rather than queueing; -lock-timeout=5m is the usual CI setting |
terraform force-unlock LOCK_ID | Removes a stuck lock. “The force-unlock command requires a unique lock ID”, printed in the error when the lock is held. Use only for a lock you hold whose automatic release failed; unlocking someone else’s active run “could cause multiple writers” |
The lock ID is a nonce: it ensures you unlock the exact lock the error reported, not a newer one acquired in the meantime. There is no force-unlock for backends whose locks release automatically with the session, which is why the pg backend documents that force-unlock is not supported.
How each backend locks
The exam asks “which backends support locking” more often than it should. Current answer: all of the built-in backends can lock, with one conditional.
| Backend | Locking mechanism | Notes |
|---|---|---|
local | OS file lock | Same-machine protection only |
s3 | Native lock file <key>.tflock in the bucket (use_lockfile = true), or the deprecated DynamoDB table | Detail below |
azurerm | Azure Blob Storage native capabilities (a lease on the state blob) | Also “consistency checking” |
gcs | Supported natively | Docs just say “This backend supports state locking” |
consul | Consul sessions; lock = true by default | Can be disabled |
pg | Postgres advisory locks | Released with the session, so no force-unlock |
kubernetes | A Lease resource next to the state Secret | Secret named tfstate-<workspace>-<secret_suffix> |
oci | If-None-Match: * conditional write creating a .lock object | Added in recent releases |
cos, oss | Native (Tencent COS) / Alibaba Tablestore table (oss) | Rare on the exam |
http | Only if lock_address is set; defaults to disabled. Uses LOCK / UNLOCK HTTP methods, expects 423 or 409 when held | The one backend that can silently run without locking |
remote / cloud | HCP Terraform workspace lock | Also serialises runs |
The http backend is the trap. A configuration with only address set has no locking at all; the docs mark lock_address and unlock_address as “Defaults to disabled”. A scenario about “state corrupted by concurrent applies against a custom HTTP state service” is answered by adding lock_address and unlock_address, not by -lock=true.
The S3 Backend: Native Locking and the DynamoDB Deprecation
The S3 backend is the one the exam names most, and it changed meaningfully in 2025.
use_lockfile replaces the DynamoDB table
Terraform 1.10 introduced S3-native locking as an experiment, and Terraform 1.11 (27 February 2025) made it generally available: “S3 native state locking is now generally available” and “we’ve deprecated the DynamoDB-related arguments in favor of this new locking mechanism”. The current docs describe dynamodb_table as “deprecated and will be removed in a future minor version”, and the DynamoDB endpoint settings likewise.
use_lockfile = true | dynamodb_table (deprecated) | |
|---|---|---|
| Lock object | <key>.tflock in the same bucket, e.g. networking/prod/terraform.tfstate.tflock | An item in a DynamoDB table with a LockID partition key of type String |
| Extra infrastructure | None | A table per state store |
| IAM | s3:GetObject, s3:PutObject, s3:DeleteObject on the .tflock object | dynamodb:DescribeTable, GetItem, PutItem, DeleteItem |
| Migration | Both can be set at once “to support migration”; run with both, then drop the table | Still works with a deprecation warning on 1.12 and 1.13 |
| Default | false, so a block with neither setting does not lock | Not set |
That last row is the exam trap: an S3 backend with only bucket, key and region has no locking. Since the 004 exam is pinned to Terraform 1.12, the “correct” modern answer to “add locking to an S3 backend” is use_lockfile = true, while “create a DynamoDB table with a LockID key” is the legacy answer that still appears in older material. If a question offers both, the native lock file is the one HashiCorp now recommends.
Everything else in the block
- Required:
bucket,key,region(region can also come fromAWS_REGIONorAWS_DEFAULT_REGION). - Encryption:
encrypt = trueturns on server-side encryption for the state and lock objects;kms_key_idselects a KMS key and needskms:Encrypt,kms:Decryptandkms:GenerateDataKey;sse_customer_keysupplies an SSE-C key, which should come fromAWS_SSE_CUSTOMER_KEYbecause a value in the block “persists it toterraform.tfstate” (the backend state file). - Minimum IAM:
s3:ListBucketon the bucket pluss3:GetObjectands3:PutObjecton the key.s3:DeleteObjecton the state key is not needed because “Terraform does not delete it”; delete is needed only for the lock object and for non-default workspace keys. - Versioning: the docs call enabling bucket versioning “highly recommended” so you can recover from accidental deletions. Versioning is your undo; the backend has none of its own.
- Workspaces: the default workspace lives at
key; every other workspace lives at<workspace_key_prefix>/<workspace>/<key>, andworkspace_key_prefixdefaults toenv:, soenv:/development/networking/prod/terraform.tfstate. - Account guard rails:
allowed_account_idsandforbidden_account_ids(mutually exclusive) makeinitfail if the credentials resolve to the wrong account, which is the cheap answer to “prevent a dev pipeline from touching prod state”. - Credentials:
access_key/secret_key(must be set together),token,profile,shared_config_filesandshared_credentials_files, theassume_roleblock (role_arnrequired,duration15 minutes to 12 hours,external_id,session_name,source_identity,policy,policy_arns,tags; multiple blocks are assumed in order), andassume_role_with_web_identityfor OIDC withweb_identity_tokenorweb_identity_token_file. The documented guidance is to prefer environment variables and the shared config files over anything in the block. - S3-compatible stores:
endpoints { s3 = ... },use_path_style,skip_credentials_validation,skip_region_validation,skip_requesting_account_id,skip_metadata_api_checkandskip_s3_checksumexist for MinIO-style targets, and HashiCorp supports them “best effort” only.
A full production block today looks like this:
terraform {
backend "s3" {
bucket = "acme-terraform-state"
key = "networking/prod/terraform.tfstate"
region = "eu-west-1"
use_lockfile = true
encrypt = true
kms_key_id = "arn:aws:kms:eu-west-1:123456789012:key/abcd-1234"
allowed_account_ids = ["123456789012"]
assume_role {
role_arn = "arn:aws:iam::123456789012:role/terraform-state"
}
}
}
The Other Backends at Exam Depth
You do not need every argument, but each backend has one or two facts that distinguish it in a question.
| Backend | Required | Workspace layout | Distinguishing facts |
|---|---|---|---|
azurerm | storage_account_name, container_name, key | Blob named <key>env:<workspace> | Entra ID auth recommended: OIDC/workload identity (use_oidc, ARM_OIDC_TOKEN), managed identity (use_msi), Azure CLI (use_cli); use_azuread_auth switches data-plane access to RBAC (Storage Blob Data Contributor) instead of the account key; snapshot = true snapshots the blob before each write; access keys and SAS tokens are “not recommended for new workloads”; resource_group_name only needed with lookup_blob_endpoint |
gcs | bucket (must already exist) | <prefix>/<workspace>.tfstate, default workspace as default.tfstate | encryption_key (customer-supplied, 32-byte base64) vs kms_encryption_key (Cloud KMS); credentials / GOOGLE_BACKEND_CREDENTIALS / GOOGLE_CREDENTIALS with the warning that the provider also reads GOOGLE_CREDENTIALS; impersonate_service_account needs the Token Creator role; object versioning “highly recommended” |
http | address | Caller-defined | update_method defaults to POST; lock_address/unlock_address off by default; retry_max 2, retry_wait_min 1 s, retry_wait_max 30 s; basic auth via TF_HTTP_USERNAME/TF_HTTP_PASSWORD; mTLS via PEM arguments; skip_cert_verification false by default |
pg | conn_str (or PG_CONN_STR / libpq env vars) | Rows keyed by workspace name in table states | Schema terraform_remote_state auto-created unless skip_schema_creation; advisory locks, no force-unlock |
kubernetes | secret_suffix | Secret tfstate-<workspace>-<secret_suffix>, chunked with -<index> when large | Lease-based locking; auth via config_path, in_cluster_config or host/token; needs Secret read/write in the namespace |
consul | path | <path>-env:<workspace> | lock defaults true; gzip option; CONSUL_HTTP_TOKEN |
oci | bucket, namespace (and region for most setups) | Prefixed keys with .lock objects | Auth via API key, instance/resource principal, security token or OKE workload identity; KMS and SSE-C encryption |
remote | organization, workspaces { name } or workspaces { prefix } | HCP Terraform workspaces | Superseded by the cloud block; prefix becomes tags on migration |
The gcs credential warning is a nice exam detail: setting GOOGLE_CREDENTIALS for the backend silently configures the Google provider too, so a backend-only service account can end up managing resources.
Workspaces and Backends
CLI workspaces are “named instances of state inside a single backend”. The exam facts are the layouts above plus one principle: switching workspaces never touches the backend configuration, only which key or object Terraform reads. Non-default workspaces need extra permissions on some backends; the S3 docs add s3:DeleteObject on <workspace_key_prefix>/*/<key> because terraform workspace delete removes the object. If the backend does not support workspaces at all, terraform workspace new fails with a backend error.
During init -migrate-state, Terraform notices if the source backend has several workspaces and asks whether to copy all of them. When the destination backend is single-workspace, it asks you to pick one. The multi-workspace question is where most “lost staging state during migration” stories come from.
terraform_remote_state: Reading Another Backend
The terraform_remote_state data source takes a backend, an optional workspace, a config object mirroring that backend’s block, and defaults for outputs that may be missing. It exposes only root-module outputs through outputs. The security trade-off is explicit in the docs: the reader “must have access to the entire state snapshot, which often includes some sensitive information”, and the docs recommend alternatives such as tfe_outputs for HCP Terraform or publishing values to SSM Parameter Store or Consul so that access to the shared values can differ from access to the state. Our multiple states guide compares those three options in practice.
data "terraform_remote_state" "network" {
backend = "s3"
workspace = "prod"
config = {
bucket = "acme-terraform-state"
key = "networking/terraform.tfstate"
region = "eu-west-1"
use_lockfile = true
}
}
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_id
}
Note that the data source reads state and never locks it; it does not conflict with a run in the producing configuration.
The cloud Block vs the remote Backend
HCP Terraform is configured with a cloud block, which is not a backend in the backend "x" sense even though it occupies the same role. Our HCP Terraform guide covers runs, policy and the private registry; the backend-level facts are:
cloud { organization, hostname (default app.terraform.io), workspaces { name | tags, project } }; exactly one ofnameortags.tagsnow accepts a map of key/value strings as well as the legacy key-only list. The same values can come fromTF_CLOUD_ORGANIZATION,TF_CLOUD_HOSTNAME,TF_CLOUD_PROJECTandTF_WORKSPACE.- Credentials come from
terraform login(stored in the CLI credentials file), acredentials "app.terraform.io" { token = ... }block in the CLI configuration, orTF_TOKEN_app_terraform_io. Thetokenattribute inside the block is possible but discouraged for the same plain-text reason as other backends. - Replacing
backend "remote"with acloudblock keeps the same workspaces; aprefixbecomestags, and afterwards you address workspaces by full name. - Migrating from any other backend is the normal
initmigration prompt, with the twist that Terraform may ask you to rename workspaces to globally unique names such asnetworking-prod-eu-west-1. - With
cloudorremote,planandapplyexecute remotely by default; every other backend only stores state.
Credentials: Where They Should Come From
Three documented statements settle every “where do I put the secret” question:
- “We recommend using environment variables to supply credentials and other sensitive data.”
- Values in the block or in
-backend-config“are written in plain text to the.terraformsubdirectory and to plan files”. - “Do not check this directory into Git.”
So the ranking is: workload identity or instance role (no secret at all) > environment variables and shared credential files > -backend-config file kept out of version control > hard-coded values, which is never the answer. Backend credentials are also separate from provider credentials: the S3 backend’s assume_role and the AWS provider’s assume_role are configured independently, which is how a pipeline can read state in a central “state account” while deploying into a workload account.
Scenario Drill
| Scenario | Answer | Why |
|---|---|---|
| Move existing local state to a new S3 bucket without recreating resources | Add the backend "s3" block, terraform state pull as a backup, terraform init -migrate-state, confirm the copy | Migration copies state; -reconfigure would leave it behind |
Another engineer already moved state to the new bucket; your init errors with “Backend configuration changed” | terraform init -reconfigure | The destination is already populated; do not re-copy |
| CI must migrate without prompts | terraform init -force-copy | Answers yes to migration questions |
A validate-only CI job has no cloud credentials and init fails on the backend | terraform init -backend=false then terraform validate | Skips backend configuration, still installs providers |
| One root module must target dev and prod state stores | Empty backend "s3" {} + -backend-config=dev.s3.tfbackend / prod.s3.tfbackend | Partial configuration |
backend "s3" { bucket = var.bucket } fails | Backend blocks cannot reference variables; use partial configuration | ”Variables not allowed” |
S3 backend with bucket, key, region only; two applies corrupted state | Add use_lockfile = true | No locking by default; DynamoDB is deprecated |
| Existing DynamoDB-locked backend must move to native locking safely | Set both use_lockfile = true and dynamodb_table, run normally, then remove dynamodb_table | Documented dual-lock migration |
apply was killed; next run reports the state is locked with an ID | terraform force-unlock <ID> only after confirming no run is active | Lock ID is required |
| Runs in a busy pipeline fail instantly with lock errors | -lock-timeout=5m | Default is 0s |
| Custom HTTP state service shows concurrent writes | Configure lock_address and unlock_address | HTTP locking is off unless set |
| Accidentally applied prod config with dev credentials | allowed_account_ids / forbidden_account_ids in the S3 backend | init fails on the wrong account |
| State must be encrypted with a specific KMS key | encrypt = true + kms_key_id, grant Encrypt/Decrypt/GenerateDataKey | SSE-KMS |
| Recover a state file someone deleted from the bucket | Restore the previous object version | Versioning is the undo |
Where is the staging workspace state in S3? | env:/staging/<key> | workspace_key_prefix default env: |
| Where is it with the local backend? | terraform.tfstate.d/staging/terraform.tfstate | workspace_dir default |
Secrets appear in .terraform/terraform.tfstate | Expected for anything passed via -backend-config; move secrets to env vars | Backend state is plain text |
| Child module declares a backend | Ignored; only the root module’s backend counts | Root-module rule |
Config has both cloud {} and backend "s3" {} | Invalid; remove one | Mutually exclusive |
Migrating from backend "remote" with prefix to the cloud block | Replace the block, turn prefix into tags, address workspaces by full name afterwards | Documented migration |
| Team wants read-only access to another configuration’s outputs without exposing the whole state | Publish to SSM/Consul or use tfe_outputs, not terraform_remote_state | Remote state needs full snapshot access |
Setting GOOGLE_CREDENTIALS for the GCS backend changed provider behaviour | Use GOOGLE_BACKEND_CREDENTIALS for the backend | Provider reads GOOGLE_CREDENTIALS too |
| Azure backend should stop using the storage account key | use_azuread_auth = true with OIDC or managed identity and Storage Blob Data Contributor | Entra ID auth is the recommended path |
Configuration references backend "swift" | Removed in Terraform 1.3; migrate to a supported backend | Removed backends |
Common Mistakes
- Running
-reconfigurewhen you meant-migrate-state. Nothing is lost, but the new backend is empty and the next plan wants to create everything. Your old state is still at the old location. - Assuming an S3 backend locks out of the box. It locks only with
use_lockfile = trueor the deprecated DynamoDB table. - Putting access keys in the block or in
-backend-config“just for CI”. They are written in plain text to.terraformand plan files. - Forgetting the backup.
terraform state pull > backup.tfstatebefore every migration is the documented expectation. - Believing the local backend has no locking. It locks with OS file locks.
- Using
force-unlockon someone else’s lock. The ID requirement exists to make you think; multiple writers corrupt state. - Treating
terraform_remote_stateas a scoped read. It needs full read access to the snapshot, sensitive values included. - Expecting a child module’s backend block to do something. Only the root module’s block is honored.
- Mixing
cloudandbackend. The configuration will not initialize.
Frequently Asked Questions
What is the difference between -migrate-state and -reconfigure?
-migrate-state copies the state from the previously configured backend to the new one, prompting first. -reconfigure makes Terraform forget the previous backend configuration and adopt the new one without touching any state. Use migrate when you are moving state; use reconfigure when the state already exists at the new location or you are intentionally switching to a different, populated state store.
Does the S3 backend still need a DynamoDB table?
No. Since Terraform 1.11, use_lockfile = true locks with an object in the same bucket, and the DynamoDB arguments are deprecated and scheduled for removal. Both can be set simultaneously while you migrate. The 004 exam tests on Terraform 1.12, where native locking is the current recommendation.
Why can’t a backend block use variables?
Terraform must know where state lives before it can evaluate variables, locals or data sources, because evaluation itself depends on state and providers. Partial configuration through -backend-config is the supported way to inject environment-specific values.
What is in .terraform/terraform.tfstate?
The backend state: the backend type and the merged configuration (including anything passed with -backend-config), plus a hash Terraform uses to detect configuration changes on the next init. It is not your resource state, but it can contain credentials, so it must not be committed.
Which backends support state locking?
All current built-in backends can lock, with the http backend locking only when lock_address is configured, and the S3 backend locking only when use_lockfile or the deprecated dynamodb_table is set. The pg backend uses advisory locks and does not support force-unlock.
How does -lock-timeout differ from -lock=false?
-lock-timeout keeps locking enabled but waits up to the given duration for a held lock instead of failing immediately (the default is 0s). -lock=false disables locking entirely for that command, which the docs recommend against.
Is the cloud block a backend?
Functionally it fills the same role (where state lives, who locks it) and is mutually exclusive with a backend block, but it also moves plan and apply execution to HCP Terraform and is configured with its own block and environment variables rather than through backend "remote".
Conclusion
Objective 6 rarely asks “what is a backend”. It asks whether you know that the block cannot hold a variable, that partial configuration is the sanctioned workaround and that its merged result sits in plain text under .terraform; that a changed backend stops init until you choose between copying state and ignoring it; that an S3 block without use_lockfile does not lock and the DynamoDB table is on its way out; that the local backend locks too; and that the http backend will happily run unlocked. Layer on the per-backend workspace layouts, the force-unlock ID rule, the terraform_remote_state trade-off and the cloud block’s exclusivity, and most state-management questions collapse to one or two eliminations.
The fastest way to make those eliminations automatic is to run the commands. Initialize a project locally, migrate it to S3 with -migrate-state, break the backend on purpose and fix it with -reconfigure, kill an apply and recover with force-unlock, then pressure-test the reflexes against realistic distractors. Sailor.sh’s HashiCorp Terraform Associate Mock Exam Bundle includes eight 60-question exams whose explanations spell out why the -reconfigure answer, the DynamoDB answer or the “variables in the backend block” answer loses to the one the scenario actually demands. Pair this guide with the Terraform Associate exam guide, the 30-day study plan and the provider installation internals guide for the other half of what init does, then test yourself on the Terraform Associate practice questions before booking the real thing.