← Back to Central Guide
New Subscription Setup Prerequisites

New Subscription Setup Guide

Everything that must be configured manually before running create-environment.yaml on a new Azure subscription. Changing the subscription ID alone is not enough — this guide covers all prerequisites.

7Azure Steps
3GitHub Steps
1Terraform Config
01

Complete Prerequisites Checklist

All manual steps required before Terraform can provision infrastructure. Every item must be completed in order.

flowchart TD A[1. App Registration
+ OIDC Federation] --> B[2. Subscription RBAC
Contributor + UAA] B --> C[3. Resource Groups
3 manual RGs] C --> D[4. Key Vault
+ Secrets] D --> E[5. TF State Storage
+ Container] E --> F[6. Global Artifacts Storage
+ Upload Blobs] F --> G[7. SQLMI
+ VNet for Peering] G --> H[8. GitHub Environment
Variables + Secrets] H --> I[9. terraform.tfvars
Update Values] I --> J[10. Run create-environment
mode: apply]

⚠️ Critical

If ANY of these steps are missed, create-environment.yaml will fail. The most common failures are: missing OIDC federation (auth fails), missing Key Vault (Terraform data source fails), missing installer blobs (VM bootstrap fails).

02

Azure Identity & OIDC

Set up passwordless authentication between GitHub Actions and Azure.

Step 1: Create App Registration

1
Microsoft Entra ID → App registrations → New registration
2
Name: pearl-cicd-<environment> (e.g., pearl-cicd-test)
3
Account type: Single tenant. No redirect URI needed.
4
Note the Application (client) ID → becomes AZURE_CLIENT_ID
5
Note the Directory (tenant) ID → becomes AZURE_TENANT_ID

Step 2: Add Federated Credentials

1
Open App Registration → Certificates & secrets → Federated credentials → Add credential
2
Select GitHub Actions deploying Azure resources
3
Organization: Asset-Services-Group-Limited
4
Repository: message-direct
5
Entity type: Environment, Value: pearl-test

Why OIDC?

No client secrets to rotate. GitHub presents a short-lived token, Azure validates it against the federated credential. The token is scoped to a specific repository and environment — more secure than stored secrets.

Step 3: Grant Subscription RBAC

Role Scope Why
Contributor Subscription Create/modify all resources via Terraform
User Access Administrator Subscription Assign RBAC roles to VMs (Key Vault, Storage)
# Get the service principal object ID
SP_OBJECT_ID=$(az ad sp show --id <CLIENT_ID> --query id -o tsv)

# Assign Contributor
az role assignment create \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Contributor" \
  --scope "/subscriptions/<SUBSCRIPTION_ID>"

# Assign User Access Administrator
az role assignment create \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "User Access Administrator" \
  --scope "/subscriptions/<SUBSCRIPTION_ID>"
03

Create Resource Groups

Three resource groups must exist before Terraform runs. They are NOT Terraform-managed.

Resource Group Purpose Contains
pearl-test Key Vault resource group Key Vault (pearl-test-kv)
rg-pearl-tfstate-test Terraform state backend Storage account for .tfstate
rg-pearl-global-artifacts Shared installer/artifact storage Global artifacts storage account
az group create --name "pearl-test" --location "ukwest"
az group create --name "rg-pearl-tfstate-test" --location "ukwest"
az group create --name "rg-pearl-global-artifacts" --location "ukwest"
04

Key Vault

Pre-create the Key Vault and populate mandatory secrets. Terraform reads these via data blocks.

Create Key Vault

az keyvault create \
  --name "pearl-test-kv" \
  --resource-group "pearl-test" \
  --location "ukwest" \
  --enable-rbac-authorization true \
  --sku standard

Grant Access

Principal Role Purpose
Service Principal (CI/CD) Key Vault Secrets Officer Terraform creates/reads connection string secrets
Operator (your user) Key Vault Secrets Officer Manual secret management
VMs (managed identity) Key Vault Secrets User VMs read connection strings at deploy time

Required Secrets (Populate Manually)

Secret Name Value Used By
TelerikDLL-PassKey Encryption passphrase for Telerik DLL zip CI build (Prepare-CiBuildDependencies)
AjaxDLL-PassKey Encryption passphrase for Ajax DLL zip CI build (Prepare-CiBuildDependencies)
github-runner-pat GitHub PAT (repo scope) Self-hosted runner registration (optional)

Auto-generated by Terraform

After terraform apply, Terraform will create additional secrets in the Key Vault: sqlmi-connection-string (using your SQL admin password). These do NOT need manual creation.

05

Terraform State Storage

Create the storage account that holds terraform.tfstate. Uses Azure AD auth (no access keys).

# Create storage account
az storage account create \
  --name "stpearltfstate<UNIQUE>" \
  --resource-group "rg-pearl-tfstate-test" \
  --location "ukwest" \
  --sku Standard_LRS \
  --allow-blob-public-access false \
  --min-tls-version TLS1_2

