Registers an existing Azure Arc-enabled machine as the agent that performs a Storage Mover copy — targeting
hashicorp/azurerm ~> 4.0.
- 🖥️ Registers an existing Arc-enabled machine against a storage mover, with an optional description.
- 🔴 It registers a machine; it does not create one. Both Arc fields identify a Hybrid Compute machine that must already exist, already be Arc-enabled, and already be running the agent software. This module provisions no hardware, onboards nothing to Arc, and installs nothing.
- 🔴 Four of the five arguments are force-new. Only
descriptionupdates in place, so re-pointing an agent at a different machine is a destroy-and-create. - 🔴 The two Arc fields are both strings, so transposing them type-checks perfectly. One takes a
Microsoft.HybridCompute/machinesResource ID; the other takes that machine's SMBIOS UUID. Both are validated by shape here, and each message names the other field. - 🔴
namehas no pattern at all — only a not-empty check — while itsazurerm_storage_mover_projectsibling in the same service enforces a real regex. A name legal here can be illegal there. - 🔗 A job definition references this agent by NAME, not by Resource ID, so
namematters more thanidin a composition. - 🏷️ Carries no
tagsand nolocation, and exposes no computed attributes at all.
💡 Why it matters: this is a two-line resource with two easily-transposed opaque strings and a hard prerequisite it cannot see. Almost everything that goes wrong here goes wrong before Terraform is involved, so the module's job is to make the boundary explicit.
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"]
MV["terraform-azurerm-storage-mover"]
THIS["terraform-azurerm-storage-mover-agent"]
PRJ["terraform-azurerm-storage-mover-project"]
ARC["azurerm_arc_machine data source, an existing Arc-enabled machine"]
ONBOARD["Arc onboarding and the agent software, installed OUTSIDE Terraform"]
JOB["azurerm_storage_mover_job_definition"]
SRC["terraform-azurerm-storage-mover-source-endpoint"]
TGT["terraform-azurerm-storage-mover-target-endpoint"]
RG -->|"name and location"| MV
MV -->|"id"| THIS
MV -->|"id"| PRJ
MV -->|"id"| SRC
MV -->|"id"| TGT
ARC -->|"id and vm_uuid, both force-new here"| THIS
ONBOARD -.->|"must already be done; this module installs nothing"| ARC
PRJ -->|"id, as storage_mover_project_id"| JOB
THIS -.->|"name, as agent_name, NOT the Resource id"| JOB
SRC -.->|"name"| JOB
TGT -.->|"name"| JOB
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 MV target
class RG,PRJ,ARC,ONBOARD,JOB,SRC,TGT sib
Two edges carry the point of this module. The Arc machine arrives from a data source, not from a
sibling module — there is nothing in this library that creates one, and Arc onboarding plus the agent
software install happen entirely outside Terraform. And the job definition consumes this agent's name,
not its Resource ID, which is the opposite of how the project is consumed.
flowchart TB
MV["storage_mover_id, force-new"]
NM["name, force-new, NO pattern at all"]
AID["arc_virtual_machine_id, force-new, Microsoft.HybridCompute machines only"]
AUU["arc_virtual_machine_uuid, force-new, canonical 36-char UUID only"]
DESC["description, the ONLY updatable argument"]
THIS["azurerm_storage_mover_agent.this"]
ONM["name, which is what a job definition takes"]
OID["id"]
OSWAP["the_two_arc_fields_are_both_strings_so_a_swap_type_checks"]
OSUB["the_arc_machine_is_in_a_different_subscription"]
OREG["this_resource_registers_a_machine_it_does_not_create_one"]
MV --> THIS
NM --> THIS
AID --> THIS
AUU --> THIS
DESC --> THIS
THIS --> ONM
THIS --> OID
THIS --> OSWAP
THIS --> OSUB
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 ONM target
class MV,NM,AID,AUU,DESC,OID,OSWAP,OSUB,OREG sib
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_storage_mover_agent |
1 (this) |
a registration record; it creates no machine |
/subscriptions/SUB/resourceGroups/RG/providers/Microsoft.StorageMover/storageMovers/MOVER/agents/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
- 🔴 Force-new:
name,storage_mover_id,arc_virtual_machine_id,arc_virtual_machine_uuid— four of five. The update function tests exactlydescriptionand sends nothing else. All four are declared inline, so unlike the parent mover, searching the resource file finds the whole set. - 🔴
arc_virtual_machine_idmust be aMicrosoft.HybridCompute/machinesID. An Azure VM ID underMicrosoft.Compute/virtualMachinesis a different resource type and is rejected — an Arc-enabled server is not an Azure VM. - 🔴
arc_virtual_machine_uuidis parsed strictly: exactly 36 characters, hyphens at positions 8, 13, 18 and 23, hexadecimal elsewhere. Braces, aurn:uuid:prefix and the unhyphenated 32-character form are all rejected. - 🔴
namecarries no pattern, while the project and both endpoints share one regex — this resource is the only one of the mover's four children without a naming rule. None of them appears in the binary schema. ⚠️ A job definition names this agent throughagent_name— the name, not the ID.⚠️ The Arc machine is routinely in a different resource group or subscription from the mover.- ✅ The update is a read-modify-write, so an out-of-band change is preserved rather than reverted.
- ✅ A requires-import guard is present, feature-flag-wrapped.
- ✅ The delete polls for completion, so a destroy does not race a following create of the same name.
- ✅ Every argument is read back on refresh, so drift here is detectable.
- ✅ All four timeouts exist — 30m create, 5m read, 30m update, 30m delete.
Least-privilege, at the smallest scope that works:
Contributoron the storage mover, or a custom role granting write and delete onMicrosoft.StorageMover/storageMovers/agents. These are ARM management-plane calls.- 🔴 Read access in the Arc machine's subscription and resource group too, which are frequently not the mover's. A permission gap there tends to surface as a validation failure on the machine ID rather than as an access error.
⚠️ Registering an agent grants it nothing. What the agent may read and write is decided by the endpoints and by the agent's own identity on the machine.
🔓 This module handles no credential. The SMBIOS UUID identifies hardware; it does not authenticate to anything, which is why it is not marked sensitive.
- An existing storage mover.
- An existing Azure Arc-enabled machine, with the Storage Mover agent software installed and running. None of that happens here.
- The
Microsoft.StorageMoverandMicrosoft.HybridComputeresource providers registered on their respective subscriptions. - The caller configures
provider "azurerm" { features {} }, authentication, and the subscription.
terraform-azurerm-storage-mover-agent/
├── providers.tf # required_version, pinned azurerm ~> 4.0, no provider block
├── variables.tf # 6 typed inputs, 14 validations, the universal timeouts tail
├── main.tf # the keystone `this`, dynamic timeouts
├── outputs.tf # 30 outputs: id first, then identity, then posture flags
├── README.md # this file
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT
└── .gitignore
data "azurerm_arc_machine" "boston" {
name = "boston-host-01"
resource_group_name = "rg-onprem"
}
module "agent" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-mover-agent.git?ref=v1.0.0"
storage_mover_id = module.mover.id
name = "agent-boston-01"
# Both come from the SAME data source -- never type the UUID by hand.
arc_virtual_machine_id = data.azurerm_arc_machine.boston.id
arc_virtual_machine_uuid = data.azurerm_arc_machine.boston.vm_uuid
description = "Boston datacentre agent"
}💡 Wiring both fields from one data source removes the two most likely failures at once: a hand-copied UUID, and a UUID that belongs to a different machine than the ID does.
Consumes
| Input | Type | Source |
|---|---|---|
storage_mover_id |
string |
terraform-azurerm-storage-mover → id |
arc_virtual_machine_id |
string |
azurerm_arc_machine data source → id |
arc_virtual_machine_uuid |
string |
azurerm_arc_machine data source → vm_uuid |
Emits
| Output | Description |
|---|---|
id |
Resource ID of the registration (first) |
name |
What a job definition's agent_name takes — by name, not by id |
arc_machine_subscription_id |
Parsed; often not the mover's |
the_arc_machine_is_in_a_different_subscription |
Computed |
this_resource_registers_a_machine_it_does_not_create_one |
Constant true |
the_name_has_no_pattern_unlike_its_project_sibling |
Constant true |
1 · The recommended shape: both Arc fields from one data source
data "azurerm_arc_machine" "boston" {
name = "boston-host-01"
resource_group_name = "rg-onprem"
}
module "agent" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-mover-agent.git?ref=v1.0.0"
storage_mover_id = module.mover.id
name = "agent-boston-01"
arc_virtual_machine_id = data.azurerm_arc_machine.boston.id
arc_virtual_machine_uuid = data.azurerm_arc_machine.boston.vm_uuid
}💡 The data source exports
vm_uuid— described by the provider as the machine's unique SMBIOS ID — alongsideid. Taking both from one place means they cannot disagree about which machine you meant.
2 · 🔴 What this resource does not do
output "agent_readiness" {
value = {
registered = module.agent.id != null
# Constant true. Registration is not readiness.
creates_nothing = module.agent.this_resource_registers_a_machine_it_does_not_create_one
}
}🔴 A successful apply means the mover now knows about a machine. It does not mean the machine exists in any useful state, is powered on, has the Storage Mover agent installed, or can reach anything.
⚠️ Three things happen outside Terraform, in order: build the machine, onboard it to Azure Arc, and install the Storage Mover agent on it. Only then does this resource have something real to point at.
3 · 🔴 An Azure VM is not an Arc machine
# REJECTED at plan -- a different resource type entirely.
arc_virtual_machine_id = "/subscriptions/.../providers/Microsoft.Compute/virtualMachines/vm1"
# ACCEPTED -- Arc-enabled servers live under Microsoft.HybridCompute.
arc_virtual_machine_id = "/subscriptions/.../providers/Microsoft.HybridCompute/machines/boston-host-01"🔴 The provider validates this field against
Microsoft.HybridCompute/machinesspecifically, so an Azure VM ID is refused rather than accepted and failed later. This module mirrors that rule exactly.
ℹ️ The distinction is real rather than pedantic: an Arc-enabled server is a machine running anywhere — a datacentre, another cloud, a branch office — projected into Azure. A Storage Mover agent runs on one because the data being moved is usually not in Azure yet.
4 · 🔴 The two fields that swap silently
# BOTH of these are strings, so Terraform's type checker is perfectly happy.
arc_virtual_machine_id = "3bb2c024-eba9-4d18-9e7a-1d772fcc5fe9" # the UUID
arc_virtual_machine_uuid = "/subscriptions/.../machines/boston-host-01" # the ID🔴 A transposition type-checks. Nothing in the schema catches it, because both fields are plain strings — which is why this module validates each by shape and each error message names the other field.
ℹ️
the_two_arc_fields_are_both_strings_so_a_swap_type_checksis emitted so the hazard is visible in a review rather than only in an error message.
💡 Example 1 avoids the problem entirely rather than catching it.
5 · 🔴 The UUID parser is stricter than most
# ACCEPTED -- the canonical 36-character form Azure returns. Case does not matter.
arc_virtual_machine_uuid = "3bb2c024-eba9-4d18-9e7a-1d772fcc5fe9"
arc_virtual_machine_uuid = "3BB2C024-EBA9-4D18-9E7A-1D772FCC5FE9"
# ALL REJECTED at plan:
arc_virtual_machine_uuid = "{3bb2c024-eba9-4d18-9e7a-1d772fcc5fe9}" # braces
arc_virtual_machine_uuid = "urn:uuid:3bb2c024-eba9-4d18-9e7a-1d772fcc5fe9" # urn form
arc_virtual_machine_uuid = "3bb2c024eba94d189e7a1d772fcc5fe9" # unhyphenated
arc_virtual_machine_uuid = "3bb2c024-eba9-4d18-9e7a-1d772fcc5fe" # truncated🔴 The provider requires exactly 36 characters, with hyphens at positions 8, 13, 18 and 23 and hexadecimal digits everywhere else. Forms that other tools accept are refused here, and this module mirrors the rule exactly so the refusal happens offline rather than at apply.
⚠️ A well-formed UUID belonging to the wrong machine is caught by nothing. It plans cleanly and either fails at apply or registers an agent nobody meant. That is the failure example 1 removes.
6 · 🔴 The naming rule this resource does NOT share with its sibling
# All THREE are accepted here -- this resource applies no pattern to `name`.
name = "agent.boston.01"
name = "-agent"
name = "an-agent-name-longer-than-sixty-four-characters-aaaaaaaaaaaaaaaaaaa"
# All THREE are REJECTED by terraform-azurerm-storage-mover-project, whose regex is
# ^[0-9a-zA-Z][-_0-9a-zA-Z]{0,63}$ -- no dots, no leading separator, 64 characters max.🔴 This resource is the exception, not one half of a pair. It checks only that the name is not empty. The project, the source endpoint and the target endpoint all enforce the same regex — so of the mover's four children, three agree and this one differs, which is the worst shape for an assumption. None of the rules appears in the binary schema.
💡 The practical consequence is for whoever generates names. A convention that satisfies the other three satisfies this resource too, but not the reverse — so derive one convention from the regex and it works everywhere in the family.
ℹ️ Both modules emit the asymmetry —
the_name_has_no_pattern_unlike_its_project_siblinghere, andthe_name_rule_is_stricter_than_its_agent_siblingthere — so neither side implies a shared rule.
7 · Only the description updates
module "agent" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-mover-agent.git?ref=v1.0.0"
storage_mover_id = module.mover.id
name = "agent-boston-01"
arc_virtual_machine_id = data.azurerm_arc_machine.boston.id
arc_virtual_machine_uuid = data.azurerm_arc_machine.boston.vm_uuid
# The ONLY argument on this resource that updates in place.
description = "Boston datacentre agent -- phase 2"
}
⚠️ Four of the five arguments are force-new, so replacing the machine an agent points at is a destroy-and-create rather than an edit. Readforce_new_fieldsbefore planning a change.
ℹ️ The update is a read-modify-write: the provider fetches the agent, overwrites the description and PUTs the whole object back, so a property changed outside Terraform is preserved rather than reverted.
8 · When the machine is in another subscription
output "agent_topology" {
value = {
mover_subscription = module.agent.storage_mover_subscription_id
arc_subscription = module.agent.arc_machine_subscription_id
cross_subscription = module.agent.the_arc_machine_is_in_a_different_subscription
cross_rg = module.agent.the_arc_machine_is_in_a_different_resource_group
}
}ℹ️ Both are computed, and both are legal. The machine belongs with the infrastructure it is part of; the mover belongs with the migration programme. They are frequently not in the same place.
⚠️ The applying identity needs read access in BOTH. A permission gap on the machine tends to surface as a validation failure on the machine ID rather than as an access error, which sends people looking in the wrong direction.
💡 It also means a resource lock or policy on the mover's resource group does not protect the machine, and vice versa.
9 · Several agents on one mover
data "azurerm_arc_machine" "hosts" {
for_each = toset(["boston-host-01", "dublin-host-01"])
name = each.key
resource_group_name = "rg-onprem"
}
module "agents" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-mover-agent.git?ref=v1.0.0"
for_each = data.azurerm_arc_machine.hosts
storage_mover_id = module.mover.id
name = "agent-${each.key}"
arc_virtual_machine_id = each.value.id
arc_virtual_machine_uuid = each.value.vm_uuid
}💡 One agent per site is the usual shape, because an agent copies from the network it sits on.
✅ The
for_eachover the data source keeps each machine's ID and UUID together, so they cannot be mismatched across sites — the failure mode that a flat list of UUIDs invites.
10 · What the job definition actually consumes
resource "azurerm_storage_mover_job_definition" "archive" {
name = "archive-nightly"
storage_mover_project_id = module.project.id
copy_mode = "Additive"
source_name = azurerm_storage_mover_source_endpoint.nfs.name
target_name = azurerm_storage_mover_target_endpoint.blob.name
# The agent by NAME -- not module.agent.id.
agent_name = module.agent.name
}🔴
agent_nametakes the name. Passing the Resource ID is a string either way, so it is accepted at plan and fails at apply.
⚠️ Changing this agent's name does two things at once: it replaces the agent, and it breaks every job definition that named the old one.
ℹ️ Note the parentage while you are here — the job definition hangs off the project, while this agent and both endpoints hang off the mover.
11 · Adopting an agent that is already registered
terraform import 'module.agent.azurerm_storage_mover_agent.this' \
"/subscriptions/SUB/resourceGroups/RG/providers/Microsoft.StorageMover/storageMovers/mover-exit/agents/agent-boston-01"✅ A create refuses to overwrite an existing registration of the same name — the provider reads first and returns a requires-import error.
⚠️ That guard is wrapped in a provider feature flag the CALLER configures in its ownfeatures {}block, and a module cannot see or override it.
✅ Import recovers everything here, because every argument is read back from the service — the name, the mover, both Arc fields and the description. That is not true everywhere in this provider.
12 · 🏗️ End-to-end composition
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-migration"
location = "eastus"
}
module "mover" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-mover.git?ref=v1.0.0"
name = "mover-datacentre-exit"
resource_group_name = module.rg.name
location = module.rg.location
}
module "project" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-mover-project.git?ref=v1.0.0"
storage_mover_id = module.mover.id
name = "boston-exit"
}
# The machine already exists and is already Arc-enabled. Nothing here creates it.
data "azurerm_arc_machine" "boston" {
name = "boston-host-01"
resource_group_name = "rg-onprem"
}
module "agent" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-mover-agent.git?ref=v1.0.0"
storage_mover_id = module.mover.id
name = "agent-boston-01"
arc_virtual_machine_id = data.azurerm_arc_machine.boston.id
arc_virtual_machine_uuid = data.azurerm_arc_machine.boston.vm_uuid
description = "Boston datacentre agent"
}
output "migration_scaffold" {
value = {
mover = module.mover.name
project_id = module.project.id
agent_name = module.agent.name
cross_sub = module.agent.the_arc_machine_is_in_a_different_subscription
# Honest about what is still missing:
moves_data = !module.mover.this_resource_moves_no_data_by_itself
}
}🔴
moves_dataisfalse. There is still no source endpoint, no target endpoint and no job definition, so nothing copies — and this composition applies perfectly cleanly. That is what a half-built migration looks like.
⚠️ agent_nameis what the job definition will consume, which is why the output emitsmodule.agent.namerather than its ID.
💡 The agent is the piece Terraform cannot finish: the machine must be built, onboarded to Arc, and running the Storage Mover agent software before this registration means anything.
Parent — storage_mover_id (required, force-new).
Identity — name (required, force-new, no pattern).
The machine — arc_virtual_machine_id, arc_virtual_machine_uuid (both required, both force-new).
Optional — description, the only argument that updates in place.
Tail — timeouts. There is no tags argument and no location.
Full schemas
variable "arc_virtual_machine_id" {
type = string # Microsoft.HybridCompute/machines ONLY -- an Azure VM is rejected
}
variable "arc_virtual_machine_uuid" {
type = string # canonical 36-char UUID ONLY -- no braces, no urn:, no unhyphenated form
}
variable "name" {
type = string # not-empty is the provider's ONLY rule here -- the project and both
# endpoints share a regex this resource does not have
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Notes |
|---|---|---|
id |
Resource ID of the registration | first |
name |
What a job definition's agent_name takes |
composition |
storage_mover_id |
The parent mover, as configured | |
storage_mover_name |
Parsed from the mover ID | |
storage_mover_resource_group_name |
Parsed from the mover ID | |
storage_mover_subscription_id |
Parsed from the mover ID | |
arc_virtual_machine_id |
The Arc machine, as configured | |
arc_machine_name |
Parsed from the Arc machine ID | |
arc_machine_resource_group_name |
Parsed from the Arc machine ID | RBAC |
arc_machine_subscription_id |
Parsed from the Arc machine ID | RBAC |
arc_virtual_machine_uuid |
The SMBIOS UUID, as configured | not sensitive |
description |
Free text, or null | updatable |
this_resource_registers_a_machine_it_does_not_create_one |
Constant true |
design |
is_referenced_by_a_job_definition_by_name_not_by_id |
Constant true |
composition |
the_arc_machine_is_in_a_different_resource_group |
Computed | |
the_arc_machine_is_in_a_different_subscription |
Computed | access |
only_description_is_updatable |
Constant true |
change planning |
force_new_fields |
All four | change planning |
the_id_is_built_only_from_force_new_fields |
Constant true |
|
replacing_the_parent_mover_destroys_this_agent |
Constant true |
blast radius |
the_name_has_no_pattern_unlike_its_project_sibling |
Constant true |
naming |
the_two_arc_fields_are_both_strings_so_a_swap_type_checks |
Constant true |
|
the_uuid_should_be_read_from_the_arc_machine_not_typed |
Constant true |
|
create_refuses_an_existing_agent |
Constant true by default |
|
the_import_guard_can_be_disabled_by_a_provider_feature |
Constant true |
|
all_configured_fields_are_read_back_so_drift_is_detectable |
Constant true |
drift |
the_delete_polls_for_completion |
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.
Everything difficult about this resource happens before Terraform is involved. The agent runs on a machine that must be built, onboarded to Azure Arc, and running the Storage Mover agent software — none of which this resource does, and none of which it can see. What it writes is a registration record: the mover now knows a machine by ID and by SMBIOS UUID. A successful apply is therefore a much weaker statement than it looks, which is why the module says so as a constant output rather than leaving the reader to infer it.
The two Arc fields are the sharpest edge, and the type system offers no help. One is a Resource ID under
Microsoft.HybridCompute/machines, the other a UUID; both are strings, so transposing them is a perfectly
valid configuration as far as Terraform is concerned. Both are validated by shape here, and each message
names the other field, because "invalid format" would send someone to fix the wrong argument. The provider
is genuinely strict on both: an Azure VM ID under Microsoft.Compute is refused because an Arc-enabled
server is a different resource type, and the UUID parser accepts only the canonical 36-character form —
braces, a urn:uuid: prefix and the unhyphenated form are all rejected. Both rules are mirrored exactly
here, so the refusals happen at parse time.
Wiring the azurerm_arc_machine data source is worth more than any validation. It removes the failure
rather than catching it: both fields come from one lookup, so they cannot disagree about which machine was
meant, and nobody has to copy a UUID by hand. The distinction that matters is between a malformed UUID,
which is caught offline, and a well-formed UUID belonging to the wrong machine, which is caught by
nothing at all and either fails at apply or quietly registers an agent nobody intended.
The naming rule is where this family surprises people. This resource applies no pattern to name — only
a not-empty check — while azurerm_storage_mover_project, in the same service package, enforces
^[0-9a-zA-Z][-_0-9a-zA-Z]{0,63}$. Names with dots, names beginning with a hyphen, and names longer than 64
characters are all legal here and illegal there. Both behaviours are proven by fixtures on both sides, and
each module emits the asymmetry, because the natural assumption — that resources in one service share a
naming convention — is wrong here and produces a failure only on the next resource you write. It is worse
than a straight disagreement: the project and both endpoints share one regex and this resource alone has
none, so three agreeing examples make the exception look like the rule.
In a composition, name matters more than id. A job definition names this agent through agent_name,
so the name is the contract. That also makes a rename doubly disruptive: it replaces the agent, because
name is force-new, and it breaks whatever referenced the old one. Four of the five arguments are force-new
in total, so almost any change here is a destroy-and-create; only description updates in place, through a
read-modify-write that preserves properties changed outside Terraform rather than reverting them.
| Concern | This module's position | Why |
|---|---|---|
| The Arc machine | not created, and the fact emitted | the prerequisite is invisible from here and easy to assume away |
| The two Arc fields | validated by shape, each message naming the other | a transposition type-checks, so only shape catches it |
| The UUID rule | mirrored exactly | the provider's parser is strict; mirroring turns an apply failure into a parse one |
name |
provider rule mirrored, not shared with the other three children | cloning their regex here would reject legal input |
| Cross-subscription topology | computed and reported | legal and common; it changes what the applying identity must read |
| The SMBIOS UUID | not marked sensitive | it identifies hardware and authenticates to nothing |
name vs id |
name documented as the composition output |
the job definition consumes the name |
🔒 There is no credential on this resource. The security-relevant point is scope: registering an agent grants it nothing, and what it may read and write is decided elsewhere entirely.
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 14 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.
19 negative fixtures fired the checks they targeted, and 7 positive fixtures passed clean. The negatives
cover the mover ID four ways, all three name rules, the Arc machine ID four ways — empty, an Azure VM ID, a
storage mover ID, and a bare UUID in the swap position — and the UUID six ways, including braces, the
urn:uuid: form, the unhyphenated 32-character form, a truncated value and a non-hex digit, each of which
the provider's own parser also rejects.
The positives prove the naming asymmetry deliberately and in both directions: agent.boston.01, -agent
and a 65-character name all pass here, and the same three values are negative fixtures in
terraform-azurerm-storage-mover-project, where they fire. An upper-case UUID also passes, because the
parser is case-insensitive on hex.
The computed outputs were driven through the same harness rather than assumed: the parsed mover and machine names, and both the cross-resource-group and cross-subscription flags, checked in a same-place case and a different-place case.
What only a real plan/apply exercises: whether the mover exists, whether the Arc machine exists, and the
requires-import guard.
What nothing exercises: whether the UUID belongs to the machine the ID names. That pairing is why the data source is the recommended wiring rather than a convenience.
id = ".../storageMovers/mover-exit/agents/agent-boston-01"
name = "agent-boston-01"
storage_mover_name = "mover-exit"
arc_machine_name = "boston-host-01"
arc_machine_resource_group_name = "rg-onprem"
arc_virtual_machine_uuid = "3bb2c024-eba9-4d18-9e7a-1d772fcc5fe9"
the_arc_machine_is_in_a_different_resource_group = true
the_arc_machine_is_in_a_different_subscription = true
force_new_fields = ["name", "storage_mover_id", "arc_virtual_machine_id", "arc_virtual_machine_uuid"]
| Symptom | Cause | Fix |
|---|---|---|
| The agent registers but copies nothing | Registration is not readiness | Build, Arc-onboard and install the agent software on the machine |
must be an Azure Arc machine Resource ID |
An Azure VM ID was passed | Arc-enabled servers live under Microsoft.HybridCompute, not Microsoft.Compute |
arc_virtual_machine_id looks like a UUID |
The two Arc fields were transposed | Both are strings, so only this check catches it |
must be a canonical 36-character UUID |
Braces, a urn: prefix, or an unhyphenated value |
The provider's parser accepts only the canonical form |
| Apply fails on a well-formed UUID | The UUID belongs to a different machine | Read vm_uuid from the same data source as the ID |
| A name legal here is rejected on a project | The two resources have different name rules | Derive both from the project's stricter regex |
| A job definition cannot find the agent | agent_name was given the Resource ID |
It takes the name |
| A rename planned a replacement | name is force-new |
Expected; it also breaks job definitions naming the old one |
| Changing the machine planned a replacement | Both Arc fields are force-new | Only description updates in place |
| A validation error names the machine, but access is the problem | The Arc machine is in another subscription | Grant the applying identity read access there |
| The agent vanished after an unrelated change | The parent mover was replaced | Replacing the mover destroys every child |
| A create fails with a requires-import error | An agent of that name is already registered | Import it — see example 11 |
azurerm_storage_mover_agentazurerm_arc_machinedata sourceazurerm_storage_mover_job_definition- Azure Storage Mover agent overview
- Azure Arc-enabled servers
- Sibling modules:
terraform-azurerm-storage-mover,terraform-azurerm-storage-mover-project,terraform-azurerm-resource-group - This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."

