Skip to content

Deploy the Spoke Canvas

After the hub, deploy one test spoke to prove that the spoke pipeline, the spoke identity and the connection to the hub work. The test spoke is also what you show at the hand-over. Real spokes follow the standard change New spoke; this page covers what is different the first time.

Before you start

Background on what the deployment creates and which permissions it needs: Spoke canvas. On why subscriptions are not taken from a pool: Subscription provisioning.

  • [ ] The hub is deployed in the platform environment you target (Deploy the platform)
  • [ ] The spoke identity has its permissions and the Graph admin consent has been given (Prepare identity)
  • [ ] SpokeCommon.tenantinfo in mcp.json holds the administrative unit ID, the group and service principal prefixes, the subscription naming template and the secret rotation period. With the separate spoke canvas deployment, the common settings are in common.tf (Spoke common settings)
  • [ ] If you use the separate spoke canvas pipeline: Prepare the spoke pipeline is done
  • [ ] If the test spoke creates Azure DevOps or GitHub resources: Prepare spoke integrations is done
  • [ ] If you use IPAM pools: the pools are in the common settings (Spoke JSON reference)
  • [ ] The spoke state container exists (Prepare agents)
  • [ ] An empty (or test) subscription is available for the spoke. The pipeline cannot create subscriptions without a CSP billing account.
  • [ ] The team group that will get access exists in Entra ID (ProductTeamGroupNames)
  • [ ] The management group for the spoke exists in the hub's management_group_structure
  • [ ] An IP range for the spoke is reserved in the IP plan

Steps

  1. Create the spoke file in <env-folder>/spokes/<team>/, for example canvas_<workload>_dev.json. Start from the example in New spoke and the spoke JSON reference. The blocks to fill:

    Block Content for the test spoke
    workloadShortName, environment A short name (at most 6 characters) and a non-production environment
    azureSubscriptionId, azureSubscriptionName, managementGroupId The test subscription, its name and a landing zone management group
    tags Owner, department, workload, pii, cost centre, environment
    adInfo ProductTeamGroupNames and PIMGroupConfig (nopim, self-approve or approval per spoke group)
    targetRegions The primary region of the hub (and extra regions if you want to test them)
    resourceconfiguration.network[] A virtual network from the reserved range, with a default subnet, an NSG per subnet and peerHub: true

    Remove the sample spoke files that came with the template, so they do not run.

  2. Check the entry pipeline spoke: pinned version, agent pool, service connection (or federated credential), mcp.json of the right platform environment, environments and mode/whatIf. In the reference configuration repository spoke.yaml has whatIf: true and so only plans the canvas (Pipelines).

  3. Create the approval environment for production spokes if you use one, with approvers and no self-approval (Prepare repositories).
  4. Run spoke with spokeGlob set to the one test spoke file, first as plan. Read the plan: the first run creates the resource group, network, hub connection, groups, service principal, Key Vault, storage account and role assignments (What gets created).
  5. Apply, then run again and confirm that the second plan shows no changes. Known plan noise for spokes is described in Known issues.
  6. Verify with the list in New spoke: management group, tags, virtual network, hub connection, the four spoke groups, PIM activation by a member of the team group.
  7. Merge the spoke file to main through a pull request. If you use build validation on the configuration repository, the spoke config tests run on that pull request.

Keep the test spoke until the validation is done. Then either keep it as the example for the hand-over or remove it with Delete spoke.

Common first-run problems

Symptom Cause
The backend fails to initialize The spoke state container does not exist, or the identity has no blob data role on the account
Creating groups or PIM settings fails with a permission error Missing Graph permission or admin consent, or the identity is not Groups Administrator on the administrative unit
The config test rejects the file The managementGroupId is not in the hub's management group structure, the environment is not in the pipeline's environments, or the JSON does not match the schema
The team group cannot be found The group in ProductTeamGroupNames does not exist in Entra ID (the display name must match)
The hub connection fails The hub in that region is not deployed yet, or the address range overlaps with another network

Updating a spoke later

After the first spoke, changes follow Change spoke: a branch, an edit of the spoke file, a pull request that someone else reviews, and a pipeline run for that one file. Some changes are not possible because of limits in Terraform or Azure, for example changing a subnet that virtual machines use.

Older layout

In versions with separate spoke repositories, the same work looked like this:

  • The spoke file lived in the spoke configuration repository at spokes/tfspokes/<workload>/canvas_<...>.json. The administrative unit ID was set in common.tf, and spokeschema.json held the schema.
  • The pipeline was SpokeDeployment.yaml (Azure DevOps) or the Spoke Deployment workflow (GitHub), and a separate pipeline verified spoke files on every pull request.
  • The pipeline is described in Prepare the spoke pipeline. Spoke files had to be named canvas_<...>.json and contain _prod_ or _nonprod_.
  • A variable group held the service connection, the tenant ID, the Terraform version, the state storage account, resource group and subscription, the spoke state container, the log container and the filter that selects spoke files.
  • On GitHub the repository secret for the state container name had to be changed to the spoke state container, and the spoke identity needed Storage Blob Data Contributor on the storage account.
  • The environment for production spoke deployments had approvers, with no self-approval.