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.
Complete Prerequisites Checklist
All manual steps required before Terraform can provision infrastructure. Every item must be completed in order.
+ 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).
Azure Identity & OIDC
Set up passwordless authentication between GitHub Actions and Azure.
Step 1: Create App Registration
pearl-cicd-<environment> (e.g., pearl-cicd-test)AZURE_CLIENT_IDAZURE_TENANT_IDStep 2: Add Federated Credentials
Asset-Services-Group-Limitedmessage-directpearl-testWhy 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) |
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>"
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 "rg-pearl-tfstate-test" --location "ukwest"
az group create --name "rg-pearl-global-artifacts" --location "ukwest"
Key Vault
Pre-create the Key Vault and populate mandatory secrets. Terraform reads these via data blocks.
Create Key Vault
--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.
Terraform State Storage
Create the storage account that holds terraform.tfstate. Uses Azure AD auth (no access keys).
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 |
Global Artifacts Storage
Stores CI build dependencies (encrypted DLL bundles) and VM bootstrap installers.
--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)"
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.
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.tfvars → sql_connection_fqdn_override |
| Port | 1433 |
terraform.tfvars → sql_connection_port_override |
| Admin password | (secret) | GitHub secret → TF_VAR_SQL_ADMIN_PASSWORD |
| SQLMI VNet ID | /subscriptions/.../virtualNetworks/vnet-pearlsqlmitest |
terraform.tfvars → sqlmi_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.
GitHub Environment Setup
Create the GitHub environment and configure all required variables.
pearl-test (must match the federated credential entity)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 |
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.
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 |
Update terraform.tfvars
Subscription-specific values in src/terraform/environments/test/terraform.tfvars.
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
Run Create Environment
Final step — once all prerequisites are satisfied.
testplan — verify the plan is clean with no errorsapply — provision all infrastructure (~15 minutes)environment-metadata.json artifact to verify valuesWhat 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 |
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.