GitHub Actions Automation Workflows
This page explains the GitHub Actions workflows used by the repository. These workflows build containers, validate infrastructure changes, and deploy platform resources by using passwordless sign-in to Azure through OpenID Connect.
Workflow Overview
| Workflow | Trigger | Purpose |
|---|---|---|
| .builds.yml | Called by other workflows | Reusable container build workflow for azure-proxy images |
| .deployer.yml | Called by other workflows | Reusable two-stage tools deployer: initial-setup/infra (resource group, network, monitoring) then Azure Bastion + jumpbox via the bcgov action |
| .deployer-using-secure-tunnel.yml | Called by other workflows | Reusable Terraform deployer for infra-ai-hub through the Bastion SOCKS tunnel + Privoxy |
| .lint.yml | Called by other workflows | Reusable validation: pre-commit (terraform fmt + tflint), conventional commits, fork check |
| add-or-remove-module.yml | Manual (workflow_dispatch) | Manually apply/plan/destroy the tools Bastion + jumpbox stack (via the bcgov action) |
| manual-dispatch.yml | Manual (workflow_dispatch) | Run plan/apply/destroy for dev/test/prod; prod apply requires a semver tag (e.g. v1.2.3) and creates a GitHub Release |
| merge-main.yml | Push to main |
Automatic post-merge: semantic version tag via conventional commits, apply infrastructure to test |
| pr-open.yml | Pull request events + manual trigger | PR validation: lint, container builds, deploy Bastion in tools, and plan against test |
| pages.yml | Push to main (docs and Terraform roots) + manual trigger | Generate docs and deploy GitHub Pages site |
Tenant Onboarding Portal Workflows
The tenant onboarding portal now keeps Terraform in tenant-onboarding-portal/infra while the application code lives under tenant-onboarding-portal/backend and tenant-onboarding-portal/frontend. GitHub Actions provisions App Service from the root infra/ folder, builds the frontend separately, copies the SPA into the backend deployment bundle, and deploys the backend package to App Service via zip deployment.
| Workflow | Portal-specific behavior |
|---|---|
| pr-open.yml | Detects changes anywhere under tenant-onboarding-portal/, provisions a preview App Service in tools, builds frontend/ and backend/, then deploys the backend zip for PR validation. |
| pr-close.yml | Destroys the preview portal environment created for the PR after the pull request is closed. |
| merge-main.yml | On merges to main, detects portal changes, provisions the shared tools App Service if needed, builds the frontend and backend, deploys to the staging slot, health-checks it, then swaps staging into production. |
| portal-deploy.yml | Manual tools redeploy for the same backend/frontend bundle, following the same provision, staging deploy, health check, and slot-swap path as the automated main deployment. |
Developer SDLC Flow (Branch → PR → TEST → PROD)
The repository enforces a promote-through-environments workflow. Every change flows through a validated path before reaching production.
- Create feature branch from
main(e.g.feat/<work-item>,fix/<issue>) and commit changes. - Open PR to
main, which triggerspr-open.yml:- Lint (pre-commit: terraform fmt + tflint, conventional commit title, fork check)
- Container image builds
- Terraform plan against
test(via secure tunnel) — summary appended to PR description
- Merge to main once PR checks and code review pass.
- Auto-apply to test + semver tag:
merge-main.ymlstarts two jobs concurrently — semantic versioning (conventional commit history →v1.2.3tag +CHANGELOG.mdupdate) and Bastion bootstrap intools. Container images are then re-tagged with the semver version. Once the Bastion + jumpbox are up, it applies infrastructure totestthrough the Bastion tunnel. After a successful apply, integration tests run automatically (two phases — see below). - Promote to prod: Use
manual-dispatch.yml, selectprod+apply, and provide the semver tag. The workflow verifies all container images exist with that tag, then deploys using the semver-tagged images (notlatest). This is gated — requires approval from designated reviewers (configured in the GitHubprodenvironment protection rules). - Release created: After successful prod apply, a GitHub Release is automatically created from the deployed tag with deployment metadata.
prod environment has required reviewers configured as a GitHub Environment protection rule. Any workflow job targeting prod will pause and wait for manual approval before executing. Only designated reviewers can approve.dev from a feature/PR branch for targeted validation; use this sparingly and co-ordinate with other devs. The dev environment does not include App Gateway or DNS Zone — full ingress path validation is only available in test and prod.Feature Branch
│
└── Pull Request ──► pr-open.yml
├─ .lint.yml (fmt, tflint, conventional commits, fork check)
├─ .builds.yml
├─ .deployer.yml (tools Bastion + jumpbox via bcgov action)
├─ .deployer-using-secure-tunnel.yml (test plan)
└─ Update PR description with plan summary
│
▼
Merge to main
│
▼
merge-main.yml
├─ Semantic version tag (v1.2.3) ─────────────────── concurrent
├─ Tag container images (latest → v1.2.3) ──────── after semver
├─ Deploy Bastion + jumpbox (tools) ──────────────────────────┤
└─ Apply to test (via Bastion tunnel)
└─ Integration tests (post-apply)
├─ Direct: pytest group=direct
├─ AI evaluation (optional, judge model configured)
└─ Via proxy: pytest group=proxy (KV private endpoint)
│
▼
manual-dispatch.yml (prod + apply + tag)
├─ ⏸ Requires prod environment approval
├─ Verify container images exist for semver tag
├─ Apply to prod using semver-tagged images
└─ Create GitHub Release
Advanced Branching Patterns
The basic SDLC flow above covers the single-feature-per-PR path. In practice, teams often need to coordinate dependent work or bundle multiple features into a single release. Two patterns handle this: Stacked PRs and Release PRs.
Stacked PRs (Dependent Feature Chains)
Use stacked PRs when a feature depends on another feature that hasn't merged to main yet. Each PR in the stack targets the previous branch instead of main.
When to Use
- Feature B requires infrastructure created by Feature A (e.g. a new subnet added in A is referenced in B)
- Breaking a large change into reviewable, incremental PRs
- Multiple developers working on sequential pieces of a story
How It Works
- Create
feat/base-networkfrommain→ open PR #1 targetingmain - Create
feat/add-apim-policyfromfeat/base-network→ open PR #2 targetingfeat/base-network - Each PR triggers
pr-open.ymlindependently for lint and plan - Merge PR #1 first (bottom of the stack) — this triggers
merge-main.yml - Retarget PR #2 to
mainand rebase onto updatedmain - Merge PR #2 — triggers another
merge-main.ymlrun with its own semver tag
main ─────────────────────────┬───────────────────┬──────►
\ │ merge PR #1 │ merge PR #2
└─ feat/base-network ─────┘ (v1.3.0) │
\ │
└─ feat/add-apim-policy ─── rebase ──────┘
(v1.4.0)
main yet. After merging the base PR, rebase the dependent branch onto main to pick up any squash-merge differences before merging.pr-open.yml against test. The plan for PR #2 will show changes from both branches while PR #1 is open (because the diff includes the base). After PR #1 merges, re-running PR #2's checks shows only its own changes. Each merge to main produces its own semver tag.Release PRs (Bundled Multi-Feature Releases)
Use a Release PR when multiple developers are working on separate features that should ship together as a single coordinated release. All feature branches merge into a shared release branch, which then opens one PR to main.
When to Use
- A planned release includes work from multiple developers across different modules
- Features need integration testing together before merging to
main - You want one semver tag to represent the entire release bundle (e.g. a sprint deliverable)
- Coordinating breaking changes that span multiple Terraform stacks
How It Works
- Create a release branch from
main:release/sprint-42(orrelease/apim-v2, etc.) - Developers create feature branches from the release branch:
feat/new-tenant-config→ PR targetingrelease/sprint-42feat/apim-rate-limits→ PR targetingrelease/sprint-42fix/dns-zone-ttl→ PR targetingrelease/sprint-42
- Feature PRs are reviewed and merged into the release branch (these merges do not trigger
merge-main.ymlsince they don't targetmain) - Optionally deploy the release branch to
devviamanual-dispatch.ymlto validate the combined changes - When all features are complete, open one Release PR:
release/sprint-42→main - The Release PR triggers
pr-open.yml— the plan shows the aggregate of all bundled changes - Merge the Release PR →
merge-main.ymlcreates one semver tag and applies totest
main ─────────────────────────────────────────┬──────────►
\ │ merge Release PR
└─ release/sprint-42 ───┬──────┬──────────┘ (v2.0.0)
\ │ │
├─ feat/tenant ───┘ │
│ │
└─ feat/rate-limits ─────┘
pr-open.yml (lint and plan), giving each feature its own review cycle. The final Release PR to main runs the full pipeline and shows a combined plan. Only the merge to main triggers merge-main.yml for semver tagging and test apply.feat: sprint 42 release — tenant config and rate limits) since it controls the semver bump. Use feat: for minor, fix: for patch, or include BREAKING CHANGE in the body for major.Choosing Between Stacked PRs and Release PRs
| Stacked PRs | Release PR | |
|---|---|---|
| Best for | Sequential/dependent changes by 1–2 developers | Parallel independent features by multiple developers |
| Semver tags | One tag per merged PR (e.g. v1.3.0, v1.4.0) | One tag for the entire bundle (e.g. v2.0.0) |
| Review granularity | Each PR is reviewed independently with a focused diff | Feature PRs reviewed individually; Release PR shows combined diff |
| merge-main.yml runs | Triggers once per PR merged to main | Triggers once for the entire release |
| Risk | Rebase conflicts after base merges | Release branch can drift from main if long-lived |
| Recommendation | Keep stacks shallow (2–3 deep max) | Keep release branches short-lived; rebase from main regularly |
When Do I Need Self-Hosted Runners?
ubuntu-24.04. When a workflow needs to reach a private endpoint, the Terraform deployer opens an Azure Bastion SOCKS tunnel to a jumpbox in the tools virtual network and runs Privoxy on that runner to bridge it to an HTTP proxy. Data-access traffic flows through that tunnel. Because of that setup, this repository does not need self-hosted runners for its own pipelines.
The optional
github_runners_aca module, deployed through add-or-remove-module.yml, can create self-hosted runners inside the virtual network. Use that option when other repositories or workloads need long-lived compute that already lives inside the private network, not for this repository's own automation.
The table below describes the general pattern. Data-plane work that can't use the Bastion tunnel approach — for example, other repos without access to the tools Bastion — would still need self-hosted runners.
| Operation | Plane | Public Runner? | Example |
|---|---|---|---|
| Create resources (VMs, VNets, Key Vault) | Control | ✓ Yes | azurerm_key_vault |
| Configure settings, RBAC roles | Control | ✓ Yes | azurerm_role_assignment |
| Deploy private endpoints | Control | ✓ Yes | azurerm_private_endpoint |
| Read/write Key Vault secrets | Data | ✗ No | azurerm_key_vault_secret |
| Read/write Storage blobs | Data | ✗ No | azurerm_storage_blob |
| Terraform state (if private) | Data | ✗ No | Backend storage account |
Public Runners Work For
- Deploying all infrastructure modules
- Network, Bastion, Jumpbox, Proxy
- Any resource that doesn't read secrets
- Documentation builds (pages.yml)
Self-Hosted Required For
- Terraform using
azurerm_key_vault_secret - Private state backend (blocked by PE)
- Any code that reads secrets at plan time
- Database migrations, blob uploads
github_runners_aca_enabled if you actually need data plane access in CI/CD..deployer.yml (Reusable Workflow)
Reusable workflow (.Tools Environment Deployer) that provisions the shared tools environment in two ordered stages: (1) an initial-setup job runs the initial-setup/infra Terraform — resource group, spoke network wiring, and monitoring (Log Analytics workspace + Application Insights); (2) a bastion job provisions the Azure Bastion + jumpbox by calling the bcgov/action-deployer-vm-bastion-alz action (in the tools subscription), reusing the Log Analytics workspace from stage 1 as a bring-your-own workspace for Bastion diagnostics. It replaces the legacy proxy-based deploy. On destroy, the initial-setup stage is intentionally skipped so tearing down the Bastion stack does not remove the shared tools infrastructure.
Inputs
| Input | Type | Description |
|---|---|---|
command |
string | Terraform command passed to the action (plan, apply, destroy). Always the tools environment. |
Key Features
- Two-stage deploy: the
initial-setupjob (resource group, network, monitoring) runs first; thebastionjobneedsit and consumes its outputs - Shared Log Analytics workspace: the LAW created in stage 1 is passed to the Bastion action as
existing_log_analytics_workspace_id, avoiding a duplicate workspace - OIDC Authentication: The bcgov action authenticates with federated credentials (no secrets)
- Tools subscription: Deploys into the tools subscription via
TOOLS_SUBSCRIPTION_ID - Bastion native tunnelling: Standard SKU Bastion + jumpbox with Entra ID SSH login
- Cost-saving automation:
enable_bastion_automationrunbooks delete/recreate the Bastion on a schedule - Safe teardown:
destroyskips the initial-setup stage, preserving shared tools infra (RG, network, LAW)
Required Permissions
permissions: id-token: write # Required for OIDC token generation contents: read # Required for repository checkout
id-token: write permission, the workflow cannot generate the OIDC token needed for Azure authentication..deployer-using-secure-tunnel.yml (Reusable Workflow)
Reusable Terraform workflow for infra-ai-hub. It ensures the tools Bastion is up, opens a Bastion SOCKS tunnel and starts Privoxy, then runs stack deployment and (for apply in dev/test) integration tests.
Highlights
- Validated environments:
dev,test,prod - Bastion tunnel bootstrap: ensures the Bastion (in the tools subscription) is up, then
az network bastion ssh -- -Dopens a SOCKS5 proxy (Entra ID auth) - Proxy-aware Terraform: Privoxy bridges the SOCKS proxy to
HTTP_PROXY/HTTPS_PROXY(withNO_PROXYfor the control plane) - Integration tests (after successful
applyin dev/test) — two phases:- Direct (no proxy):
uv run python ./run-tests.py --group direct— APIM and App Gateway are public endpoints; proxy not required - AI evaluation (optional):
uv run python ./run-evaluation.pyruns only when the judge-model secrets and vars are configured - Via proxy:
uv run python ./run-tests.py --group proxy— Key Vault is private-endpoint only (public_network_access_enabled=false), so the key-rotation suite must route through the Bastion tunnel
- Direct (no proxy):
.lint.yml (Reusable Workflow)
Reusable validation workflow used by PR checks. Runs Terraform formatting and linting, enforces PR title conventions, and blocks forks.
What It Runs
- Pre-commit hooks:
terraform fmt+tflint(diff-based when possible, full-repo fallback) - Conventional Commits: Validates PR title follows Conventional Commits spec (e.g.
feat:,fix:,chore:) - Fork check: Rejects PRs from forked repositories
- Summary on failure: When any check fails, prints a consolidated error summary with common reasons
.builds.yml (Container Build Workflow)
Reusable workflow for building and pushing container images to GitHub Container Registry (GHCR). Supports matrix builds for multiple packages including the azure-proxy services.
Built Packages
- azure-proxy/privoxy - Privoxy HTTP→SOCKS bridge (fronts the Bastion SOCKS tunnel)
- jobs/apim-key-rotation - APIM subscription key rotation Container App Job
Image Tagging Strategy
Container images are tagged at different stages of the pipeline:
| Stage | Tags Applied | Example |
|---|---|---|
PR Build (pr-open.yml) |
PR number, run number, latest |
42, 42-7, latest |
Main Merge (merge-main.yml) |
Semver tag added to latest image |
v1.2.3 |
Prod Deploy (manual-dispatch.yml) |
Deploys using the semver-tagged image | v1.2.3 (immutable) |
The latest tag is used for dev and test environments. Production always uses the semver tag to ensure the exact tested image is what runs in prod.
add-or-remove-module.yml
Manual workflow for managing the tools Bastion + jumpbox stack on demand. It calls .deployer.yml (the bcgov action) to apply, plan, or destroy the stack.
When to Use
- apply: Bring the Bastion + jumpbox back up (e.g. after a teardown)
- destroy: Tear the stack down to save costs
- plan: Preview changes without applying
How to Run
- Go to Actions tab in GitHub
- Select "Deploy or Remove Bastion Host"
- Click "Run workflow"
- Choose the
command(apply / plan / destroy)
Workflow Inputs
| Input | Options | Description |
|---|---|---|
command |
apply, plan, destroy | Terraform command for the Bastion stack (always the tools environment) |
pr-open.yml (Pull Request Checks)
Runs on pull request events (and manual dispatch) to provide fast CI signal before merge.
Automated Checks
- Validation: Invokes
.lint.yml(pre-commit fmt + tflint, conventional commits, fork check) - Container Build Validation: Invokes
.builds.ymlfor proxy images - Tools Bastion Bootstrap: Invokes
.deployer.ymlto deploy the Bastion + jumpbox intools(bcgov action) - Plan Against Test: Invokes
.deployer-using-secure-tunnel.ymlwithcommand=plan - PR Description Update: Appends the test plan summary to the PR description under AI Hub Infra Changes
- Final Result: Reports pass/fail per job with clear ✅/❌ indicators; PR-Description is non-blocking
No Changes to AI Hub Infra in this PR.Bastion cost-saving (handled by the action's runbooks)
There is no longer a separate scheduled-cleanup workflow. Cost savings are built into the
bcgov/action-deployer-vm-bastion-alz
action via Azure Automation runbooks (enabled by enable_bastion_automation: "true"):
Lifecycle automation
- Delete-BastionHost: deletes the Standard SKU Bastion host daily off-hours (the host is the main cost)
- Create-BastionHost: recreates it on weekday mornings
- Jumpbox auto-stop/start: the VM is deallocated nightly and started on weekdays
The deploy pipeline also ensures the Bastion is up on demand (running Create-BastionHost if the host is missing) before opening the tunnel — see .deployer-using-secure-tunnel.yml.
manual-dispatch.yml (Manual Promotion Workflow)
Use this workflow to run plan, apply, or destroy for dev, test, and prod.
How It Works
- Validates inputs — prod apply requires a
deploy_tag(a semver tag likev1.2.3from a successfulmerge-main.ymlrun) - Verifies all container images exist with the semver tag (prod apply only)
- Deploys/refreshes the Bastion + jumpbox in
toolsvia.deployer.yml(bcgov action) - Opens the Bastion SOCKS tunnel in
.deployer-using-secure-tunnel.yml - Executes selected Terraform command in chosen environment — prod uses the semver-tagged container images (e.g.
:v1.2.3) while dev/test use:latest - For prod apply only: Creates a GitHub Release from the deployed tag with deployment metadata
prod environment has required reviewers configured as a GitHub Environment protection rule. Any workflow job targeting prod will pause and wait for manual approval from designated reviewers before executing. Additionally, prod apply requires you to specify which semver git tag to deploy — ensuring only test-validated commits reach production.dev from a PR branch using workflow dispatch to test specific features. The dev environment does not include App Gateway or DNS Zone; end-to-end ingress and DNS validation is only supported in test and prod.merge-main.yml (Auto Apply + Semantic Version on Main)
This workflow runs on every push to main. It creates a semantic version tag using Conventional Commits and then applies infrastructure to test.
Execution Flow
- Concurrently on push to main:
- Semantic version:
TriPSs/conventional-changelog-actioninspects commits since last tag (feat:→ minor,fix:→ patch,BREAKING CHANGE→ major), creates a git tag (e.g.v1.2.3), and pushes an updatedCHANGELOG.md - Bastion bootstrap: deploys/refreshes the Bastion + jumpbox in
toolsvia.deployer.yml(bcgov action)
- Semantic version:
- Tag container images: After the semver tag is created, all container images (
azure-proxy/privoxy,jobs/apim-key-rotation) that have a:latesttag are re-tagged with the semver version (e.g.:v1.2.3). This runs in parallel via a matrix strategy. - Once the Bastion + jumpbox are live,
.deployer-using-secure-tunnel.ymlopens the SOCKS tunnel to them - Run
applyagainsttest(through the Bastion tunnel) - After successful apply, integration tests run automatically in two phases (direct + via proxy — see
.deployer-using-secure-tunnel.ymlsection)
manual-dispatch.yml, you provide a version tag (e.g. v1.2.3) to ensure the exact commit that passed test is what gets applied to production. The version is derived from commit messages, so feat: and fix: prefixes directly control versioning.Release Process (TEST → PROD)
Production releases follow a tag-based promotion flow:
Steps to Release to Production
- Merge your PR to
main— the test deployment and semantic versioning run automatically - Verify the test deployment succeeded and a version tag was created (visible in Actions summary and repo tags, e.g.
v1.2.3) - Go to Actions → Deploy to Environments (Manual Dispatch)
- Select:
environment=prod,command=apply,deploy_tag=v1.2.3 - Click Run workflow — the job will pause waiting for prod environment approval
- A designated reviewer approves the deployment in the Actions UI
- After successful apply, a GitHub Release is automatically created with deployment details
Release Contents
Each GitHub Release created for prod includes:
- The git tag that was deployed
- Commit SHA
- Who triggered the deployment
- Link to the workflow run
- Timestamp
Concurrency Strategy
Several workflows and reusable jobs use concurrency to prevent race conditions and conflicting Terraform operations.
| Workflow/Job | Concurrency Group | Why It Helps |
|---|---|---|
.deployer.yml |
tools |
Serializes tools/bootstrap changes so multiple runs don't mutate shared proxy infra at the same time. |
.deployer-using-secure-tunnel.yml |
${environment_name} |
Ensures only one Terraform operation per environment runs at once (for example, only one test apply). |
pr-open.yml builds job |
builds-${PR number} (cancel in-progress) |
Stops stale image builds when a newer commit arrives in the same PR. |
manual-dispatch.yml |
manual-deploy-${run_id} |
Keeps each manually triggered deployment isolated and traceable to a single run. |
merge-main.yml |
deploy-test-on-main |
Queues merges to main so test applies execute in order and avoid state lock contention. |
APIM Key Rotation
APIM subscription key rotation is now handled by a Container App Job (scheduled) deployed as a custom container. The job source is at jobs/apim-key-rotation/ and the Terraform module at infra-ai-hub/modules/key-rotation-function/.
.builds.yml (Matrix Entry)
The key rotation container image is built by the shared .builds.yml reusable workflow as a matrix entry alongside other containers (privoxy, pii-redaction-service).
- Called by
pr-open.ymlon PR open/update - Triggers on changes to
jobs/apim-key-rotation/or the workflow itself - Uses
bcgov/action-builder-ghcrfor image build and push - Tagged with PR number, run number, and
latest
Semver Image Tagging
On merge to main, merge-main.yml re-tags the :latest image with the semver version (e.g. :v1.2.3). This ensures prod deployments use an immutable, version-pinned image rather than the mutable :latest tag.
- Dev/test use
:latest(with aFORCE_IMAGE_PULLenv var to trigger re-pull each Terraform apply) - Prod uses the semver tag (e.g.
:v1.2.3) — immutable and traceable - Terraform
container_image_tag_job_key_rotationvariable controls which tag is deployed
pages.yml (Documentation)
Deploys this documentation site to GitHub Pages when changes are pushed to docs or Terraform roots (so generated references stay current).
Triggers
- Push to main: When files in
docs/**,initial-setup/infra/**, orinfra-ai-hub/**change - Manual: Can be triggered manually via
workflow_dispatch
Deployment Steps
- Checkout repository
- Run
docs/generate-tf-docs.shto refresh Terraform reference content - Run
docs/build.shto generate HTML from templates - Configure GitHub Pages
- Upload
docs/folder as artifact - Deploy to GitHub Pages
Environment Secrets
Each GitHub environment requires these secrets (created by initial-azure-setup.sh):
| Secret | Description | Source |
|---|---|---|
AZURE_CLIENT_ID |
Managed Identity client ID | Created by setup script |
AZURE_TENANT_ID |
Azure AD tenant ID | Your Azure subscription |
AZURE_SUBSCRIPTION_ID |
Target subscription ID | Your Azure subscription |
VNET_RESOURCE_GROUP_NAME |
Resource group containing VNet | Your infrastructure |
VNET_NAME |
Existing VNet name | Your infrastructure |
VNET_ADDRESS_SPACE |
VNet CIDR block | Your infrastructure |
SOURCE_VNET_ADDRESS_SPACE |
Source VNet(tools) CIDR for NSG rules | Your infrastructure |
SUBNET_ALLOCATION |
JSON object for subnet_allocation (map(map(string))) |
Azure Blob (network-info/subnet-allocation) then copied to GitHub secret |
EXTERNAL_PEERED_PROJECTS |
Optional JSON object for external_peered_projects (map(object)) |
Azure Blob (network-info/subnet-allocation) then copied to GitHub secret |
Subnet Allocation Process (Blob First)
For visibility and team collaboration, keep network JSON in Azure Blob first, then copy the same JSON into GitHub environment secrets.
- Update the environment file in Blob storage (for example,
subnet-allocation-dev.jsonorsubnet-allocation-test.jsoninnetwork-info/subnet-allocation). - Copy the JSON payload and paste it into the matching GitHub environment secret
SUBNET_ALLOCATION. - If direct APIM access from peered VNets is needed, copy the
external_peered_projectsJSON payload into optional secretEXTERNAL_PEERED_PROJECTS. - Run
planin that environment to validate before apply.
subnet_allocation = { ... } or external_peered_projects = { ... }) and do not paste escaped JSON with backslashes.How JSON Flows into Terraform
- GitHub Actions reads
secrets.SUBNET_ALLOCATION. - The reusable workflow maps it to
TF_VAR_subnet_allocation. - Terraform parses the JSON into
var.subnet_allocation(map(map(string))). - If
secrets.EXTERNAL_PEERED_PROJECTSis present, the workflow exports it asTF_VAR_external_peered_projectsand Terraform parses it intovar.external_peered_projects.
Common Errors You Might See
- Missing secret:
Error: Required environment secret SUBNET_ALLOCATION is missing or empty. - Invalid JSON:
Error: SUBNET_ALLOCATION must be valid JSON and a non-empty JSON object. - Invalid optional peered-project JSON: workflow JSON parse step fails when
EXTERNAL_PEERED_PROJECTSis not a valid JSON object. - Type/shape mismatch in Terraform: validation errors from
variable "subnet_allocation"(invalid CIDR, duplicate subnet names, missing PE subnet, unsupported subnet key). - Peered-project validation errors: Terraform errors for
external_peered_projects(invalid CIDR, priority outside 400–499, duplicate priority, invalid project name format). - Wrong precedence assumptions: values in
-var-fileoverrideTF_VAR_*; keepsubnet_allocationout of committedshared.tfvars.
Running Workflows Locally
For local development and testing, use the deployment scripts in each Terraform root:
TF_VAR_subnet_allocation before plan/apply. Source of truth is the tools storage account tftoolsaihubtracking, container tools, path network-info/subnet-allocation/.# Load required subnet allocation JSON (example: prod)
az account set --subscription "da4cf6-tools - AI Services Hub"
tmpfile=$(mktemp)
az storage blob download \
--account-name tftoolsaihubtracking \
--container-name tools \
--name network-info/subnet-allocation/subnet-allocation-prod.json \
--auth-mode login \
--file "$tmpfile"
export TF_VAR_subnet_allocation="$(jq -c . "$tmpfile")"
rm -f "$tmpfile"
# Switch back to your deployment subscription before running terraform
az account set --subscription "da4cf6-dev - AI Services Hub"
# Ensure you're logged in az login # Initial setup / tools ./initial-setup/infra/deploy-terraform.sh init ./initial-setup/infra/deploy-terraform.sh plan ./initial-setup/infra/deploy-terraform.sh apply
# AI Hub stacks ./infra-ai-hub/scripts/deploy-terraform.sh plan dev ./infra-ai-hub/scripts/deploy-terraform.sh apply test
TF_VAR_subnet_allocation is exported in the shell.