Skip to content

Spoke JSON Reference

One JSON file per spoke (one workload in one spoke environment), stored under <env-folder>/spokes/.

  • Schema: src/Xebia.Mcp/Files/json/spokeschema.json (JSON Schema draft-07).
  • Consumer: src/Xebia.Mcp/Files/terraform/spoke-canvas/deploy/spoke/ (locals.tf, main.tf).

For the walkthrough of a new file, see New spoke. Settings that are shared by all spokes (tenant, regions, IPAM pools, PIM profiles) are in Spoke common settings.

Req = required by the schema. Used = read by the Terraform. Fields that are in the schema but not used are marked no effect.

Top level

Field Type Req Description
workloadShortName string, max 6 yes Short workload code. Used in resource names, group names and the state key. Do not change it after creation.
environment string yes dev, tst, test, acc, acc<n>, prd, prod, shrd, shared. The pipeline groups them into the stages dev / tst / acc / prd / shared, but names and the state key use the raw value. Do not change it after creation.
azureSubscriptionId string yes Existing subscription ID (see New spoke).
azureSubscriptionName string yes The subscription is renamed to this. If empty, SpokeCommon.tenantinfo.SubsciptionNamingTemplate is used.
managementGroupId string yes Management group name (not the full ID), for example cloudplatform-landingzones-corp. Must be an id in <env-folder>/<targetRegions.primary>/foundation.auto.tfvars (config test).
subType string no No effect.
resourceGroup.name string/null no Overrides the resource group name rg-canvas-<wl>-<env>-<region>-001.
tags object yes Required keys: owner, department, workload, pii (yes/no), costcentre, environment. Extra keys are allowed. Applied to the subscription and the canvas resources, plus spoke_canvas_version. The deployment also adds a reserved tag hidden-XebiaSpokeMetadata with JSON-encoded metadata from the spoke file. Do not put that tag in the file.
adInfo object yes Identity, see below.
targetRegions.primary string yes Region short name, for example weu.
targetRegions.additional array yes Extra regions ([] for none).
azdo object no Creates an Azure DevOps repo and service connection. Omit the key entirely to skip this; null fails the schema. See Spoke integrations.
extensions object no The schema allows only apim, acr, servicebus, eventhub, each as { "enabled": bool }. They use the shared services in mcp.json SpokeCommon.shared_services (what each extension sets up). The Terraform also reads extensions.github (see Spoke integrations) and extensions.integrations, but the schema of some versions rejects them. servicebus.enabled: true needs group-name keys that the schema rejects, so it cannot pass the config test today (Known issues).
resourceconfiguration object yes Resources, see below.

adInfo

Field Type Req Description
ProductTeamGroupNames string[] yes Display names of existing Entra ID groups that get access through the spoke groups.
PIMGroupConfig.Readers / .Contributors / .Owners enum yes nopim = active membership, self-approve = PIM-eligible without approval, approval = PIM-eligible with approval. The profiles are defined in pim_configurations (common settings). The Support group is not configurable: the deployment logic always sets it (RBAC and PIM).
ad_OwnersMembers, ad_OwnersMembersUPN string[] (e-mail / UPN) no Initial owners of the spoke groups. Together with the deployment identity and the spoke SPN, they become owners of all four spoke groups, at creation only (ignore_changes).
ad_ReadersMembers, ad_ReadersMembersUPN array no Become members of the Readers group, at creation only.
ad_EngineersMembers, ad_EngineersMembersUPN array no No effect (read, but not applied to any group).
PIMAssignments object no Per-spoke override of the RBAC role map, keyed by exact Azure role name, with levels and/or pim_profile. See RBAC and PIM for spokes.
SPNGraphPermissions string[] no (not in schema) Microsoft Graph app roles granted to the spoke service principal.
SupportManagementInfo object no No effect (not read by the Terraform).

resourceconfiguration.network[]

Field Type Req Description
name string yes VNet name. Placeholders ##wlshortname##, ##environment## and ##region## are replaced.
region string yes Region short name of this VNet.
addressPrefix string[] (CIDR) yes, unless ipamPool is used VNet address space, for example 10.20.4.0/23. No overlap check. Mutually exclusive with ipamPool.
ipamPool string no Name of an IPAM pool from networking.ipam_pools in the common settings. Use instead of addressPrefix. See below.
numberOfIpAddresses number no Size of the block to allocate from the pool, for example 256 for a /24. Only with ipamPool. Default 256.
peerHub bool yes Create the virtual hub connection (default true).
dnsServerOverwrite string yes "" = use the hub DNS server of the region. Otherwise an IP.
InternetSecurityEnabled bool no (not in schema) internet_security_enabled on the hub connection (default true: internet via the hub firewall).
overwritePeeringConnectionName string no Overrides the hub connection name (default: VNet name with vnet- → con-).
useIPAM bool no No effect.
subnets[] array yes See below.
networksecuritygroups[] array yes { name, rules[] }. An NSG is referenced by subnets with the same name. Don't change the priority of an existing rule (it is recreated, which can fail): add a new rule with a new name and priority, and remove the old rule later. Each rule requires name, priority (string), direction, access, protocol (*, Tcp, Udp, Icmp, Ah, Esp), source_port_range, destination_port_range, source_address_prefix, destination_address_prefix. Rule names must be unique and say what the rule is for. priority is unique per NSG, from 100 to 4096 (lower is higher priority). Ports are 1 to 65535, a range (2048-2055) or a list (80,443). Address prefixes are a CIDR, a range, or a service tag.
routetables[] array yes { name, routes[] }. A route is { name, properties: { addressPrefix, nextHopType, nextHopIpAddress } }. nextHopType is one of VirtualNetworkGateway, VnetLocal, Internet, VirtualAppliance, None.

