How to: import resources into terraform

So you’d like to start using infrastructure as code, but have a bunch of existing Azure resources? Or maybe you’ve added some resources through UI and want to include them in existing terraform script?

terraform import can help you with that (although not as much as it could).

Gameplan

The algorithm is rather straightforward:

  1. Define the desired resource in terraform, specifying only the required attributes.
  2. Run terraform import to map the resource to actual Azure one.
  3. Run terraform plan to get a diff between terraform’s resource and Azure one.
  4. Update terraform script to match the diff and go to point 3. Repeat points 3.-4. until terraform matches the Azure resource.
  5. Run terraform apply to update terraform state.

Case 1 - Resource Group

Let’s try this in practice. The simplest resource in Azure is a resource group.

1. Define terraform resource

Add resource definition:

# resource-group.tf

resource "azurerm_resource_group" "rg" {
  name     = "hmdev-sample-rg"    # the name of your pre-existing resource group
  location = "westeurope"
}

Your provider configuration may vary, but here’s a simple one for starters:

# provider.tf

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

provider "azurerm" {
  features {}
}

Init first:

terraform init
2. Import

First, let’s try plan to see what’s terraform planning to do:

> terraform plan

Terraform will perform the following actions:

  # azurerm_resource_group.rg will be created
  + resource "azurerm_resource_group" "rg" {
      + id       = (known after apply)
      + location = "westeurope"
      + name     = "hmdev-sample-rg"
    }

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

No surprise, terraform wants to create a new resource group - because it have that resource in its state, thus doesn’t know it exists.

We need to import it with terraform import {ADDR} {ID}.
The command expects two arguments:

  • ADDR is the resoirce address in terraform script (which is simply {resource_type}.{resource_name}, i.e. ` azurerm_resource_group.rg`).
  • ID is Azure resourceId. How do I know the resource id? I go to portal.azure.com, and navigate to the specific resource. Then, in Overview section, there’s a little link called “JSON View”, which shows the Resource ID, along with all RM data.
> terraform import azurerm_resource_group.rg subscriptions/ddc49e36-1815-46a9-9c8a-fea443074404/resourceGroups/hmdev-sample-rg

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 import simply adds an entry to the state that tells terraform that resource azurerm_resource_group.rg from the script is mapped to that specific Azure resource. It also gets the properties of the actual resource and puts them in the state file. (try doing terraform show to see what’s in the state file).

3. Plan

Now, let’s try plan again:

> terraform plan
...
# azurerm_resource_group.rg will be updated in-place
~ resource "azurerm_resource_group" "rg" {
    id       = "/subscriptions/ddc49e36-1815-46a9-9c8a-fea443074404/resourceGroups/hmdev-sample-rg"
    name     = "hmdev-sample-rg"
    ~ tags     = {
        - "foo" = "bar" -> null
    }
    # (1 unchanged attribute hidden)

    # (1 unchanged block hidden)
}

Ok, We’re getting somewhere. Now terraform recognizes the existing resource, but shows some differences. I’ve added a tag on the resource group (via portal) and terraform is telling me that this tag is not part of terraform’s resource definition and it will be removed.

4. Update. Repeat.

Let’s add the missing tag then:

resource "azurerm_resource_group" "rg" {
  name     = "hmdev-sample-rg"
  location = "westeurope"
  tags = {
    "foo" = "bar"
  }
}

Plan again… and voila:

> terraform plan

No changes. Your infrastructure matches the configuration.
5. Apply

Now I can safely do terraform apply - the state will be updated and no changes to actual infra will be made.

Case 2 - Web App

Ok, we nailed the resource group, now let’s something more complicated - a Web App.

1. Define resource

Start again by adding a resource to terraform:

resource "azurerm_app_service_plan" "serviceplan" {
  name                = "hmdev-sample-free-plan"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  sku {
    tier = "Standard"
    size = "S1"
  }
}

resource "azurerm_app_service" "sample-webapp" {
  name                = "hmdev-sample-webapp"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  app_service_plan_id = azurerm_app_service_plan.serviceplan.id

  
  site_config {
  }

  app_settings = {
  }

  logs {
  }
}

App service needs a service plan, so I added this as well. Fortunately, terraform docs include all the required pieces for a given resource type, so you don’t need to guess.

2. Import

Import the resources:

> terraform import azurerm_app_service_plan.serviceplan /subscriptions/ddc49e36-1815-46a9-9c8a-fea443074404/resourceGroups/hmdev-sample-rg/providers/Microsoft.Web/serverfarms/hmdev-sample-free-plan
> terraform import azurerm_app_service.sample-webapp /subscriptions/ddc49e36-1815-46a9-9c8a-fea443074404/resourceGroups/hmdev-sample-rg/providers/Microsoft.Web/sites/hmdev-sample-webapp 

Note: Service Plan is simple enough, but webapp is a bit more tricky one. Some optional attributes might be ignored in state diff if you don’t define them. That’s why we have site_config, app_settings and logs blocks defined.

3. Plan

Let’s see the plan:

> terraform plan

Terraform will perform the following actions:

  # azurerm_app_service.sample-webapp will be updated in-place
  ~ resource "azurerm_app_service" "sample-webapp" {
      ~ app_settings                      = {
          - "WEBSITE_NODE_DEFAULT_VERSION"   = "~16" -> null
        }
        id                                = "/subscriptions/ddc49e36-1815-46a9-9c8a-fea443074404/resourceGroups/hmdev-sample-rg/providers/Microsoft.Web/sites/hmdev-sample-webapp"
        name                              = "hmdev-sample-webapp"

        # (14 unchanged attributes hidden)

      ~ logs {
          ~ detailed_error_messages_enabled = true -> false
            # (1 unchanged attribute hidden)


            # (2 unchanged blocks hidden)
        }

      ~ site_config {
          - use_32_bit_worker_process            = true -> null
            # (18 unchanged attributes hidden)

            # (1 unchanged block hidden)
        }


        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
4. Update. Repeat.

Analyze the diff and update terraform script to match the real resource. Repeat that until the plan doesn’t contain any changes (or you’re happy with the planned changes).

6. Apply

Once you’re happy with the plan, apply it.

Summary

That’s it. The process is not complicated, but may require a few (sometimes tedious) iterations. Before doing the final apply step, be sure to compare your script with the actual resource (i.e. in the portal) and look for any non-default settings - there might be things that terraform plan won’t tell you about.