Import existing resources to Terraform
Posted on May 12, 2022 by Adrian Wyssmann ‐ 9 min read
When working with Terraform, the changes are good that you are not starting from scratch, but you already have something setup previously and now want to manage it with Terraform
Import a resource
Terraform allows you to import existing resources. I will show this with a simple example on hcloud. First I create a server manually in the console:

Before we can run terraform import
we have to manually create a resource
configuration in example.tf
as follows:
resource "hcloud_server" "node1" {
name = "node1"
server_type = "cx11"
}
The actual import
command looks as follows
terraform import [options] ADDRESS ID
Where
ADDRESS
is aresource address
, which identifies zero or more resource instances in your overall configuration.ID
is the provider specific id of the resource to be imported.
So in my case, ADDRESS
is hcloud_server.node1
and ID
is 20389321
as we can see here

So running terraform import hcloud_server.node1 20389321
is the way to go:
terraform import hcloud_server.node1 20389321
hcloud_server.node1: Importing from ID "20389321"...
hcloud_server.node1: Import prepared!
Prepared hcloud_server for import
hcloud_server.node1: Refreshing state... [id=20389321]
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.
This generates a terraform.tfstate
, which contains the server resource we previously created:
{
"version": 4,
"terraform_version": "1.1.9",
"serial": 8,
"lineage": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "hcloud_server",
"name": "node1",
"provider": "provider[\"registry.terraform.io/hetznercloud/hcloud\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"backup_window": "",
"backups": false,
"datacenter": "hel1-dc2",
"delete_protection": false,
"firewall_ids": [
29038
],
"id": "20389321",
"ignore_remote_firewall_ids": null,
"image": "ubuntu-20.04",
"ipv4_address": "65.21.185.45",
"ipv6_address": "2a01:4f9:c012:543d::1",
"ipv6_network": "2a01:4f9:c012:543d::/64",
"iso": null,
"keep_disk": null,
"labels": {},
"location": "hel1",
"name": "ubuntu-2gb-hel1-1",
"network": [],
"placement_group_id": null,
"rebuild_protection": false,
"rescue": null,
"server_type": "cx11",
"ssh_keys": null,
"status": "running",
"timeouts": {
"create": null
},
"user_data": null
},
"sensitive_attributes": [],
"private": "XXXXXX"
}
]
}
]
}
Now, that we have the object, our example.tf
still looks as defined above. So how to we get the a more complete tf
file which? It requires some manual work. You would run a terraform plan
, which would show you the difference, between the state in terraform.tfstate
vs. the definition in your configuration files:
$ terraform plan
hcloud_server.node1: Refreshing state... [id=20389321]
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:
# hcloud_server.node1 must be replaced
-/+ resource "hcloud_server" "node1" {
+ backup_window = (known after apply)
~ datacenter = "hel1-dc2" -> (known after apply)
~ firewall_ids = [
- 29038,
] -> (known after apply)
~ id = "20389321" -> (known after apply)
+ ignore_remote_firewall_ids = false
- image = "ubuntu-20.04" -> null # forces replacement
~ ipv4_address = "65.21.185.45" -> (known after apply)
~ ipv6_address = "2a01:4f9:c012:543d::1" -> (known after apply)
~ ipv6_network = "2a01:4f9:c012:543d::/64" -> (known after apply)
+ keep_disk = false
- labels = {} -> null
~ location = "hel1" -> (known after apply)
~ name = "ubuntu-2gb-hel1-1" -> "node1"
~ status = "running" -> (known after apply)
# (4 unchanged attributes hidden)
- timeouts {}
}
Plan: 1 to add, 0 to change, 1 to destroy.
As in example.tf
we did not specify an image
, terraform plans to remove that. We also can see the *known after apply
which basically means unknown
. It’s a bit confusing, cause as soon as I adjust my example.tf
adding the missing image
….
resource "hcloud_server" "node1" {
name = "node1"
server_type = "cx11"
image = "ubuntu-20.04"
}
… after running a terraform plan
, these unknowns are gone:
$ terraform plan
hcloud_server.node1: Refreshing state... [id=20389321]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# hcloud_server.node1 will be updated in-place
~ resource "hcloud_server" "node1" {
id = "20389321"
+ ignore_remote_firewall_ids = false
+ keep_disk = false
~ name = "ubuntu-2gb-hel1-1" -> "node1"
# (13 unchanged attributes hidden)
# (1 unchanged block hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
This looks better, but there are still elements, which will be added. Still, that should be fine, so running terraform apply
, will apply the changes, including renaming the node to node1
.
Remove objects from state
Using terraform state rm
allows you to remove a binding to an existing remote object without first destroying it. According to Terraform, this should be “a less common situation”, I still show this by an example. Let’s add a second node node2
to example.tf
…
resource "hcloud_server" "node1" {
name = "node1"
server_type = "cx11"
image = "ubuntu-20.04"
}
resource "hcloud_server" "node2" {
name = "node2"
server_type = "cx11"
image = "ubuntu-20.04"
}
… and then run terraform apply
, which will create the second node.

Now let’s destroy everything with Terraform, but keep node2
. For that we remove node2
from the state file by running
terraform state rm "hcloud_server.node2"
Removed hcloud_server.node2
Successfully removed 1 resource instance(s).
So now, when we run terraform destroy
it will only destroy node1
$ terraform destroy
hcloud_server.node1: Refreshing state... [id=20390671]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# hcloud_server.node1 will be destroyed
- resource "hcloud_server" "node1" {
- backups = false -> null
- datacenter = "hel1-dc2" -> null
- delete_protection = false -> null
- firewall_ids = [
- 29038,
] -> null
- id = "20390671" -> null
- ignore_remote_firewall_ids = false -> null
- image = "ubuntu-20.04" -> null
- ipv4_address = "65.21.185.45" -> null
- ipv6_address = "2a01:4f9:c012:543d::1" -> null
- ipv6_network = "2a01:4f9:c012:543d::/64" -> null
- keep_disk = false -> null
- labels = {} -> null
- location = "hel1" -> null
- name = "node1" -> null
- rebuild_protection = false -> null
- server_type = "cx11" -> null
- status = "running" -> null
- timeouts {}
}
Plan: 0 to add, 0 to change, 1 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
hcloud_server.node1: Destroying... [id=20390671]
hcloud_server.node1: Destruction complete after 0s
Destroy complete! Resources: 1 destroyed.

A more complex example with hetzner firewalls
As we have seen above, when we imported node1
, it actually had a firewall attached. This is cause I have some [Firewalls][hcloud_firewalls] defined:

As we destroyed everything, let’s create a new node node-with-firewall
, which has one of the firewalls attached.

Before we create our terraform configuration, we have a look at the hcloud provider docu. Based on what I see there, I probably need, anhcloud_server object, two hcloud_firewall objects and a
hcloud_firewall_attachment, where latter “attaches resource to a Hetzner Cloud Firewall”. Hence my terraform configuration (example.tf
) looks as follows:
resource "hcloud_server" "node-with-firewall" {
name = "node-with-firewall"
server_type = "cx11"
image = "ubuntu-20.04"
}
resource "hcloud_firewall" "firewall-no-access" {
name = "firewall-no-access"
}
resource "hcloud_firewall" "firewall-ssh-only" {
name = "firewall-ssh-only"
}
resource "hcloud_firewall_attachment" "fw_ref" {
firewall_id = hcloud_firewall.firewall-no-access.id
server_ids = [hcloud_server.node-with-firewall.id]
}
Now let’s import these objects, as we learned above. For servers and firewalls the id can be found easily so I run
$ terraform import hcloud_server.node-with-firewall 20392242
...
hcloud_server.node-with-firewall: Refreshing state... [id=20392242]
Import successful!
...
$ terraform import hcloud_firewall.firewall-no-access 29038
...
hcloud_firewall.firewall-no-access: Refreshing state... [id=29038]
Import successful!
...
$ terraform import hcloud_firewall.firewall-ssh-only 29037
...
hcloud_firewall.firewall-ssh-only: Refreshing state... [id=29037]
Import successful!
...
The hcloud_firewall_attachment does not need to be imported,
$ terraform plan
...
# hcloud_firewall.firewall-ssh-only will be updated in-place
~ resource "hcloud_firewall" "firewall-ssh-only" {
id = "29037"
name = "firewall-ssh-only"
# (1 unchanged attribute hidden)
- rule {
- destination_ips = [] -> null
- direction = "in" -> null
- port = "22" -> null
- protocol = "tcp" -> null
- source_ips = [
- "0.0.0.0/0",
- "::/0",
] -> null
}
}
# hcloud_firewall_attachment.fw_ref will be created
+ resource "hcloud_firewall_attachment" "fw_ref" {
+ firewall_id = 29038
+ id = (known after apply)
+ server_ids = [
+ 20392242,
]
}
Plan: 1 to add, 1 to change, 0 to destroy.
...
As we can see, firewall-ssh-only
is missing the rules in example.tf
, hence I add them:
resource "hcloud_server" "node-with-firewall" {
name = "node-with-firewall"
server_type = "cx11"
image = "ubuntu-20.04"
}
resource "hcloud_firewall" "firewall-no-access" {
name = "firewall-no-access"
}
resource "hcloud_firewall" "firewall-ssh-only" {
name = "firewall-ssh-only"
rule {
destination_ips = []
direction = "in"
port = "22"
protocol = "tcp"
source_ips = [
"0.0.0.0/0",
"::/0",
]
}
}
resource "hcloud_firewall_attachment" "fw_ref" {
firewall_id = hcloud_firewall.firewall-no-access.id
server_ids = [hcloud_server.node-with-firewall.id]
}
After, I run a terraform apply
which succeeds. Now let’s change the firewall assignment of our server to the firewall-ssh-only
:
resource "hcloud_firewall_attachment" "fw_ref" {
firewall_id = hcloud_firewall.firewall-ssh-only.id
server_ids = [hcloud_server.node-with-firewall.id]
}
Let’s see what terraform plan
says:
...
Terraform will perform the following actions:
# hcloud_firewall_attachment.fw_ref will be updated in-place
~ resource "hcloud_firewall_attachment" "fw_ref" {
~ firewall_id = 29038 -> 29037
id = "29038"
# (2 unchanged attributes hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
...
Once we apply the changes with terraform apply
we can see, that the server is assigned to firewall-ssh-only

Unfortunately, the node is also still assigned to firewall-no-access
. This is not wrong, cause a server can be attached to multiple firewall objects. Still it’s not what I want, hence the correct way is to add a second hcloud_firewall_attachment object to my terraform configuration, while I comment out the first object, which will trigger a destruction of it:
# resource "hcloud_firewall_attachment" "fw_ref" {
# firewall_id = hcloud_firewall.firewall-no-access.id
# server_ids = [hcloud_server.node-with-firewall.id]
# }
resource "hcloud_firewall_attachment" "fw_ref_2" {
firewall_id = hcloud_firewall.firewall-ssh-only.id
server_ids = [hcloud_server.node-with-firewall.id]
}
That should do the trick, looking at the changes terraform apply
wants to do
$ terraform apply
...
# (because hcloud_firewall_attachment.fw_ref is not in configuration)
- resource "hcloud_firewall_attachment" "fw_ref" {
- firewall_id = 29038 -> null
- id = "29038" -> null
- label_selectors = [] -> null
- server_ids = [
- 20392242,
] -> null
}
# hcloud_firewall_attachment.fw_ref_2 will be created
+ resource "hcloud_firewall_attachment" "fw_ref_2" {
+ firewall_id = 29037
+ id = (known after apply)
+ server_ids = [
+ 20392242,
]
}
Plan: 1 to add, 0 to change, 1 to destroy.
...
Finally we got the correct configuration, where we switched the firewall assignment of our server to firewall-only-ssh

Summary
In this post I covered the basic concept of importing existing resources and how to remove the binding of objects Terraform manages. Real live example may be much more complex and it’s important you have a look at the documentation of your module, as well understand the basics of the resources you manage.