Sometimes resources exist in OpenStack before Terraform does — created by hand in
Horizon, by a script, or by a previous tool. Importing brings them under
Terraform management without recreating them: you write matching configuration,
attach the real resource to it in state, and from then on Terraform manages it
normally. This guide covers both the classic terraform import command and the
newer config-driven import {} blocks, plus how to find OpenStack IDs.
See also: state management and the import helper
scripts/tf-import.sh.
- Write a
resourceblock that will represent the existing resource. - Find the resource's OpenStack ID.
- Import it (command or
import {}block). - Run
terraform planand iterate on the config until the diff is empty.
The goal is no changes on plan — that proves your config faithfully
describes what already exists.
Terraform imports by the provider's ID, which for OpenStack is almost always the resource UUID. Find it with the CLI:
export OS_CLOUD=openstack
openstack server list # instances
openstack network list # networks
openstack subnet list
openstack router list
openstack security group list
openstack volume list
openstack floating ip list
openstack image list
openstack loadbalancer list # OctaviaGet the exact ID of one resource:
openstack server show web-1 -f value -c idSome resources need a composite ID (parent/child) — for example a subnet's allocation or a router interface. Check the provider's resource documentation "Import" section for the expected format before importing those.
First write the resource block (it can be minimal to start):
resource "openstack_compute_instance_v2" "web" {
name = "web-1"
# other required arguments filled in as you iterate
}Then import by address and ID:
cd path/to/config
export OS_CLOUD=openstack
terraform init -input=false
terraform import openstack_compute_instance_v2.web 0a1b2c3d-....-uuidOr use the repo helper, which inits and reminds you to verify:
# tf-import.sh <dir> <terraform-address> <openstack-id>
scripts/tf-import.sh . openstack_compute_instance_v2.web 0a1b2c3d-....-uuidNow reconcile the config with reality:
terraform planIf the plan wants to change or replace anything, your config doesn't match yet.
Use terraform state show openstack_compute_instance_v2.web to read the imported
attributes and copy the relevant ones into your block. Repeat until plan is
clean.
Terraform >= 1.5 supports declarative import blocks. They live in code, are
reviewable in a PR, run as part of apply, and — unlike the command — can
generate a starting configuration for you.
import {
to = openstack_compute_instance_v2.web
id = "0a1b2c3d-....-uuid"
}
resource "openstack_compute_instance_v2" "web" {
# you can let Terraform generate this for you (below), then tidy it up
}Generate config automatically, then plan/apply:
terraform plan -generate-config-out=generated.tf
# review and clean up generated.tf, then:
terraform applyAfter the import is applied and the resource is in state, delete the
import {} block — it has done its job. Import blocks are also handy for
bulk-importing many resources with for_each.
Whichever method you use, the acceptance test is the same:
terraform plan # must report "No changes."A clean plan means Terraform now owns the resource and won't try to alter it. Keep a state backup from before the import in case you need to back out.
- Wrong ID type. Import needs the UUID, not the name. Resolve it first
(
openstack ... show <name> -f value -c id). - Composite IDs. Router interfaces, member/pool relationships, and a few
others use
parent/childstyle IDs. Check the resource's Import docs. - Default / cloud-managed attributes. Computed fields (timestamps, generated
IPs) appear after import; don't set them in config. Use
state showto see what's computed vs. what you must declare. ignore_changesmismatches. The reference example ignoresimage_name; when importing instances, mirror suchlifecyclesettings so plan stays clean.- One resource at a time (command).
terraform importhandles a single address per call. For many resources, preferimport {}blocks withfor_each. - Data sources can't be imported. A
datablock reads existing resources by query — if you only need to reference something, use a data source instead of importing it. - Secrets aren't recoverable. Importing an application credential won't recover its secret; that's only shown at creation.
- No locking on Swift. Don't import while another apply runs against the same state — see remote state.