Skip to content

Commit e8c8aa6

Browse files
feat(iac): add relay HC automation, site tagging, and staged rollout strategy
- Move relay.tf into arc-infra module (co-located with Arc RG it depends on) - Replace single environment-level HC with dynamic per-machine for_each, keyed by Arc resource name (SiteCode); creates hc-<SiteCode> + listen SAS rule automatically for each machine discovered in the Arc resource group - Add site identity params to arc-setup.ps1: SiteCode, SiteName, NHSRegion, PacsVendor, SiteType, DeploymentRing; SiteCode sets --resource-name on the Arc agent (overrides hostname) and all params are stamped as Azure tags - Update vm.tf to pass ring0 site metadata for the test VM - Fix deploy.ps1 bat files to cd to BaseInstallPath so load_dotenv() finds the shared .env outside release directories; NSSM AppDirectory updated to match Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c7f4862 commit e8c8aa6

5 files changed

Lines changed: 126 additions & 62 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# The relay namespace is owned by dtos-manage-breast-screening ("manbrs").
2+
# This module creates one Hybrid Connection + listen-only auth rule per Arc-enabled
3+
# machine, auto-discovered by querying the Arc resource group.
4+
# HC names are derived from the Arc resource name (= SiteCode set at onboarding).
5+
#
6+
# Trigger: run `terraform apply` after each Arc onboarding to pick up new machines.
7+
8+
locals {
9+
relay_namespace_rg = "rg-manbrs-${var.env_config}-uks"
10+
relay_namespace_name = "relay-manbrs-${var.env_config}"
11+
}
12+
13+
data "azurerm_relay_namespace" "gateway" {
14+
count = var.enable_arc_servers ? 1 : 0
15+
16+
name = local.relay_namespace_name
17+
resource_group_name = local.relay_namespace_rg
18+
}
19+
20+
# Discover all Arc-enabled machines registered in the Arc resource group.
21+
# Each machine's name is the SiteCode set during onboarding (e.g. gw-RVJ-01).
22+
data "azurerm_resources" "arc_machines" {
23+
count = var.enable_arc_servers ? 1 : 0
24+
25+
resource_group_name = azurerm_resource_group.arc_enabled_servers[0].name
26+
type = "Microsoft.HybridCompute/machines"
27+
}
28+
29+
locals {
30+
arc_machines = var.enable_arc_servers ? {
31+
for m in data.azurerm_resources.arc_machines[0].resources : m.name => m
32+
} : {}
33+
}
34+
35+
# One Hybrid Connection per Arc machine (e.g. hc-gw-RVJ-01).
36+
resource "azurerm_relay_hybrid_connection" "per_machine" {
37+
for_each = local.arc_machines
38+
39+
name = "hc-${each.key}"
40+
resource_group_name = local.relay_namespace_rg
41+
relay_namespace_name = local.relay_namespace_name
42+
requires_client_authorization = true
43+
}
44+
45+
# Listen-only SAS rule per HC — distributed to each gateway site via the deploy pipeline.
46+
# The cloud app uses a namespace-level Send SAS and does not need per-site keys.
47+
resource "azurerm_relay_hybrid_connection_authorization_rule" "per_machine_listen" {
48+
for_each = local.arc_machines
49+
50+
name = "listen"
51+
hybrid_connection_name = azurerm_relay_hybrid_connection.per_machine[each.key].name
52+
namespace_name = local.relay_namespace_name
53+
resource_group_name = local.relay_namespace_rg
54+
55+
listen = true
56+
send = false
57+
manage = false
58+
}

infrastructure/modules/gateway-test-vm/vm.tf

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,23 @@ resource "azurerm_virtual_machine_run_command" "arc_setup" {
123123
value = var.arc_onboarding_spn_client_id
124124
}
125125

