Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure Search Service Terraform Module

Provisions a hardened Azure AI Search service (azurerm_search_service) as a single, self-contained unit, targeting hashicorp/azurerm ~> 4.0. The empty call yields a private, Entra-ID-only service; every relaxation is an explicit caller opt-in.


Terraform azurerm module type resources


🧩 Overview

  • 🔎 Creates one Azure AI Search service (azurerm_search_service) — the managed retrieval engine behind keyword, semantic, and vector search.
  • 🔐 Ships private and Entra-ID-only by default: public_network_access_enabled = false and local_authentication_enabled = false.
  • 🧮 Exposes tier and capacity — sku, replica_count, partition_count, hosting_mode — with validation on the closed value sets.
  • 🧠 Supports semantic ranking (semantic_search_sku) for relevance re-ranking on top of full-text search.
  • 🪪 Provisions a system-assigned managed identity by default, so the service can reach data sources and Key Vault without embedded secrets.
  • 🧱 Offers a public-endpoint IP allow-list (allowed_ips), a trusted-services bypass (network_rule_bypass_option), and customer-managed-key enforcement (customer_managed_key_enforcement_enabled).

💡 Why it matters: Azure AI Search is the retrieval layer for most Retrieval-Augmented Generation (RAG) workloads. Getting its exposure and authentication posture right at provisioning time keeps index data — often derived from sensitive source documents — off the public internet and behind Entra ID role-based access, rather than behind long-lived API keys.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

flowchart LR
  rg["terraform-azurerm-resource-group"]:::sib
  st["terraform-azurerm-storage-account"]:::sib
  cos["terraform-azurerm-cosmosdb-account"]:::sib
  cog["terraform-azurerm-cognitive-account"]:::sib
  ra["terraform-azurerm-role-assignments"]:::sib
  pe["terraform-azurerm-private-endpoint"]:::sib
  app["Application / RAG workload"]:::sib
  mod["terraform-azurerm-search-service"]:::me
  svc["azurerm_search_service"]:::keystone

  rg -->|"resource_group_name + location"| mod
  mod -->|"creates"| svc
  st -->|"indexer data source"| svc
  cos -->|"indexer data source"| svc
  svc -->|"vector search for RAG"| cog
  ra -->|"Search Index Data Reader / Contributor"| svc
  app -->|"query via Entra ID"| svc
  pe -->|"private endpoint access"| svc

  classDef me fill:#0078D4,color:#fff,stroke:#004578,stroke-width:1px;
  classDef keystone fill:#004578,color:#fff,stroke:#002a4a,stroke-width:1px;
  classDef sib fill:#eef3f8,color:#0a2540,stroke:#b9c9da,stroke-width:1px;
Loading

This is a standalone primitive. It consumes a resource group (name + location) and, at run time, reaches sibling data stores (Storage, Cosmos DB) as indexer sources. It pairs with terraform-azurerm-cognitive-account to serve vector search for RAG, is granted data-plane access through terraform-azurerm-role-assignments, and is fronted by terraform-azurerm-private-endpoint for private access.


🧬 What this module builds

flowchart LR
  in1["name / resource_group_name / location"]:::sib
  in2["sku + replica_count + partition_count"]:::sib
  in3["identity: SystemAssigned"]:::sib
  in4["secure defaults: private + Entra-only"]:::sib
  svc["azurerm_search_service.this"]:::keystone
  o1["id"]:::sib
  o2["name"]:::sib
  o3["endpoint"]:::sib
  o4["identity_principal_id"]:::sib

  in1 -->|"placement"| svc
  in2 -->|"tier and capacity"| svc
  in3 -->|"managed identity"| svc
  in4 -->|"hardened posture"| svc
  svc -->|"resource id"| o1
  svc -->|"service name"| o2
  svc -->|"query endpoint"| o3
  svc -->|"principal id"| o4

  classDef keystone fill:#004578,color:#fff,stroke:#002a4a,stroke-width:1px;
  classDef sib fill:#eef3f8,color:#0a2540,stroke:#b9c9da,stroke-width:1px;
