The on-premises end of a sync group β a path on a registered server β targeting
hashicorp/azurerm ~> 4.0.
- π§ Syncs a path on an already-registered server against the group's cloud endpoint.
- π΄ Cloud tiering moves files OFF the server. With it on, cold files are replaced by pointers and the bytes live in the Azure file share β a relocation of data, not a replication. It defaults to false.
- π΄ The server must already be registered, by installing the Azure File Sync agent on the server. That never happens through Azure Resource Manager, and no module in this library does it.
- π΄ This resource and the cloud endpoint in the same group share almost nothing β a typed provider resource against a legacy one, four updatable arguments against none, four timeouts against three.
β οΈ namehas no validator at all β not a pattern, not a length bound, not even a not-empty check β while the cloud endpoint's name has a real regex.- β One cross-field rule IS checkable offline and is enforced: the registered server and the sync group must belong to the same Storage Sync Service.
β οΈ volume_free_space_percentis sent even when tiering is off, and defaults to 20.- π·οΈ Carries no
tagsand nolocation.
π‘ Why it matters: the tiering switch is a boolean that decides whether your server keeps its files. Everything else here is plumbing; that one argument changes where the data physically is.
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"]
SVC["terraform-azurerm-storage-sync"]
GRP["terraform-azurerm-storage-sync-group"]
THIS["terraform-azurerm-storage-sync-server-endpoint"]
CLOUD["terraform-azurerm-storage-sync-cloud-endpoint"]
REG["a REGISTERED SERVER, agent installed and registered ON the server"]
PATH["a path on that server, never reached from here"]
FILES["the files themselves, which TIERING moves off the server"]
RG -->|"name and location"| SVC
SVC -->|"id"| GRP
GRP -->|"id"| THIS
GRP -->|"id"| CLOUD
SVC -->|"id, the server registers with the SERVICE not the group"| REG
REG -->|"registered_server_id, checked to share a service with the group"| THIS
THIS -.->|"server_local_path, stored verbatim"| PATH
THIS -.->|"cloud tiering ON replaces cold files with pointers"| FILES
CLOUD -.->|"where the tiered bytes actually live"| FILES
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 FILES target
class RG,SVC,GRP,CLOUD,REG,PATH sib
The registered server enters from the Storage Sync Service, not from the group β one registration carries endpoints in several groups. And the dotted edge into the files is the one that matters: with tiering on, they leave the server for the share the cloud endpoint points at.
flowchart TB
GRP["storage_sync_group_id, force-new"]
NM["name, force-new, NO provider validator at all"]
REG["registered_server_id, force-new, must share a service with the group"]
PATH["server_local_path, force-new, never reached"]
TIER["cloud_tiering_enabled, UPDATABLE, default false"]
POL["volume_free_space_percent and tier_files_older_than_days, UPDATABLE"]
IDP["initial_download_policy, FORCE-NEW unlike the others"]
LCM["local_cache_mode, UPDATABLE"]
THIS["azurerm_storage_sync_server_endpoint.this"]
OMOVE["cloud_tiering_moves_files_off_the_server"]
OSTAGED["a_tiering_policy_is_configured_but_tiering_is_off"]
ODIFF["this_resource_and_the_cloud_endpoint_share_almost_nothing"]
OREG["the_server_must_already_be_registered"]
GRP --> THIS
NM --> THIS
REG --> THIS
PATH --> THIS
TIER --> THIS
POL --> THIS
IDP --> THIS
LCM --> THIS
THIS --> OMOVE
THIS --> OSTAGED
THIS --> ODIFF
THIS --> OREG
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 OMOVE target
class GRP,NM,REG,PATH,TIER,POL,IDP,LCM,OSTAGED,ODIFF,OREG sib
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_storage_sync_server_endpoint |
1 (this) |
one path on one registered server |
/subscriptions/SUB/resourceGroups/RG/providers/Microsoft.StorageSync/storageSyncServices/SERVICE/syncGroups/GROUP/serverEndpoints/NAME
| 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
- π΄ Cloud tiering moves files off the server, replacing them with pointers. It defaults to false and updates in place, so turning it on is an edit rather than a replacement. Turning it off does not automatically recall tiered files.
- π΄
volume_free_space_percentis sent whether or not tiering is enabled, and defaults to 20. Setting it with tiering off is accepted and inert. - π΄
namehas NO validator at all. The cloud endpoint in the same group validates its name with a real regex. - π΄ This resource and the cloud endpoint share almost no behaviour. Copying a
timeoutstail from here to there loses theupdatekey in silence, because that resource declares only three. β οΈ initial_download_policyis force-new while the other three policy arguments update in place.β οΈ server_local_pathcarries only a not-empty check β nothing verifies the path, its filesystem, or whether it already syncs elsewhere.β οΈ The registered server hangs off the SERVICE, not the group.β οΈ tier_files_older_than_dayshas an upper bound of the maximum 32-bit integer β mirrored, not narrowed β and the provider sends it only when set.- β
Both enums are case-sensitive:
initial_download_policyandlocal_cache_mode. - β The delete polls, and the create uses a custom poller that waits for the endpoint to come up.
- β A requires-import guard is present, feature-flag-wrapped.
- β All four timeouts exist β 30m create, 5m read, 30m update, 30m delete.
Least-privilege, at the smallest scope that works:
Contributoron the Storage Sync Service, or a custom role granting write and delete onMicrosoft.StorageSync/storageSyncServices/syncGroups/serverEndpoints. ARM management-plane calls.β οΈ No permission on the server is involved, and none can be granted from Azure. The server's side of the trust comes from its registration, performed locally with the agent.- π΄ Deleting a server endpoint stops the sync for that path. It does not delete data on the server β but with tiering enabled, files already tiered exist there only as pointers, so removing the endpoint leaves the server without the data those pointers referenced. That is what withholding delete protects.
π This module handles no credential and emits none.
- An existing sync group with a cloud endpoint already in it.
- A registered server β the Azure File Sync agent installed and registered from the server, against the same Storage Sync Service the group belongs to.
- The path on that server, existing and on a supported filesystem.
- The
Microsoft.StorageSyncresource provider registered on the subscription. - The caller configures
provider "azurerm" { features {} }, authentication, and the subscription.
terraform-azurerm-storage-sync-server-endpoint/
βββ providers.tf # required_version, pinned azurerm ~> 4.0, no provider block
βββ variables.tf # 10 typed inputs, 20 validations, a four-key timeouts tail
βββ main.tf # the keystone `this`, dynamic timeouts
βββ outputs.tf # 36 outputs: id first, then identity, then posture flags
βββ README.md # this file
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore
module "server_endpoint" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync-server-endpoint.git?ref=v1.0.0"
storage_sync_group_id = module.sync_group.id
name = "hq-server"
registered_server_id = var.registered_server_id # registered ON the server
server_local_path = "D:\\shares\\documents"
# Tiering defaults to false: the server keeps a full copy.
}π΄ With
cloud_tiering_enabled = falsethis is a replication β the server keeps everything. Turn tiering on and cold files move to Azure.
Consumes
| Input | Type | Source |
|---|---|---|
storage_sync_group_id |
string |
terraform-azurerm-storage-sync-group β id |
registered_server_id |
string |
an existing registered server β no module creates one |
β These two are checked against each other β both must name the same Storage Sync Service.
Emits
| Output | Description |
|---|---|
id |
Resource ID of the server endpoint (first) |
cloud_tiering_is_enabled |
Computed β whether files leave the server |
cloud_tiering_moves_files_off_the_server |
Constant true |
a_tiering_policy_is_configured_but_tiering_is_off |
Computed |
updatable_fields |
The four that change in place |
this_resource_and_the_cloud_endpoint_share_almost_nothing |
Constant true |
1 Β· A replication, with tiering off
module "server_endpoint" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync-server-endpoint.git?ref=v1.0.0"
storage_sync_group_id = module.sync_group.id
name = "hq-server"
registered_server_id = var.registered_server_id
server_local_path = "D:\\shares\\documents"
}β Tiering defaults to false, so the server keeps a full local copy and Azure holds a synced replica.
cloud_tiering_is_enabledisfalse.
βΉοΈ That is the conservative starting point, and a reasonable one: tiering can be turned on later without replacing the endpoint.
2 Β· π΄ What tiering actually does
module "tiered_endpoint" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync-server-endpoint.git?ref=v1.0.0"
storage_sync_group_id = module.sync_group.id
name = "hq-server"
registered_server_id = var.registered_server_id
server_local_path = "D:\\shares\\documents"
cloud_tiering_enabled = true
volume_free_space_percent = 40
tier_files_older_than_days = 30
}π΄ Cold files leave the server. They are replaced by pointers, and the bytes live in the Azure file share the cloud endpoint names. The namespace stays, so the directory still looks complete.
β οΈ volume_free_space_percentis the proportion kept FREE, so a higher number tiers more data to Azure, not less. 40 means the server keeps 60% of the volume in use at most.
π΄ This is not a backup. A tiered file exists in one place. Losing the share loses the data, and deleting this endpoint leaves the server holding pointers to bytes it no longer has.
βΉοΈ
cloud_tiering_moves_files_off_the_serveris emitted as a constant because none of this is visible in a plan.
3 Β· A policy staged ahead of the switch
module "staged" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync-server-endpoint.git?ref=v1.0.0"
storage_sync_group_id = module.sync_group.id
name = "hq-server"
registered_server_id = var.registered_server_id
server_local_path = "D:\\shares\\documents"
# The policy is set; the switch is not.
cloud_tiering_enabled = false
volume_free_space_percent = 40
}
β οΈ The provider writesvolume_free_space_percentunconditionally, so this is accepted and completely inert until tiering is turned on.
βΉοΈ
a_tiering_policy_is_configured_but_tiering_is_offistrue. It is reported, not refused, because staging a policy ahead of the switch is a legitimate thing to do β and so is forgetting the switch. Only the author knows which this is.
β Both are updatable, so the switch is flipped later with an edit rather than a replacement.
4 Β· β The cross-field rule that IS enforced
# REJECTED at plan -- the server is registered with a DIFFERENT service.
module "mismatched" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync-server-endpoint.git?ref=v1.0.0"
storage_sync_group_id = module.sync_group.id # .../storageSyncServices/sync-hq/syncGroups/...
name = "hq-server"
registered_server_id = var.branch_server_id # .../storageSyncServices/sync-branch/registeredServers/...
server_local_path = "D:\\shares\\documents"
}β This one can be checked offline, so it is. Both IDs carry
/storageSyncServices/<name>/, and a server registered with one service cannot host an endpoint in another service's group β the combination is structurally impossible, not merely unusual.
βΉοΈ The rule is placed on
registered_server_id, because a validation may reference its own variable plus others only one way. Its message namesstorage_sync_group_idso you know which pair to compare β which is why the error mentions an argument you may not have been editing.
π‘ It is one of very few cross-field rules in this family that is checkable at all. Most β the share/account pairing on the cloud endpoint, the region requirement β are not.
5 Β· π΄ The name rule this resource does not have
# ALL accepted here -- the provider applies NO validator to `name`:
name = "hq_server.01"
name = "HQ Server"
name = "a"
# This module refuses only what is certainly wrong:
name = " " # REJECTED: whitespace
name = " hq " # REJECTED: never trimmed by the provider
name = "/subscriptions/x/y" # REJECTED: a Resource IDπ΄ The provider validates this field not at all β no pattern, no length bound, not even a not-empty check. This module adds only the checks for values that cannot be a name.
β οΈ The cloud endpoint in the same sync group has a real regex on its name. Two children of one group, and only one of them has a naming rule β so a convention cannot be assumed to travel between them.
βΉοΈ
the_name_has_no_validator_unlike_the_cloud_endpointis emitted so the asymmetry is visible in a review rather than discovered on the other resource.
6 Β· The two policy enums
# initial_download_policy -- what happens ONCE, at creation. FORCE-NEW.
initial_download_policy = "NamespaceThenModifiedFiles" # default
initial_download_policy = "NamespaceOnly" # namespace only; files arrive on open
initial_download_policy = "AvoidTieredFiles" # skip files already tiered
# local_cache_mode -- ONGOING behaviour. Updatable.
local_cache_mode = "UpdateLocallyCachedFiles" # default
local_cache_mode = "DownloadNewAndModifiedFiles"
# REJECTED -- both are compared case-sensitively:
initial_download_policy = "namespaceonly"
local_cache_mode = "updatelocallycachedfiles"
β οΈ initial_download_policyis force-new and the others are not, which is consistent with what it describes β an initial download happens once β but it does mean changing your mind replaces the endpoint and restarts the sync.
βΉοΈ
NamespaceOnlyis the light-touch choice on a large share: the server gets the directory structure immediately and pulls file contents as they are opened.
7 Β· The free-space policy, and which direction it runs
volume_free_space_percent = 1 # keep 1% free -- almost nothing is tiered
volume_free_space_percent = 20 # the provider's default
volume_free_space_percent = 90 # keep 90% free -- most data lives in Azure
# REJECTED at plan:
volume_free_space_percent = 0 # the provider's range starts at 1
volume_free_space_percent = 101
β οΈ It is the proportion kept FREE, so a high value tiers MORE. That reads backwards to most people the first time, which is why the module's description and error message both say it explicitly.
βΉοΈ
tier_files_older_than_daysis an additional lever, not a replacement: leave it unset and only free space drives tiering. Its upper bound is the maximum 32-bit integer, which this module mirrors rather than narrowing β inventing a shorter ceiling would reject a legal value.
β οΈ Both are updatable, so a tiering policy can be tightened or relaxed without touching the endpoint.
8 Β· π΄ The registration this module cannot perform
variable "registered_server_id" {
type = string
description = "Registered by installing the Azure File Sync agent ON the server."
}π΄ A registered server is created from the server, by installing the Azure File Sync agent and registering it with the Storage Sync Service. There is no ARM call for it, no Terraform resource, and no module in this library.
β οΈ It hangs off the SERVICE, not the sync group β.../storageSyncServices/<service>/registeredServers/<id>β so one registration carries endpoints in several groups of that service.
βΉοΈ The ID's last segment is a server-assigned identifier rather than a hostname, so it is not something to recognise at a glance.
registered_server_nameis emitted to have it to hand.
β The service-match rule in example 4 is the one thing about the registration this module can check.
9 Β· What replaces the endpoint and what does not
output "server_endpoint_change_planning" {
value = {
force_new = module.server_endpoint.force_new_fields
updatable = module.server_endpoint.updatable_fields
}
}force_new = ["name", "storage_sync_group_id", "registered_server_id",
"server_local_path", "initial_download_policy"]
updatable = ["cloud_tiering_enabled", "volume_free_space_percent",
"tier_files_older_than_days", "local_cache_mode"]
β The tiering switch itself is updatable, which is the useful part: tiering can be turned on or off without replacing the endpoint or restarting the sync.
β οΈ Turning tiering off does not automatically recall tiered files. The setting stops further tiering; recall is a separate operation on the server.
π΄ Re-pointing at a different path replaces the endpoint, which stops and restarts the sync for that path.
10 Β· π΄ Copying a timeouts tail from the cloud endpoint
# Legal HERE -- this resource has an update function:
timeouts = {
create = "30m"
read = "5m"
update = "30m"
delete = "30m"
}
# Paste that same block into terraform-azurerm-storage-sync-cloud-endpoint and
# the `update` key VANISHES -- no error, no warning, no plan diff.π΄ The cloud endpoint in the same sync group declares only THREE timeouts, because every one of its arguments is force-new and it has no update function. Terraform's object-type conversion silently discards a key the declared type does not have.
β οΈ The two modules look like siblings and are not, which is what makes this paste the most available mistake across the pair.the_timeouts_block_has_four_keys_here_and_three_on_the_cloud_endpointis emitted from this side and the matching fact from the other.
βΉοΈ The create here uses a custom poller that waits for the endpoint to come up, so an apply can take noticeably longer than the resource's small surface suggests.
11 Β· Several paths on one server
locals {
synced_paths = {
documents = "D:\\shares\\documents"
finance = "D:\\shares\\finance"
}
}
module "server_endpoints" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync-server-endpoint.git?ref=v1.0.0"
for_each = local.synced_paths
storage_sync_group_id = module.sync_group.id
name = "hq-${each.key}"
registered_server_id = var.registered_server_id
server_local_path = each.value
cloud_tiering_enabled = true
}
β οΈ Two endpoints in the SAME group both sync against the same cloud endpoint, which is rarely what you want for unrelated paths β a group is the unit of "these things are the same data". Separate paths usually belong in separate groups, each with its own file share.
π΄ Nothing checks whether a path is already syncing in another group. That conflict surfaces on the server, not at apply.
βΉοΈ One registration serves them all, because the registered server belongs to the service.
12 Β· ποΈ End-to-end composition
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-sync"
location = "eastus"
}
module "sync_service" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync.git?ref=v1.0.0"
name = "sync-hq"
resource_group_name = module.rg.name
location = module.rg.location
}
module "sync_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync-group.git?ref=v1.0.0"
storage_sync_id = module.sync_service.id
name = "hq-documents"
}
module "storage" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"
name = "stgsynchq01"
resource_group_name = module.rg.name
location = module.rg.location # same region as the service
shares = {
hq = { name = "hq-share", quota = 100 }
}
}
module "cloud_endpoint" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync-cloud-endpoint.git?ref=v1.0.0"
storage_sync_group_id = module.sync_group.id
name = "hq-cloud"
storage_account_id = module.storage.id
file_share_name = "hq-share"
}
# THE SERVER SIDE -- this module. The registration already exists.
module "server_endpoint" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-sync-server-endpoint.git?ref=v1.0.0"
storage_sync_group_id = module.sync_group.id
name = "hq-server"
registered_server_id = var.registered_server_id
server_local_path = "D:\\shares\\documents"
cloud_tiering_enabled = true
volume_free_space_percent = 40
}
output "file_sync" {
value = {
group = module.sync_group.name
share = module.cloud_endpoint.file_share_name
server_path = module.server_endpoint.server_local_path
tiering = module.server_endpoint.cloud_tiering_is_enabled
staged = module.server_endpoint.a_tiering_policy_is_configured_but_tiering_is_off
}
}β This is a complete Azure File Sync deployment β service, group, share, cloud endpoint and server endpoint. It is the whole sync sub-family.
π΄
tieringistrue, so files will leave the server intohq-share. That share is now part of the live data path, not a copy of it.
β οΈ registered_server_idcomes from a variable, because nothing in Terraform creates a registration β the agent is installed and registered on the server first.
Parent β storage_sync_group_id (required, force-new; the group, not the service).
Identity β name (required, force-new; no provider validator at all).
The server β registered_server_id (required, force-new, checked to share a service with the
group), server_local_path (required, force-new).
Tiering β cloud_tiering_enabled (default false), volume_free_space_percent (default 20),
tier_files_older_than_days β all updatable.
Policies β initial_download_policy (force-new), local_cache_mode (updatable).
Tail β timeouts, with four keys. There is no tags argument and no location.
Full schemas
variable "cloud_tiering_enabled" {
type = bool # UPDATABLE. true means cold files LEAVE the server.
default = false
}
variable "volume_free_space_percent" {
type = number # 1-100. The proportion kept FREE, so higher tiers MORE.
default = 20 # Sent to the service even when tiering is off.
}
variable "initial_download_policy" {
type = string # FORCE-NEW, case-sensitive:
default = "NamespaceThenModifiedFiles"
# | "NamespaceOnly" | "AvoidTieredFiles"
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
# FOUR keys here. The cloud endpoint in the same group has THREE.
})
default = null
}| Output | Description | Notes |
|---|---|---|
id |
Resource ID of the server endpoint | first |
name |
The endpoint's name | no provider validator |
storage_sync_group_id |
The parent group | |
sync_group_name |
Parsed from the group ID | |
storage_sync_id |
Computed grandparent service | the scope both IDs share |
storage_sync_name |
Parsed from the group ID | |
resource_group_name |
Parsed from the group ID | |
subscription_id |
Parsed from the group ID | |
registered_server_id |
The server, as configured | |
registered_server_name |
Computed last segment | |
the_registered_server_and_the_group_share_a_service |
Constant true β enforced |
|
server_local_path |
The synced path, verbatim | |
cloud_tiering_enabled |
Whether files leave the server | security |
volume_free_space_percent |
What drives tiering | |
tier_files_older_than_days |
Age threshold, or null | |
initial_download_policy |
One-off behaviour | force-new |
local_cache_mode |
Ongoing behaviour | updatable |
cloud_tiering_moves_files_off_the_server |
Constant true |
security |
cloud_tiering_is_enabled |
Computed | security |
the_free_space_policy_is_sent_even_when_tiering_is_off |
Constant true |
|
a_tiering_policy_is_configured_but_tiering_is_off |
Computed | |
the_server_must_already_be_registered |
Constant true |
prerequisite |
nothing_here_reaches_the_server |
Constant true |
design |
the_registered_server_hangs_off_the_service_not_the_group |
Constant true |
|
the_name_has_no_validator_unlike_the_cloud_endpoint |
Constant true |
naming |
this_resource_and_the_cloud_endpoint_share_almost_nothing |
Constant true |
design |
the_timeouts_block_has_four_keys_here_and_three_on_the_cloud_endpoint |
Constant true |
caller correctness |
force_new_fields |
Five, including the download policy | change planning |
updatable_fields |
Four, including the tiering switch | change planning |
the_id_is_built_only_from_force_new_fields |
Constant true |
|
replacing_the_parent_group_destroys_this_endpoint |
Constant true |
blast radius |
create_refuses_an_existing_server_endpoint |
Constant true by default |
|
the_import_guard_can_be_disabled_by_a_provider_feature |
Constant true |
|
the_delete_polls_for_completion |
Constant true |
|
all_four_timeouts_are_honoured_here |
Constant true |
|
this_resource_supports_no_azure_resource_tags |
Constant true |
tagging |
There is no secret on this resource, so nothing is redacted.
One boolean here decides whether the server keeps its files. With cloud_tiering_enabled on, cold files
are removed from the server's disk and replaced by pointers; the namespace stays intact, so the directory
still looks complete, and the bytes live in the Azure file share the group's cloud endpoint names. That is
the point of Azure File Sync for most deployments and it is also a genuine relocation of data rather than a
replication β a tiered file exists in one place, so the share is part of the live data path and not a copy
of it. None of that is visible in a plan, which is why the module emits it as a constant alongside a
computed flag for the current state.
The switch is updatable and the policy is sent regardless, which produces one legal-but-inert
combination. volume_free_space_percent is written unconditionally by the provider, so configuring a
tiering policy while tiering is off is accepted and does nothing until the switch is flipped. That is
reported rather than refused, because staging a policy ahead of the change is a reasonable thing to do β and
so is forgetting the switch, and only the author knows which. The direction of that percentage is worth
repeating too: it is the proportion of the volume kept free, so a higher number tiers more data away,
which reads backwards the first time.
This resource and the cloud endpoint in the same group look like a pair and share almost nothing. One is
a typed provider resource, the other legacy. This one has four updatable arguments; that one has none. This
one declares four timeouts; that one declares three, because it has no update function at all. Copying a
timeouts tail between them therefore fails in one direction completely silently β Terraform's object-type
conversion drops a key the declared type does not have, with no error and no plan diff. Both modules emit
the difference, because a paste between two modules that look like siblings is the most available mistake
across the pair.
Almost nothing about the two IDs this resource takes can be checked β except one thing, which is
enforced. Both registered_server_id and storage_sync_group_id carry the Storage Sync Service in their
path, and a server registered with one service structurally cannot host an endpoint in another's group. That
makes it a configuration rule whose violation is impossible rather than merely unusual, so it is refused at
validate rather than reported. It is placed on registered_server_id, since a validation may reference its
own variable plus others only one way, which is why its message names an argument the caller may not have
been editing. It is one of very few cross-field rules in this family that is checkable at all β the cloud
endpoint's share/account pairing and its region requirement are not.
What this module cannot do at all is the part people expect it to. The registered server is created by
installing the Azure File Sync agent on the server and registering it β there is no ARM call for that, no
Terraform resource, and no module here. The path is stored verbatim: nothing checks that it exists, that its
filesystem is supported, or that it is already syncing in another group. And name has no provider
validator whatsoever, while the cloud endpoint's does β three different naming behaviours inside one small
family, which is why each module states its own rather than implying a shared convention.
| Concern | This module's position | Why |
|---|---|---|
| Cloud tiering | defaults false, and the consequence emitted as a constant | it relocates data, and nothing in a plan says so |
| Policy without the switch | reported, not refused | inert and legal; staging it is legitimate |
| The service-match rule | ENFORCED, placed on registered_server_id |
the mismatch is structurally impossible, and it is checkable offline |
name |
only certainly-wrong shapes refused | the provider imposes nothing; the cloud endpoint's regex would reject legal input |
tier_files_older_than_days |
upper bound mirrored, not narrowed | a shorter ceiling would refuse a legal value |
| The timeouts difference | emitted from both modules | the paste between look-alike siblings is silent in one direction |
| The registration | named as out of reach | it happens on the server, and no module can perform it |
π The security-relevant control is delete. Removing a server endpoint stops the sync β and where tiering is on, the server holds pointers rather than files, so the data those pointers referenced is no longer reachable from the server.
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 20 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.
20 negative fixtures fired the checks they targeted, and 10 positive fixtures passed clean. The negatives cover the group ID three ways including the Storage Sync Service's ID passed as the group's, three name rules, the registered server ID three ways, the local path two ways, both numeric ranges at both ends, and both enums in a wrong-value and a wrong-case form.
π΄ One negative exists for the cross-field rule: a registered server belonging to sync-branch paired
with a group belonging to sync-hq, which fires the service-mismatch check. Its needle had to be kept short
because the console wraps the message between "Storage" and "Sync" β the real message was printed before the
needle was shortened, to confirm the right check had fired.
The positives prove the boundaries: volume_free_space_percent at 1 and at 100,
tier_files_older_than_days at 1, both non-default values of each enum, a name containing a dot and
an underscore β shapes the cloud endpoint would refuse β and a tiering policy set while tiering is
off, which is reported rather than refused.
The computed outputs were driven through the same harness: the parsed service and registered-server names, and the tiering and staged-policy flags across all three combinations of switch and policy.
What only a real plan/apply exercises: whether the group, its cloud endpoint and the registered server
exist, and the requires-import guard.
What nothing exercises: whether the path exists on the server, whether its filesystem is supported, or whether it is already syncing in another group.
id = ".../syncGroups/hq-documents/serverEndpoints/hq-server"
name = "hq-server"
sync_group_name = "hq-documents"
storage_sync_name = "sync-hq"
server_local_path = "D:\\shares\\documents"
cloud_tiering_enabled = true
volume_free_space_percent = 40
tier_files_older_than_days = null
initial_download_policy = "NamespaceThenModifiedFiles"
local_cache_mode = "UpdateLocallyCachedFiles"
cloud_tiering_is_enabled = true
a_tiering_policy_is_configured_but_tiering_is_off = false
updatable_fields = ["cloud_tiering_enabled", "volume_free_space_percent", "tier_files_older_than_days", "local_cache_mode"]
| Symptom | Cause | Fix |
|---|---|---|
| Files vanished from the server | Cloud tiering replaced cold files with pointers | Expected; check cloud_tiering_is_enabled |
| More data tiered than expected | volume_free_space_percent is the proportion kept FREE |
A higher number tiers more |
| A tiering policy had no effect | cloud_tiering_enabled is false; the policy is sent but inert |
Check a_tiering_policy_is_configured_but_tiering_is_off |
| Turning tiering off did not bring files back | The setting stops further tiering; it does not recall | Recall is a separate operation on the server |
belong to DIFFERENT Storage Sync Services |
The server is registered with another service | Compare the /storageSyncServices/<name> segment of both IDs |
volume_free_space_percent must be between 1 and 100 |
0 or above 100 | The provider's range starts at 1 |
tier_files_older_than_days must be 1 or greater |
0 was passed | Omit the argument to leave the policy unset |
| An enum was rejected for case | Both enums are compared exactly | NamespaceOnly, not namespaceonly |
| Changing the download policy planned a replacement | initial_download_policy is force-new |
The other three policy arguments are not |
An update timeout worked here but vanished elsewhere |
The cloud endpoint declares only three timeouts | See example 10 |
| Apply fails on the registered server | It is not registered, or the ID is stale | Registration happens on the server, not in Azure |
| The path conflicts with another sync | Nothing checks whether a path already syncs | That surfaces on the server, not at apply |
azurerm_storage_sync_server_endpointazurerm_storage_sync_cloud_endpointazurerm_storage_sync_group- Azure File Sync cloud tiering
- Registering a server with Azure File Sync
- Sibling modules:
terraform-azurerm-storage-sync,terraform-azurerm-storage-sync-group,terraform-azurerm-storage-sync-cloud-endpoint - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."