Importing resources with Terraform before and after Terraform 1.5
Tags: terraform
Published: 2023-07-08 10:00:00

If you are lucky to work on a greenfield project you might not even be aware that it is possible to import existing resources into your Terraform state. Most of us have to interact with _legacy_[^legacy] infrastructure of some sort. 

What does it mean to import an existing resource into your Terraform state? An _existing resource_ is simply a resource that has been created through some other means than your current Terraform configuration where you want to import the resource. The resource might have been created using another infrastructure-as-code tool, it might have been created using point-and-click in a GUI, or it might even have been created using a different Terraform configuration. No matter how it was created, you might be interested in having the new Terraform configuration you are writing manage the resource for you. This is where the _import_ operation comes into the picture. In simple terms, when you import a resource you create a space for the resource in your state file and you fill in the details corresponding to the imported resource. You must also write the corresponding HCL so that you can work with the resource. The end result is that it appears as if Terraform created the resource to start with. You can now do whatever operation that Terraform is able to do on your imported resource.

Terraform 1.5 introduced a new experience for importing resources. Spoiler alert: it is pretty easy to use and it works very well! Before Terraform 1.5 you had to use the Terraform CLI to import a resource into your state. It also works well, but it is ultimately an imperative operation and it could be difficult to achieve. The new experience uses the new `import` block which makes the import experience declarative in nature.

In this post I will compare the traditional way of importing resources into your Terraform state with the new-and-improved experience using the `import` block!

## What we will be importing

As always it is easier to demonstrate something if the complexity of the example is kept to a minimum, so that is what I will strive for here as well. I will work with the Azure provider for Terraform, and I will create two sample resources:

- A resource group
- A storage account

If you are new to Azure and Terraform there is a lot of setting up your environment that you will need to go through, the details of which I am not going to include here. I'll add some links in a footnote[^setup].

You can use any method to set up the two resources that will be imported, I will use the Azure CLI. First I will create a resource group:

```console
$ az group create \
    --name rg-terraform-import \
    --location swedencentral

{
  "id": "/subscriptions/<sub id>/resourceGroups/rg-terraform-import",
  "location": "swedencentral",
  "name": "rg-terraform-import",
  ...
  "type": "Microsoft.Resources/resourceGroups"
}
```

The output is truncated a bit, I left the important details. The resource `id` is especially important because we will need it to identify which resource to import. Next I will create a storage account in my resource group:

```console
$ az storage account create \
    --name sttfimport \
    --resource-group rg-terraform-import \
    --location swedencentral \
    --sku Standard_LRS

{
  "id": "/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport",
  "kind": "StorageV2",
  "location": "swedencentral",
  "name": "sttfimport",
  "resourceGroup": "rg-terraform-import",
  "sku": {
    "name": "Standard_LRS",
    "tier": "Standard"
  },
  ...
  "type": "Microsoft.Storage/storageAccounts"
}
```

I have once again truncated the output but kept the important details. Again, the `id` is needed during the import operation.

## Pre Terraform steps

Now we move on from the Azure CLI to Terraform. I will define my Terraform configuration in a single file named `main.tf`. Common for all of my examples in this post is that I need to specify that I want to use the Azure provider (named `azurerm`):

```hcl
// main.tf

terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
    }
  }
}

provider "azurerm" {
  features {}
}
```

We can run `terraform init` to initialize the configuration, and then make sure our state is empty by running `terraform state list`:

```console
$ terraform state list

<empty>
```

## Importing resources using the CLI

Now we are ready to start importing resources. First we will use the traditional method using the CLI. 

When you want to import a resource you need to have a target in your Terraform configuration. A target is simply a `resource` block that corresponds to the resource you want to import. To achieve this I add two `resource` blocks, one for my resource group and one for my storage account:

```hcl
// main.tf

// ...

resource "azurerm_resource_group" "rg" {
  name     = "rg-terraform-import"
  location = "swedencentral"
}

resource "azurerm_storage_account" "st" {
  name                     = "sttfimport"
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}
```