Loading

Resource inventory

Resource Count Role
azurerm_search_service.this 1 The keystone Search Service.
identity {} (dynamic) 0–1 Optional managed identity (system-assigned by default).
timeouts {} (dynamic) 0–1 Optional operation timeouts.

✅ Provider / Versions

Requirement Value
Terraform floor >= 1.12.0
Provider hashicorp/azurerm, pinned ~> 4.0
Provider block None in this module — the caller configures provider "azurerm" { features {} }, auth, and subscription.

Schema notes that bite (verified against the live provider schema):

  • name, resource_group_name, and location are force-new — changing any of them replaces the service.
  • hosting_mode is force-new, and HighDensity is only accepted when sku = "standard3".
  • sku changes are partly force-new: tier upgrades within Basic and Standard apply in place, but downgrades, any change to or from free, and any change to or from the storage-optimized tiers replace the service.
  • 🔴 replica_count and partition_count CAN be set on the free tier. An earlier revision of this document said they could not. The provider refuses only values greater than 1 there — and its own schema default of 1 is sent on every apply, including on free, so the arguments are never absent. The real per-tier ceilings, none of which were documented and all of which the module now checks at terraform validate: replicas 1 on free, 3 on basic, 12 elsewhere; partitions 1 on free, 3 on basic, 3 on standard3 with HighDensity, 12 elsewhere.
  • 🔴 The module's hosting_mode check used to be case-SENSITIVE while the provider's is not. hosting_mode is the only enum on this resource whose provider check ignores case, so "highdensity" was accepted by the provider and refused by this module — and because a failed validation {} blocks terraform destroy as well as apply, anyone who had used that spelling could not manage the service through the module at all. The check now compares case-insensitively, matching the provider.
  • ⚠️ **authentication_failure_mode is REFUSED, not ignored, when local_authentication_enabled = false.** The provider's CustomizeDifferrors outright. Since this module defaults that flag tofalse, setting the failure mode alone is the *default* path into that error, not an unusual one — so the module now catches it at terraform validate`.
  • ⚠️ The IP firewall is not supported on the free tier at all, so allowed_ips entries there configure nothing.
  • allowed_ips are only honored when public_network_access_enabled = true; with public access disabled, only private endpoint connections are accepted.
  • authentication_failure_mode is only honored when local_authentication_enabled = true.
  • semantic_search_sku cannot be set when sku = "free", and semantic ranking is region-limited.
  • primary_key, secondary_key, and query_keys are computed and sensitive — this module deliberately does not emit them.

🔑 Required Azure RBAC Roles / Permissions