126+
# Site identity tags — stamped onto the Arc resource for Terraform HC discovery
127+
# and ADO pipeline ring targeting. ring0 = test VM only.
128+
parameter {
129+
name = "SiteCode"
130+
value = "${var.app_short_name}-${var.env_config}"
131+
}
132+
133+
parameter {
134+
name = "SiteType"
135+
value = "static"
136+
}
137+
138+
parameter {
139+
name = "DeploymentRing"
140+
value = "ring0"
141+
}
142+
126143
protected_parameter {
127144
name = "ServicePrincipalSecret"
128145
value = data.azurerm_key_vault_secret.arc_onboarding_spn_secret.value

infrastructure/terraform/outputs.tf

Lines changed: 0 additions & 44 deletions
This file was deleted.

scripts/powershell/arc-setup.ps1

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ param(
66
[string]$ResourceGroup,
77
[string]$Location,
88
[string]$ServicePrincipalId,
9-
[string]$ServicePrincipalSecret
9+
[string]$ServicePrincipalSecret,
10+
# Site identity - controls the Arc resource name and Azure tags.
11+
# SiteCode becomes the Arc machine name in Azure (e.g. gw-RVJ-01).
12+
# Defaults to the machine hostname when not supplied (test/dev use).
13+
[string]$SiteCode = "", # e.g. gw-RVJ-01 (ODS code + instance)
14+
[string]$SiteName = "", # e.g. North-Bristol-NHS-Trust (no spaces)
15+
[string]$NHSRegion = "", # nw|neyh|mids|eoe|lon|se|sw
16+
[string]$PacsVendor = "", # sectra|fujifilm|agfa|philips|carestream
17+
[string]$SiteType = "static", # static|mobile
18+
[string]$DeploymentRing = "ring0" # ring0|ring1|ring2|ring3|ring4
1019
)
1120

1221
$ErrorActionPreference = 'Stop'
@@ -26,6 +35,13 @@ try {
2635
Write-Log "=========================================" "INFO"
2736
Write-Log "Azure Arc Combined Setup Started" "INFO"
2837
Write-Log "=========================================" "INFO"
38+
Write-Log "SiteCode : $(if ($SiteCode) { $SiteCode } else { '(hostname)' })" "INFO"
39+
Write-Log "SiteName : $(if ($SiteName) { $SiteName } else { '(not set)' })" "INFO"
40+
Write-Log "NHSRegion : $(if ($NHSRegion) { $NHSRegion } else { '(not set)' })" "INFO"
41+
Write-Log "PacsVendor : $(if ($PacsVendor) { $PacsVendor } else { '(not set)' })" "INFO"
42+
Write-Log "SiteType : $SiteType" "INFO"
43+
Write-Log "DeploymentRing : $DeploymentRing" "INFO"
44+
Write-Log "=========================================" "INFO"
2945

3046
$CorrelationId = [guid]::NewGuid().ToString()
3147

@@ -121,20 +137,38 @@ try {
121137
$azcmagentExe = "$env:ProgramW6432\AzureConnectedMachineAgent\azcmagent.exe"
122138
if (-Not (Test-Path $azcmagentExe)) { throw "azcmagent.exe not found at $azcmagentExe" }
123139

124-
Write-Log "Subscription : $SubscriptionId" "INFO"
140+
Write-Log "Subscription : $SubscriptionId" "INFO"
125141
Write-Log "Resource Group: $ResourceGroup" "INFO"
126142
Write-Log "Location : $Location" "INFO"
127143

128-
& "$azcmagentExe" connect `
129-
--service-principal-id "$ServicePrincipalId" `
130-
--service-principal-secret "$ServicePrincipalSecret" `
131-
--resource-group "$ResourceGroup" `
132-
--tenant-id "$TenantId" `
133-
--location "$Location" `
134-
--subscription-id "$SubscriptionId" `
135-
--cloud "$env:CLOUD" `
136-
--tags 'ArcSQLServerExtensionDeployment=Disabled' `
137-
--correlation-id "$CorrelationId"
144+
# Build tags - all site metadata is stamped onto the Arc resource for Terraform
145+
# discovery and ADO pipeline targeting. SiteName must not contain spaces.
146+
$tags = "ArcSQLServerExtensionDeployment=Disabled"
147+
$tags += " Programme=BreastScreening"
148+
$tags += " SiteType=$SiteType"
149+
$tags += " DeploymentRing=$DeploymentRing"
150+
if ($SiteCode) { $tags += " SiteCode=$SiteCode" }
151+
if ($SiteName) { $tags += " SiteName=$SiteName" }
152+
if ($NHSRegion) { $tags += " NHSRegion=$NHSRegion" }
153+
if ($PacsVendor) { $tags += " PacsVendor=$PacsVendor" }
154+
155+
# Build connect arguments. --resource-name sets the Azure resource name,
156+
# overriding the default (hostname). Required for meaningful HC naming in Terraform.
157+
$connectArgs = @(
158+
'connect',
159+
'--service-principal-id', $ServicePrincipalId,
160+
'--service-principal-secret', $ServicePrincipalSecret,
161+
'--resource-group', $ResourceGroup,
162+
'--tenant-id', $TenantId,
163+
'--location', $Location,
164+
'--subscription-id', $SubscriptionId,
165+
'--cloud', $env:CLOUD,
166+
'--correlation-id', $CorrelationId,
167+
'--tags', $tags
168+
)
169+
if ($SiteCode) { $connectArgs += @('--resource-name', $SiteCode) }
170+
171+
& "$azcmagentExe" @connectArgs
138172

139173
if ($LASTEXITCODE -ne 0) { throw "Connection failed with exit code $LASTEXITCODE" }
140174
Write-Log "Successfully connected to Azure Arc!" "SUCCESS"

scripts/powershell/deploy.ps1

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,11 @@ try {
393393

394394
foreach ($svc in $services) {
395395
$batPath = Join-Path $versionDir "start-$($svc.Name).bat"
396-
$scriptPath = Join-Path "src" $svc.Script
397396
$batContent = @(
398397
'@echo off',
399-
'cd /d "%~dp0"',
400-
'set "PYTHONPATH=src"',
401-
('".venv\Scripts\python.exe" "' + $scriptPath + '"')
398+
"cd /d `"$BaseInstallPath`"",
399+
'set "PYTHONPATH=current\src"',
400+
('"current\.venv\Scripts\python.exe" "current\src\' + $svc.Script + '"')
402401
) -join "`r`n"
403402
[System.IO.File]::WriteAllText($batPath, $batContent, [System.Text.Encoding]::ASCII)
404403
}
@@ -476,7 +475,7 @@ foreach ($svc in $services) {
476475
}
477476

478477
Invoke-Nssm -NssmPath $nssmExe -Arguments @("install", $svc.Name, "$batPath") -Description "install $($svc.Name)"
479-
Invoke-Nssm -NssmPath $nssmExe -Arguments @("set", $svc.Name, "AppDirectory", "$currentJunction") -Description "set AppDirectory"
478+
Invoke-Nssm -NssmPath $nssmExe -Arguments @("set", $svc.Name, "AppDirectory", "$BaseInstallPath") -Description "set AppDirectory"
480479
Invoke-Nssm -NssmPath $nssmExe -Arguments @("set", $svc.Name, "Description", "Manage Breast Screening Gateway - $($svc.Name)") -Description "set Description"
481480
Invoke-Nssm -NssmPath $nssmExe -Arguments @("set", $svc.Name, "Start", "SERVICE_AUTO_START") -Description "set Start"
482481

@@ -535,7 +534,7 @@ if ($cutoverFailed) {
535534
$batPath = Join-Path $currentJunction "start-$($svc.Name).bat"
536535
if (Test-Path $batPath) {
537536
& $nssmExe set $svc.Name Application "$batPath" 2>&1 | Out-Null
538-
& $nssmExe set $svc.Name AppDirectory "$currentJunction" 2>&1 | Out-Null
537+
& $nssmExe set $svc.Name AppDirectory "$BaseInstallPath" 2>&1 | Out-Null
539538
}
540539
}
541540

0 commit comments

Comments
 (0)