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:

manually created server
server manually created in the hcloud 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 a resource 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

hcloud-server id
the id of our hcloud server

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.

second server node
Terraform created the second node as specified in example.tf

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.
first server node destroyed
Terraform removed node1, but did not touch node 2

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:

hetzner firewalls
Existing Firewall objects in the Hetzner Console

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

node with firewall attached
A manually created server with an existing firewall 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

server attached to both fw
Server is now assigned to firewall-ssh-only, but also firewall-no-access

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

server with only ssh-only-fw
Our server has now firewall-ssh-only attached, instead firewall-no-access

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.