Skip to main content
RunBook Academy

← All labs in Terraform

Lab · intermediate · ~25 min

Lab: Importing Existing Infrastructure

C · Simulation

Objectives

  • Create a real-world resource outside Terraform
  • Adopt the resource via `terraform import`
  • Inspect the imported state
  • Write the configuration to match the state
  • Verify the plan is empty after the import

Prerequisites

Objective

By the end of this lab, you will have:

  • Created a real-world resource outside Terraform.
  • Adopted the resource via terraform import.
  • Inspected the imported state.
  • Written the configuration to match the state.
  • Verified the plan is empty after the import.

The lab demonstrates the import workflow: state is populated by the import; the configuration is written by the operator.

Architecture

A pre-existing file resource that Terraform will adopt:

+-----------------------------+
| Pre-existing file           |
|       ↓                     |
| ~/rb-import-lab/             |
|   └── pre-existing.txt      |
|       ↓                     |
| Terraform import            |
|       ↓                     |
| local_file.pre_existing      |
+-----------------------------+

The file exists before any Terraform configuration. The import adopts the file into Terraforms state.

Requirements

  • A Linux or macOS workstation with shell access.
  • The Terraform CLI 1.9.x or later installed.

Scenario

You have a file that was created long before Terraform was introduced. The team has decided to manage the file with Terraform. The file is real. The configuration is empty.

The import workflow:

  1. Create the real-world resource (the file).
  2. Write the resource block in the configuration (empty arguments).
  3. Run terraform import.
  4. Inspect the imported state.
  5. Copy the state attributes into the configuration.
  6. Verify the plan is empty.

Tasks

Task 1: Create the pre-existing file

mkdir -p ~/rb-import-lab
cd ~/rb-import-lab

# Create the pre-existing file
echo "This file was created before Terraform." > ~/rb-import-lab/pre-existing.txt

# Verify
cat ~/rb-import-lab/pre-existing.txt
ls -la ~/rb-import-lab/

The file exists. The directory has no Terraform configuration.

Task 2: Write the empty configuration

Create main.tf:

terraform {
  required_version = ">= 1.9.0"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

# The resource block declares the resource.
# The arguments will be filled in after the import.
resource "local_file" "pre_existing" {
  # filename and content will be filled in
}

The configuration has the resource block but no arguments. Terraform will learn the arguments from the state.

Task 3: Initialise

terraform init

The local provider is downloaded.

Task 4: Plan against the empty configuration

terraform plan

Expected:

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

The plan proposes to create the file. The state is empty.

This is the wrong action. Do NOT apply.

Task 5: Import the existing file

The import syntax for local_file is the file path:

terraform import local_file.pre_existing "${PWD}/pre-existing.txt"

The output:

local_file.pre_existing: Importing from ID "/home/user/rb-import-lab/pre-existing.txt"...
local_file.pre_existing: Import prepared!
  Prepared local_file for import
local_file.pre_existing: Refreshing state... [id=abc123def456]

Import successful!

The resources that were imported are shown above. You now need to run
terraform plan to see the changes that will be made by importing
these resources.

The import:

  • Called the provider to look up the file.
  • Updated the state to include the files attributes.
  • Did NOT modify the configuration.

Verify the state:

terraform state list

Expected:

local_file.pre_existing

The state has the resource.

terraform state show local_file.pre_existing

The output shows the attributes:

# local_file.pre_existing:
resource "local_file" "pre_existing" {
  content             = "This file was created before Terraform."
  content_base64      = "..."
  directory_permission = "0777"
  file_permission      = "0777"
  filename             = "/home/user/rb-import-lab/pre-existing.txt"
  id                   = "abc123def456"
}

The state has the files content and metadata.

Task 6: Plan after the import

terraform plan

The plan shows the change to make the configuration match the state:

# local_file.pre_existing will be updated in-place
~ resource "local_file" "pre_existing" {
    ~ content             = "This file was created before Terraform." -> "This file was created before Terraform."
    ~ content_base64      = "..." -> "..."
    # ...
  }

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

The plan proposes to set the content to the same value. The plan is technically a “change” but the actual file content is unchanged.

This is the wrong outcome. The configuration should match the state, not the other way around.

Task 7: Fill in the configuration

Update main.tf with the values from the state:

resource "local_file" "pre_existing" {
  filename = "${path.module}/pre-existing.txt"
  content  = "This file was created before Terraform.\n"
}

The content is the value from the state. The filename is resolved to ${path.module}/pre-existing.txt (the working directory + filename).

Question 1: What does the plan show now?

Answer

The plan should be empty:

No changes. Your infrastructure matches the configuration.

The configuration matches the state matches the real world.

Run the plan:

terraform plan

Expected:

No changes. Your infrastructure matches the configuration.

The plan is empty.

Task 8: Verify the configuration

cat ~/rb-import-lab/pre-existing.txt

The file content is unchanged.

terraform show

The state shows the resource.

Task 9: Modify the configuration

Edit main.tf to change the content:

resource "local_file" "pre_existing" {
  filename = "${path.module}/pre-existing.txt"
  content  = "This file is now managed by Terraform.\n"
}

Run the plan:

terraform plan

The plan shows the change:

# local_file.pre_existing will be updated in-place
~ resource "local_file" "pre_existing" {
    ~ content             = "This file was created before Terraform.\n" -> "This file is now managed by Terraform.\n"
  }

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

The plan proposes to update the file.

Apply:

terraform apply

Verify:

cat ~/rb-import-lab/pre-existing.txt

Expected:

This file is now managed by Terraform.

Task 10: Clean up

terraform destroy

The destroy removes the file from the real world.

The destroy is destructive in production. In this lab, the file is disposable.

Validation

The lab is successful if:

  • The pre-existing file was imported into state.
  • The configuration was written to match the state.
  • The plan was empty after the configuration update.
  • The apply succeeded.

Expected Outcome

At the end of the lab:

+---------------------------------+
| ~/rb-import-lab/                  |
|   .terraform/                    |
|   .terraform.lock.hcl            |
|   main.tf                        |
+---------------------------------+

The pre-existing file is gone (destroyed). The state is empty. The configuration is the only source of truth.

Cleanup

cd ~/rb-import-lab
terraform destroy
rm -rf .terraform .terraform.lock.hcl terraform.tfstate*

The main.tf is the only artefact worth keeping.

What You Learned

You learned the import workflow:

  1. Import populates state, not the configuration. The operator writes the configuration to match the state.
  2. The import ID is critical. A wrong import ID binds state to the wrong resource.
  3. The state-to-configuration mapping is non-trivial. The state has every attribute; the configuration has only the arguments.
  4. A non-empty plan after import is a signal. The configuration does not match the state.
  5. The configuration should match the state. The plan should be empty after a successful import.

Deliverables

  • · A real-world resource created outside Terraform
  • · An adopted resource via import
  • · A configuration that matches the state
  • · An empty plan after the import

Verification status

Last reviewed
2026-08-12
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.