subnets[]

Field Type Req Description
name string yes Placeholders are replaced.
addressPrefix string (CIDR) yes, unless an IPAM pool is used Must lie within the VNet address space. Mutually exclusive with ipamPool and numberOfIpAddresses.
ipamPool string no Only needed when the subnet must come from a different pool than its VNet. Subnets inherit the pool of the VNet.
numberOfIpAddresses number yes for a subnet that uses a pool Size of the subnet, for example 16 for a /28. A subnet has no default.
isprivatelinksubnet bool yes Subnet intended for Private Endpoints. Each network[] entry needs at least one subnet with true; the first one is used for the spoke's Key Vault and storage private endpoints. Designate one subnet for this purpose.
privateEndpointNetworkPoliciesEnabled bool yes true → Enabled. Otherwise RouteTableEnabled for private link subnets, else Disabled.
privateLinkServiceNetworkPoliciesEnabled bool no
serviceEndpoints [{ "service": "<name>" }] yes For example Microsoft.Storage, Microsoft.KeyVault.
delegations [{ "name", "actions": [] }] yes name from the schema list (66 values, for example Microsoft.Web/serverFarms, Microsoft.App/environments, Microsoft.DBforPostgreSQL/flexibleServers).
linkedroutetablename string/null yes Name of a route table in routetables[], or null. Recent versions need no extra routes, so a route table should be the exception.
linkednetworksecuritygroupname string no Name of an NSG in networksecuritygroups[]. Empty → a default NSG is created and associated.

Address space from an IPAM pool

Instead of hardcoding address prefixes, a spoke can ask an Azure Virtual Network Manager IPAM pool for address space. Only the pool name and the size are configured. Azure picks the addresses.

"network": [
  {
    "name": "vnet-##wlshortname##-##environment##-##region##-001",
    "region": "weu",
    "ipamPool": "dev-weu",
    "numberOfIpAddresses": 512,
    "peerHub": true,
    "dnsServerOverwrite": "",
    "subnets": [
      {
        "name": "snet-##wlshortname##-default-##environment##-##region##-001",
        "isprivatelinksubnet": false,
        "numberOfIpAddresses": 256,
        "privateEndpointNetworkPoliciesEnabled": false,
        "serviceEndpoints": [],
        "delegations": [],
        "linkedroutetablename": null,
        "linkednetworksecuritygroupname": ""
      }
    ],
    "networksecuritygroups": [],
    "routetables": []
  }
]

Rules:

  • The pool name must exist in networking.ipam_pools of the common settings. Otherwise the plan fails and lists the available names.
  • addressPrefix and ipamPool exclude each other, for a VNet and for a subnet.
  • A VNet without numberOfIpAddresses gets 256 addresses. Every subnet has to state its own size.
  • A subnet inherits the pool of its VNet. Asking for numberOfIpAddresses under a VNet with a hardcoded addressPrefix fails the plan, because there is no pool to allocate from.
  • The addresses are not known when you write the file. Write NSG rules and routes against service tags or the VirtualNetwork tag, not against literal CIDRs.
  • The allocated ranges are in the deployment output: networking.virtualnetwork.<vnet>.resVirtualNetworkAddressSpace and resAllSubnets.

Optional resource blocks

If a block is absent, the defaults in the Terraform apply. Storage account, Key Vault and Log Analytics are deployed by default. If a block is present, all its schema-required keys must be present.

Block Required keys when present Notes
storageaccount EnablePublicAccess, CreatePrivateEndpoint, AccountReplicationType, AccessTier (Cool/Hot/Premium), AccountKind, AccountTier, infrastructure_encryption_enabled, allow_nested_items_to_be_public Optional: deploy (default true), name, PrivateEndpointList (blob, queue, file, table, dfs, web), EnableSAS, Containers. A storage account forces a Key Vault (customer-managed key).
keyvault enablepublicaccess, createprivateendpoint, EnableRBACAuthorization (must be true), SKUName, SoftDeleteRetentionInDays, EnabledForDeployment, EnabledForDiskEncryption, EnabledForTemplateDeployment Optional: deploy (default true), name.
loganalytics deploy, sku, RetentionInDays, AllowResourceOnlyPermissions Optional: name. forwardactivitylog has no effect.
networkwatcher deploy Default false.
privatedns – name: creates a private DNS zone linked to the hub DNS resolver VNet. null = none.

Defaults and notes

Setting Default or note
Storage account EnablePublicAccess false. true should be an exception.
Storage account / Key Vault CreatePrivateEndpoint true. false should be an exception.
AccountReplicationType, AccessTier, AccountTier ZRS, Hot, Standard
PrivateEndpointList Lowercase names only. One private endpoint per listed subtype.
Containers List of container names to create in the storage account (for example one for Terraform state).
Key Vault SoftDeleteRetentionInDays 30. Allowed 7 to 90.
Key Vault deploy: false No Key Vault and no private endpoint, and no service principal secret is stored. Ignored (forced to true) while the storage account is deployed.
Storage account deploy: false No storage account and no private endpoints.
loganalytics.deploy true. false should be an exception.
Network watcher One per region when enabled, in a dedicated resource group, not in the canvas resource group.

azdo

Field Description
projectName Existing Azure DevOps project
repoName Repository to create (##wlshortname## and ##environment## are replaced)
serviceConnectionName Azure RM service connection to create for the spoke SPN (##wlshortname## and ##environment## are replaced)
useFederatedCredentials true = workload identity federation instead of a secret. No secret is generated or stored in the Key Vault, and a federated credential is created on the app registration. Default false. Not in the schema of some versions, read by Terraform.

What the deployment does with this block, and the GitHub equivalent: Spoke integrations.