Skip to main content
Terraform · lesson 16 of 17
Terraform advanced Lesson 16 of 17

terraform import: Bringing Existing Infrastructure Under Management

An import that succeeds and then plans to replace what it imported, the import-block and -generate-config-out fix, and the review the generated code needs.

By SkillByExample · Updated

terraform import state

terraform import tells Terraform that an object which already exists belongs to a resource block, without creating or destroying anything. Getting from “imported” to “plan reports no changes” is the actual work. Examples use Terraform 1.16.5 with hashicorp/random 3.9.1, hashicorp/time 0.14.2 and hashicorp/local 2.9.1, so the “existing infrastructure” is a token and a timestamp rather than a cloud resource. The workflow is identical for an S3 bucket.

Not every resource supports import

A config file someone created by hand, and a block that describes it:

terraform {
  required_providers {
    local  = { source = "hashicorp/local", version = "~> 2.9" }
    random = { source = "hashicorp/random", version = "~> 3.9" }
    time   = { source = "hashicorp/time", version = "~> 0.14" }
  }
}

resource "local_file" "server_conf" {
  filename = "${path.module}/preexisting-server.conf"
  content  = "listen_port=8080\n"
}
$ terraform import -no-color local_file.server_conf ./preexisting-server.conf
local_file.server_conf: Importing from ID "./preexisting-server.conf"...

Error: Resource Import Not Implemented

This resource does not support import. Please contact the provider developer
for additional information.

The provider refused. Each resource type implements import (or doesn’t), and nothing in your config can change that. For a type like this, the options are to let Terraform recreate the object or to leave it unmanaged.

Import succeeds, and the plan wants to replace it

An API token was generated by hand months ago and clients already use it. Replace the local_file block with this one:

# An API token someone generated by hand months ago; clients already use it.
resource "random_string" "api_token" {
  length  = 16
  special = false
}
$ terraform import -no-color random_string.api_token Xk9pQ2mLr7Tw
random_string.api_token: Importing from ID "Xk9pQ2mLr7Tw"...
random_string.api_token: Import prepared!
  Prepared random_string for import
random_string.api_token: Refreshing state... [id=Xk9pQ2mLr7Tw]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