Least-privilege, at the smallest scope that works:

  • Search Service Contributor on the target resource group (or a custom role granting Microsoft.Search/searchServices/*) to create and manage the service.
  • Granting data-plane access to callers (index read/write) is a separate concern — assign Search Index Data Reader or Search Index Data Contributor to consuming identities via terraform-azurerm-role-assignments, scoped to this service's id.

Azure Prerequisites

  • An existing resource group in a supported US Azure region (this module does not create it).
  • The Microsoft.Search resource provider registered on the target subscription.
  • A submitted quota-increase request for the standard2, standard3, storage_optimized_l1, and storage_optimized_l2 tiers.
  • The caller configures the provider "azurerm" { features {} } block, authentication, and subscription; the module declares none of these.

📁 Module Structure

terraform-azurerm-search-service/
├── providers.tf     # required_version + azurerm ~> 4.0; no provider block
├── variables.tf     # deeply-typed object() schemas + tags/timeouts tail
├── main.tf          # keystone azurerm_search_service.this; dynamic identity + timeouts, try()
├── outputs.tf       # id first, then name, endpoint, identity principal/tenant, CMK status
├── README.md        # this document
├── SCOPE.md         # the cross-module contract
├── LICENSE          # MIT
└── .gitignore       # canonical library ignore set

⚙️ Quick Start

The smallest real call — a private, Entra-ID-only Standard service:

module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-platform-eastus"
  resource_group_name = "rg-platform-eastus"
  location            = "eastus"
  sku                 = "standard"

  tags = {
    environment = "production"
    workload    = "search"
  }
}

ℹ️ The caller configures the provider, authentication, and the mandatory features {} block:

provider "azurerm" {
  features {}
}

Always pin the module with ?ref=v1.0.0; never track a branch.


🔌 Cross-Module Contract

Consumes

Input Type Source module
resource_group_name string terraform-azurerm-resource-group (name)
location string caller / terraform-azurerm-resource-group (location)
identity.identity_ids list(string) terraform-azurerm-user-assigned-identity (id)

Emits

Output Description Consumed by
id Search Service Resource ID (first) search-shared-private-link-service, role-assignments, private-endpoint, diagnostics
name Search Service name diagnostics / tagging conventions
endpoint HTTPS query endpoint application / RAG workload
identity_principal_id Managed identity principal ID role-assignments on Storage / Cosmos DB / Key Vault
identity_tenant_id Managed identity tenant ID cross-tenant references
customer_managed_key_encryption_compliance_status CMK compliance status governance reporting

📚 Example Library

The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:

variable "search_identity_id" {
  description = "id of an existing search identity that these examples reference but do not create."
  type        = string
}
1 · Minimal secure call

The empty-posture call: private, Entra-ID-only, system-assigned identity, no API keys.

module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-min-eastus"
  resource_group_name = "rg-platform-eastus"
  location            = "eastus"
  sku                 = "standard"
}

🔒 With no overrides, public_network_access_enabled = false and local_authentication_enabled = false — the service is reachable only over private endpoints and only via Microsoft Entra ID.

2 · Basic tier for small workloads
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-basic-eastus"
  resource_group_name = "rg-team-eastus"
  location            = "eastus"
  sku                 = "basic"
}

💡 basic runs on a dedicated cluster and supports up to 3 replicas / 1 partition — a good floor for small production indexes.

3 · Replicas and partitions for scale
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-scale-eastus2"
  resource_group_name = "rg-platform-eastus2"
  location            = "eastus2"
  sku                 = "standard"

  replica_count   = 3
  partition_count = 3
}

💡 Query throughput scales with replica_count; storage and indexing throughput scale with partition_count. Search Units billed = replicas x partitions.

4 · High-density hosting
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-multitenant-eastus2"
  resource_group_name = "rg-saas-eastus2"
  location            = "eastus2"
  sku                 = "standard3"

  hosting_mode    = "HighDensity"
  partition_count = 3
}

⚠️ hosting_mode = "HighDensity" is only valid with sku = "standard3", caps partitions at 3, and is force-new — changing it later replaces the service. It targets multi-tenant designs needing up to 1000 indexes.

5 · Semantic ranking
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-semantic-eastus"
  resource_group_name = "rg-ai-eastus"
  location            = "eastus"
  sku                 = "standard"

  semantic_search_sku = "standard"
}

ℹ️ Semantic ranking re-scores results with a language-understanding model. It cannot be enabled on the free tier and is available only in certain regions.

6 · Mixed Entra ID + API key authentication

Entra-ID-only is the default. Enable local authentication only when a workload cannot use Entra ID.

module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-mixedauth-eastus"
  resource_group_name = "rg-legacy-eastus"
  location            = "eastus"
  sku                 = "standard"

  local_authentication_enabled = true
  authentication_failure_mode  = "http401WithBearerChallenge"
}

⚠️ Turning on local_authentication_enabled keeps the computed admin and query API keys live. authentication_failure_mode is only honored in this mixed mode. Prefer Entra ID and leave both off where you can.

7 · User-assigned managed identity
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-uami-eastus"
  resource_group_name = "rg-platform-eastus"
  location            = "eastus"
  sku                 = "standard"

  identity = {
    type         = "UserAssigned"
    identity_ids = [var.search_identity_id]
  }
}

💡 A user-assigned identity survives service re-creation and can be granted access to data sources ahead of time, decoupling RBAC setup from the service lifecycle.

8 · System- and user-assigned identity together
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-dualid-eastus"
  resource_group_name = "rg-platform-eastus"
  location            = "eastus"
  sku                 = "standard"

  identity = {
    type         = "SystemAssigned, UserAssigned"
    identity_ids = [var.search_identity_id]
  }
}

ℹ️ Use the system-assigned identity for CMK and the user-assigned identity for shared data-source access, or vice versa.

9 · Public endpoint with an IP allow-list
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-public-eastus"
  resource_group_name = "rg-edge-eastus"
  location            = "eastus"
  sku                 = "standard"

  public_network_access_enabled = true
  allowed_ips                   = ["203.0.113.10", "198.51.100.0/24"]
}

🔴 An earlier revision of this document had this backwards, in the dangerous direction. It said an empty allowed_ips with public access on "blocks all public inbound traffic". It does the opposite. Microsoft's API reference is explicit: the rules "define the inbound network(s) with allowing access to the search service endpoint. At the meantime, all other public IP networks are blocked by the firewall" — the blocking is a consequence of listing rules. The three real states are:

configuration reachable from
public_network_access_enabled = false (the module default) private endpoints only
= true, allowed_ips = [] every public network
= true, allowed_ips = […] only the listed sources

Requests still authenticate in every state — this is a network control, not an authorization one. The module emits public_ip_firewall_is_open so the middle row is visible in an output rather than inferred.

10 · Trusted Azure services bypass
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-bypass-eastus"
  resource_group_name = "rg-ai-eastus"
  location            = "eastus"
  sku                 = "standard"

  public_network_access_enabled = true
  network_rule_bypass_option    = "AzureServices"
}

🔒 network_rule_bypass_option defaults to None. Set AzureServices only when a paired Microsoft service (for example an Azure AI service or a portal import) must reach a network-restricted service.

11 · Customer-managed-key enforcement
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-cmk-eastus"
  resource_group_name = "rg-regulated-eastus"
  location            = "eastus"
  sku                 = "standard"

  identity = {
    type = "SystemAssigned"
  }

  customer_managed_key_enforcement_enabled = true
}

⚠️ Data is always encrypted at rest with Microsoft-managed keys. Enable customer_managed_key_enforcement_enabled only once every downstream object (indexes, synonym maps) is configured with a customer-managed key, otherwise object creation is rejected.

12 · Private access only (default posture, made explicit)
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-private-eastus"
  resource_group_name = "rg-platform-eastus"
  location            = "eastus"
  sku                 = "standard"

  public_network_access_enabled = false
}

🔒 With public access off, front the service with terraform-azurerm-private-endpoint (sub-resource searchService) and a private DNS zone so clients resolve the endpoint privately.

13 · Many services with for_each
locals {
  search_services = {
    eastus  = { location = "eastus", sku = "standard" }
    westus2 = { location = "westus2", sku = "standard" }
  }
}

module "search" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"
  for_each = local.search_services

  name                = "srch-platform-${each.key}"
  resource_group_name = "rg-platform-${each.key}"
  location            = each.value.location
  sku                 = each.value.sku
}

💡 A stable map key per region keeps for_each from re-indexing when you add or remove a region.

14 · Tags and operation timeouts
module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-tagged-eastus"
  resource_group_name = "rg-platform-eastus"
  location            = "eastus"
  sku                 = "standard"

  tags = {
    environment = "production"
    cost_center = "1234"
    owner       = "data-platform"
  }

  timeouts = {
    create = "30m"
    delete = "30m"
  }
}

ℹ️ The tags + timeouts tail is present on every module in this suite for consistent governance and lifecycle control.

15 · 🏗️ End-to-end composition

A resource group, a hardened Search Service, and data-plane role assignments wired from real sibling outputs. Pair it with terraform-azurerm-cognitive-account to complete a vector RAG stack.

provider "azurerm" {
  features {}
}

module "rg" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-rag-eastus"
  location = "eastus"
}

module "search" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-search-service.git?ref=v1.0.0"

  name                = "srch-rag-eastus"
  resource_group_name = module.rg.name
  location            = module.rg.location
  sku                 = "standard"

  semantic_search_sku = "standard"

  identity = {
    type = "SystemAssigned"
  }
}

# Grant the application identity data-plane access to the search indexes,
# and let a platform team manage the service itself.
module "search_access" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"

  scope = module.search.id

  role_assignments = {
    app_reader = {
      role_definition_name = "Search Index Data Reader"
      principal_id         = var.application_principal_id
    }
    platform_admin = {
      role_definition_name = "Search Service Contributor"
      principal_id         = var.platform_group_object_id
    }
  }
}

💡 The Search Service's own identity_principal_id output feeds role assignments on the paired terraform-azurerm-cognitive-account and any Storage / Cosmos DB indexer data sources, so the service reaches its data — and the embedding model — without a single API key.


📥 Inputs

Required: name, resource_group_name, location, sku.

Capacity: replica_count, partition_count, hosting_mode, semantic_search_sku.

Authentication: local_authentication_enabled, authentication_failure_mode.

Network: public_network_access_enabled, allowed_ips, network_rule_bypass_option.

Encryption & identity: customer_managed_key_enforcement_enabled, identity.

Universal tail: tags, timeouts.

Full schemas
Name Type Default Description
name string — Globally unique service name; force-new.
resource_group_name string — Existing resource group; force-new.
location string — Azure region; force-new.
sku string — free | basic | standard | standard2 | standard3 | storage_optimized_l1 | storage_optimized_l2.
replica_count number null Replicas; not valid on free.
partition_count number null Partitions (1,2,3,4,6,12); not valid on free.
hosting_mode string "Default" Default | HighDensity; force-new. Compared case-insensitively, matching the provider — the only enum here that does.
semantic_search_sku string null free | standard; not valid on free tier.
local_authentication_enabled bool false API key auth alongside Entra ID.
authentication_failure_mode string null http401WithBearerChallenge | http403; mixed-mode only.
public_network_access_enabled bool false Public endpoint exposure.
allowed_ips list(string) [] Public-endpoint IP allow-list.
network_rule_bypass_option string "None" None | AzureServices.
customer_managed_key_enforcement_enabled bool false Reject non-CMK objects.
variable "identity" {
  type = object({
    type         = string                 # "SystemAssigned" | "UserAssigned" | "SystemAssigned, UserAssigned"
    identity_ids = optional(list(string)) # required when type includes "UserAssigned"
  })
  default = { type = "SystemAssigned" }
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
}

variable "tags" {
  type    = map(string)
  default = {}
}

🧾 Outputs

Output Description Notes
id Search Service Resource ID Emitted first.
name Search Service name —
location Azure region, in the canonical form Azure uses. Read from the resource, not var.location.
endpoint HTTPS query endpoint —
identity_principal_id Managed identity principal ID null when no identity is configured.
identity_tenant_id Managed identity tenant ID null when no identity is configured.
customer_managed_key_encryption_compliance_status CMK compliance status Computed.

🔒 The admin keys (primary_key, secondary_key) and query_keys are computed and sensitive; this module deliberately does not emit them. Prefer Entra ID role-based data-plane access.


🧠 Architecture Notes

  • Force-new fields. name, resource_group_name, location, and hosting_mode all force replacement. sku is partly force-new: upgrades within Basic/Standard apply in place, while downgrades and any move to or from the free or storage-optimized tiers replace the service. Treat a tier change as a potentially destructive operation and review the plan.
  • Free-tier constraints. The free tier rejects replica_count, partition_count, and semantic_search_sku. The module defaults replica_count and partition_count to null so the empty call works on any tier.
  • Interdependent fields, and the two behave differently. allowed_ips have no effect while public_network_access_enabled = false — with public access off, private endpoints are the exclusive path and no IP rule changes that. authentication_failure_mode, by contrast, is refused outright when local_authentication_enabled = false: the provider's CustomizeDiff returns an error, so the plan fails rather than the setting being dropped. The module now catches both at terraform validate, along with the per-tier capacity ceilings and the free-tier firewall restriction, so none of them waits for an apply.
  • Secret handling. API keys are never accepted as input and never emitted as output. Data-plane access is expected to run through Entra ID role assignments against the service id.
  • Identity lifecycle. The default system-assigned identity is created with the service and destroyed with it; a user-assigned identity outlives the service and is the better choice when data-source RBAC must be prepared in advance.
  • features {} dependence. The provider will not initialize without a caller-side provider "azurerm" { features {} } block. That is expected — library modules never carry one.

🧱 Design Principles

The empty call produces the hardened service; each relaxation is a value the caller must type:

Concern Secure default (empty call) Opt-out (caller types it)
Public network access public_network_access_enabled = false set to true
Local (API key) authentication local_authentication_enabled = false set to true
Trusted-service bypass network_rule_bypass_option = "None" set to "AzureServices"
Public IP allow-list allowed_ips = [] add addresses / CIDRs (with public access on)
Managed identity system-assigned identity provisioned pass identity = null
Encryption at rest Microsoft-managed keys, always on enable customer_managed_key_enforcement_enabled for CMK
Secret exposure keys never accepted or emitted —

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module with ?ref=v1.0.0 — never a branch.
  • This library is plan-only during authoring; a human runs terraform plan and terraform apply from CI against real credentials.

🧪 Testing

The offline proof gate is what this module is validated against:

  • terraform init -backend=false — resolves the pinned provider without a backend.
  • terraform validate — proves the configuration is internally consistent and type-correct against the pinned provider schema. This catches every typing mistake the object() schemas and validation {} blocks are designed to surface (a bad sku, an out-of-range hosting_mode, a malformed identity).
  • terraform fmt -check — enforces canonical formatting.

Neither validate nor fmt calls Azure. Only terraform plan (run by a human, against real credentials) exercises the ARM API and confirms tier availability, quota, and region support.


💬 Example Output

$ terraform output
id                    = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-rag-eastus/providers/Microsoft.Search/searchServices/srch-rag-eastus"
name                  = "srch-rag-eastus"
endpoint              = "https://srch-rag-eastus.search.windows.net"
identity_principal_id = "11111111-2222-3333-4444-555555555555"
identity_tenant_id    = "66666666-7777-8888-9999-aaaaaaaaaaaa"
customer_managed_key_encryption_compliance_status = "Compliant"

🔍 Troubleshooting

Symptom Cause Fix
partition_count cannot be set when sku is free Capacity fields supplied on the free tier Leave replica_count / partition_count null on free, or move to basic/standard.
hosting_mode HighDensity requires sku standard3 HighDensity set on a lower tier Set sku = "standard3", or use hosting_mode = "Default".
Plan shows the service being replaced after a tier change sku change is force-new (downgrade, free, or storage-optimized) Confirm the change is intended; upgrades within Basic/Standard apply in place.
allowed_ips appear to have no effect public_network_access_enabled = false IP rules only apply with public access on; otherwise use a private endpoint.
Clients get 403 with a valid API key local_authentication_enabled = false Use Entra ID role-based access, or enable local auth if the client cannot use Entra ID.
Provider fails to initialize in isolation No caller-side features {} block Add provider "azurerm" { features {} } to the root module.
Object creation rejected as non-compliant customer_managed_key_enforcement_enabled = true without CMK on objects Configure CMK on every object, or disable enforcement.

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."