Skip to content

Deploy Integration Resources

How a workload team deploys integration resources (Logic Apps, Function Apps, Container Apps, Data Factory) into its spoke with the integration modules: the spoke outputs file, the configuration, and the reusable deployment workflow with its safety rules.

Reference design

The pipeline in the reference design is GitHub Actions with a reusable workflow. Your platform may use other tooling or other names. Ask your platform team for the template, the runner group and the release to use.

Overview

flowchart LR
    a["1. Spoke outputs<br/>one file per environment"] --> b["2. Your configuration<br/>module call + maps"]
    b --> c["3. Review and plan<br/>pull request, plan-only run"]
    c --> d["4. Manual run<br/>reusable workflow: apply"]
    d --> e["5. Deploy your application code"]

Step 1: get the spoke outputs

The modules need the facts of your spoke: resource group, network and subnets, Key Vault, storage account and Log Analytics workspace. These are kept in a variable file with one spoke_config object, one file per environment, for example config/spoke-outputs-<environment>.tfvars. It is based on the outputs of the spoke deployment, so that your configuration stays aligned with the spoke and you do not retype values.

spoke_config = {
  name                         = "<workload>-<environment>"
  subscription_id              = "<subscription-id>"
  resource_group_name          = "<resource-group>"
  location                     = "West Europe"
  tenant_id                    = "<tenant-id>"
  environment                  = "<environment>"
  key_vault_id                 = "<key-vault-resource-id>"
  key_vault_name               = "<key-vault-name>"
  storage_account_id           = "<storage-account-resource-id>"
  storage_account_name         = "<storage-account-name>"
  virtual_network_id           = "<vnet-resource-id>"
  virtual_network_name         = "<vnet-name>"
  subnet_ids                   = { "<subnet-name>" = "<subnet-resource-id>" }
  subnet_names                 = { "<subnet-name>" = "<subnet-name>" }
  log_analytics_workspace_id   = "<workspace-resource-id>"
  log_analytics_workspace_name = "<workspace-name>"
}

The field list is on Modules. Ask your platform team how your spoke produces this file. Never edit the values by hand when the spoke changes: take the new outputs.

Step 2: write the configuration

Call the root module and fill the maps you need (logic_apps, function_apps with service_plans, container_apps_environment, data_factories), see Modules. Give every environment its own configuration values through the variable files, and keep the code the same.

Step 3: review and plan

Open a pull request and have it reviewed. Then start the deployment workflow (step 4) with apply switched off: it runs terraform fmt, init, validate and plan and shows the plan. Read it. You expect resources in your spoke, no changes to resources you did not touch, and no unexpected deletes.

Step 4: run the reusable workflow

Deployments do not start automatically. A person starts the deployment workflow by hand and chooses the environment and whether to apply.

The reference design offers a reusable workflow, so every workload repository uses the same, reviewed deployment path. The workload repository holds a small caller:

name: Terraform deployment

on:
  workflow_dispatch:
    inputs:
      environment:
        description: Environment to plan or deploy
        required: true
        type: choice
        options: [dev, tst, acc, prd]
        default: dev
      enableApply:
        description: Apply the plan
        required: true
        type: boolean
        default: false
      runnerGroup:
        description: Self-hosted runner group
        required: true
        type: string
        default: <runner-group>

permissions:
  actions: read
  contents: read
  id-token: write

jobs:
  deploy:
    uses: <organisation>/<platform-repository>/.github/workflows/terraform-deploy.yml@<release-tag>
    with:
      environment: ${{ inputs.environment }}
      enable_apply: ${{ inputs.enableApply }}
      runner_group: ${{ inputs.runnerGroup }}
      module_owner: <organisation>
      module_repository: <module-repository>
    secrets:
      AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
      AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      MODULE_GITHUB_APP_ID: ${{ vars.<module-app-id-variable> }}
      MODULE_GITHUB_APP_PRIVATE_KEY: ${{ secrets.<module-app-key-secret> }}
Input Required Purpose
environment yes Selects dev, tst, acc or prd and the matching repository environment
runner_group yes The self-hosted runner group (runners inside the network)
module_owner, module_repository yes Owner and name of the private repository that holds the Terraform modules
enable_apply no Default false. Apply is allowed only from the main branch.
terraform_version no Terraform CLI version. The reusable workflow has a default.

Secrets and permissions:

  • The caller passes the Azure sign-in values (AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID) from the selected environment. Sign-in to Azure uses OpenID Connect (the id-token: write permission), not a stored password.
  • A GitHub App identity (ID and private key) lets the workflow download the private Terraform modules. Configure the ID as a repository variable and the key as a repository secret.
  • The called workflow cannot grant permissions that the caller withheld, so actions: read, contents: read and id-token: write must be in the caller. The caller repository must be allowed to use reusable workflows from the repository that hosts them.

Safety rules of the workflow

  • The selected environment needs a backend file (config/<environment>.azurerm.tfbackend) and a spoke outputs file (config/spoke-outputs-<environment>.tfvars), plus an existing repository environment.
  • Format, init, validate and plan always run before an apply.
  • Apply is off by default and is rejected unless the workflow runs from main.
  • An apply to acc or prd also requires at least one required reviewer on that environment.
  • One call deploys one environment. Promotion and order between environments are yours.
  • Pin the workflow to a release tag or commit, not to a moving branch. Your platform team publishes and announces releases.

Step 5: deploy your application

Terraform deploys the empty service. You deploy your code with your own pipeline: workflows to a Logic App, code to a Function App, images to the registry and then a new revision of a Container App. The resources are reachable only through private paths, so your pipeline must run on a runner inside the network (Build agents).

After the deployment

  • Resources can reach other resources in the spoke through VNet integration. Inbound access is by private endpoint only.
  • Request firewall rules for anything outside your spoke (Request and change).
  • Do not change Terraform-managed resources in the portal. The next deployment undoes it.