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:
- Create the real-world resource (the file).
- Write the resource block in the configuration (empty arguments).
- Run
terraform import. - Inspect the imported state.
- Copy the state attributes into the configuration.
- 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:
- Import populates state, not the configuration. The operator writes the configuration to match the state.
- The import ID is critical. A wrong import ID binds state to the wrong resource.
- The state-to-configuration mapping is non-trivial. The state has every attribute; the configuration has only the arguments.
- A non-empty plan after import is a signal. The configuration does not match the state.
- The configuration should match the state. The plan should be empty after a successful import.