Terraform Import, State Inspection, and Debug Logging
Importing brings infrastructure that already exists - created in a console, by a script, or by another tool - under Terraform management by recording it in state. There are two ways to do it in Terraform 1.12: the classic terraform import CLI command, which requires you to write the resource block first and updates only state, and the import block, which makes import a plannable, reviewable part of the normal workflow and can even generate the configuration for you with -generate-config-out. Once resources are managed, you maintain them by inspecting state with terraform state list, terraform state show, terraform show, and terraform output, and by debugging failures with verbose logging through TF_LOG, TF_LOG_PATH, and the core/provider-specific variants. This lesson covers exam objective 7 in full, with the exact commands, flags, and log levels the exam expects you to distinguish.
On this page7 sections
- Import existing resources with terraform import, writing the resource block first
- Use import blocks for config-driven, plannable import, including bulk import
- Generate starter configuration for imported resources with -generate-config-out
- Inspect managed infrastructure with terraform state list, state show, show, and output
- Enable verbose logging with TF_LOG and persist it with TF_LOG_PATH
- Scope log output to Terraform core or providers with TF_LOG_CORE and TF_LOG_PROVIDER
terraform import brings existing resources under management
The terraform import command records an existing real-world object in Terraform state so Terraform manages it from then on. Its syntax is terraform import ADDRESS ID, where ADDRESS is the resource address in your configuration (for example aws_s3_bucket.logs) and ID is the provider-specific identifier of the real object - an instance ID for aws_instance, a bucket name for aws_s3_bucket, a self-link style path for many Google resources. Each provider's documentation lists the ID format each resource type expects.
The rule the exam loves: you must write the resource block before you import. The CLI command refuses to import to an address that does not exist in configuration, and it updates state only - it never writes or edits your .tf files. The workflow is therefore: write the block, import, then run terraform plan and adjust the block's arguments until the plan is clean.
resource "aws_s3_bucket" "logs" {
bucket = "acme-log-archive"
}
terraform import aws_s3_bucket.logs acme-log-archive
terraform plan # reconcile config with the imported attributesThe command imports one resource per invocation, needs provider credentials configured (it reads the real object through the provider), and acquires the state lock like any state-writing operation. If the plan after import still shows changes, your configuration does not yet match reality - keep editing the block, not the state.
Config-driven import: the import block
The import block, available since Terraform 1.5 and the recommended approach in 1.12, turns import into part of the normal plan/apply cycle instead of a one-off CLI action. You declare in configuration what should be imported to where, and the import happens - visibly - when the plan is applied:
import {
to = aws_instance.web
id = "i-0a1b2c3d4e5f67890"
}
resource "aws_instance" "web" {
ami = "ami-0abcd1234example"
instance_type = "t3.micro"
}This buys you three things over terraform import. It is plannable: terraform plan shows the resource will be imported (and whether the configuration matches), so the operation gets code review like any other change. It is repeatable and bulk-capable: import blocks can use for_each and expressions in id, so one block can import a whole set of objects. And it is CI-friendly - no human running ad-hoc commands against production state.
After a successful apply the resource is in state and the import block has done its job; you can delete the block (leaving it is harmless - a completed import is a no-op). Like the CLI command, an import block never changes the real object; it only records it in state.
Generating configuration with -generate-config-out
The -generate-config-out flag solves the tedious half of importing: writing configuration that matches an object you did not create. When a plan contains import blocks whose target resources have no corresponding resource block, Terraform can generate that HCL for you:
import {
to = google_compute_instance.legacy
id = "projects/acme-prod/zones/us-central1-a/instances/legacy-vm"
}
terraform plan -generate-config-out=generated.tfTerraform reads the real object through the provider and writes a best-effort resource block for each configless import target into the named file, which must not already exist. The generated code is a starting point, not a finished product: review it, prune computed or redundant attributes, replace hard-coded values with variables and references, then run plan again until it is clean and apply to complete the import.
Keep the division of labor straight for the exam: terraform import requires hand-written configuration and touches only state; the import block plus -generate-config-out is the only path where Terraform writes configuration for you, and it only generates for import targets that lack a resource block.
Inspecting state from the CLI
Four commands cover state inspection, and the exam expects you to pick the right one for a scenario:
| Command | Shows | Typical use |
|---|---|---|
terraform state list | Addresses of every resource in state, one per line | What is Terraform managing here? |
terraform state show ADDRESS | All recorded attributes of one resource | What IP/ARN/ID did this resource get? |
terraform show | Human-readable dump of the whole state, or of a saved plan file | Full snapshot; reviewing a plan artifact |
terraform output | Root module output values | Consuming declared outputs, in scripts too |
terraform state list answers the narrow question of what exists in state - addresses only, no attributes - and can be filtered by address or with -id=. terraform state show zooms into a single resource:
terraform state list terraform state list module.network terraform state show google_compute_instance.vm
All of these read state through the configured backend, so they work the same against local or remote state, and none of them modify anything - they are safe to run at any time.
terraform show and terraform output
terraform show renders the current state - or a saved plan file - in human-readable form, and with -json emits a machine-readable document that tooling (policy checks, CI gates, diff viewers) can consume. Pointing it at a plan artifact is the standard way to inspect exactly what an apply would do:
terraform plan -out=tfplan terraform show tfplan # human-readable review of the saved plan terraform show -json tfplan # machine-readable, for automation
terraform output prints the root module's output values from state. On its own it lists all non-sensitive outputs; name one to print just that value, add -raw for an unquoted string suitable for shell substitution, or -json for structured consumption:
terraform output terraform output instance_ip terraform output -raw instance_ip terraform output -json
One nuance worth knowing: outputs marked sensitive = true are redacted in apply summaries and in the bare terraform output listing, but requesting the output by name (or using -json) prints the real value - sensitivity controls display by default, it is not encryption. The values still live in plain text inside the state file, which is part of why state storage must be protected.
Verbose logging with TF_LOG
Verbose logging is enabled with the TF_LOG environment variable - there is no CLI flag for it. Unset, logging is off; set it to a level and Terraform writes detailed logs to stderr. The levels, from most to least verbose:
| Level | Verbosity | What you get |
|---|---|---|
TRACE | Highest | Everything: internal calls, provider RPCs, raw API request/response detail |
DEBUG | High | Detailed diagnostic information without full trace noise |
INFO | Medium | High-level progress of operations |
WARN | Low | Potential problems only |
ERROR | Lowest | Errors only |
export TF_LOG=DEBUG terraform apply TF_LOG=TRACE terraform plan # one-off, without exporting
Two details show up in questions. TRACE is the most verbose and the most reliable level - and if TF_LOG is set to anything that is not a recognized level name, Terraform treats it as TRACE. And because TRACE logs include provider API traffic, log output can contain sensitive values: enable verbose logging while reproducing a bug (it is invaluable evidence for provider bug reports), then turn it off and handle the captured logs carefully.
Targeting logs: TF_LOG_PATH, TF_LOG_CORE, and TF_LOG_PROVIDER
TF_LOG_PATH persists log output by appending it to the file you name, instead of only streaming it to stderr. The catch the exam checks: TF_LOG_PATH does nothing by itself - logging must also be enabled with TF_LOG (or one of the scoped variables below).
export TF_LOG=TRACE export TF_LOG_PATH=./terraform.log terraform apply # full trace appended to ./terraform.log
When a TRACE log is overwhelming, scope it. TF_LOG_CORE sets a level for Terraform core only (graph building, state handling, plan logic), and TF_LOG_PROVIDER sets a level for provider plugins only (API calls to AWS, Google Cloud, Azure, and so on). Each accepts the same level names and overrides TF_LOG for its half of the system, so you can silence one side while turning the other up:
TF_LOG_CORE=ERROR TF_LOG_PROVIDER=TRACE terraform plan # suspect a provider/API issue TF_LOG_CORE=TRACE TF_LOG_PROVIDER=ERROR terraform plan # suspect core plan/state logic
Choosing between them is the practical skill: a resource failing with an opaque API error is a provider-side question (TF_LOG_PROVIDER=TRACE shows the actual requests and responses), while confusion about dependency ordering or state handling is core-side. When filing a bug against Terraform or a provider, a TRACE log captured via TF_LOG_PATH is the standard supporting evidence.
Tip. Expect questions on the terraform import workflow order - write the resource block, import, then plan until clean - and on the fact that import updates state, never configuration. The import block versus CLI distinction is tested, including -generate-config-out as the only config-generating path. Command-selection questions ask you to choose among terraform state list, state show, show, and output for a given scenario. Logging questions target the TF_LOG level names, TRACE as the most verbose default for unrecognized values, TF_LOG_PATH requiring TF_LOG, and scoping with TF_LOG_CORE and TF_LOG_PROVIDER.
- terraform import ADDRESS ID updates state only and requires the resource block to exist in configuration first - it never generates or edits .tf files.
- Import IDs are provider-specific (instance ID, bucket name, resource path); check the provider docs for each resource type's expected format.
- The import block (Terraform 1.5+) makes import plannable and reviewable, supports for_each for bulk imports, and is the recommended approach in 1.12.
- terraform plan -generate-config-out=FILE writes starter HCL for import targets that have no resource block; the file must not already exist and the output needs review.
- terraform state list shows addresses only; terraform state show ADDRESS shows one resource's attributes; terraform show dumps the whole state or a saved plan; terraform output prints root outputs.
- TF_LOG enables verbose logging at TRACE, DEBUG, INFO, WARN, or ERROR; TRACE is the most verbose, and unrecognized values are treated as TRACE.
- TF_LOG_PATH appends logs to a file but only works when logging is enabled via TF_LOG; TF_LOG_CORE and TF_LOG_PROVIDER scope levels to Terraform core versus provider plugins.
- Verbose logs can include sensitive API payloads - enable them to reproduce an issue, then disable and protect the captured output.
Frequently asked questions
Does terraform import create the configuration for me?
No. The terraform import CLI command only writes the mapping into state, and it fails if the target resource address is not already declared in configuration. The only way Terraform generates configuration is the config-driven path: declare an import block with no matching resource block and run terraform plan -generate-config-out=FILE, then review and refine the generated HCL before applying.
When should I use an import block instead of the terraform import command?
Prefer the import block for anything reviewed or repeated: it shows the import in terraform plan output so it can go through code review, it supports for_each for importing many objects at once, and it runs in CI like any other change. The CLI command remains fine for a quick one-off import on a resource whose block you have already written, but in Terraform 1.12 the import block is the recommended approach.
How do I see every attribute Terraform recorded for one resource?
Run terraform state show followed by the resource address, for example terraform state show aws_instance.web. It prints all attributes stored in state for that single resource. Use terraform state list first if you need to find the exact address, and terraform show if you want the entire state or a saved plan file rendered instead.
What happens if I set TF_LOG to a value that is not a real log level?
Terraform treats any unrecognized TF_LOG value as TRACE, the most verbose level. The recognized levels are TRACE, DEBUG, INFO, WARN, and ERROR. TRACE is also the most reliable level for producing complete diagnostic output, which is why it is the standard choice when capturing logs for a bug report.
Why is TF_LOG_PATH not producing a log file?
Because TF_LOG_PATH only chooses where enabled logs are appended - it does not enable logging on its own. Set TF_LOG (or TF_LOG_CORE / TF_LOG_PROVIDER) to a level such as TRACE alongside TF_LOG_PATH, run the failing command again, and the log file will be written to the path you specified.
What is the difference between TF_LOG_CORE and TF_LOG_PROVIDER?
They scope verbose logging to the two halves of Terraform. TF_LOG_CORE sets the log level for Terraform core - graph construction, plan and state logic - while TF_LOG_PROVIDER sets it for provider plugins, including the actual API requests to platforms like AWS or Google Cloud. Each overrides TF_LOG for its side, so you can trace a suspected provider bug while keeping core output quiet, or the reverse.
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.