Importing resources
Step through an example of importing an existing resource into Terraform.
Overview
Another common scenario is importing resources that have been created manually. This is not a fully automated process so this section will guide you through the basics.
In this lab you will
- Create a storage account resource
- Check for a clean
terraform plan
- Add a minimal resource block
- Import the resource
- Configure until
terraform plan
is clean again - Apply the config with the
--refresh-only
switch
Starting point
Your files should look similar to this:
-
provider.tf
terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "~>3.1" } } } provider "azurerm" { features {} storage_use_azuread = true }
-
variables.tf
variable "resource_group_name" { description = "Name for the resource group" type = string default = "terraform-basics" } variable "location" { description = "Azure region" type = string default = "West Europe" } variable "container_group_name" { description = "Name of the container group" type = string default = "terraform-basics" }
-
main.tf
locals { uniq = substr(sha1(azurerm_resource_group.basics.id), 0, 8) } resource "azurerm_resource_group" "basics" { name = var.resource_group_name location = var.location lifecycle { ignore_changes = [ tags, ] } } resource "azurerm_container_group" "basics" { name = var.container_group_name location = azurerm_resource_group.basics.location resource_group_name = azurerm_resource_group.basics.name ip_address_type = "Public" dns_name_label = "${var.container_group_name}-${local.uniq}" os_type = "Linux" container { name = "inspectorgadget" image = "jelledruyts/inspectorgadget:latest" cpu = "0.5" memory = "1.0" ports { port = 80 protocol = "TCP" } } }
-
outputs.tf
output "ip_address" { value = azurerm_container_group.basics.ip_address } output "fqdn" { value = "http://${azurerm_container_group.basics.fqdn}" }
-
terraform.tfvars
location = "UK South"
You may have set a different value for location.
Create the resource
Use the portal to create a storage account in the terraform-basics resource group.
-
Create a storage account in the portal (open in a new tab or window)
- Basics tab
- Select the correct subscription
- Select the terraform-basics resource group
- Create a valid and unique storage account name
- Select the same region as the resource group, e.g. UK South
- Leave the Performance as the default, Standard
- Change redundancy to LRS
- Advanced tab
- Disable blob public access
- Deselect Allow enabling public access on containers
- Enable hierarchical namespace
- Enable NFS v3
- Disable blob public access
- In the Networking tab
-
Disable public access and use private access
Ignore the additional steps to specify a virtual network or private endpoint. (The storage account will not be accessed in this lab.)
-
- Basics tab
-
Click on Review and create
Check the config matching the requirements and validates.
-
Click on Create
Deployment should take a few seconds. Navigate to the resource once deployment has succeeded.
-
Click on JSON View
-
View the resource ID
You will need the resource ID for the import command in the next section. However we’ll set a variable using the CLIs and use that later in the lab.
-
Set a variable for the resource ID
Use either Bash or PowerSHell to set a variable with the storage account’s resource ID.
Bash:
saId=$(az storage account list --resource-group terraform-basics --query "[0].id" --output tsv)
Powershell:
$saId = (Get-AzStorageAccount -ResourceGroupName terraform-basics)[0].id
This will set the variables to the resource ID of the first storage account found in the resource group.
Check for no diff
-
Check terraform plan is clean
terraform plan
Desired output:
azurerm_resource_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics] azurerm_container_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.ContainerInstance/containerGroups/terraform-basics] No changes. Your infrastructure matches the configuration. Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
If you get the output above then you can skip the next section and go straight to the import.
Refresh state (only if required)
terraform plan
similar to below.
azurerm_resource_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics] azurerm_container_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.ContainerInstance/containerGroups/terraform-basics] Note: Objects have changed outside of Terraform Terraform detected the following changes made outside of Terraform since the last "terraform apply": # azurerm_container_group.basics has changed ~ resource "azurerm_container_group" "basics" { id = "/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.ContainerInstance/containerGroups/terraform-basics" name = "terraform-basics" + tags = {} # (9 unchanged attributes hidden) ~ container { + environment_variables = {} name = "inspectorgadget" + secure_environment_variables = (sensitive value) # (4 unchanged attributes hidden) # (1 unchanged block hidden) } } terraform apply -refresh-only Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes. ───────────────────────────────────────────────────────────────────────────── No changes. Your infrastructure matches the configuration. Your configuration already matches the changes detected above. If you'd like to update the Terraform state to match, create and apply a refresh-only plan:
-
Follow the advice and refresh the state file
terraform apply --refresh-only
-
Rerun
terraform plan
to confirm there is now no diffterraform plan
Desired output:
azurerm_resource_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics] azurerm_container_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.ContainerInstance/containerGroups/terraform-basics] No changes. Your infrastructure matches the configuration. Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
Import into state
Now that you have confirmed that there is no diff, you can create the resource block and import.
-
Create an empty resource block
Use the Terraform azurerm docs for azurerm_storage_account to get an example block.
resource "azurerm_storage_account" "import_example" { name = "storageaccountname" resource_group_name = azurerm_resource_group.basics.name location = azurerm_resource_group.basics.location account_tier = "Standard" account_replication_type = "GRS" tags = { environment = "staging" } }
-
Copy the example into your main.tf
Note that the block above deviates from the docs page
- resource group references have been updated to azurerm_resource_group.basics.
- identifier label has been set to import_example. You would usually set the identifier to your preferred name. Please keep it as import_example for this lab.
-
Set the name to your storage account’s name
Don’t worry that the other arguments do not match your created resource yet.
-
-
Import the resource
terraform import azurerm_storage_account.import_example $saId
Example output:
azurerm_storage_account.import_example: Importing from ID "/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.Storage/storageAccounts/richeney27182818"... azurerm_storage_account.import_example: Import prepared! Prepared azurerm_storage_account for import azurerm_storage_account.import_example: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.Storage/storageAccounts/richeney27182818] 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.
-
List the identifiers
terraform state list
Expected output:
azurerm_container_group.basics azurerm_resource_group.basics azurerm_storage_account.import_example
-
Show the imported config
terraform state show azurerm_storage_account.import_example
Example output:
# azurerm_storage_account.import_example: resource "azurerm_storage_account" "import_example" { access_tier = "Hot" account_kind = "StorageV2" account_replication_type = "LRS" account_tier = "Standard" allow_nested_items_to_be_public = false cross_tenant_replication_enabled = false enable_https_traffic_only = true id = "/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.Storage/storageAccounts/richeney27182818" infrastructure_encryption_enabled = false is_hns_enabled = true location = "uksouth" min_tls_version = "TLS1_2" name = "richeney27182818" nfsv3_enabled = true primary_access_key = (sensitive value) primary_blob_connection_string = (sensitive value) primary_blob_endpoint = "https://richeney27182818.blob.core.windows.net/" primary_blob_host = "richeney27182818.blob.core.windows.net" primary_connection_string = (sensitive value) primary_dfs_endpoint = "https://richeney27182818.dfs.core.windows.net/" primary_dfs_host = "richeney27182818.dfs.core.windows.net" primary_file_endpoint = "https://richeney27182818.file.core.windows.net/" primary_file_host = "richeney27182818.file.core.windows.net" primary_location = "uksouth" primary_queue_endpoint = "https://richeney27182818.queue.core.windows.net/" primary_queue_host = "richeney27182818.queue.core.windows.net" primary_table_endpoint = "https://richeney27182818.table.core.windows.net/" primary_table_host = "richeney27182818.table.core.windows.net" primary_web_endpoint = "https://richeney27182818.z33.web.core.windows.net/" primary_web_host = "richeney27182818.z33.web.core.windows.net" public_network_access_enabled = false queue_encryption_key_type = "Service" resource_group_name = "terraform-basics" secondary_access_key = (sensitive value) secondary_connection_string = (sensitive value) shared_access_key_enabled = true table_encryption_key_type = "Service" tags = {} blob_properties { change_feed_enabled = false last_access_time_enabled = false versioning_enabled = false delete_retention_policy { days = 7 } } network_rules { bypass = [ "AzureServices", ] default_action = "Deny" ip_rules = [] virtual_network_subnet_ids = [] } queue_properties { hour_metrics { enabled = true include_apis = true retention_policy_days = 7 version = "1.0" } logging { delete = false read = false retention_policy_days = 0 version = "1.0" write = false } minute_metrics { enabled = false include_apis = false retention_policy_days = 0 version = "1.0" } } share_properties { retention_policy { days = 7 } } timeouts {} }
Check the diff
OK, so the state looks good, but run a terraform plan
and you’ll see we have more to do.
-
Run a plan
terraform plan
You should see extensive output showing what Terraform plans to change. Example output:
azurerm_resource_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics] azurerm_container_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.ContainerInstance/containerGroups/terraform-basics] azurerm_storage_account.import_example: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.Storage/storageAccounts/richeney27182818] 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: # azurerm_storage_account.import_example must be replaced -/+ resource "azurerm_storage_account" "import_example" { ~ access_tier = "Hot" -> (known after apply) ~ account_replication_type = "LRS" -> "GRS" ~ allow_nested_items_to_be_public = false -> true ~ cross_tenant_replication_enabled = false -> true ~ id = "/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.Storage/storageAccounts/richeney27182818" -> (known after apply) ~ is_hns_enabled = true -> false # forces replacement + large_file_share_enabled = (known after apply) name = "richeney27182818" ~ nfsv3_enabled = true -> false # forces replacement ~ primary_access_key = (sensitive value) ~ primary_blob_connection_string = (sensitive value) ~ primary_blob_endpoint = "https://richeney27182818.blob.core.windows.net/" -> (known after apply) ~ primary_blob_host = "richeney27182818.blob.core.windows.net" -> (known after apply) ~ primary_connection_string = (sensitive value) ~ primary_dfs_endpoint = "https://richeney27182818.dfs.core.windows.net/" -> (known after apply) ~ primary_dfs_host = "richeney27182818.dfs.core.windows.net" -> (known after apply) ~ primary_file_endpoint = "https://richeney27182818.file.core.windows.net/" -> (known after apply) ~ primary_file_host = "richeney27182818.file.core.windows.net" -> (known after apply) ~ primary_location = "uksouth" -> (known after apply) ~ primary_queue_endpoint = "https://richeney27182818.queue.core.windows.net/" -> (known after apply) ~ primary_queue_host = "richeney27182818.queue.core.windows.net" -> (known after apply) ~ primary_table_endpoint = "https://richeney27182818.table.core.windows.net/" -> (known after apply) ~ primary_table_host = "richeney27182818.table.core.windows.net" -> (known after apply) ~ primary_web_endpoint = "https://richeney27182818.z33.web.core.windows.net/" -> (known after apply) ~ primary_web_host = "richeney27182818.z33.web.core.windows.net" -> (known after apply) ~ public_network_access_enabled = false -> true ~ secondary_access_key = (sensitive value) + secondary_blob_connection_string = (sensitive value) + secondary_blob_endpoint = (known after apply) + secondary_blob_host = (known after apply) ~ secondary_connection_string = (sensitive value) + secondary_dfs_endpoint = (known after apply) + secondary_dfs_host = (known after apply) + secondary_file_endpoint = (known after apply) + secondary_file_host = (known after apply) + secondary_location = (known after apply) + secondary_queue_endpoint = (known after apply) + secondary_queue_host = (known after apply) + secondary_table_endpoint = (known after apply) + secondary_table_host = (known after apply) + secondary_web_endpoint = (known after apply) + secondary_web_host = (known after apply) ~ tags = { + "environment" = "staging" } # (9 unchanged attributes hidden) ~ blob_properties { ~ change_feed_enabled = false -> (known after apply) + default_service_version = (known after apply) ~ last_access_time_enabled = false -> (known after apply) ~ versioning_enabled = false -> (known after apply) + container_delete_retention_policy { + days = (known after apply) } + cors_rule { + allowed_headers = (known after apply) + allowed_methods = (known after apply) + allowed_origins = (known after apply) + exposed_headers = (known after apply) + max_age_in_seconds = (known after apply) } ~ delete_retention_policy { ~ days = 7 -> (known after apply) } } ~ network_rules { ~ bypass = [ - "AzureServices", ] -> (known after apply) ~ default_action = "Deny" -> (known after apply) ~ ip_rules = [] -> (known after apply) ~ virtual_network_subnet_ids = [] -> (known after apply) + private_link_access { + endpoint_resource_id = (known after apply) + endpoint_tenant_id = (known after apply) } } ~ queue_properties { + cors_rule { + allowed_headers = (known after apply) + allowed_methods = (known after apply) + allowed_origins = (known after apply) + exposed_headers = (known after apply) + max_age_in_seconds = (known after apply) } ~ hour_metrics { ~ enabled = true -> (known after apply) ~ include_apis = true -> (known after apply) ~ retention_policy_days = 7 -> (known after apply) ~ version = "1.0" -> (known after apply) } ~ logging { ~ delete = false -> (known after apply) ~ read = false -> (known after apply) ~ retention_policy_days = 0 -> (known after apply) ~ version = "1.0" -> (known after apply) ~ write = false -> (known after apply) } ~ minute_metrics { ~ enabled = false -> (known after apply) ~ include_apis = false -> (known after apply) ~ retention_policy_days = 0 -> (known after apply) ~ version = "1.0" -> (known after apply) } } + routing { + choice = (known after apply) + publish_internet_endpoints = (known after apply) + publish_microsoft_endpoints = (known after apply) } ~ share_properties { + cors_rule { + allowed_headers = (known after apply) + allowed_methods = (known after apply) + allowed_origins = (known after apply) + exposed_headers = (known after apply) + max_age_in_seconds = (known after apply) } ~ retention_policy { ~ days = 7 -> (known after apply) } + smb { + authentication_types = (known after apply) + channel_encryption_type = (known after apply) + kerberos_ticket_encryption_type = (known after apply) + versions = (known after apply) } } - timeouts {} } Plan: 1 to add, 0 to change, 1 to destroy. ───────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
Ouch! OK, let’s work through this.
-
Identify the required config updates
The good news is that it is pretty quick to deconstruct the output and work out what is important, and experience helps.
First of all, ignore those lines that include
(known after apply)
. The only ones that we need to pay attention to are those which have changes, deletes or adds where the target state is shown as a specific value such as a literal string or boolean.The output below has been manually truncated to help you to focus on what is important. Update the config files with the correct arguments and you should eventually get to a clean plan with no diff.
Terraform will perform the following actions: # azurerm_storage_account.import_example must be replaced -/+ resource "azurerm_storage_account" "import_example" { ~ account_replication_type = "LRS" -> "GRS" ~ allow_nested_items_to_be_public = false -> true ~ cross_tenant_replication_enabled = false -> true ~ is_hns_enabled = true -> false # forces replacement ~ nfsv3_enabled = true -> false # forces replacement ~ public_network_access_enabled = false -> true ~ tags = { + "environment" = "staging" } } Plan: 1 to add, 0 to change, 1 to destroy. ───────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
That is a more manageable set. Let’s get to work.
Update the config files
-
Update the replication type
The plan included:
~ account_replication_type = "LRS" -> "GRS"
Update account_replication_type string value to LRS.
account_replication_type = "LRS"
-
Add the public blob access boolean
The plan included:
~ allow_nested_items_to_be_public = false -> true
Add the allow_blob_public_access argument and set the boolean value to false. (Default is true.)
allow_nested_items_to_be_public = false
-
Check on progress
Run a diff.
terraform plan
You should see that those two changes are no longer planned. Making progress!
-
Update the main.tf to match
💪 Challenge: update the storage account resource block until you get a clean plan
OK, so we’ve done two together. TIme for you to finish off the remainder:
Terraform will perform the following actions: # azurerm_storage_account.import_example must be replaced -/+ resource "azurerm_storage_account" "import_example" { ~ cross_tenant_replication_enabled = false -> true ~ is_hns_enabled = true -> false # forces replacement ~ nfsv3_enabled = true -> false # forces replacement ~ public_network_access_enabled = false -> true ~ tags = { + "environment" = "staging" } } Plan: 1 to add, 0 to change, 1 to destroy. ───────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
References:
- the azurerm_storage_account documentation page
- the
terraform state show azurerm_storage_account.import_example
output
Check on your progress by periodically saving the file and rerunning
terraform plan
. (You may notice thatterraform plan
also validates the files first.)If you get stuck then a working config is shown at the start of the next lab.
-
Check for no diff
Confirm that terraform plan is clean
terraform plan
Example output:
azurerm_resource_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics] azurerm_container_group.basics: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.ContainerInstance/containerGroups/terraform-basics] azurerm_storage_account.import_example: Refreshing state... [id=/subscriptions/9b7a166a-267f-45a5-b480-7a04cfc1edf6/resourceGroups/terraform-basics/providers/Microsoft.Storage/storageAccounts/richeney27182818] No changes. Your infrastructure matches the configuration. Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
-
Format the files
Check that the formatting is as it should be.
terraform fmt
Example output:
main.tf
Summary
Importing resources is a little messy, but is a useful skill to have as a Terraform admin.
It can be a useful way to add in the config for complex resources. For example, the documentation for Azure Application Gateway is difficult to decipher given the range of options and possible configuration. You may find it simpler to provision the resource using the portal and then import the config.
The good news is that Microsoft employees have released a preview of Azure Terrafy (aztfy) as per this blog post.
In the next lab we will destroy the config and tidy up.
Help us improve
Azure Citadel is a community site built on GitHub, please contribute and send a pull request
Make a change