$ terraform plan -no-color
random_string.api_token: Refreshing state... [id=Xk9pQ2mLr7Tw]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # random_string.api_token must be replaced
-/+ resource "random_string" "api_token" {
      ~ id          = "Xk9pQ2mLr7Tw" -> (known after apply)
      ~ length      = 12 -> 16 # forces replacement
      ~ result      = "Xk9pQ2mLr7Tw" -> (known after apply)
      ~ special     = true -> false # forces replacement
        # (8 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

The import itself worked. The resource block was written from memory, though, and it doesn’t describe the token that exists: the token is 12 characters, not 16. The provider also recorded special = true, its default, which is what it assumes for any imported string. Applying this plan would rotate a token every client depends on. The CLI command wrote straight to state with no plan, so nobody reviewed this before state changed.

terraform state show lists what import actually recorded, which is what the block has to match:

$ terraform state show -no-color random_string.api_token
# random_string.api_token:
resource "random_string" "api_token" {
    id          = "Xk9pQ2mLr7Tw"
    length      = 12
    lower       = true
    min_lower   = 0
    min_numeric = 0
    min_special = 0
    min_upper   = 0
    number      = true
    numeric     = true
    result      = "Xk9pQ2mLr7Tw"
    special     = true
    upper       = true
}

An import block, with the config generated

Start again in a fresh directory, with no resource block at all, just an import block:

terraform {
  required_providers {
    random = { source = "hashicorp/random", version = "~> 3.9" }
  }
}

import {
  to = random_string.api_token
  id = "Xk9pQ2mLr7Tw"
}
$ terraform plan -generate-config-out=generated.tf -no-color
random_string.api_token: Preparing import... [id=Xk9pQ2mLr7Tw]
random_string.api_token: Refreshing state... [id=Xk9pQ2mLr7Tw]

Terraform will perform the following actions:

  # random_string.api_token will be imported
  # (config will be generated)
    resource "random_string" "api_token" {
        id          = "Xk9pQ2mLr7Tw"
        length      = 12
        lower       = true
        min_lower   = 0
        min_numeric = 0
        min_special = 0
        min_upper   = 0
        number      = true
        numeric     = true
        result      = "Xk9pQ2mLr7Tw"
        special     = true
        upper       = true
    }

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

Warning: Config generation is experimental

Generating configuration during import is currently experimental, and the
generated configuration format may change in future versions.

─────────────────────────────────────────────────────────────────────────────

Terraform has generated configuration and written it to generated.tf. Please
review the configuration and edit it as necessary before adding it to version
control.

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
$ cat generated.tf
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform from "Xk9pQ2mLr7Tw"
resource "random_string" "api_token" {
  keepers          = null
  length           = 12
  lower            = true
  min_lower        = 0
  min_numeric      = 0
  min_special      = 0
  min_upper        = 0
  numeric          = true
  override_special = null
  special          = true
  upper            = true
}
$ terraform apply -auto-approve -no-color | tail -3
random_string.api_token: Import complete [id=Xk9pQ2mLr7Tw]

Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.

1 to import, 0 to destroy. The import is now part of a plan that can be reviewed in a pull request, and the generated block matches state exactly. “Matches state” isn’t the same as “right”, though. special = true is only there because it’s the provider’s import default. The real token has no special characters, and if it’s ever rotated, the new one will include them. Review the generated file, delete the = null lines, and move the block into your own files.

Practice

1. After the apply, plan again. Then edit generated.tf to say special = false, which describes the token correctly, and plan once more.

Solution
$ terraform plan -no-color | grep -E "No changes|Plan:"
No changes. Your infrastructure matches the configuration.
$ terraform plan -no-color | grep -E "forces|Plan:"
      ~ special     = true -> false # forces replacement
Plan: 1 to add, 0 to change, 1 to destroy.

“No changes” is the state a finished import should reach. Correcting special makes the config more accurate but forces a replacement, because for random_string every argument is create-time only. In that case, keep the generated value, or add lifecycle { ignore_changes = [special] } until the token is rotated on purpose.

2. Run the CLI terraform import for the token again, now that the import block has already brought it in.

Solution
$ terraform import -no-color random_string.api_token Xk9pQ2mLr7Tw
random_string.api_token: Importing from ID "Xk9pQ2mLr7Tw"...
random_string.api_token: Import prepared!
  Prepared random_string for import

Error: Resource already managed by Terraform

Terraform is already managing a remote object for random_string.api_token. To
import to this address you must first remove the existing object from the
state.

One address maps to one object. The import block left in the config is fine: it’s a no-op once the address is in state. The CLI command refuses instead.

3. Import a launch timestamp as a time_static with an import block, generate its config, and expose its year as an output.

Solution
terraform {
  required_providers {
    time = { source = "hashicorp/time", version = "~> 0.14" }
  }
}

import {
  to = time_static.launched
  id = "2024-03-01T09:00:00Z"
}
$ terraform plan -generate-config-out=generated.tf -no-color | grep -E "^  #|Plan:"
  # time_static.launched will be imported
  # (config will be generated)
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
$ cat generated.tf
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform from "2024-03-01T09:00:00Z"
resource "time_static" "launched" {
  rfc3339  = "2024-03-01T09:00:00Z"
  triggers = {}
}

Then add the output and apply:

output "launch_year" {
  value = time_static.launched.year
}
$ terraform apply -auto-approve -no-color | tail -5
Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.

Outputs:

launch_year = 2024

Generate the config before adding any reference to the resource. Until a block for it exists, an output that refers to time_static.launched fails with “Reference to undeclared resource”. Only plan accepts -generate-config-out, not apply.

Next: terraform test, which checks that a configuration produces the plan you expect before it reaches a real apply.

Frequently Asked Questions

Does every resource type support terraform import?
No. Import is implemented per resource type by the provider. local_file, for example, returns 'Resource Import Not Implemented'. Each resource's documentation page ends with an Import section that shows the ID format when import is supported.
If import succeeds, does my config now match reality?
No. Import only writes the existing object into state; it never checks your resource block. An import can succeed and the very next plan still want to destroy and replace the object, for example when a hand-written random_string block says special = false but import recorded the default special = true. Always plan after importing and treat any replacement as a mismatch to fix.
What's the difference between terraform import and an import block?
terraform import (the CLI command) writes to state immediately, with no plan and no review. An import block (Terraform 1.5+) is configuration: the import shows up in plan as '1 to import', can be reviewed in a pull request, and happens during apply. Import blocks can also generate the resource block for you with -generate-config-out.
Can I trust the configuration -generate-config-out writes?
Treat it as a draft. It contains the values the provider reported at import time, including defaults the original object may never have had a reason to set, such as special = true on a token that contains no special characters. Review each argument, delete the explicit nulls, and move the block into your own files.
Should I keep import blocks after applying them?
They are harmless once applied, because an import for an address already in state is a no-op, and some teams keep them as a record. Many remove them in a follow-up change to keep the configuration tidy; the CLI command instead refuses outright with 'Resource already managed by Terraform'.