Terraform 1.5 brings imports into the plan
Configuration-driven imports make adoption reviewable before state changes. Independent checks add nonblocking assertions about the infrastructure that results.
Adopting infrastructure that already exists requires a decision about identity before Terraform can manage its lifecycle. A remote object needs a Terraform address, and configuration needs to describe the object that address will represent. Terraform 1.5, released on June 12, brings the import decision into configuration so a normal plan can show it before state changes.
The release also introduces independent check blocks. These express assertions about infrastructure without making assertion failure stop the operation. Import and checks address different stages of managing infrastructure, but both make previously separate operational intentions visible in the configuration language. An import records which existing object Terraform should adopt. A check records an expectation that can remain useful after provisioning.
I value Terraform's ability to express reusable infrastructure decisions above provider APIs. Import blocks extend that model to the beginning of management, where infrastructure often already exists. A reviewable plan is especially useful when the operation changes Terraform's understanding of ownership without creating a new remote object.
Import has a destination and a remote identity
The 1.5 import documentation defines two required arguments. to names the destination resource instance address, and id supplies the provider-specific import identifier. This release requires a literal string for id. Interpolation in that argument is not supported in the initial implementation.
An import declaration can target an AWS IoT thing that already exists. The provider configuration and credentials must identify the account and region containing that thing.
import {
to = aws_iot_thing.sensor
id = "sensor"
}
resource "aws_iot_thing" "sensor" {
name = "sensor"
}
The address is Terraform's intended location for the object. The string is the remote identifier that the provider knows how to import. Their spelling need not match. The example uses the same name to keep the relationship visible, but a configuration address is a modeling decision independent of an object's name in its service.
Import blocks belong in the root configuration. Their destination can identify a resource in a child module or a particular counted or keyed instance. The documentation shows indexed addresses for count and for_each. Those examples target existing instance addresses. They do not make the import block itself a loop.
The optional provider argument selects a provider configuration for the import when necessary. This matters for configurations with aliases, since the same resource type can exist in different accounts or regions. The import ID alone does not describe which configured provider should interpret it.
Planning makes adoption reviewable
Terraform processes the declaration while planning and performs the state import during apply. The plan includes imports alongside other proposed changes. The release changelog explicitly says the existing terraform import CLI command has not changed. Configuration-driven import is a new workflow available beside it.
The practical improvement is that the declaration can travel with a configuration change through review and automation. A reviewer can inspect the destination address and remote ID before the apply modifies state. A pipeline can execute the ordinary plan and apply sequence without reconstructing a separate series of import commands outside the configuration.
An imported object still needs resource configuration. Bringing its identity into state does not automatically make every desired argument agree with the live object. A plan may contain both the import and other changes, so its complete resource actions remain relevant. Import establishes the association required for management, while configuration determines the desired state thereafter.
The documentation describes import blocks as idempotent while the resource remains in state. A later plan does not propose the same import again for an already-managed destination. Authors may remove a completed import declaration or retain it as a record of origin. That record can explain why a resource first appeared in a configuration without a corresponding creation operation.
Retaining the declaration does not make the remote object independent of the resource lifecycle. Once imported, Terraform tracks it as a managed resource. Ordinary configuration changes can update it, and removal or destruction can cause Terraform to destroy it according to the usual lifecycle behavior. The declaration records adoption, not permanent protection against later operations.
Generated configuration starts with observed attributes
Terraform can also generate resource configuration for an import destination that lacks a resource block. The configuration-generation documentation marks this capability experimental. Its command accepts a new destination filename and refuses an existing file.
terraform plan -generate-config-out=generated.tf
The generated HCL is a starting template based on values obtained from the provider. It is not a reconstruction of the original design. Resource references, module organization, and decisions about which arguments should remain configurable are information that a remote read cannot generally supply. The documentation recommends editing and reorganizing the result before apply.
Complex schemas can also make the generated result invalid. The documentation gives an EC2 example where ipv6_address_count conflicts with ipv6_addresses. Both appear among the available attributes, but configuration must choose one form. Terraform can still write the generated file while reporting that conflict, allowing the author to remove the inappropriate argument and plan again.
This limitation follows from the distinction between observed state and accepted configuration. A provider read can return values that describe the resulting object without describing which mutually exclusive configuration choice produced it. Generation reduces transcription work, but a usable resource declaration still needs to express an intended management policy.
Checks observe without blocking repair
The check-block documentation specifies at least one assertion per check. Each assertion has a condition and an error message. Checks run as the last step of a plan or apply, and a failed assertion reports a warning instead of halting the operation.
A check can contain one scoped data source. That data source can use any provider's data-source type, but its result is visible only inside the enclosing check. If its provider reports an error, Terraform treats the failure as a warning so the operation can continue. This is different from an ordinary data source declared at module scope.
For example, an HTTP-provider data source can test whether an endpoint returns the expected status. This configuration assumes the HTTP provider is available to the module.
check "service_response" {
data "http" "service" {
url = "https://example.com"
}
assert {
condition = data.http.service.status_code == 200
error_message = "The service did not return HTTP 200."
}
}
The warning semantics have an operational purpose. A health observation may fail precisely because an apply needs to repair the infrastructure. Turning that observation into a blocking condition can prevent the change that would restore health. A check allows the expectation to remain visible while permitting the operation to proceed.
I prefer this behavior for an observation about a composed system whose health depends on several resources. For an invalid input that must never reach provisioning, variable validation remains the appropriate blocking mechanism. The choice of construct determines failure policy, so an assertion's placement is part of its meaning.
Dependencies determine when evidence exists
A scoped data source supports depends_on and provider, but not count or for_each. When a health check depends on infrastructure that Terraform is about to create, an explicit dependency can defer the read until apply. The check can then remain unknown during the initial plan instead of warning about a service that does not exist yet.
That dependency has a continuing effect. If the referenced resource changes in a later operation, the check may again remain unknown until apply. The documentation calls out this tradeoff. An explicit dependency encodes when an observation can be meaningful, while also reducing what planning can establish before that dependency completes.
Checks do not replace every custom condition. Preconditions and postconditions retain blocking behavior for assumptions or guarantees attached to resource operations. A check's warning is useful for a broader health expectation, but it does not enforce a requirement that execution must satisfy before continuing. Terraform 1.5 lets configuration describe both adoption and observation with their respective execution semantics.
The release also changes state persistence during local operations. Terraform periodically attempts to save snapshots to its backend during apply, reducing the interval of unpersisted work if the process terminates unexpectedly. On a graceful interruption request, it immediately tries to persist the latest snapshot. These are attempts to reduce lost state, not a promise that every remote change and state write become atomic. They matter alongside import because state is the record that connects configured addresses to managed objects. More frequent persistence narrows the period during which completed work may lack a saved representation in the backend.