Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Storage Sync Server Endpoint Terraform Module

The on-premises end of a sync group β€” a path on a registered server β€” targeting hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Risk


🧩 Overview

  • πŸ–§ 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.
  • ⚠️ name has 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_percent is sent even when tiering is off, and defaults to 20.
  • 🏷️ Carries no tags and no location.

πŸ’‘ 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.


❀️ Support this project

If this module saved you time:


πŸ—ΊοΈ Where this fits in the family

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
Loading

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.


🧬 What this module builds

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
Loading

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

βœ… Provider / Versions

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_percent is sent whether or not tiering is enabled, and defaults to 20. Setting it with tiering off is accepted and inert.
  • πŸ”΄ name has 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 timeouts tail from here to there loses the update key in silence, because that resource declares only three.
  • ⚠️ initial_download_policy is force-new while the other three policy arguments update in place.
  • ⚠️ server_local_path carries 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_days has 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_policy and local_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.

πŸ”‘ Required Azure RBAC Roles / Permissions

Least-privilege, at the smallest scope that works:

  • Contributor on the Storage Sync Service, or a custom role granting write and delete on Microsoft.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.


Azure Prerequisites

  • 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.StorageSync resource provider registered on the subscription.
  • The caller configures provider "azurerm" { features {} }, authentication, and the subscription.

πŸ“ Module Structure

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

βš™οΈ Quick Start

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 = false this is a replication β€” the server keeps everything. Turn tiering on and cold files move to Azure.


πŸ”Œ Cross-Module Contract

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

πŸ“š Example Library

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_enabled is false.

ℹ️ 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_percent is 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_server is 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 writes volume_free_space_percent unconditionally, so this is accepted and completely inert until tiering is turned on.

ℹ️ a_tiering_policy_is_configured_but_tiering_is_off is true. 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 names storage_sync_group_id so 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_endpoint is 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_policy is 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.

ℹ️ NamespaceOnly is 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_days is 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_name is 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_endpoint is 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.

πŸ”΄ tiering is true, so files will leave the server into hq-share. That share is now part of the live data path, not a copy of it.

⚠️ registered_server_id comes from a variable, because nothing in Terraform creates a registration β€” the agent is installed and registered on the server first.


πŸ“₯ Inputs

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
}

🧾 Outputs

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.


🧠 Architecture Notes

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.


🧱 Design Principles

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.


πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module with ?ref=v1.0.0 β€” never a branch. This module is plan-only from a workstation; a human applies from CI.


πŸ§ͺ Testing

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.


πŸ’¬ Example Output

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"]

πŸ” Troubleshooting

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

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."