In my last post, I looked at Terraform Actions. Terraform Actions are a new feature that allows you to trigger actions on resources, such as powering on/off a VM. As we discussed in the article, actions were one of multiple announcements at HashiConf 2025, and I’m determined to find the time to explore each one and document as I go!

In this post, we will explore Terraform Search at an introductory level together - errors and all!


Overview

Reading the HashiConf announcement post HashiCorp stated that the new search functionality would…

Discover and import resources in bulk more efficiently and accurately.

Sounds interesting! Importing has always been a little bit of a pain, though it has gotten better over the years, moving away from the old terraform import command to the newer import block approach. But when importing resources, you still need to poke about portals or APIs trying to identify the IDs of the resources you want to import one at a time, or use third-party tools such as aztfexport.

Terraform search (currently in Beta and available from Terraform 1.14) now brings an end-to-end workflow directly into Terraform to make this easier. Time to hit the docs…


The Basics

Reading over the documentation, it became apparent that this new functionality is dependant on a new resource type having been written into the provider. The high-level workflow is as follows:

  1. Define Queries - Use one or more list blocks in your code to define the resources you want to import.
  2. Execute the Search - Run a search using the terraform query to see what it finds.
  3. Review the Search Results - Review the matching results and ensure you are happy with what you’ll be importing.
  4. Generate Code - Generate the Terraform HCL code and import blocks required to import the identified resources.
  5. Apply the Configuration - Apply the changes to import the resources into state as normal, allowing Terraform to manage them moving forward

Finding a Resource Type

As mentioned, the search workflow is dependant on a new resource type having been defined in the provider. So, let’s pop over to the AzureRM GitHub page and do a quick search to see if there is a new resource type we can use…

Terraform Search in AzureRM provider

Bingo! Looks like both virtual network and storage accounts are supported. Let’s go check the registry documentation…

AzureRM provider documentation

Time to give it a go.


Working Through an Example

Best way to hopefully understand this, is to work through an example and see where we get.

Prerequisites

We need to be running at least Terraform 1.14.0, so let’s download that and unzip it…

curl -LO https://releases.hashicorp.com/terraform/1.14.0-beta2/terraform_1.14.0-beta2_darwin_amd64.zip && unzip terraform_1.14.0-beta2_darwin_amd64.zip

Terraform Version

I’m also going to need an existing resource that is not managed through Terraform. I’m going to use a storage account for this example. In the words of Blue Peter… “Here’s one I made earlier…”

Existing Storage Account


Define the Queries

Next, I need to define the queries using the new list block. Let’s plonk some code into main.tf to search based on a specific resource group…

# main.tf 

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

provider "azurerm" {
  features {}
  subscription_id = "4595d981-87d1-4772-bd3a-1f5471da6c24"
}

list "azurerm_storage_account" "example" {
  provider = azurerm
  config {
    resource_group_name = "general-storage"
  }
}

And give terraform init a whirl…

Terraform Init Error

Oh ☹️ - not what I was expecting to see, but I guess that’s what happens when you don’t read the documentation properly! Poking about a bit more, I realised the new list block needs to go in a .tfquery.hcl file as documented here.

So, let’s move that and try an init again…

# main.tf 

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

provider "azurerm" {
  features {}
  subscription_id = "4595d981-87d1-4772-bd3a-1f5471da6c24"
}
# test.tfquery.hcl

list "azurerm_storage_account" "example" {
  provider = azurerm
  config {
    resource_group_name = "general-storage"
  }
}

A terraform init now works as expected…

Terraform Init Success

What about a terraform plan? I’ve nothing defined, so presumably nothing is going to be flagged here…

Terraform Plan Success

As I expected! Good. Moving on.


Running a Query

Let’s now try the new terraform query command and see what happens.

First Query

Success! Not the most helpful of outputs just yet, but on the plus side,I have gotten the name, resource group and subscription ID without having to poke about the portal. Now, what was all this about config generation?!


Generating Configuration

Using the -generate-config-out=<filename> flag, we should be able to have Terraform generate the HCL code and import blocks for the resources matching our query. Let’s give it a try:

