A directory inside an Azure file share, with its metadata — targeting
hashicorp/azurerm ~> 4.0.
- 📁 Creates one directory inside an Azure file share, with its data-plane metadata.
- 🔴 It takes a data-plane URL, not an ARM Resource ID.
storage_share_urlishttps://<account>.file.core.windows.net/<share>— the opposite of nearly every other module in this library, which makes passing a Resource ID the natural mistake. This module refuses one by name. - 🔴 The provider will not catch that for you offline. Its own validator on this argument only
parses the URL properly once the provider has been configured;
terraform validatenever configures one, so a Resource ID, a wrong-cloud suffix and an unrelated host all pass the provider's check at validate. This module's check is the only one that fires offline. - 🔴 A deprecated alternative argument exists and is not exposed here.
storage_share_idis still accepted by the provider on this line and its own notice says it will be removed in version 5.0. - 🔴 The provider marks
storage_share_urlOptional on 4.x, not Required — it is Required only inside a 5.0 branch gated behind a feature helper that is off by default. Reading the base schema gives the wrong answer for the pinned line. - 🔴 A forward slash in
namebuilds a hierarchy, deliberately — soreports/2026/01is one record standing in for three directories, two of which Terraform does not own. ⚠️ These are DATA-PLANE calls against the file endpoint; an ARM role does not authorise them.⚠️ namemay not be only dots, and there is no maximum length in the provider's check.- 🏷️ Carries no
tagsand nolocation;metadatais data-plane metadata, not Azure tags.
💡 Why it matters: this is a two-argument resource whose first argument is the one thing this library never asks for anywhere else — a URL — and whose second can quietly describe a tree.
If this module saved you time:
- ⭐ Star the repository — it helps other people find it.
- 💼 Connect on LinkedIn — linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee — buymeacoffee.com/microsoftexpert
flowchart TB
RG["terraform-azurerm-resource-group"]
SA["terraform-azurerm-storage-account, whose shares map creates the share"]
SHARE["a file share, addressed here by its data-plane URL"]
THIS["terraform-azurerm-storage-share-directory"]
FILE["terraform-azurerm-storage-share-file"]
MID["the intermediate levels of a slashed name, NOT managed here"]
RBAC["terraform-azurerm-role-assignments, scoped to the ACCOUNT"]
RG -->|"name and location"| SA
SA -->|"shares map"| SHARE
SHARE -->|"URL, NOT an ARM Resource id"| THIS
THIS -.->|"a slashed name leaves these unowned"| MID
THIS -->|"path, so the directory exists before the file"| FILE
SA -->|"the ACCOUNT is the usable RBAC scope, not this record"| RBAC
RBAC -.->|"a file data role, or a shared key"| THIS
classDef me fill:#0078D4,stroke:#004578,stroke-width:2px,color:#ffffff
classDef target fill:#004578,stroke:#00243d,stroke-width:2px,color:#ffffff
classDef sib fill:#eef3f8,stroke:#9db4c9,color:#1a2733
class THIS me
class SHARE target
class RG,SA,FILE,MID,RBAC sib
The share arrives as a URL, not a Resource ID — and it is created through the storage-account module's
shares map, because this library has no standalone module for a file SHARE. The FILES in it are a
different matter: terraform-azurerm-storage-share-file owns those, and takes this module's name as
its path, which is why that edge is solid. The dotted edges are what nothing owns — the intermediate
levels of a slashed name — and the access path that makes the calls possible.
flowchart TB
URL["storage_share_url, force-new, a DATA-PLANE URL not a Resource id"]
NM["name, force-new, a slash builds a hierarchy"]
MD["metadata, the ONLY updatable argument"]
DEP["storage_share_id, DEPRECATED, removed in 5.0, NOT rendered here"]
THIS["azurerm_storage_share_directory.this"]
OURL["this_resource_takes_a_data_plane_url_not_a_resource_id"]
ODEP["a_deprecated_alternative_argument_is_not_exposed_here"]
ODEPTH["name_depth and parent_name"]
OMID["intermediate_directories_are_not_managed_by_this_module"]
ORENAME["renaming_this_directory_destroys_its_contents"]
URL --> THIS
NM --> THIS
MD --> THIS
DEP -.->|"deliberately omitted"| THIS
THIS --> OURL
THIS --> ODEP
THIS --> ODEPTH
THIS --> OMID
THIS --> ORENAME
classDef me fill:#0078D4,stroke:#004578,stroke-width:2px,color:#ffffff
classDef target fill:#004578,stroke:#00243d,stroke-width:2px,color:#ffffff
classDef sib fill:#eef3f8,stroke:#9db4c9,color:#1a2733
class THIS me
class OURL target
class URL,NM,MD,DEP,ODEP,ODEPTH,OMID,ORENAME sib
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_storage_share_directory |
1 (this) |
one directory; files are separate |
ℹ️ The Terraform ID is a data-plane identifier built from the file endpoint — not an ARM Resource ID, and not usable as the scope of a role assignment.
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module — the caller configures the provider, its authentication, and the mandatory features {} block |
| Resources created | 1 |
Schema notes that bite
- 🔴
storage_share_urlis a DATA-PLANE URL, not an ARM Resource ID. - 🔴 The provider's validator on it is weaker offline than it looks. It parses against the
configured cloud's storage domain suffix only after the provider is configured, and falls back to a bare
non-empty-path check until then — so at
terraform validatean Azure Resource ID, a wrong-cloud suffix andhttps://example.com/xare all accepted by the provider. The strict parse needs credentials. - 🔴 A deprecated
storage_share_idstill exists on this line, paired with the URL in anExactlyOneOf, and is removed in 5.0. This module renders only the replacement. - 🔴
storage_share_urlis Optional and Computed on 4.x, Required only in the provider's 5.0 branch — which is gated behind a feature helper that is off by default. ⚠️ ExactlyOneOftests PRESENCE, not truth. Rendering only the URL satisfies it; rendering both breaks it.- 🔴 A forward slash in
nameis legal and intended — the provider's source says so. A leading slash is refused. ⚠️ namemay not consist only of dots, and the forbidden characters are" \ : | < > * ?.- 🔴 A metadata key must match
^[a-z_][a-z0-9_]+$— lowercase, and at least two characters, soais rejected along withOwnerandowner-team. - 🔴 A metadata key may not be a C# keyword, matched case-insensitively —
class,default,object,string,event,lock,nullandthisare all refused. The provider enforces both rules; this module mirrors them so the message names the key. ⚠️ No maximum name length is set by the provider, so an over-long name fails at the service.- 🔴 Data-plane calls, and the Terraform ID cannot scope a role assignment.
- ✅ Force-new:
name,storage_share_url. Onlymetadataupdates in place. - ✅ A requires-import guard is present, feature-flag-wrapped; all four timeouts exist.
- No
tags, nolocation—metadatais data-plane metadata, which Azure Policy does not see.
Least-privilege, at the smallest scope that works:
- 🔴 These are DATA-PLANE calls.
Contributoron the account does not authorise them. Storage File Data SMB Share Contributoron the storage account, or a shared key — depending on which authentication the caller's provider configuration negotiates for the file endpoint.⚠️ Scope the role to the storage account, not to this module'sid.⚠️ Network reachability to the file endpoint — a firewall that excludes the caller consumes the whole timeout before failing.
🔓 This module handles no credential and emits none.
- An existing storage account and, in it, an existing file share — created through the account
module's
sharesmap. - Network reachability from the caller to the account's file endpoint.
- The caller configures
provider "azurerm" { features {} }, authentication, and the subscription.
terraform-azurerm-storage-share-directory/
├── providers.tf # required_version, pinned azurerm ~> 4.0, no provider block
├── variables.tf # 4 typed inputs, 9 validations, the universal timeouts tail
├── main.tf # the keystone `this`, dynamic timeouts
├── outputs.tf # 27 outputs: id first, then identity, then posture flags
├── README.md # this file
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT
└── .gitignore
module "reports_dir" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-share-directory.git?ref=v1.0.0"
# A URL -- NOT a Resource ID.
storage_share_url = "https://stgfiles01.file.core.windows.net/reports"
name = "2026"
}🔴 Passing
module.storage.idhere is refused, with a message saying which value is wanted — it is the mistake this module is most likely to catch.
Consumes
| Input | Type | Source |
|---|---|---|
storage_share_url |
string |
the shares map on terraform-azurerm-storage-account |
Emits
| Output | Description |
|---|---|
id |
Terraform ID — a data-plane identifier, not an RBAC scope (first) |
share_name / storage_account_name / share_host |
Computed from the URL |
name_depth / parent_name |
Computed — how many levels, and the one above |
this_resource_takes_a_data_plane_url_not_a_resource_id |
Constant true |
metadata_keys_are_lowercase_only_and_may_not_be_csharp_keywords |
Constant true — an unusually strict key rule |
the_providers_own_url_check_is_weaker_offline_than_it_looks |
Constant true — the provider's check does not fire at plan |
a_deprecated_alternative_argument_is_not_exposed_here |
Constant true |
1 · One directory at the share root
module "reports_dir" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-share-directory.git?ref=v1.0.0"
storage_share_url = "https://stgfiles01.file.core.windows.net/reports"
name = "2026"
}ℹ️
name_depthis1andparent_nameisnull— this directory sits directly in the share.
ℹ️
share_namereadsreportsandstorage_account_namereadsstgfiles01, both parsed from the URL.
2 · 🔴 The argument that is a URL, not a Resource ID
# REJECTED at plan:
storage_share_url = module.storage.id
# "/subscriptions/.../storageAccounts/stgfiles01"
# CORRECT -- the share's data-plane endpoint:
storage_share_url = "https://stgfiles01.file.core.windows.net/reports"🔴 Nearly every other module in this library takes an ARM Resource ID in the equivalent position, so reaching for one here is the natural instinct — and it is wrong.
✅ This module refuses a Resource ID with a message naming the right value. It is the probable mistake rather than an invented rule, and it is the single thing about this resource that differs from the rest of the library.
ℹ️ There is no standalone file share module in this library — shares are created through the storage-account module's
sharesmap, so the URL is assembled from the account name and the share name.
3 · 🔴 A deprecated alternative this module does not expose
# The provider ALSO accepts this on the pinned 4.x line:
# storage_share_id = "https://stgfiles01.file.core.windows.net/reports"
#
# Its own deprecation notice: "will be removed in version 5.0 of the Provider."🔴 The two arguments sit in an
ExactlyOneOfpair. This module renders onlystorage_share_url, which satisfies the rule — becauseExactlyOneOftests whether an argument is present, not what it contains — and means nothing here changes when the deprecated one disappears.
⚠️ Rendering both would break the pairing. A module that offered the caller a choice would have to render one conditionally, which is why this one simply does not offer it.
ℹ️
a_deprecated_alternative_argument_is_not_exposed_hereis emitted so the omission reads as a decision rather than an oversight.
4 · 🔴 The schema says Optional; the module says required
# The provider's base schema marks storage_share_url REQUIRED.
# The 4.x branch then OVERWRITES it as Optional + Computed, because of the
# deprecated pairing -- and that branch is the one in force on this pinned line.🔴 Reading the base schema gives the wrong answer for 4.x. The Required version lives inside the provider's 5.0 branch, which is gated behind a feature helper that is off by default.
✅ This module requires the argument of its caller regardless. Rendering neither would produce a resource with no share; and in 5.0 the argument becomes genuinely Required, which is what this module already assumes.
ℹ️
the_provider_marks_the_share_url_optional_on_this_lineis emitted so nobody comparing the module to the schema concludes it is gratuitously strict.
5 · 🔴 A slash builds a hierarchy — and leaves levels unmanaged
module "deep" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-share-directory.git?ref=v1.0.0"
storage_share_url = "https://stgfiles01.file.core.windows.net/reports"
name = "reports/2026/01"
}name_depth = 3
parent_name = "reports/2026"
🔴 The slash is deliberate, not tolerated. The provider's own source notes it does not forbid a slash in a non-leading segment, because that is how nested directories are addressed.
⚠️ That name describes three directories and this is one record. The two levels above are not owned by Terraform, so their metadata is whatever the service produced.
💡
name_depthandparent_nameare emitted so a review sees at a glance that one resource is standing in for a tree. Both ignore empty segments, soreports//2026/still gives a depth of 2 and a parent ofreports.
6 · Declaring each level
module "tree" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-share-directory.git?ref=v1.0.0"
for_each = toset(["reports", "reports/2026", "reports/2026/01"])
storage_share_url = "https://stgfiles01.file.core.windows.net/reports"
name = each.key
metadata = { managed_by = "terraform" }
}✅ Now every level is owned by Terraform, and each carries the metadata you intended.
⚠️ Terraform does not know these are nested. Nothing ordersreportsbeforereports/2026; adddepends_onat the call site if the service requires the parent to exist first.
💡 A
for_eachover atosetkeys each instance by its own path, so adding a level does not re-index the others.
7 · 🔴 A rename is a delete
# Before:
name = "2026"
# After -- this DESTROYS the directory and its contents, then creates a new one.
name = "2026-archive"🔴
nameis force-new. Azure Files supports renaming a directory as a service operation, but not through this resource — a change here is destroy-then-create.
⚠️ Whatever is inside goes with it, including files this module did not create.renaming_this_directory_destroys_its_contentsis emitted so that is visible in a review.
🔒 So delete rights on this module are delete rights on the contents, which the permissions section states.
8 · What the name may and may not be
# ACCEPTED:
name = "2026"
name = "reports/2026/01" # a hierarchy
name = "reports.2026" # dots are fine
name = ".hidden" # a leading dot is fine
name = "<300 characters>" # no length limit is set by the provider
# REJECTED at plan:
name = "." # only dots
name = ".." # only dots
name = "/reports" # a LEADING slash
name = "reports:2026" # forbidden character
name = "a|b" # forbidden character
⚠️ The forbidden set is" \ : | < > * ?, and a name made only of dots. Everything else the provider allows, this module allows.
🔴 There is no maximum length in the provider's check, so this module invents none — an over-long name fails at the service rather than at plan.
the_provider_sets_no_maximum_name_lengthrecords that.
9 · Metadata, and where it is not visible
module "reports_dir" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-share-directory.git?ref=v1.0.0"
storage_share_url = "https://stgfiles01.file.core.windows.net/reports"
name = "2026"
metadata = {
owner_team = "finance"
retention = "7y"
}
}
⚠️ This is data-plane metadata, NOT Azure resource tags. Azure Policy, cost reporting and tag inheritance do not see it — tag the storage account instead.
✅ It is the only argument that updates in place. Both others are force-new, so metadata is the whole of what an in-place change can touch.
10 · Sovereign clouds
module "gov_dir" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-share-directory.git?ref=v1.0.0"
storage_share_url = "https://stgfiles01.file.core.usgovcloudapi.net/reports"
name = "2026"
}✅ The domain suffix differs between clouds and this module does not hard-code one. The account name is taken from the host's leading label, which is the same shape in every cloud.
ℹ️
share_hostis emitted precisely because the suffix is what tells you which cloud a resource is in, and it appears nowhere else in the module's inputs.
11 · Adopting a directory that already exists
terraform import 'module.reports_dir.azurerm_storage_share_directory.this' \
"https://stgfiles01.file.core.windows.net/reports/2026"ℹ️ The import ID is a data-plane URL, consistent with the identifier this resource emits.
✅ A create refuses to overwrite an existing directory of the same name — the provider reads first and returns a requires-import error, wrapped in a caller-configured feature flag.
⚠️ Importing adopts the existing metadata. Declaring ametadatamap afterwards replaces it on the first apply.
12 · 🏗️ End-to-end composition
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-files"
location = "eastus"
}
# The share is created through the ACCOUNT module -- there is no standalone
# file share module in this library.
module "storage" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"
name = "stgfiles01"
resource_group_name = module.rg.name
location = module.rg.location
shares = {
reports = { name = "reports", quota = 100 }
}
}
module "reports_2026" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-share-directory.git?ref=v1.0.0"
# Composed from the account name and the share name -- NOT module.storage.id.
storage_share_url = "https://${module.storage.name}.file.core.windows.net/reports"
name = "2026"
metadata = { owner_team = "finance" }
}
# The role that makes the calls above possible -- scoped to the ACCOUNT.
module "files_rbac" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.storage.id
role_assignments = {
pipeline = {
role_definition_name = "Storage File Data SMB Share Contributor"
principal_id = var.pipeline_identity_object_id
}
}
}
output "share_layout" {
value = {
account = module.reports_2026.storage_account_name
share = module.reports_2026.share_name
directory = module.reports_2026.name
depth = module.reports_2026.name_depth
rbac_scope = module.storage.id
}
}🔴 The URL is composed, not taken from an output. The account module emits
idandname, and the share URL is built from the account's name plus the share's — because a file share here is a map entry rather than a module with its own outputs.
⚠️ rbac_scopeis the ACCOUNT. This module's ownidis a data-plane identifier and cannot scope a role assignment.
💡 The files inside this directory are a separate resource whose content is uploaded from wherever Terraform runs — this module creates directories only.
Target — storage_share_url (required by this module, force-new; a URL, not a Resource ID).
Identity — name (required, force-new; slashes build a hierarchy).
Optional — metadata, the only argument that updates in place.
Tail — timeouts. There is no tags argument and no location.
Full schemas
variable "storage_share_url" {
type = string # https://<account>.file.core.windows.net/<share>
# NOT an ARM Resource ID. The provider marks this Optional on
# 4.x because of a deprecated pairing; this module requires it.
}
variable "name" {
type = string # A slash BUILDS A HIERARCHY. Forbidden: " \ : | < > * ?
# and a name made only of dots. No maximum length.
}
variable "metadata" {
type = map(string) # data-plane metadata, NOT Azure resource tags.
default = {} # Keys: ^[a-z_][a-z0-9_]+$ -- lowercase, 2+ chars, and
} # NOT a C# keyword (class, default, object, string, ...)| Output | Description | Notes |
|---|---|---|
id |
Terraform ID — a data-plane identifier | first; not an RBAC scope |
name |
The directory name, which may contain slashes | |
storage_share_url |
The share URL, as configured | a URL, not an ID |
share_name |
Computed from the URL | |
storage_account_name |
Computed from the host's leading label | RBAC |
share_host |
Computed, including the cloud suffix | which cloud |
metadata |
Data-plane metadata | not Azure tags |
name_depth |
Computed — levels named | design review |
parent_name |
Computed — the level above, or null | composition |
this_resource_takes_a_data_plane_url_not_a_resource_id |
Constant true |
design |
metadata_keys_are_lowercase_only_and_may_not_be_csharp_keywords |
Constant true |
naming policy |
the_providers_own_url_check_is_weaker_offline_than_it_looks |
Constant true |
design rationale |
a_deprecated_alternative_argument_is_not_exposed_here |
Constant true |
v5.0 |
the_provider_marks_the_share_url_optional_on_this_line |
Constant true |
|
these_are_data_plane_calls_not_arm_calls |
Constant true |
access |
the_id_is_a_data_plane_identifier_not_an_rbac_scope |
Constant true |
RBAC |
a_forward_slash_in_the_name_builds_a_hierarchy |
Constant true |
|
intermediate_directories_are_not_managed_by_this_module |
Computed | design |
the_name_may_not_be_only_dots |
Constant true |
naming |
the_provider_sets_no_maximum_name_length |
Constant true |
naming |
force_new_fields |
Two | change planning |
updatable_fields |
Metadata alone | change planning |
renaming_this_directory_destroys_its_contents |
Constant true |
blast radius |
this_resource_creates_no_files |
Constant true |
composition |
create_refuses_an_existing_directory |
Constant true by default |
|
the_import_guard_can_be_disabled_by_a_provider_feature |
Constant true |
|
all_four_timeouts_are_honoured_here |
Constant true |
|
fields_azure_returns_on_read |
Where drift is detectable | drift |
this_resource_supports_no_azure_resource_tags |
Constant true |
tagging |
There is no secret on this resource, so nothing is redacted.
The first argument is the one thing this library never asks for anywhere else. storage_share_url is a
data-plane endpoint, not an ARM Resource ID, and nearly every other module here takes an ID in the
equivalent position. That makes reaching for module.storage.id the natural instinct and the wrong one, so
the module refuses a Resource ID explicitly and says which value it wants. There is also no standalone file
share module in this library — shares come from the storage-account module's shares map — so in practice
the URL is composed from the account name and the share name rather than read from an output.
The provider's schema reads backwards for the pinned line, and that is worth understanding rather than
working around. The base schema marks storage_share_url Required; a branch guarded by a
features.FivePointOh() helper then overwrites it as Optional and Computed and adds a deprecated
storage_share_id beside it, the two paired in an ExactlyOneOf. That helper is off by default, so the
Optional version is the one in force on 4.x. This module requires the argument of its caller regardless —
rendering neither would produce a resource with no share — and emits the discrepancy so nobody comparing the
module against the schema concludes it is gratuitously strict.
The deprecated argument is not exposed at all, per this suite's rule against deprecated names. That
works cleanly because ExactlyOneOf tests whether an argument is present rather than what it contains: a
module rendering only the replacement satisfies the pairing, and needs no change when the deprecated one is
removed in 5.0. Offering the caller a choice would mean rendering one conditionally, which is a lot of
machinery to support an argument that is going away.
A forward slash in the name is intended, and it is the quiet one. The provider's source explicitly
declines to forbid a slash in a non-leading segment, because that is how nested directories are addressed.
So reports/2026/01 is legal and describes three directories — of which this record is one. The other two
are not owned by Terraform, so their metadata is whatever the service produced. name_depth and
parent_name are computed and emitted to make that visible in a review rather than something a reader has
to notice by counting slashes, and both ignore empty segments so a stray slash does not distort them.
Almost nothing here updates. name and storage_share_url are both force-new, so metadata is the whole
of what an in-place change can touch — and a rename is a destroy-and-create that takes the directory's
contents with it, including files this module never created. Azure Files does support renaming a directory
as a service operation; this resource does not expose it.
| Concern | This module's position | Why |
|---|---|---|
| The share URL | a Resource ID is refused by name | it is the probable mistake, and unique to this resource in this library |
| The deprecated argument | not exposed at all | ExactlyOneOf tests presence, so the replacement alone satisfies it |
| Required-vs-Optional | required of the caller, and the discrepancy emitted | the Optional marking exists only because of the deprecated pairing |
| The account name | parsed from the host's leading label | the domain suffix differs by cloud; the label does not |
| Slashed names | name_depth / parent_name computed |
one record standing in for a tree should be visible |
| Maximum length | not invented | the provider sets none; the absence is emitted as a fact |
metadata |
named as not Azure tags | Policy and cost reporting do not see it |
| Metadata keys | lowercase, 2+ chars, no C# keywords | mirrors the provider's own check, so the message names the key |
🔒 The security-relevant control is delete: destroying this record, or changing
name, removes the directory and everything inside it.
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0 — never a branch. This module is plan-only from a workstation; a human
applies from CI.
terraform validate and terraform fmt -check are the offline proof gate. All 9 validation {} blocks fire
at plan. Each was exercised with a negative fixture that fires it and a positive fixture that does not,
driven from .tfvars files through terraform console — which, unlike terraform validate on a calling
configuration, does evaluate root-module variable validations. A parse guard runs first, because a fixture
that fails to parse reports exactly like a check that did not fire.
15 negative fixtures fired the checks they targeted, and 8 positive fixtures passed clean. The negatives
cover the share URL five ways — including 🔴 an ARM Resource ID, the mistake this module exists to
catch — and the name nine ways: empty, ., .., a leading slash, and each of the forbidden characters.
The positives prove the boundaries, and several pin behaviour rather than checking a rule:
- 🔴
reports/2026/01passes, because a slash builds a hierarchy — that is the provider's documented intent, not a leniency. - A 300-character name passes, because the provider sets no length limit and inventing one would reject legal input.
.hiddenandreports.2026pass, since only a name made entirely of dots is refused.- 🔴 A US Gov endpoint passes, proving the URL check does not hard-code the public-cloud domain suffix.
The computed outputs were driven through the same harness across four cases, one of which exists purely to
pin behaviour: reports//2026/ yields a depth of 2 and a parent of reports, so a doubled or trailing
slash does not distort either derived value. A sovereign-cloud case confirms the account name is taken from
the host's leading label rather than by stripping a known suffix.
What only a real plan/apply exercises: whether the account and share exist, whether the caller can reach
the file endpoint, and the requires-import guard.
id = "https://stgfiles01.file.core.windows.net/reports/2026"
name = "2026"
storage_share_url = "https://stgfiles01.file.core.windows.net/reports"
share_name = "reports"
storage_account_name = "stgfiles01"
share_host = "stgfiles01.file.core.windows.net"
name_depth = 1
parent_name = null
metadata = { "owner_team" = "finance" }
force_new_fields = ["name", "storage_share_url"]
updatable_fields = ["metadata"]
| Symptom | Cause | Fix |
|---|---|---|
storage_share_url is an Azure Resource ID |
module.storage.id was passed |
This argument takes a data-plane URL |
Contributor was not enough |
These are data-plane calls | Grant a file data role, or use a shared key |
| A role assignment scoped to this module failed | The ID is a data-plane identifier | Scope to the storage account |
| A rename deleted the contents | name is force-new |
It is a destroy-and-create, not a rename |
| The parent directory has no metadata | Intermediate levels are not managed here | Declare each level — see example 6 |
name consists only of dots |
. or .. |
Those are not usable directory names |
name begins with a slash |
A leading slash | The path is relative to the share root |
name contains a forbidden character |
One of `" \ : | < > * ?` |
| A long name passed validate and failed at apply | The provider sets no length limit | The file service does |
| A sovereign-cloud URL was expected to fail | The check does not hard-code a domain suffix | It works in every cloud |
| The apply hung then failed | A firewall excludes the caller from the file endpoint | Data-plane reachability, not RBAC |
| A create fails with a requires-import error | A directory of that name already exists | Import it — see example 11 |
azurerm_storage_share_directoryazurerm_storage_share_fileazurerm_storage_account- Naming and referencing shares, directories and files
- Sibling modules:
terraform-azurerm-storage-account,terraform-azurerm-role-assignments. Files inside a share are owned byterraform-azurerm-storage-share-file, which takes this module'snameoutput as itspath. - This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."