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 legacy1 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 footnote2.
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:
$ 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:
$ 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
):
// 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
:
$ 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:
// 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:
$ 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 simplyresource_type.symbolic_name
. Taking my storage account as an example the address isazurerm_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 theid
field in the output from the Azure CLI commands was important, thatid
corresponds to theID
in theterraform import
command.
With this knowledge in our backpack we can move on to performing the imports. I start with my resource group:
$ 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:
$ 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:
$ 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:
$ 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
:
// 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:
// 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
:
$ 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:
$ 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:
$ 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:
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:
$ 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.