terraform query -generate-config-out=test.tf

Generating Configuration

Well, there are no errors so that’s a good start, and I can see a new test.tf file. Let’s take a look at the contents (no judging me on poor security settings - it’s a demo environment after all 😉)…

# test.tf

# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform
resource "azurerm_storage_account" "example_0" {
  provider                           = azurerm
  access_tier                        = "Hot"
  account_kind                       = "StorageV2"
  account_replication_type           = "LRS"
  account_tier                       = "Standard"
  allow_nested_items_to_be_public    = true
  allowed_copy_scope                 = ""
  cross_tenant_replication_enabled   = true
  default_to_oauth_authentication    = true
  dns_endpoint_type                  = "Standard"
  edge_zone                          = ""
  https_traffic_only_enabled         = false
  id                                 = "/subscriptions/4595d981-87d1-4772-bd3a-1f5471da6c24/resourceGroups/general-storage/providers/Microsoft.Storage/storageAccounts/mikeguy"
  infrastructure_encryption_enabled  = true
  is_hns_enabled                     = false
  large_file_share_enabled           = false
  local_user_enabled                 = true
  location                           = "uksouth"
  min_tls_version                    = "TLS1_0"
  name                               = "mikeguy"
  nfsv3_enabled                      = false
  primary_access_key                 = null # sensitive
  primary_blob_connection_string     = null # sensitive
  primary_blob_endpoint              = "https://mikeguy.blob.core.windows.net/"
  primary_blob_host                  = "mikeguy.blob.core.windows.net"
  primary_blob_internet_endpoint     = ""
  primary_blob_internet_host         = ""
  primary_blob_microsoft_endpoint    = ""
  primary_blob_microsoft_host        = ""
  primary_connection_string          = null # sensitive
  primary_dfs_endpoint               = "https://mikeguy.dfs.core.windows.net/"
  primary_dfs_host                   = "mikeguy.dfs.core.windows.net"
  primary_dfs_internet_endpoint      = ""
  primary_dfs_internet_host          = ""
  primary_dfs_microsoft_endpoint     = ""
  primary_dfs_microsoft_host         = ""
  primary_file_endpoint              = "https://mikeguy.file.core.windows.net/"
  primary_file_host                  = "mikeguy.file.core.windows.net"
  primary_file_internet_endpoint     = ""
  primary_file_internet_host         = ""
  primary_file_microsoft_endpoint    = ""
  primary_file_microsoft_host        = ""
  primary_location                   = "uksouth"
  primary_queue_endpoint             = "https://mikeguy.queue.core.windows.net/"
  primary_queue_host                 = "mikeguy.queue.core.windows.net"
  primary_queue_microsoft_endpoint   = ""
  primary_queue_microsoft_host       = ""
  primary_table_endpoint             = "https://mikeguy.table.core.windows.net/"
  primary_table_host                 = "mikeguy.table.core.windows.net"
  primary_table_microsoft_endpoint   = ""
  primary_table_microsoft_host       = ""
  primary_web_endpoint               = "https://mikeguy.z33.web.core.windows.net/"
  primary_web_host                   = "mikeguy.z33.web.core.windows.net"
  primary_web_internet_endpoint      = ""
  primary_web_internet_host          = ""
  primary_web_microsoft_endpoint     = ""
  primary_web_microsoft_host         = ""
  provisioned_billing_model_version  = ""
  public_network_access_enabled      = true
  queue_encryption_key_type          = "Service"
  resource_group_name                = "general-storage"
  secondary_access_key               = null # sensitive
  secondary_blob_connection_string   = null # sensitive
  secondary_blob_endpoint            = ""
  secondary_blob_host                = ""
  secondary_blob_internet_endpoint   = ""
  secondary_blob_internet_host       = ""
  secondary_blob_microsoft_endpoint  = ""
  secondary_blob_microsoft_host      = ""
  secondary_connection_string        = null # sensitive
  secondary_dfs_endpoint             = ""
  secondary_dfs_host                 = ""
  secondary_dfs_internet_endpoint    = ""
  secondary_dfs_internet_host        = ""
  secondary_dfs_microsoft_endpoint   = ""
  secondary_dfs_microsoft_host       = ""
  secondary_file_endpoint            = ""
  secondary_file_host                = ""
  secondary_file_internet_endpoint   = ""
  secondary_file_internet_host       = ""
  secondary_file_microsoft_endpoint  = ""
  secondary_file_microsoft_host      = ""
  secondary_location                 = ""
  secondary_queue_endpoint           = ""
  secondary_queue_host               = ""
  secondary_queue_microsoft_endpoint = ""
  secondary_queue_microsoft_host     = ""
  secondary_table_endpoint           = ""
  secondary_table_host               = ""
  secondary_table_microsoft_endpoint = ""
  secondary_table_microsoft_host     = ""
  secondary_web_endpoint             = ""
  secondary_web_host                 = ""
  secondary_web_internet_endpoint    = ""
  secondary_web_internet_host        = ""
  secondary_web_microsoft_endpoint   = ""
  secondary_web_microsoft_host       = ""
  sftp_enabled                       = false
  shared_access_key_enabled          = true
  table_encryption_key_type          = "Service"
  tags                               = {}
  blob_properties {
    change_feed_enabled           = false
    change_feed_retention_in_days = 0
    default_service_version       = ""
    last_access_time_enabled      = false
    versioning_enabled            = false
    container_delete_retention_policy {
      days = 7
    }
    delete_retention_policy {
      days                     = 7
      permanent_delete_enabled = false
    }
  }
  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 {
    create = null
    delete = null
    read   = null
    update = null
  }
}

