Optimizing Resource Migration in Terraform using Config-Driven Automation
Tags: terraform, devops, automation
Published: 2023-06-25 00:27:57

Since the release of Terraform 1.5 in June 2023, developers have been excited about the new features added to the popular HashiCorp’s IaC tool. One of the standout additions is the ability to automatically generate Terraform configurations using a new argument in the existing import command. This enhancement simplifies the process of importing external resources into Terraform management and eliminates the need for manual configuration mapping, providing a more scalable and efficient solution.

# Terraform import
Although Terraform import is not a new concept, it has significantly improved with the latest update. The initial version of the import command required developers to manually map existing resource configurations without generating any HashiCorp Configuration Language (HCL) code. Additionally, it only supported importing one resource at a time, which posed limitations.

Let’s consider a scenario where we aim to bring several resources, such as a resource group, virtual network, subnet, public IP address, and virtual network gateway, under Terraform management. Previously, we would have had to manually write the entire Terraform configuration for each resource and map them individually.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4kqb3jhpjf324mycu9o2.png)

Previously, we would have had to manually write the entire Terraform configuration for each resource and map them individually.
```
terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "3.61.0"
    }
  }
}
provider "azurerm" {
  features {}
}
resource "azurerm_resource_group" "example" {
  name     = "rg-training-uks"
  location = "uksouth"
}
resource "azurerm_virtual_network" "example" {
  name                = "vnet-training-uks-01"
  address_space       = ["10.2.0.0/16"]
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
}
resource "azurerm_subnet" "example" {
  name                 = "GatewaySubnet"
  resource_group_name  = azurerm_resource_group.example.name
  virtual_network_name = azurerm_virtual_network.example.name
  address_prefixes     = ["10.2.1.0/24"]
}
resource "azurerm_public_ip" "example" {
  name                = "pip-training-uks-01"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  allocation_method   = "Static"
  sku                 = "Standard"
}
resource "azurerm_virtual_network_gateway" "example" {
  name                = "vng-training-uks-01"
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  type     = "Vpn"
  vpn_type = "RouteBased"
  active_active = false
  enable_bgp    = false
  sku           = "Basic"
  ip_configuration {
    name                          = "vnetGatewayConfig"
    public_ip_address_id          = azurerm_public_ip.example.id
    private_ip_address_allocation = "Dynamic"
    subnet_id                     = azurerm_subnet.example.id
  }
}
```
Then map each single resource to the respective terraform resource address.
```
terraform import azurerm_resource_group.example /subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks
terraform import azurerm_virtual_network.example /subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01
terraform import azurerm_subnet.example /subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01/subnets/GatewaySubnet
terraform import azurerm_public_ip.example /subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/publicIPAddresses/pip-training-uks-01
terraform import azurerm_virtual_network_gateway.example /subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworkGateways/vng-training-uks-01
```
After each import, terraform will populate the state file (it can be either remote or local, it doesn’t make any difference) with the new resources. However, while this is pretty straightforward and might not look like a big deal, it becomes extremely time-consuming and error-prone when dealing with hundreds or thousands of resources, especially those with extensive configurations.

# Introducing Config-Driven Import
Terraform version 1.5 introduces the new config-driven import feature to address these challenges. This feature leverages an import block that defines the resource ID and the corresponding Terraform resource address for mapping. Developers can create multiple import blocks as needed, allowing for greater flexibility. However, it’s important to note that the feature does not automatically detect or generate relationships between resources; it simply generates the Terraform code with all attributes, including unused ones.