This looks exactly like what it would have looked like if I wanted to create these resources from scratch. However, if I would try to run `terraform apply` on this configuration it would fail because the two resources already exist.

To import resources with the CLI I will use the `terraform import` command. The general format of the command is:

```console
$ terraform import [options] ADDR ID
```

The two important pieces of this command is:

- `ADDR` is the address in your Terraform configuration where the HCL for this resource is located. This is the target I was talking about before. Each resource has an address in your Terraform configuration. For resources in your root module the address is simply `resource_type.symbolic_name`. Taking my storage account as an example the address is `azurerm_storage_account.st`.
- `ID` is the resource ID of the resource you want to import. This ID corresponds to an identifier in "the real world". This is how Terraform knows which resource you want to target. This is why the `id` field in the output from the Azure CLI commands was important, that `id` corresponds to the `ID` in the `terraform import` command.

With this knowledge in our backpack we can move on to performing the imports. I start with my resource group:

```console
$ terraform import azurerm_resource_group.rg /subscriptions/<sub id>/resourceGroups/rg-terraform-import 

azurerm_resource_group.rg: Importing from ID "/subscriptions/<sub id>/resourceGroups/rg-terraform-import"...
azurerm_resource_group.rg: Import prepared!
  Prepared azurerm_resource_group for import
azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/<sub id>/resourceGroups/rg-terraform-import]

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.
```

The output indicates that the import succeeded! I continue with my storage account:

```console
$ terraform import azurerm_storage_account.st /subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport

azurerm_storage_account.st: Importing from ID "/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport"...
azurerm_storage_account.st: Import prepared!
  Prepared azurerm_storage_account for import
azurerm_storage_account.st: Refreshing state... [id=/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport]

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.
```

Once again the import was successful! Let's see what our state contains now:

```console
$ terraform state list

azurerm_resource_group.rg
azurerm_storage_account.st
```

Since we have the resources in our state, what happens if we run `terraform plan`? The following happens:

```console
$ terraform plan

azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/<sub id>/resourceGroups/rg-terraform-import]
azurerm_storage_account.st: Refreshing state... [id=/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport]

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:

  # azurerm_storage_account.st will be updated in-place
  ~ resource "azurerm_storage_account" "st" {
      + cross_tenant_replication_enabled  = true
        id                                = "/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport"
      ~ min_tls_version                   = "TLS1_0" -> "TLS1_2"
        name                              = "sttfimport"
        tags                              = {}
        # (35 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

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

First of all, the plan says `0 to add`. That is good! It means Terraform knows about the resource group and the storage account so it won't try to create them. However, we also see `1 to change`. This is a common occurrence when you import resources. This has to do with default values used by the Azure CLI versus default values used by the Azure provider for Terraform, there is a difference between them so Terraform will correct this difference - hence the `1 to change` for the storage account. In general it is safe to accept the changes, but it is a good idea to look through what changes there are just to be sure.

You might have noticed that I did not use a `-dry-run` flag or something similar when I imported my resources, that is because it does not exist for this command. It either works, or it doesn't. Remember to take a backup of your state file before you start importing resources! Funny how I waited until the end of this section before I added that very important warning, you're welcome!

## Importing resources using the resource block

Now let us take a look at the new import experience using the `import` block.

Just as in the previous example I need to have a target for my resource, so I once again add the resource group and storage account to `main.tf`:

```hcl
// main.tf

// ...

resource "azurerm_resource_group" "rg" {
  name     = "rg-terraform-import"
  location = "swedencentral"
}

resource "azurerm_storage_account" "st" {
  name                     = "sttfimport"
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}
```

The next step is to add two `import` blocks:

```hcl
// main.tf

// ...

import {
  id = "/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport"
  to = azurerm_storage_account.st
}

import {
  id = "/subscriptions/<sub id>/resourceGroups/rg-terraform-import"
  to = azurerm_resource_group.rg
}
```

We can see the similarity with the `terraform import` CLI command. I provide an `id` to identify the resource in Azure, and a `to` property to indicate the target resource in my Terraform configuration. We have basically moved the imperative `terraform import` command to declarative code in `import` blocks.

Now we follow the regular Terraform workflow and run a `terraform plan`:

```console
$ terraform plan

