A single-purpose module that provisions an
azurerm_subnet_service_endpoint_storage_policy— a policy that narrows a subnet'sMicrosoft.Storageservice-endpoint access down to an explicit allow-list of Storage accounts or aliases. Targetshashicorp/azurerm ~> 4.0.
- Creates one
azurerm_subnet_service_endpoint_storage_policyin a supplied resource group and region. - Accepts up to two policy definitions, each naming the Storage accounts (by resource ID) or Storage aliases the subnet may reach.
- Carries
tagsand optional per-operationtimeouts— the universal tail on every module in this suite. - Emits the policy
idfor a subnet to consume through itsservice_endpoint_policy_ids.
💡 Why it matters: A bare
Microsoft.Storageservice endpoint on a subnet grants that subnet optimized-route access to all Storage in the region. A service-endpoint storage policy tightens that from "all storage" to a named allow-list, so a compromised or misconfigured workload cannot reach arbitrary Storage accounts over the service endpoint.
If this module saves you time, please consider supporting the work:
- ⭐ Star the repository on GitHub.
- 💼 Connect on LinkedIn — linkedin.com/in/microsoftexpert.
- ☕ Buy me a coffee — buymeacoffee.com/microsoftexpert.
flowchart LR
rg["terraform-azurerm-resource-group"]
sa["terraform-azurerm-storage-account"]
me["terraform-azurerm-subnet-service-endpoint-storage-policy"]
v["azurerm_subnet_service_endpoint_storage_policy"]
subnet["terraform-azurerm-subnet"]
rg -->|"resource_group_name"| me
sa -->|"service_resources"| me
me -->|"creates"| v
v -->|"service_endpoint_policy_ids"| subnet
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
class me me;
class v target;
class rg,sa,subnet ext;
A resource group supplies resource_group_name; a Storage account supplies the IDs that populate service_resources. This module creates the policy, and a subnet consumes the policy's id through service_endpoint_policy_ids.
flowchart LR
in_id["name / resource_group_name / location"]
in_def["definitions[] (service + service_resources)"]
in_tags["tags"]
res["azurerm_subnet_service_endpoint_storage_policy.this"]
out["id / name"]
in_id -->|"input"| res
in_def -->|"input"| res
in_tags -->|"input"| res
res -->|"output"| out
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
class res me;
Resource inventory
| Resource | Cardinality | Role |
|---|---|---|
azurerm_subnet_service_endpoint_storage_policy.this |
single (keystone) | The service-endpoint storage policy, including its inline definition blocks. |
definition (nested) |
up to 2 per policy | Each names the Storage accounts (by ID) or aliases the subnet may reach. |
The policy and its definition blocks share one lifecycle; the definitions are rendered from the definitions list with a dynamic block, so there is no separate child resource to manage.
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm", including the mandatory features {} block, plus auth, subscription, and region. |
Schema notes that bite
name,resource_group_name, andlocationare force-new — changing any of them destroys and recreates the policy.- At most two
definitionblocks are supported; a third is rejected at parse time by avalidation {}block. - Within a single definition,
service_resourcesmust be either Storage account resource IDs (withservice = "Microsoft.Storage") or aliases (withservice = "Global") — never a mix. Mixing IDs and aliases in one definition is rejected by the Azure API. serviceis validated to be"Microsoft.Storage"or"Global"when set; leaving it null defers to the provider default.
Least-privilege at the resource group scope:
Microsoft.Network/serviceEndpointPolicies/writeMicrosoft.Network/serviceEndpointPolicies/readMicrosoft.Network/serviceEndpointPolicies/delete
The built-in Network Contributor role covers these.
- An existing resource group (this module does not create one).
- The Storage account IDs (or aliases) the policy should permit — referenced by ID, never created here.
- The
Microsoft.Networkresource provider registered on the target subscription.
terraform-azurerm-subnet-service-endpoint-storage-policy/
├── providers.tf # required_version + azurerm ~> 4.0 pin; no provider block
├── variables.tf # typed inputs: name, resource_group_name, location, definitions, tags, timeouts
├── main.tf # keystone azurerm_subnet_service_endpoint_storage_policy.this + dynamic definition/timeouts
├── outputs.tf # id (first), name
├── README.md # this document
├── SCOPE.md # cross-module contract
├── LICENSE # MIT
└── .gitignore # canonical library ignore set
provider "azurerm" {
features {}
}
module "storage_endpoint_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "prod-storage-endpoint-policy"
resource_group_name = "rg-network-prod"
location = "eastus"
definitions = [{
name = "allow-prod-storage"
service = "Microsoft.Storage"
service_resources = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data-prod/providers/Microsoft.Storage/storageAccounts/prodsa",
]
}]
tags = {
environment = "prod"
owner = "platform-network"
}
}ℹ️ The caller owns the
provider "azurerm"block — includingfeatures {}, authentication, subscription, and region. This module never declares one.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group |
definitions[*].service_resources |
list(string) |
terraform-azurerm-storage-account (account IDs) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Resource ID of the service endpoint storage policy | terraform-azurerm-subnet / terraform-azurerm-virtual-network (service_endpoint_policy_ids) |
name |
Policy name | references |
1 · Minimal — a single Storage account allow-list
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "min-storage-policy"
resource_group_name = "rg-network-prod"
location = "eastus"
definitions = [{
name = "allow-single-account"
service = "Microsoft.Storage"
service_resources = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/appdata01",
]
}]
}💡 The smallest useful call: one definition, one Storage account. Any subnet that attaches this policy can reach only
appdata01over theMicrosoft.Storageservice endpoint.
2 · Multiple Storage accounts in one definition
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "multi-account-policy"
resource_group_name = "rg-network-prod"
location = "eastus2"
definitions = [{
name = "allow-app-storage"
service = "Microsoft.Storage"
service_resources = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/appdata01",
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/appdata02",
]
}]
}🔒 A single definition can list many account IDs — all must belong to the
Microsoft.Storageservice class.
3 · Global aliases instead of account IDs
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "alias-policy"
resource_group_name = "rg-network-prod"
location = "westus2"
definitions = [{
name = "allow-region-aliases"
service = "Global"
service_resources = ["/services/Azure/Batch", "/services/Azure/MachineLearning"]
}]
}
⚠️ When a definition uses aliases, itsservicemust be"Global"and every entry inservice_resourcesmust be an alias path — never a Storage account resource ID.
4 · Two definitions — IDs and aliases kept separate
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "split-policy"
resource_group_name = "rg-network-prod"
location = "eastus"
definitions = [
{
name = "allow-accounts"
service = "Microsoft.Storage"
service_resources = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/appdata01",
]
},
{
name = "allow-aliases"
service = "Global"
service_resources = ["/services/Azure/Batch"]
},
]
}💡 The two-definition limit is used here to keep account IDs and aliases in separate definitions — the correct pattern when you need both.
5 · With a definition description
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "documented-policy"
resource_group_name = "rg-network-prod"
location = "centralus"
definitions = [{
name = "allow-audited-storage"
description = "Restrict subnet egress to the audited data-lake accounts only."
service = "Microsoft.Storage"
service_resources = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-lake/providers/Microsoft.Storage/storageAccounts/datalake01",
]
}]
}ℹ️
descriptionis optional per definition and shows up in the Azure portal to explain the definition's intent.
6 · Tagged for cost and ownership
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "tagged-policy"
resource_group_name = "rg-network-prod"
location = "eastus"
definitions = [{
name = "allow-prod-storage"
service = "Microsoft.Storage"
service_resources = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/prodsa",
]
}]
tags = {
environment = "prod"
cost_center = "cc-4821"
owner = "platform-network"
managed_by = "terraform"
}
}💡 Every resource that supports
tagscarries them here; use them for cost allocation and ownership reporting.
7 · Referencing a Storage account module output
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "wired-policy"
resource_group_name = module.resource_group.name
location = "eastus"
definitions = [{
name = "allow-app-storage"
service = "Microsoft.Storage"
service_resources = [module.storage_account.id]
}]
}🔒 Prefer wiring
module.storage_account.idover hand-copying a resource ID — the plan then fails loudly if the account is renamed or removed.
8 · Several accounts from a for_each of Storage modules
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "fleet-policy"
resource_group_name = "rg-network-prod"
location = "eastus2"
definitions = [{
name = "allow-fleet-storage"
service = "Microsoft.Storage"
service_resources = [for sa in module.storage_accounts : sa.id]
}]
}💡 A
forexpression collapses a keyed map of Storage account modules into one allow-list, so the policy tracks the fleet automatically.
9 · Custom timeouts for slow control-plane operations
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "timed-policy"
resource_group_name = "rg-network-prod"
location = "eastus"
definitions = [{
name = "allow-prod-storage"
service = "Microsoft.Storage"
service_resources = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/prodsa",
]
}]
timeouts = {
create = "30m"
delete = "30m"
}
}ℹ️
timeoutsaccepts Go duration strings; leave it null to use provider defaults.
10 · Empty definitions — a placeholder policy
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "placeholder-policy"
resource_group_name = "rg-network-prod"
location = "eastus"
# definitions defaults to [] — the policy exists but restricts nothing yet.
}
⚠️ A policy with no definitions is a scaffold you fill in later. Attach it to a subnet only once it carries the definitions you intend to enforce.
11 · Multi-region — one policy per region
locals {
regions = {
east = { location = "eastus", account = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-east/providers/Microsoft.Storage/storageAccounts/easastsa" }
west = { location = "westus2", account = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-west/providers/Microsoft.Storage/storageAccounts/westsa" }
}
}
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
for_each = local.regions
name = "storage-policy-${each.key}"
resource_group_name = "rg-network-${each.key}"
location = each.value.location
definitions = [{
name = "allow-regional-storage"
service = "Microsoft.Storage"
service_resources = [each.value.account]
}]
}💡 The policy is a regional resource — instantiate one per region with
for_each, sincelocationis force-new and cannot be changed in place.
12 · Two definitions with descriptions and tags
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "full-policy"
resource_group_name = "rg-network-prod"
location = "eastus"
definitions = [
{
name = "allow-tier1-accounts"
description = "Tier-1 production Storage accounts."
service = "Microsoft.Storage"
service_resources = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/tier1a",
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/tier1b",
]
},
{
name = "allow-platform-aliases"
description = "Platform service aliases."
service = "Global"
service_resources = ["/services/Azure/Batch"]
},
]
tags = {
environment = "prod"
owner = "platform-network"
}
}🔒 This exercises the module's full breadth: both definitions (one IDs, one aliases), descriptions, and tags — all at once.
13 · 🏗️ End-to-end composition — resource group + storage account → policy → subnet
provider "azurerm" {
features {}
}
# 1. Resource group
module "resource_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-network-prod"
location = "eastus"
}
# 2. Storage accounts whose IDs the policy will permit
module "storage_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"
name = "prodappdata01"
resource_group_name = module.resource_group.name
location = module.resource_group.location
}
# 3. The service-endpoint storage policy — permits the account above plus a Global alias
module "storage_endpoint_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet-service-endpoint-storage-policy.git?ref=v1.0.0"
name = "prod-storage-endpoint-policy"
resource_group_name = module.resource_group.name
location = module.resource_group.location
definitions = [
{
name = "allow-prod-accounts"
service = "Microsoft.Storage"
service_resources = [module.storage_account.id]
},
{
name = "allow-platform-aliases"
service = "Global"
service_resources = ["/services/Azure/Batch"]
},
]
tags = {
environment = "prod"
owner = "platform-network"
}
}
# 4. A subnet that applies the policy via service_endpoint_policy_ids
module "subnet" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-subnet.git?ref=v1.0.0"
name = "snet-app"
resource_group_name = module.resource_group.name
virtual_network_name = "vnet-prod"
address_prefixes = ["10.10.1.0/24"]
service_endpoints = ["Microsoft.Storage"]
service_endpoint_policy_ids = [module.storage_endpoint_policy.id]
}💡 The full wiring: the resource group feeds
resource_group_nameandlocation, the Storage account feedsservice_resources, this module produces the policyid, and the subnet enables theMicrosoft.Storageservice endpoint and binds the policy — completing the least-exposure path from "all storage" to a named allow-list.
Identity & placement
| Name | Type | Required | Description |
|---|---|---|---|
name |
string |
✅ | Policy name. Force-new. |
resource_group_name |
string |
✅ | Resource group. Force-new. |
location |
string |
✅ | Azure region. Force-new. |
Policy definitions & universal tail
| Name | Type | Default | Description |
|---|---|---|---|
definitions |
list(object(...)) |
[] |
Up to two definitions; each names Storage account IDs (service = "Microsoft.Storage") or aliases (service = "Global"). |
tags |
map(string) |
{} |
Tags applied to the policy. |
timeouts |
object({ create, read, update, delete }) |
null |
Optional per-operation timeouts (Go duration strings). |
Full input schemas
variable "name" {
type = string
}
variable "resource_group_name" {
type = string
}
variable "location" {
type = string
}
variable "definitions" {
type = list(object({
name = string
service_resources = list(string)
service = optional(string)
description = optional(string)
}))
default = []
# length(var.definitions) <= 2
# service, when set, must be "Microsoft.Storage" or "Global"
}
variable "tags" {
type = map(string)
default = {}
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Kind |
|---|---|---|
id |
Resource ID of the service endpoint storage policy (emitted first) | Passthrough |
name |
Name of the service endpoint storage policy, as recorded in state | Passthrough |
resource_group_name |
Resource group holding the policy | Passthrough |
location |
Azure region of the policy, in the provider's normalized form (lower case, spaces removed) regardless of how it was written on input | Passthrough |
tags |
Tags applied to the policy | Passthrough |
definition_count |
Number of definition blocks on the policy, 0 to 2 | Passthrough |
definition_names |
Definition names in declaration order | Passthrough |
duplicate_definition_names |
Definition names used more than once, or an empty list | Passthrough |
service_by_definition |
Map of definition name to its service value (Microsoft.Storage for Resource-ID definitions, Global for alias definitions) |
Passthrough |
service_resources_by_definition |
Map of definition name to the deduplicated entries it permits | Passthrough |
service_resource_count_by_definition |
Map of definition name to how many distinct entries it permits | Derived |
all_service_resources |
Every distinct entry across all definitions -- the policy's complete allow list in one place, which is what a reviewer actually needs to see | Passthrough |
total_service_resource_count |
Number of distinct entries across all definitions | Passthrough |
storage_account_ids |
Entries that name an individual Storage account, matched against the exact Microsoft.Storage/storageAccounts type and anchored so a child resource ID under an account is not counted as the account itself |
Passthrough |
subscription_scoped_service_resources |
Entries that name a whole subscription (/subscriptions/<sub>) |
Passthrough |
resource_group_scoped_service_resources |
Entries that name a whole resource group | Passthrough |
management_group_scoped_service_resources |
Entries that name a management group | Passthrough |
alias_service_resources |
Entries that are service aliases | Passthrough |
grants_broader_than_a_single_storage_account |
Passthrough | |
definitions_with_duplicate_service_resources |
Definitions that list the same entry more than once, or an empty list | Passthrough |
default_service_resources_per_definition_limit |
200 | Passthrough |
definitions_at_or_over_default_service_resource_limit |
Definitions whose distinct entry count has reached 200, or an empty list | Passthrough |
force_new_inputs |
The inputs that force replacement rather than an in-place update | Derived |
effective_timeouts |
The timeouts actually in force, with the provider's own defaults substituted for anything left unset (create 30m, read 5m, update 30m, delete 30m) | Derived |
is_allow_list_not_deny_list |
Constant | |
takes_effect_only_when_attached_to_a_subnet |
Constant | |
requires_storage_service_endpoint_on_the_subnet |
Constant | |
attachment_upgrades_storage_endpoint_scope_to_global |
Constant | |
denies_all_storage_when_attached_with_no_definitions |
Passthrough | |
multiple_policies_on_a_subnet_union_their_allow_lists |
Constant | |
denies_classic_storage_accounts |
Constant | |
denies_managed_storage_accounts |
Constant | |
ra_grs_secondary_allowed_with_primary |
Always true | Constant |
must_share_region_and_subscription_with_the_virtual_network |
Constant |
- Force-new fields.
name,resource_group_name, andlocationall force replacement. Becauselocationis immutable, model a multi-region estate as one module instance per region rather than trying to move a policy. - Two-definition ceiling. Azure supports at most two
definitionblocks per policy; the module enforces this at parse time with avalidation {}block, so a third definition failsplan— notapply. - IDs vs. aliases are exclusive within a definition. A definition's
service_resourcesmust be all Storage account resource IDs (service = "Microsoft.Storage") or all aliases (service = "Global") — never both. When you need each, split them across the two available definitions (see Example 4). - Subnet wiring is external. This module owns only the policy. The subnet that enforces it is created elsewhere and points at this module's
idthroughservice_endpoint_policy_ids; the subnet must also enable theMicrosoft.Storageservice endpoint for the policy to take effect. features {}dependence. The policy is created against the caller'sprovider "azurerm", which will not initialize without afeatures {}block. That block, and all authentication, belong to the caller's root module.
| Concern | This module's default | How to change it |
|---|---|---|
| Definition count | Capped at 2, validated at parse time | Not adjustable — it is an Azure platform limit. |
service value |
Validated to "Microsoft.Storage" or "Global" when set |
Set the field explicitly per definition. |
| Storage exposure | Least-exposure — a subnet that attaches this policy reaches only the named accounts/aliases, not all regional Storage | List every account or alias the subnet legitimately needs. |
| Tags | Carried via the universal tail (tags, default {}) |
Pass a tags map. |
The empty call produces a policy that restricts everything (no definitions) rather than one that permits everything — the caller must type each account or alias it wants to allow.
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0 — never a branch. This module is authored plan-only; a human applies it from CI.
terraform init -backend=falseresolves the provider without a backend.terraform validateproves the type contract: a malformed definition, a third definition, or an out-of-rangeservicevalue fails here, before any Azure call.terraform fmt -checkenforces canonical formatting.- What only
terraform planexercises: resolution of real Storage account IDs and the runtime rejection of IDs mixed with aliases in one definition — neither is visible tovalidatealone.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod/providers/Microsoft.Network/serviceEndpointPolicies/prod-storage-endpoint-policy"
name = "prod-storage-endpoint-policy"| Symptom | Cause | Fix |
|---|---|---|
error: definition.service must be "Microsoft.Storage" or "Global" |
A definition set service to an unsupported value. |
Use "Microsoft.Storage" (for account IDs) or "Global" (for aliases). |
error: A maximum of two definitions is supported |
The definitions list has more than two entries. |
Consolidate into at most two definitions; group account IDs and aliases into one definition each. |
| API rejects the policy: mixed resources in a definition | A single definition's service_resources mixed Storage account IDs with aliases. |
Split IDs and aliases into two separate definitions — one Microsoft.Storage, one Global. |
| Subnet still reaches unlisted Storage over the endpoint | The policy is not attached, or the subnet lacks the Microsoft.Storage service endpoint. |
Set the subnet's service_endpoint_policy_ids to this module's id and enable Microsoft.Storage in service_endpoints. |
| Plan shows the policy being replaced after a rename | name, resource_group_name, or location changed — all force-new. |
Expect replacement; treat these as immutable, or create a new instance for a new region. |
- Provider resource:
azurerm_subnet_service_endpoint_storage_policy - Consuming resource:
azurerm_subnet(service_endpoint_policy_ids) - Sibling modules:
terraform-azurerm-subnet,terraform-azurerm-virtual-network,terraform-azurerm-storage-account,terraform-azurerm-resource-group - This module's cross-module contract:
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."