import {
  to       = azurerm_storage_account.example_0
  provider = azurerm
  identity = {
    name                = "mikeguy"
    resource_group_name = "general-storage"
    subscription_id     = "4595d981-87d1-4772-bd3a-1f5471da6c24"
  }
}

Whilst pretty verbose (these import tools always tend to be), it largely looks ok. There are clearly a load of defaults we could clean up, and some attributes that are going to cause problems (as they aren’t configuration items - e.g. id), but let’s give the plan/apply a whirl anyway…

Wall of Errors

Not quite what we’d hoped, but as I expected!

A tonne of different errors relating to empty strings, deprecated fields, state-only fields, and so on. Not quite perfect yet, and like many of the other tools, some tidy up work is needed.

Let’s give test.tf a bit of TLC and remove the state only fields, and loads of the empty strings/defaults…

# test.tf

resource "azurerm_storage_account" "example_0" {
  access_tier                       = "Hot"
  account_kind                      = "StorageV2"
  account_replication_type          = "LRS"
  account_tier                      = "Standard"
  allow_nested_items_to_be_public   = true
  cross_tenant_replication_enabled  = true
  default_to_oauth_authentication   = true
  dns_endpoint_type                 = "Standard"
  https_traffic_only_enabled        = false
  infrastructure_encryption_enabled = true
  is_hns_enabled                    = false
  large_file_share_enabled          = false
  local_user_enabled                = true
  location                          = "uksouth"
  min_tls_version                   = "TLS1_0"
  name                              = "mikeguy"
  nfsv3_enabled                     = false
  public_network_access_enabled     = true
  queue_encryption_key_type         = "Service"
  resource_group_name               = "general-storage"
  sftp_enabled                      = false
  shared_access_key_enabled         = true
}

import {
  to       = azurerm_storage_account.example_0
  provider = azurerm
  identity = {
    name                = "mikeguy"
    resource_group_name = "general-storage"
    subscription_id     = "4595d981-87d1-4772-bd3a-1f5471da6c24"
  }
}

Trying the plan again…

Plan Failed Again

Doesn’t seem to like the fact I’ve removed the provider argument, so let’s just add that back in and try again…

Plan Success

Looks better 😀. Let’s carry on with an apply…

Apply Success

And there we have it. We’ve successfully imported the storage account through the new workflow, and it can now be managed as normal through Terraform.


Conclusion

Whilst we’ve only looked at a single resource type example in this post, you can see how if you had a load of different things to import and generate config for, that this will be useful. At least it will, once the support trickles into the providers which is the main downside of this approach.

One to keep your eye on as time progresses!