Multi-Environment Capability
One dedicated production VM (master branch only) and one shared test VM hosting multiple environments per feature branch. Push a feature branch β get a test environment automatically. Merge to master β deploy to production with approval.
Feature branches auto-deploy to the shared test VM as separate IIS sites. Merging to master deploys to production with approval. Branch deletion cleans up automatically.
Test environments use the existing VM β no additional infrastructure. Only the production VM is new (~Β£110-220/month). Multiple environments share one VM and one database set.
Architecture β The 2-VM Model
A single production VM for stable deployments and a single shared test VM that hosts all feature-branch environments simultaneously.
Production VM (Dedicated)
Trigger: Manual dispatch only (workflow_dispatch)
Approval: Required (GitHub Environment gate)
IIS: Single Default Web Site, all apps
Database: Production databases
Destroy: Never β permanent
Shared Test VM (Multi-Env)
Trigger: Push to feature/*, env/*
Approval: Not required (auto-deploy)
IIS: One site per branch environment
Database: Shared test databases
Destroy: Auto on branch delete or daily orphan scan
Branch β Environment mapping
master β Production VM (dedicated, approval-gated, auto-creates VM on first merge)
test β Infrastructure owner for shared test VM (create/destroy via Terraform)
feature/auth-refactor β IIS site "auth-refactor" on shared VM (cannot touch VM itself)
feature/billing-fix β IIS site "billing-fix" on shared VM (cannot touch VM itself)
fix/report-export β IIS site "report-export" on shared VM (cannot touch VM itself)
Infrastructure Ownership β Who controls what
test branch β ONLY branch that can create/destroy the shared test VM (Terraform)
master branch β ONLY branch that can create the production VM (auto-bootstrap, no destroy)
feature/* branches β Can ONLY deploy/remove IIS sites. Cannot touch VMs, Terraform, or other environments.
Feature branches ride ON infrastructure β they don't manage it.
How It Works β Triggers & Developer Flow
No new VMs are created. The shared test VM is always running. A test "environment" is just an IIS site + folder on disk β created automatically when you push.
When does a test environment get created?
β Creating a branch does NOT trigger anything.
β No new VM is provisioned.
β
Trigger: PUSHING CODE to a feature/* or env/* branch.
When you git push origin feature/my-thing:
1. GitHub detects a push to a branch matching feature/**
2. deploy-test.yaml fires automatically
3. Code is built, then an Azure Run Command creates an IIS site on the existing shared test VM
4. Subsequent pushes to the same branch update the existing environment (not duplicate)
The "environment" is just: a folder (F:\envs\my-thing\), an IIS website, and an app pool. That's it.
| Event | Branch Pattern | Workflow | Result |
|---|---|---|---|
| Push code | feature/*, env/* |
deploy-test.yaml |
Build β Deploy IIS site on shared test VM |
| Push again | Same branch | deploy-test.yaml |
Build β Update existing site (idempotent) |
| Manual dispatch | master |
deploy-production.yaml |
Ensure infra β Build β Approval β Deploy to production VM |
| Push code | test |
No deploy workflow | Safe working branch β infrastructure owner for test VM |
| Manual dispatch | test |
create-environment.yaml |
Terraform creates/ensures shared test VM |
| Delete branch | feature/*, env/* |
cleanup-test-env.yaml |
Remove IIS site + files (NOT the VM itself) |
| Daily 3am | β | cleanup-test-env.yaml |
Remove orphan sites (branch no longer exists) |
What feature branches CANNOT do
β Cannot create or destroy the shared test VM
β Cannot affect other branches' IIS sites
β Cannot modify infrastructure (Terraform)
β
Can only deploy/update/remove their OWN IIS site on the existing VM
Developer Workflow Steps
git push origin feature/auth-refactor β this is the ONLY trigger. GitHub Actions builds and deploys automatically.http://auth-refactor.pearl.test/pearl-azure β your branch is running in isolation from other branches.Production VM β Auto-Bootstrap & Protected
The production VM is managed via manual dispatch of deploy-production.yaml. The workflow includes an ensure-infrastructure job (Terraform, idempotent β first time creates the VM, subsequent times skips). Deployment requires manual approval.
| Aspect | Production Configuration |
|---|---|
| Deploy trigger | Manual dispatch only (workflow_dispatch) |
| Infrastructure | Auto-created by ensure-infrastructure job (Terraform idempotent β first time creates, subsequent times skips) |
| Approval | GitHub Environment protection rule β manual approval required before deploy |
| IIS Structure | Default Web Site with standard app structure at F:\apps\ |
| Worker Services | Full set: QueueProcessor, SystemChecker, AISpooler, Totem |
| Database | Production databases (PearlData, PearlQueues, PearlUsers, PearlBilling) |
| Deploy method | Azure Run Commands β same proven pattern as current deploy |
Self-bootstrapping production
First dispatch: Terraform creates the production VM (~10-15 min extra) β build β approval β deploy
Every subsequent dispatch: Terraform sees "no changes" (~30s) β build β approval β deploy
No manual infrastructure provisioning. Just dispatch deploy-production.yaml and the ensure-infrastructure job handles it.
Safety
Production cannot be deployed to without manual dispatch and approval. There is no destroy workflow for production. Only manual dispatch triggers the production pipeline β feature/fix/env branches go to the test VM automatically.
Shared Test VM β Multiple Environments
One VM hosts all test environments simultaneously. Each feature branch gets its own IIS site, app pool, and file directory β but they share the same VM resources and test databases.
File Isolation
Each environment deploys to its own directory:
F:\envs\auth-refactor\pearl-azure\
F:\envs\billing-fix\pearl-azure\
F:\envs\report-export\pearl-azure\
IIS Isolation
Each environment gets its own IIS site + app pool:
Site: pearl-auth-refactor
Pool: PearlAppPool-auth-refactor
Host: auth-refactor.pearl.test
Worker Services (Shared)
One set of worker services processes from the shared test database. Workers don't need per-branch isolation since the database is shared anyway.
Capacity
The shared VM can comfortably host 5-10 simultaneous environments. Each environment only consumes disk space for its deployed files (~200-400MB per env). IIS app pool isolation prevents one environment's crash from affecting others.
IIS Multi-Site Structure
Each test environment is a separate IIS website with host-header binding. The host header routes incoming requests to the correct environment without port conflicts.
| IIS Site Name | Host Header | Physical Path | App Pool |
|---|---|---|---|
pearl-auth-refactor |
auth-refactor.pearl.test |
F:\envs\auth-refactor\ |
PearlAppPool-auth-refactor |
pearl-billing-fix |
billing-fix.pearl.test |
F:\envs\billing-fix\ |
PearlAppPool-billing-fix |
pearl-report-export |
report-export.pearl.test |
F:\envs\report-export\ |
PearlAppPool-report-export |
Why host-header binding
All environments listen on port 80. IIS uses the Host header to route to the correct site. No port conflicts. Developers just need the correct hostname in their browser (or hosts file).
Database Strategy β Shared
Simple and fast: one database set for production, one shared set for all test environments.
| Database Set | Used By | Connection Strings |
|---|---|---|
| Production databases | Production VM only | Production web.config β PearlData, PearlQueues, etc. |
| Test databases (shared) | ALL test environments | All test web.configs β PearlData_test, PearlQueues_test, etc. |
Trade-offs
Pro: Zero spin-up delay for databases. No per-env restore needed. Simple to manage.
Con: Test environments can see each other's data changes. One developer's queue job might be picked up by another's test. This is acceptable for feature testing.
CI/CD Workflow Design
Three new workflows handle the full lifecycle: test deploy, production deploy, and cleanup.
deploy-test.yaml
Trigger: Push to feature/*, env/*
Action: Build β Stage to blob β Run Command β Invoke-RemoteArtifactDeployment.ps1 β Deploy-TestEnvironment.ps1
Approval: None (auto-deploy)
Pattern: Same as deploy.yaml (SAS URLs, blob staging, output blob parsing)
deploy-production.yaml
Trigger: Manual dispatch only (workflow_dispatch)
Action: Ensure infra β Build β Approval β Stage to blob β Run Command β Deploy to web + worker VMs
Approval: Required (GitHub Environment gate)
Pattern: Full pipeline with optional infrastructure refresh
cleanup-test-env.yaml
Trigger: Branch delete + daily schedule
Action: Run Command β Cleanup-TestEnvironment.ps1 (direct, no artifact download)
Approval: None (automatic)
Speed: ~30 seconds
Access And Routing
How developers reach each test environment in their browser.
http://auth-refactor.pearl.test/pearl-azure10.2.1.10)Host: auth-refactor.pearl.test header matches the correct IIS site binding.pearl-auth-refactor serves content from F:\envs\auth-refactor\Developer Setup
Step 1: Allowlist Your IP
The firewall blocks all traffic by default. Add your IP to firewall_allowed_source_ips in
src/terraform/environments/test/terraform.tfvars, then run terraform apply.
See _internal_docs/Managing-External-Web-Access.md for full instructions.
Step 2: Get the Firewall Public IP
terraform output firewall_public_ip
or: az network public-ip show -n pip-pearl-test-firewall -g rg-pearl-test-hub --query ipAddress -o tsv
Note: The IP changes after each environment destroy/rebuild cycle.
Step 3: Configure DNS (choose one)
Option A β Hosts file (per environment):
| Windows | C:\Windows\System32\drivers\etc\hosts (edit as Admin) |
| macOS / Linux | /etc/hosts (edit with sudo) |
Add entries:
<firewall-ip> auth-refactor.pearl.test
<firewall-ip> billing-fix.pearl.test
Option B β Wildcard DNS (recommended, one-time):
*.pearl.test β <firewall-ip>
Configure via dnsmasq, corporate DNS, or local DNS resolver. Covers all environments automatically.
Step 4: Access Your Environment
http://{env-name}.pearl.test/pearl-azure/login.aspx
Helper Script
List all active environments
Run locally (requires az CLI):
./scripts/show-test-environments.sh
Shows: firewall IP, all deployed environments, ready-to-use URLs, and hosts file entries.
Troubleshooting
Can't reach the site (timeout)?
1. Check your IP is allowlisted: curl -s ifconfig.me
2. Verify firewall IP is correct (may have changed after rebuild)
3. Confirm DNS resolves: ping {env-name}.pearl.test should show the firewall IP
4. Verify VM is running in Azure Portal
Wrong env or 404?
β’ Ensure a deploy-test workflow succeeded for your branch
β’ Branch must use feature/, fix/, or env/ prefix
β’ Host header must match IIS binding exactly
Lifecycle Flow Diagrams
Visual representation of the full create β deploy β destroy lifecycle.
End-to-End Flow
F:\\apps\\pearl-azure"] PROD_IIS --> PROD_DB[("Production Databases")] end subgraph TEST["Shared Test VM - Multi-Environment"] TEST_WF -->|auto-deploy| TEST_RC[Azure Run Command] TEST_RC --> ENV1["IIS: pearl-auth-refactor
Host: auth-refactor.pearl.test
F:\\envs\\auth-refactor\\"] TEST_RC --> ENV2["IIS: pearl-billing-fix
Host: billing-fix.pearl.test
F:\\envs\\billing-fix\\"] ENV1 --> TEST_DB[("Shared Test Databases")] ENV2 --> TEST_DB CLEAN -->|remove| ENV1 end
CI/CD Sequence
Implementation Roadmap
Total estimated effort: 5-7 working days for full capability.
| Phase | Effort | Deliverable |
|---|---|---|
| 1. Deploy scripts | 1-2 days | Deploy-TestEnvironment.ps1 + Cleanup-TestEnvironment.ps1 |
| 2. Production workflow | 1 day | deploy-production.yaml with approval gate |
| 3. Test workflow | 1-2 days | deploy-test.yaml triggered on feature branches |
| 4. Cleanup workflow | 0.5-1 day | cleanup-test-env.yaml on branch delete + schedule |
| 5. Access setup | 0.5 day | DNS/hosts configuration + documentation |
| 6. Demonstrate | 1 day | 2 feature branches running simultaneously, one merged |
Cost summary
Production VM (new): ~Β£110-220/month
Shared test VM: Β£0 additional (already exists)
Test environments: Β£0 (same VM, just disk space)
Total new cost: ~Β£110-220/month (production VM only)
Safety guardrails
β’ Production requires manual approval β cannot auto-deploy
β’ Production has no destroy workflow
β’ Test environments cleaned up on branch delete + daily orphan scan at 03:00 UTC
β’ Maximum ~10 concurrent test environments (soft limit)
β’ Disk space monitoring alerts at 80% usage