In the case of the Azure resources mentioned earlier, it becomes significantly easier to bring them under Terraform management using the new import blocks. Instead of manually writing the entire configuration, we can create separate import blocks for each resource.
```
terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "3.61.0"
    }
  }
}
provider "azurerm" {
  features {}
}
import {
  id = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks"
  to = azurerm_resource_group.example
}
import {
  id = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01"
  to = azurerm_virtual_network.example
}
import {
  id = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01/subnets/GatewaySubnet"
  to = azurerm_subnet.example 
}
import {
  id = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/publicIPAddresses/pip-training-uks-01"
  to = azurerm_public_ip.example 
}
import {
  id = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworkGateways/vng-training-uks-01"
  to = azurerm_virtual_network_gateway.example 
}
```
Once the import blocks are defined, we initialize Terraform and run the import command. This streamlined approach eliminates the need for extensive manual configuration and saves considerable time and effort.
```
terraform init
terraform plan -generate-config-out autogenerated.tf
```
As of now, the config-driven import feature is still in experimental mode. This means that some resources may encounter issues during generation, particularly due to dependencies between parameters. One common issue arises when a property is set to a non-null value, causing other parameters to expect specific values. If these expected values are not provided, it will result in plan failures.
```
$ terraform plan -generate-config-out autogenerated.tf
azurerm_resource_group.example: Preparing import... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks]
azurerm_virtual_network.example: Preparing import... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01]
azurerm_public_ip.example: Preparing import... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/publicIPAddresses/pip-training-uks-01]
azurerm_virtual_network_gateway.example: Preparing import... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworkGateways/vng-training-uks-01]
azurerm_subnet.example: Preparing import... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01/subnets/GatewaySubnet]
azurerm_virtual_network.example: Refreshing state... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01]
azurerm_public_ip.example: Refreshing state... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/publicIPAddresses/pip-training-uks-01]
azurerm_subnet.example: Refreshing state... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01/subnets/GatewaySubnet]
azurerm_resource_group.example: Refreshing state... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks]
azurerm_virtual_network_gateway.example: Refreshing state... [id=/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworkGateways/vng-training-uks-01]

Terraform planned the following actions, but then encountered a problem:

  # azurerm_public_ip.example will be imported
  # (config will be generated)
    resource "azurerm_public_ip" "example" {
        allocation_method       = "Static"
        ddos_protection_mode    = "VirtualNetworkInherited"
        id                      = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/publicIPAddresses/pip-training-uks-01"
        idle_timeout_in_minutes = 4
        ip_address              = "20.68.57.190"
        ip_tags                 = {}
        ip_version              = "IPv4"
        location                = "uksouth"
        name                    = "pip-training-uks-01"
        resource_group_name     = "rg-training-uks"
        sku                     = "Standard"
        sku_tier                = "Regional"
        tags                    = {}
        zones                   = []
    }

  # azurerm_resource_group.example will be imported
  # (config will be generated)
    resource "azurerm_resource_group" "example" {
        id       = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks"
        location = "uksouth"
        name     = "rg-training-uks"
        tags     = {}
    }

  # azurerm_virtual_network_gateway.example will be imported
  # (config will be generated)
    resource "azurerm_virtual_network_gateway" "example" {
        active_active              = false
        enable_bgp                 = false
        generation                 = "None"
        id                         = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworkGateways/vng-training-uks-01"
        location                   = "uksouth"
        name                       = "vng-training-uks-01"
        private_ip_address_enabled = false
        resource_group_name        = "rg-training-uks"
        sku                        = "Standard"
        tags                       = {}
        type                       = "ExpressRoute"
        vpn_type                   = "PolicyBased"

        ip_configuration {
            name                          = "default"
            private_ip_address_allocation = "Dynamic"
            public_ip_address_id          = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/publicIPAddresses/pip-training-uks-01"
            subnet_id                     = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01/subnets/GatewaySubnet"
        }
    }

Plan: 3 to import, 0 to add, 0 to change, 0 to destroy.
╷
│ Warning: Config generation is experimental
│ 
│ Generating configuration during import is currently experimental, and the generated configuration format may change in future versions.
╵
╷
│ Error: expected flow_timeout_in_minutes to be in the range (4 - 30), got 0
│ 
│   with azurerm_virtual_network.example,
│   on autogenerated.tf line 5:
│   (source code not available)
│ 
╵
╷
│ Error: Value for unconfigurable attribute
│ 
│   with azurerm_virtual_network.example,
│   on autogenerated.tf line 9:
│   (source code not available)
│ 
│ Can't configure a value for "subnet.0.id": its value will be decided automatically based on the result of applying this configuration.
╵
╷
│ Error: Value for unconfigurable attribute
│ 
│   with azurerm_virtual_network.example,
│   on autogenerated.tf line 9:
│   (source code not available)
│ 
│ Can't configure a value for "subnet.1.id": its value will be decided automatically based on the result of applying this configuration.
╵
╷
│ Error: Not enough list items
│ 
│   with azurerm_subnet.example,
│   on autogenerated.tf line 6:
│   (source code not available)
│ 
│ Attribute service_endpoint_policy_ids requires 1 item minimum, but config has only 0 declared.
╵
```
However, even in case of error, Terraform will still generate the HCL configuration, and developers can update the configurations by setting the values accordingly. The code generated by Terraform will create a new .tf file, listing the resources specified in the import blocks.
```
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform from "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks"
resource "azurerm_resource_group" "example" {
  location = "uksouth"
  name     = "rg-training-uks"
  tags     = {}
}

# __generated__ by Terraform from "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/publicIPAddresses/pip-training-uks-01"
resource "azurerm_public_ip" "example" {
  allocation_method       = "Static"
  ddos_protection_mode    = "VirtualNetworkInherited"
  ddos_protection_plan_id = null
  domain_name_label       = null
  edge_zone               = null
  idle_timeout_in_minutes = 4
  ip_tags                 = {}
  ip_version              = "IPv4"
  location                = "uksouth"
  name                    = "pip-training-uks-01"
  public_ip_prefix_id     = null
  resource_group_name     = "rg-training-uks"
  reverse_fqdn            = null
  sku                     = "Standard"
  sku_tier                = "Regional"
  tags                    = {}
  zones                   = []
}

# __generated__ by Terraform from "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworkGateways/vng-training-uks-01"
resource "azurerm_virtual_network_gateway" "example" {
  active_active                    = false
  default_local_network_gateway_id = null
  edge_zone                        = null
  enable_bgp                       = false
  generation                       = "None"
  location                         = "uksouth"
  name                             = "vng-training-uks-01"
  private_ip_address_enabled       = false
  resource_group_name              = "rg-training-uks"
  sku                              = "Standard"
  tags                             = {}
  type                             = "ExpressRoute"
  vpn_type                         = "PolicyBased"
  ip_configuration {
    name                          = "default"
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/publicIPAddresses/pip-training-uks-01"
    subnet_id                     = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01/subnets/GatewaySubnet"
  }
}

# __generated__ by Terraform
resource "azurerm_virtual_network" "example" {
  address_space           = ["10.2.0.0/16"]
  bgp_community           = null
  dns_servers             = []
  edge_zone               = null
  flow_timeout_in_minutes = 0
  location                = "uksouth"
  name                    = "vnet-training-uks-01"
  resource_group_name     = "rg-training-uks"
  subnet = [{
    address_prefix = "10.2.0.0/24"
    id             = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01/subnets/default"
    name           = "default"
    security_group = ""
    }, {
    address_prefix = "10.2.1.0/24"
    id             = "/subscriptions/xxxxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-training-uks/providers/Microsoft.Network/virtualNetworks/vnet-training-uks-01/subnets/GatewaySubnet"
    name           = "GatewaySubnet"
    security_group = ""
  }]
  tags = {}
}

# __generated__ by Terraform
resource "azurerm_subnet" "example" {
  address_prefixes                              = ["10.2.1.0/24"]
  name                                          = "GatewaySubnet"
  private_endpoint_network_policies_enabled     = false
  private_link_service_network_policies_enabled = true
  resource_group_name                           = "rg-training-uks"
  service_endpoint_policy_ids                   = []
  service_endpoints                             = []
  virtual_network_name                          = "vnet-training-uks-01"
}
```
It’s important to note that these resources will not be connected, requiring minimal rework to establish relationships. Additionally, every resource will have all attributes, including optional ones.

# References
- **Terraform import official documentation:** https://developer.hashicorp.com/terraform/cli/import