azurerm_resource_group.rg: Preparing import... [id=/subscriptions/<sub id>/resourceGroups/rg-terraform-import]
azurerm_resource_group.rg: Refreshing state... [id=/subscriptions/<sub id>/resourceGroups/rg-terraform-import]
azurerm_storage_account.st: Preparing import... [id=/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport]
azurerm_storage_account.st: Refreshing state... [id=/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport]

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:

  # azurerm_resource_group.rg will be imported
    resource "azurerm_resource_group" "rg" {
        id       = "/subscriptions/<sub id>/resourceGroups/rg-terraform-import"
        location = "swedencentral"
        name     = "rg-terraform-import"
        tags     = {}
    }

  # azurerm_storage_account.st will be updated in-place
  # (imported from "/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport")
  ~ resource "azurerm_storage_account" "st" {
        access_tier                       = "Hot"
        account_kind                      = "StorageV2"
        account_replication_type          = "LRS"
        account_tier                      = "Standard"
        (... output truncated ...)
    }

Plan: 2 to import, 0 to add, 1 to change, 0 to destroy.
```

I removed some of the output because there was a lot of it! The important thing is that we see `2 to import` in the summary at the bottom. We have now safely validated that the import should work, without triggering the import itself. If we are satisfied with the plan we can go ahead and apply the changes:

```console
$ terraform apply

(... output truncated ...)

Apply complete! Resources: 2 imported, 0 added, 1 changed, 0 destroyed.
```

I can verify that my resources are imported by taking a look at my current state:

```console
$ terraform state list

azurerm_resource_group.rg
azurerm_storage_account.st
```

So, now we have imported our resources. What should we do with the `import` blocks that we added? What happens if we run `terraform apply` again? It is safe to leave the blocks in our code if we wish. They can serve as documentation that shows these resources were imported. If you have no need for that kind of documentation then you can safely remove the import blocks. Nothing will happen to the imported resources.

## Generating Terraform configuration for imported resources

There is another feature (actually an _experimental feature_) you can use with the new `import` block. There is a new flag for the `terraform plan` command that allows us to let Terraform generate `resource` blocks for the resources we import. To try this out I remove my `resource` blocks from `main.tf`, but I leave the `import` blocks as is:

```hcl
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.64"
    }
  }
}

provider "azurerm" {
  features {}
}

import {
  id = "/subscriptions/<sub id>/resourceGroups/rg-terraform-import/providers/Microsoft.Storage/storageAccounts/sttfimport"
  to = azurerm_storage_account.st
}

import {
  id = "/subscriptions/<sub id>/resourceGroups/rg-terraform-import"
  to = azurerm_resource_group.rg
}
```

Next I run `terraform plan` and add the `-generate-config-out` flag:

```console
$ terraform plan -generate-config-out=imported.tf
```

Terraform creates a file called `imported.tf` (you provide the name to the `-generate-config-out` flag) containing the two imported resources. It then performs a regular `plan` operation.

In my example there was a number of errors generated during the plan because some of the property values that Terraform set for my storage account was not valid. This is unfortunate, but since this is an experimental feature we can't expect everything to work. However, I can easily edit the imported resource configuration to make it work. This still speeds up the import process!

## Summary

Importing resources is a necessary evil sometimes, so it is good to know it is fairly easy to perform. The new `import` block turns the previous imperative experience using `terraform import` to a declarative experience that follows the regular Terraform workflow.

My example was fairly basic. If you must import multiple resources, and perhaps shuffle some resources around in your state, then things can quickly become complicated. Just remember to take a backup of your state file before you start doing any operations on it so that you can get back to where you started.

[^legacy]: Not necessarily legacy as in old! I mean legacy as in pre-existing.
[^setup]: Azure CLI is available [here](https://learn.microsoft.com/cli/azure/install-azure-cli) and Terraform is available [here](https://developer.hashicorp.com/terraform/downloads)