# Create container
az storage container create \
  --account-name "stpearltfstate<UNIQUE>" \
  --name "tfstate" \
  --auth-mode login

# Grant SP access (required for use_azuread_auth)
az role assignment create \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Contributor" \
  --scope "$(az storage account show --name stpearltfstate<UNIQUE> --query id -o tsv)"

Backend config values

These 3 values map to GitHub environment variables:

TF_BACKEND_RESOURCE_GROUP_NAME rg-pearl-tfstate-test
TF_BACKEND_STORAGE_ACCOUNT_NAME stpearltfstate<UNIQUE>
TF_BACKEND_CONTAINER_NAME tfstate
06

Global Artifacts Storage

Stores CI build dependencies (encrypted DLL bundles) and VM bootstrap installers.

az storage account create \
  --name "stglobalartifacts<UNIQUE>" \
  --resource-group "rg-pearl-global-artifacts" \
  --location "ukwest" \
  --sku Standard_LRS \
  --allow-blob-public-access false

az storage container create \
  --account-name "stglobalartifacts<UNIQUE>" \
  --name "artifacts" \
  --auth-mode login

# Grant CI/CD read access
az role assignment create \
  --assignee-object-id "$SP_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Storage Blob Data Reader" \
  --scope "$(az storage account show --name stglobalartifacts<UNIQUE> --query id -o tsv)"
07

Upload Required Blobs

All blobs that must exist before VMs can bootstrap or CI can build.

CI Build Dependencies

Blob Path Description
ci-build-dependencies/telerik_dll.zip 7-Zip encrypted Telerik DLL bundle
ci-build-dependencies/ajax_dll.zip 7-Zip encrypted Ajax DLL bundle

After uploading, compute SHA256: shasum -a 256 <file> | awk '{print $1}' — these hashes go into GitHub repo variables.

VM Bootstrap Installers

These are downloaded by Custom Script Extensions during terraform apply. Missing blobs = failed VM provisioning.

Blob Path Used By Purpose
common/nssm.zip Worker + Build NSSM service manager
common/dotnet-framework-481-runtime.exe Worker + Build .NET 4.8.1 runtime
build/nuget.exe All VMs NuGet CLI
build/vs_buildtools.exe Build + Worker Visual Studio Build Tools
build/actions-runner-win-x64.zip Build GitHub Actions self-hosted runner
build/git-installer.exe Build Git for Windows
web/openjdk.msi Web Java (for Apache Solr)
web/solr.zip Web Apache Solr search engine
web/memcached.zip Web Memcached cache server

⚠️ All blobs must exist BEFORE terraform apply

The VM Custom Script Extensions download these during bootstrap. If any blob is missing, the extension fails, the VM shows as "failed" in Azure, and Terraform reports an error. There is no retry — you must fix the blob and re-run apply.

08

SQL Managed Instance

A pre-existing SQLMI with restored production databases. NOT created by Terraform.

Required Databases

Database Purpose
PearlData Main application data
PearlUsers User authentication
PearlBilling Billing data

Values Needed

Value Example Goes Into
SQLMI FQDN pearlsqlmitest.xxx.database.windows.net terraform.tfvarssql_connection_fqdn_override
Port 1433 terraform.tfvarssql_connection_port_override
Admin password (secret) GitHub secret → TF_VAR_SQL_ADMIN_PASSWORD
SQLMI VNet ID /subscriptions/.../virtualNetworks/vnet-pearlsqlmitest terraform.tfvarssqlmi_peering_vnet_id

VNet Peering

Terraform creates a VNet peering between the spoke VNet and the SQLMI VNet. The SQLMI must be in a VNet that allows peering (in the same region or global peering enabled). You provide the full resource ID of the SQLMI VNet.

09

GitHub Environment Setup

Create the GitHub environment and configure all required variables.

1
Go to Repository → Settings → Environments → New environment
2
Name: pearl-test (must match the federated credential entity)
3
(Optional) Add required reviewers for deploy/destroy protection

Environment Variables

Variable Value Purpose
AZURE_CLIENT_ID App Registration client ID OIDC authentication
AZURE_TENANT_ID Entra tenant ID OIDC authentication
AZURE_SUBSCRIPTION_ID Target subscription ID All Azure operations
TF_BACKEND_RESOURCE_GROUP_NAME rg-pearl-tfstate-test Terraform state backend
TF_BACKEND_STORAGE_ACCOUNT_NAME stpearltfstate<suffix> Terraform state backend
TF_BACKEND_CONTAINER_NAME tfstate Terraform state backend
10

GitHub Secrets

Sensitive values stored as environment secrets.

Secret Value Used By
TF_VAR_SQL_ADMIN_PASSWORD SQLMI admin password Terraform → creates connection strings in KV
TF_VAR_VM_ADMIN_PASSWORD VM local admin password (pearladmin) Terraform → sets Windows admin password
TF_VAR_FIREWALL_ALLOWED_SOURCE_IPS ["1.2.3.4/32","5.6.7.8/32"] Terraform → Firewall DNAT source filter

