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_poolsof the common settings. Otherwise the plan fails and lists the available names. addressPrefixandipamPoolexclude each other, for a VNet and for a subnet.- A VNet without
numberOfIpAddressesgets 256 addresses. Every subnet has to state its own size. - A subnet inherits the pool of its VNet. Asking for
numberOfIpAddressesunder a VNet with a hardcodedaddressPrefixfails 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
VirtualNetworktag, not against literal CIDRs. - The allocated ranges are in the deployment output:
networking.virtualnetwork.<vnet>.resVirtualNetworkAddressSpaceandresAllSubnets.
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.