Bridging Terraform and the Azure AI Search Data Plane: Building a First-Class Index Resource
When I implemented the Azure AI Search index resource, the interesting part was not just creating a new Terraform resource. It was learning how to fit a true data plane object into a provider that is mostly organized around ARM-style resources and management plane patterns.
I was not starting from zero either. I branched from a pull request opened by a colleague at Microsoft, and that shaped the whole experience. The core design was already there: the resource would accept an ARM-style search_service_id, derive the Search endpoint from it, and then use the Azure AI Search data plane SDK to manage the index itself.
resource "azurerm_search_service" "test" {
name = "srch-tftest-${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location
sku = "standard"
authentication_failure_mode = "http403"
identity {
type = "SystemAssigned"
}
}
My work became an exercise in understanding that architecture, extending it, and validating that it behaved like a first-class Terraform resource.
Starting from My Colleague’s Pull Request
Branching from my colleague’s PR gave me a useful starting point because the hardest structural work and decisions had already been made.
We needed to have the Data Plane SDK added to the AzureRM provider in order to tap into all those beautiful resources therein. Terraform would now be able to seamlessly translate between the control plane resources (the Azure Search service itself) and the data plane resources (i.e. Data Sources, Indices, and Indexers). For a Terraform developer they would just be azurerm resources — an ID that looked and behaved like the provider’s other resources, but CRUD operations would execute against the Search service endpoint through it’s data plane SDK.
I will say that I’m genuinely happy to see this direction gaining traction. I had been advocating for stronger data plane support for AI services, and it’s encouraging to see my colleagues at Microsoft leaning into that approach with Azure AI Search. Hopefully, this pattern continues and extends into other areas like Azure AI Foundry, where the same separation between control plane and data plane is just as important.
Turning the Design into a Working Resource
The first milestone was creating the resource itself and getting a basic index to work end to end. In practice, that meant defining the schema for the index fields, mapping those fields into the SDK’s SearchIndex model, and making sure the create path checked for preexisting indexes before taking ownership.
type SearchIndexModel struct {
Name string `tfschema:"name"`
SearchServiceId string `tfschema:"search_service_id"`
Fields []SearchIndexField `tfschema:"fields"`
CorsOptions []CorsOptions `tfschema:"cors_options"`
DefaultScoringProfile string `tfschema:"default_scoring_profile"`
}
The Search Index Model is rather robust having many child entities. The most important one was the Index Field.
type SearchIndexField struct {
Name string `tfschema:"name"`
Type string `tfschema:"type"`
Key bool `tfschema:"key"`
Searchable bool `tfschema:"searchable"`
Filterable bool `tfschema:"filterable"`
Sortable bool `tfschema:"sortable"`
Facetable bool `tfschema:"facetable"`
Retrievable bool `tfschema:"retrievable"`
Analyzer string `tfschema:"analyzer"`
SearchAnalyzer string `tfschema:"search_analyzer"`
IndexAnalyzer string `tfschema:"index_analyzer"`
SynonymMaps []string `tfschema:"synonym_maps"`
}
The simplest working shape of the resource was an index with a key field and one or two searchable fields. That seems modest, but it established the main pattern. Terraform configuration was decoded into an internal model, that model was expanded into SDK types, and the resource used CreateOrUpdate on the data plane client to submit the index definition.
resource "azurerm_search_index" "test" {
name = "idx-test"
search_service_id = azurerm_search_service.test.id
fields {
name = "id"
type = "Edm.String"
key = true
retrievable = true
}
fields {
name = "title"
type = "Edm.String"
searchable = true
filterable = true
retrievable = true
}
}
Once that path worked, the resource began to feel less experimental. It was no longer just a design concept inherited from a colleague’s PR. It was something I could actually apply in a live Terraform configuration.
Using a Real Terraform Configuration
The working sample configuration made the experience much more concrete. Instead of testing the resource in abstraction, I could see how a real user would provision the full stack around it. The configuration starts by generating a random suffix, creating a resource group, and then provisioning an Azure Search service with authentication_failure_mode = "http403" and a system-assigned identity. After that, it uses azurerm_client_config to look up the current principal and assigns the Search Service Contributor role on the Search service before creating the index.
data "azurerm_client_config" "current" {}
resource "azurerm_role_assignment" "search_contributor" {
principal_id = data.azurerm_client_config.current.object_id
scope = azurerm_search_service.test.id
role_definition_name = "Search Service Contributor"
skip_service_principal_aad_check = true
}
A key feature of this setup is that it uses Azure RBAC to grant Terraform access to the data plane. Instead of relying on admin keys, the configuration explicitly assigns the appropriate role to the executing principal. That role assignment is what allows Terraform to authenticate against the Search service endpoint and successfully create and manage the index. Without it, the data plane calls would fail even if the infrastructure itself were provisioned correctly.
resource "azurerm_search_index" "test" {
name = "idx-test"
search_service_id = azurerm_search_service.test.id
/* Other Stuff */
depends_on = [azurerm_role_assignment.search_contributor]
}
That detail ended up being just as important as the resource implementation itself. The index resource depends on data plane permissions, and RBAC provides a clean, secure, and production-aligned way to grant that access.
The sample index definition also reflects the kind of scenario I had in mind while implementing the resource. It creates an index named idx-test with a few fields.
The id field is the key and retrievable. The others can be whatever types I’m interested, marked searchable, filterable, and retrievable.
After the terraform apply my first Index was created!
Conclusion
Implementing the Azure AI Search index resource was really an exercise in translating between systems. On one side was Terraform, with its expectations around state, import, and declarative updates. On the other side was Azure AI Search, where resources are provisioned and managed by two systems the ARM Control Plane and the Azure AI Search Data Plane.
I am super excited to see more resources added to the AzureRM provider making it much easier to manage emerging AI workloads on Azure more effectively through Terraform.