Password Requirements

VM admin password must be 12+ characters with uppercase, lowercase, number, and special character. SQLMI password has the same requirements. Use a password manager to generate these — they are rarely needed manually.

11

Repository Variables

Non-sensitive variables at repository level (shared across all environments).

Variable Value Purpose
CI_BUNDLE_STORAGE_ACCOUNT stglobalartifacts<suffix> Storage account for DLL bundles
CI_BUNDLE_KEYVAULT_NAME pearl-test-kv Key Vault with encryption keys
CI_BUNDLE_CONTAINER artifacts Container name for bundles
CI_BUNDLE_TELERIK_BLOB_NAME ci-build-dependencies/telerik_dll.zip Telerik bundle blob path
CI_BUNDLE_AJAX_BLOB_NAME ci-build-dependencies/ajax_dll.zip Ajax bundle blob path
CI_BUNDLE_SHA256_TELERIK SHA256 hash of telerik zip Integrity verification
CI_BUNDLE_SHA256_AJAX SHA256 hash of ajax zip Integrity verification
CI_BUNDLE_TELERIK_SECRET_NAME TelerikDLL-PassKey KV secret name for Telerik passphrase
CI_BUNDLE_AJAX_SECRET_NAME AjaxDLL-PassKey KV secret name for Ajax passphrase
12

Update terraform.tfvars

Subscription-specific values in src/terraform/environments/test/terraform.tfvars.

# ── Key Vault (must already exist) ──
existing_key_vault_resource_group_name = "pearl-test"
existing_key_vault_name               = "pearl-test-kv"

# ── RBAC principal IDs for Key Vault ──
key_vault_secrets_officer_principal_id           = "<SP_OBJECT_ID>"
operator_key_vault_secrets_officer_principal_id = "<YOUR_USER_OBJECT_ID>"

# ── Installer storage ──
installer_storage_account_resource_group_name = "rg-pearl-global-artifacts"
installer_storage_account_name                = "stglobalartifacts<suffix>"
installer_storage_container_name              = "artifacts"

# ── SQL Managed Instance ──
sql_connection_fqdn_override = "<SQLMI_FQDN>"
sql_connection_port_override = 1433

# ── VNet peering to SQLMI ──
sqlmi_peering_vnet_id = "/subscriptions/<SUB_ID>/resourceGroups/<RG>/providers/Microsoft.Network/virtualNetworks/<VNET>"

How to find principal IDs

SP_OBJECT_ID: az ad sp show --id <CLIENT_ID> --query id -o tsv
YOUR_USER_OBJECT_ID: az ad signed-in-user show --query id -o tsv

13

Run Create Environment

Final step — once all prerequisites are satisfied.

1
Actions → Create Environment → Run workflow
2
Target environment: test
3
Mode: plan — verify the plan is clean with no errors
4
Mode: apply — provision all infrastructure (~15 minutes)
5
Download environment-metadata.json artifact to verify values

What Terraform Creates

Resource Name Purpose
Resource Group (hub) rg-pearl-test-hub Firewall + Bastion
Resource Group (spoke) rg-pearl-test-spoke VMs + storage
VNet (hub) vnet-pearl-test-hub Firewall subnet
VNet (spoke) vnet-pearl-test-spoke VM subnets
Azure Firewall fw-pearl-test DNAT + egress filtering
Public IP pip-pearl-test-firewall External access to test envs
Web VM vm-pearl-test-web IIS multi-site host
Worker VM vm-pearl-test-worker Background services
Storage Account stpearltest<suffix> Deploy scripts/artifacts blob
VNet Peering spoke ↔ SQLMI VNet Database connectivity
NAT Rules Ports 8100-8129 DNAT to web VM for test access
14

Troubleshooting

Common failures and how to fix them.

Error Cause Fix
AuthorizationFailed SP missing subscription roles Grant Contributor + User Access Administrator
FederatedIdentityCredentialNotFound OIDC credential missing/wrong environment name Add federated credential for pearl-test
ResourceGroupNotFound Key Vault RG not created manually Create RG pearl-test
KeyVaultNotFound Key Vault not pre-created Create Key Vault before apply
BlobNotFound during bootstrap Installer blob missing from storage Upload all blobs from Phase 3
InvalidPassword Password doesn't meet complexity 12+ chars, upper+lower+number+special
Terraform state locked Previous run interrupted az storage blob lease break
StorageBlobDataContributor needed SP can't write to tfstate Grant role on tfstate storage account

General troubleshooting approach

1. Check the workflow run log for the exact error message.
2. If Terraform plan fails → OIDC, backend, or resource group issue.
3. If Terraform apply fails on VMs → bootstrap blobs missing or password issue.
4. If RBAC assignment fails → User Access Administrator role missing on SP.
5. If VNet peering fails → SQLMI VNet ID incorrect or cross-subscription peering not enabled.