@fortytwoservices/ai-universe-setup 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/app.bicep +145 -0
- package/assets/docker-compose.yml +50 -0
- package/assets/main.bicep +233 -0
- package/assets/modules/auditSecret.bicep +28 -0
- package/assets/modules/backendApp.bicep +107 -0
- package/assets/modules/containerApp.bicep +99 -0
- package/assets/modules/containerAppsEnv.bicep +43 -0
- package/assets/modules/controlGalaxyApp.bicep +129 -0
- package/assets/modules/entraClientSecret.bicep +31 -0
- package/assets/modules/innovationJob.bicep +91 -0
- package/assets/modules/insights.bicep +29 -0
- package/assets/modules/keyVault.bicep +42 -0
- package/assets/modules/platformIdentity.bicep +69 -0
- package/assets/modules/postgres.bicep +116 -0
- package/assets/modules/registry.bicep +38 -0
- package/assets/modules/storage.bicep +30 -0
- package/bin/setup.mjs +7 -0
- package/dist/azure/bicep.d.ts +9 -0
- package/dist/azure/bicep.d.ts.map +1 -0
- package/dist/azure/bicep.js +57 -0
- package/dist/azure/bicep.js.map +1 -0
- package/dist/azure/entra.d.ts +7 -0
- package/dist/azure/entra.d.ts.map +1 -0
- package/dist/azure/entra.js +45 -0
- package/dist/azure/entra.js.map +1 -0
- package/dist/azure/github.d.ts +5 -0
- package/dist/azure/github.d.ts.map +1 -0
- package/dist/azure/github.js +28 -0
- package/dist/azure/github.js.map +1 -0
- package/dist/azure/images.d.ts +15 -0
- package/dist/azure/images.d.ts.map +1 -0
- package/dist/azure/images.js +53 -0
- package/dist/azure/images.js.map +1 -0
- package/dist/azure/prerequisites.d.ts +8 -0
- package/dist/azure/prerequisites.d.ts.map +1 -0
- package/dist/azure/prerequisites.js +56 -0
- package/dist/azure/prerequisites.js.map +1 -0
- package/dist/azure/prompts.d.ts +10 -0
- package/dist/azure/prompts.d.ts.map +1 -0
- package/dist/azure/prompts.js +88 -0
- package/dist/azure/prompts.js.map +1 -0
- package/dist/azure/resource-group.d.ts +2 -0
- package/dist/azure/resource-group.d.ts.map +1 -0
- package/dist/azure/resource-group.js +8 -0
- package/dist/azure/resource-group.js.map +1 -0
- package/dist/azure/run.d.ts +2 -0
- package/dist/azure/run.d.ts.map +1 -0
- package/dist/azure/run.js +87 -0
- package/dist/azure/run.js.map +1 -0
- package/dist/azure/summary.d.ts +3 -0
- package/dist/azure/summary.d.ts.map +1 -0
- package/dist/azure/summary.js +13 -0
- package/dist/azure/summary.js.map +1 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +21 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/local/compose.d.ts +5 -0
- package/dist/local/compose.d.ts.map +1 -0
- package/dist/local/compose.js +40 -0
- package/dist/local/compose.js.map +1 -0
- package/dist/local/prerequisites.d.ts +3 -0
- package/dist/local/prerequisites.d.ts.map +1 -0
- package/dist/local/prerequisites.js +60 -0
- package/dist/local/prerequisites.js.map +1 -0
- package/dist/local/prompts.d.ts +6 -0
- package/dist/local/prompts.d.ts.map +1 -0
- package/dist/local/prompts.js +23 -0
- package/dist/local/prompts.js.map +1 -0
- package/dist/local/run.d.ts +2 -0
- package/dist/local/run.d.ts.map +1 -0
- package/dist/local/run.js +50 -0
- package/dist/local/run.js.map +1 -0
- package/dist/local/summary.d.ts +2 -0
- package/dist/local/summary.d.ts.map +1 -0
- package/dist/local/summary.js +16 -0
- package/dist/local/summary.js.map +1 -0
- package/dist/logo.d.ts +2 -0
- package/dist/logo.d.ts.map +1 -0
- package/dist/logo.js +8 -0
- package/dist/logo.js.map +1 -0
- package/dist/ui.d.ts +7 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +16 -0
- package/dist/ui.js.map +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +45 -0
- package/dist/utils.js.map +1 -0
- package/package.json +49 -0
package/assets/app.bicep
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Patch template — the deployment Fortytwo runs to ship a new kernel image to a
|
|
2
|
+
// customer WITHOUT touching their data or config.
|
|
3
|
+
//
|
|
4
|
+
// This template's deployment graph contains ONLY the container apps and the
|
|
5
|
+
// innovation job. Postgres (all customer data AND all config — providers,
|
|
6
|
+
// models, policies, wisdom, galaxies, connectors, branding, users), the Key
|
|
7
|
+
// Vault secrets, the storage account, and the identity role assignments are
|
|
8
|
+
// referenced as EXISTING resources, never declared here, so a patch can neither
|
|
9
|
+
// recreate nor overwrite them. Customer state survives every image bump.
|
|
10
|
+
//
|
|
11
|
+
// Onboarding (the one-time full provision) uses main.bicep. Patches use this.
|
|
12
|
+
//
|
|
13
|
+
// Deploy:
|
|
14
|
+
// az deployment group create \
|
|
15
|
+
// --resource-group <customer>-rg \
|
|
16
|
+
// --template-file app.bicep \
|
|
17
|
+
// --parameters portalName=<customer> \
|
|
18
|
+
// backendImage=<acr>/ai-universe-backend:<new-tag> \
|
|
19
|
+
// controlGalaxyImage=<acr>/ai-universe-control-galaxy:<new-tag> \
|
|
20
|
+
// entraTenantId=<...> entraAdminClientId=<...> portalOrgDid=<...>
|
|
21
|
+
//
|
|
22
|
+
// ACA rolls a new revision per app; existing revisions drain. No secret values
|
|
23
|
+
// are re-supplied because the secrets already live in the customer Key Vault.
|
|
24
|
+
|
|
25
|
+
targetScope = 'resourceGroup'
|
|
26
|
+
|
|
27
|
+
@description('Short, kebab-case portal name. Must match the original deployment.')
|
|
28
|
+
@minLength(2)
|
|
29
|
+
@maxLength(32)
|
|
30
|
+
param portalName string
|
|
31
|
+
|
|
32
|
+
@description('Azure region. Defaults to the resource group location.')
|
|
33
|
+
param location string = resourceGroup().location
|
|
34
|
+
|
|
35
|
+
@description('New backend image reference (login server + repo + tag).')
|
|
36
|
+
param backendImage string
|
|
37
|
+
|
|
38
|
+
@description('New control-galaxy image reference (login server + repo + tag).')
|
|
39
|
+
param controlGalaxyImage string
|
|
40
|
+
|
|
41
|
+
@description('Entra tenant id (unchanged from onboarding).')
|
|
42
|
+
param entraTenantId string
|
|
43
|
+
|
|
44
|
+
@description('Admin/config-plane Entra app registration client id (unchanged).')
|
|
45
|
+
param entraAdminClientId string
|
|
46
|
+
|
|
47
|
+
@description('Portal organization DID (unchanged).')
|
|
48
|
+
param portalOrgDid string
|
|
49
|
+
|
|
50
|
+
@description('Portal organization display name (unchanged).')
|
|
51
|
+
param portalOrgName string = portalName
|
|
52
|
+
|
|
53
|
+
// Conventional secret names written by main.bicep at onboarding.
|
|
54
|
+
var backendConnectionSecretName = 'postgres-connection-string-backend'
|
|
55
|
+
var controlConnectionSecretName = 'postgres-connection-string-control'
|
|
56
|
+
var auditSecretName = 'audit-signing-key'
|
|
57
|
+
var entraClientSecretName = 'entra-client-secret'
|
|
58
|
+
|
|
59
|
+
// --- Existing resources (read-only; never mutated by this template) --------
|
|
60
|
+
|
|
61
|
+
resource environment 'Microsoft.App/managedEnvironments@2024-03-01' existing = {
|
|
62
|
+
name: '${portalName}-cae'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
|
|
66
|
+
name: '${portalName}-platform-id'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = {
|
|
70
|
+
name: take('${replace(portalName, '-', '')}acr', 50)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resource keyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
|
|
74
|
+
// Must match keyVault.bicep's vaultName expression EXACTLY. uniqueString is
|
|
75
|
+
// deterministic per resource group, so this reconstructs the onboarded name.
|
|
76
|
+
name: '${take(portalName, 9)}-kv-${take(uniqueString(resourceGroup().id), 6)}'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = {
|
|
80
|
+
name: '${portalName}-insights'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Container apps (the only things this template deploys) -----------------
|
|
84
|
+
|
|
85
|
+
module backend 'modules/backendApp.bicep' = {
|
|
86
|
+
name: 'backendApp'
|
|
87
|
+
params: {
|
|
88
|
+
portalName: portalName
|
|
89
|
+
location: location
|
|
90
|
+
environmentId: environment.id
|
|
91
|
+
identityId: identity.id
|
|
92
|
+
acrLoginServer: registry.properties.loginServer
|
|
93
|
+
backendImage: backendImage
|
|
94
|
+
keyVaultUri: keyVault.properties.vaultUri
|
|
95
|
+
databaseSecretName: backendConnectionSecretName
|
|
96
|
+
auditSecretName: auditSecretName
|
|
97
|
+
insightsConnectionString: appInsights.properties.ConnectionString
|
|
98
|
+
entraTenantId: entraTenantId
|
|
99
|
+
entraAdminClientId: entraAdminClientId
|
|
100
|
+
portalOrgDid: portalOrgDid
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module controlGalaxy 'modules/controlGalaxyApp.bicep' = {
|
|
105
|
+
name: 'controlGalaxyApp'
|
|
106
|
+
params: {
|
|
107
|
+
portalName: portalName
|
|
108
|
+
location: location
|
|
109
|
+
environmentId: environment.id
|
|
110
|
+
identityId: identity.id
|
|
111
|
+
acrLoginServer: registry.properties.loginServer
|
|
112
|
+
controlGalaxyImage: controlGalaxyImage
|
|
113
|
+
backendBaseUrl: 'https://${backend.outputs.fqdn}'
|
|
114
|
+
keyVaultUri: keyVault.properties.vaultUri
|
|
115
|
+
databaseSecretName: controlConnectionSecretName
|
|
116
|
+
auditSecretName: auditSecretName
|
|
117
|
+
entraClientSecretName: entraClientSecretName
|
|
118
|
+
insightsConnectionString: appInsights.properties.ConnectionString
|
|
119
|
+
entraTenantId: entraTenantId
|
|
120
|
+
entraAdminClientId: entraAdminClientId
|
|
121
|
+
portalOrgDid: portalOrgDid
|
|
122
|
+
portalOrgName: portalOrgName
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The implementer job is stateless (state lives in the proposal record + the
|
|
127
|
+
// PR it opens), so a patch can safely roll a new implementer image alongside
|
|
128
|
+
// the apps. It references the existing shared environment + identity + vault by
|
|
129
|
+
// name, never recreating them. Keeping the image param default in sync with
|
|
130
|
+
// main.bicep is intentional so a patch and an onboarding produce the same job.
|
|
131
|
+
module innovationJob 'modules/innovationJob.bicep' = {
|
|
132
|
+
name: 'innovationJob'
|
|
133
|
+
params: {
|
|
134
|
+
portalName: portalName
|
|
135
|
+
location: location
|
|
136
|
+
environmentId: environment.id
|
|
137
|
+
identityId: identity.id
|
|
138
|
+
identityClientId: identity.properties.clientId
|
|
139
|
+
keyVaultName: keyVault.name
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
output backendUrl string = 'https://${backend.outputs.fqdn}'
|
|
144
|
+
output controlGalaxyUrl string = 'https://${controlGalaxy.outputs.fqdn}'
|
|
145
|
+
output implementerJobName string = innovationJob.outputs.jobName
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Fortytwo AI Universe — local evaluation stack
|
|
2
|
+
# Generated by @fortytwoservices/ai-universe-setup --local
|
|
3
|
+
version: '3.9'
|
|
4
|
+
services:
|
|
5
|
+
backend:
|
|
6
|
+
image: fortytwoacr.azurecr.io/ai-universe-backend:1.0.0
|
|
7
|
+
container_name: '${COMPOSE_PROJECT_NAME:-portal}-backend'
|
|
8
|
+
ports:
|
|
9
|
+
- '6101:6101'
|
|
10
|
+
environment:
|
|
11
|
+
NODE_ENV: development
|
|
12
|
+
DATABASE_URL: 'postgresql://postgres:postgres@postgres:5432/universe'
|
|
13
|
+
DEMO_MOCK_CHAT: 'true'
|
|
14
|
+
AUDIT_SIGNING_KEY: 'local-dev-key-not-for-production'
|
|
15
|
+
depends_on:
|
|
16
|
+
postgres:
|
|
17
|
+
condition: service_healthy
|
|
18
|
+
restart: unless-stopped
|
|
19
|
+
|
|
20
|
+
control-galaxy:
|
|
21
|
+
image: fortytwoacr.azurecr.io/ai-universe-control-galaxy:1.0.0
|
|
22
|
+
container_name: '${COMPOSE_PROJECT_NAME:-portal}-control-galaxy'
|
|
23
|
+
ports:
|
|
24
|
+
- '5174:5174'
|
|
25
|
+
environment:
|
|
26
|
+
BACKEND_BASE_URL: 'http://backend:6101'
|
|
27
|
+
depends_on:
|
|
28
|
+
- backend
|
|
29
|
+
restart: unless-stopped
|
|
30
|
+
|
|
31
|
+
postgres:
|
|
32
|
+
image: postgres:16-alpine
|
|
33
|
+
container_name: '${COMPOSE_PROJECT_NAME:-portal}-postgres'
|
|
34
|
+
ports:
|
|
35
|
+
- '5435:5432'
|
|
36
|
+
environment:
|
|
37
|
+
POSTGRES_USER: postgres
|
|
38
|
+
POSTGRES_PASSWORD: postgres
|
|
39
|
+
POSTGRES_DB: universe
|
|
40
|
+
volumes:
|
|
41
|
+
- postgres_data:/var/lib/postgresql/data
|
|
42
|
+
healthcheck:
|
|
43
|
+
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
|
44
|
+
interval: 5s
|
|
45
|
+
timeout: 5s
|
|
46
|
+
retries: 10
|
|
47
|
+
restart: unless-stopped
|
|
48
|
+
|
|
49
|
+
volumes:
|
|
50
|
+
postgres_data:
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Fortytwo AI Universe — Azure provisioning orchestrator (galaxy platform).
|
|
2
|
+
//
|
|
3
|
+
// One deployment = one customer, self-hosted in the customer's tenant
|
|
4
|
+
// (Fortytwo runs the deploy). Provisions, in the customer's resource group:
|
|
5
|
+
// - Container Registry (images imported from the Fortytwo central ACR)
|
|
6
|
+
// - Shared user-assigned identity (AcrPull + Key Vault Secrets User)
|
|
7
|
+
// - Key Vault, Postgres (two databases), App Insights, Storage
|
|
8
|
+
// - Container Apps environment
|
|
9
|
+
// - Backend kernel container app (gateway /v1, MCP /mcp, control-plane /admin/api)
|
|
10
|
+
// - Control galaxy container app (admin SPA + BFF over the backend config API)
|
|
11
|
+
// - Innovation Implementer job (short-lived, on the shared environment)
|
|
12
|
+
//
|
|
13
|
+
// NOT created here (manual one-time setup):
|
|
14
|
+
// - Entra ID app registrations. The admin/config plane uses ONE shared app
|
|
15
|
+
// registration (backend + control galaxy share its audience); the customer's
|
|
16
|
+
// IT admin creates it and supplies the client id + tenant id as parameters.
|
|
17
|
+
// Per-galaxy product registrations are minted by the backend provisioner at
|
|
18
|
+
// "create Galaxy" time, not here.
|
|
19
|
+
// - Image push/import. Fortytwo CI pushes images to the central Fortytwo ACR;
|
|
20
|
+
// the deploy step imports them into this customer ACR (see README).
|
|
21
|
+
|
|
22
|
+
targetScope = 'resourceGroup'
|
|
23
|
+
|
|
24
|
+
@description('Short, kebab-case name for the customer deployment (e.g. "acme-portal"). Used as a prefix for every resource.')
|
|
25
|
+
@minLength(2)
|
|
26
|
+
@maxLength(32)
|
|
27
|
+
param portalName string
|
|
28
|
+
|
|
29
|
+
@description('Azure region. Defaults to the resource group location.')
|
|
30
|
+
param location string = resourceGroup().location
|
|
31
|
+
|
|
32
|
+
@description('Postgres administrator login.')
|
|
33
|
+
param postgresAdmin string = 'portal_admin'
|
|
34
|
+
|
|
35
|
+
@secure()
|
|
36
|
+
@description('Postgres administrator password. Pass via parameter file env var; never commit.')
|
|
37
|
+
param postgresAdminPassword string
|
|
38
|
+
|
|
39
|
+
@secure()
|
|
40
|
+
@description('Audit HMAC signing key shared by the backend and control galaxy. Pass via env var; never commit.')
|
|
41
|
+
param auditSigningKey string
|
|
42
|
+
|
|
43
|
+
@secure()
|
|
44
|
+
@description('Entra app-registration client secret for connector OBO (used by the control galaxy). Pass via env var; never commit.')
|
|
45
|
+
param entraClientSecret string
|
|
46
|
+
|
|
47
|
+
@description('Postgres Flexible Server SKU.')
|
|
48
|
+
@allowed([
|
|
49
|
+
'Standard_B1ms'
|
|
50
|
+
'Standard_B2s'
|
|
51
|
+
'Standard_D2s_v3'
|
|
52
|
+
])
|
|
53
|
+
param postgresSku string = 'Standard_B1ms'
|
|
54
|
+
|
|
55
|
+
@description('Postgres storage size in GB.')
|
|
56
|
+
param postgresStorageGb int = 32
|
|
57
|
+
|
|
58
|
+
@description('Container Registry SKU.')
|
|
59
|
+
@allowed([
|
|
60
|
+
'Standard'
|
|
61
|
+
'Premium'
|
|
62
|
+
])
|
|
63
|
+
param registrySku string = 'Standard'
|
|
64
|
+
|
|
65
|
+
@description('Fully qualified backend image reference (login server + repo + tag).')
|
|
66
|
+
param backendImage string
|
|
67
|
+
|
|
68
|
+
@description('Fully qualified control-galaxy image reference (login server + repo + tag).')
|
|
69
|
+
param controlGalaxyImage string
|
|
70
|
+
|
|
71
|
+
@description('Entra ID tenant id.')
|
|
72
|
+
param entraTenantId string
|
|
73
|
+
|
|
74
|
+
@description('Admin/config-plane Entra app registration client id (shared audience: backend + control galaxy).')
|
|
75
|
+
param entraAdminClientId string
|
|
76
|
+
|
|
77
|
+
@description('Portal organization DID (identity namespace for agents and audit).')
|
|
78
|
+
param portalOrgDid string
|
|
79
|
+
|
|
80
|
+
@description('Portal organization display name.')
|
|
81
|
+
param portalOrgName string = portalName
|
|
82
|
+
|
|
83
|
+
// --- Foundational resources ------------------------------------------------
|
|
84
|
+
|
|
85
|
+
module insights 'modules/insights.bicep' = {
|
|
86
|
+
name: 'insights'
|
|
87
|
+
params: {
|
|
88
|
+
portalName: portalName
|
|
89
|
+
location: location
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module storage 'modules/storage.bicep' = {
|
|
94
|
+
name: 'storage'
|
|
95
|
+
params: {
|
|
96
|
+
portalName: portalName
|
|
97
|
+
location: location
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module keyVault 'modules/keyVault.bicep' = {
|
|
102
|
+
name: 'keyVault'
|
|
103
|
+
params: {
|
|
104
|
+
portalName: portalName
|
|
105
|
+
location: location
|
|
106
|
+
tenantId: entraTenantId
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module registry 'modules/registry.bicep' = {
|
|
111
|
+
name: 'registry'
|
|
112
|
+
params: {
|
|
113
|
+
portalName: portalName
|
|
114
|
+
location: location
|
|
115
|
+
sku: registrySku
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Shared identity must exist (with AcrPull + KV Secrets User) before any
|
|
120
|
+
// container app references it for image pull and secret resolution.
|
|
121
|
+
module identity 'modules/platformIdentity.bicep' = {
|
|
122
|
+
name: 'platformIdentity'
|
|
123
|
+
params: {
|
|
124
|
+
portalName: portalName
|
|
125
|
+
location: location
|
|
126
|
+
registryName: registry.outputs.registryName
|
|
127
|
+
keyVaultName: keyVault.outputs.vaultName
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module postgres 'modules/postgres.bicep' = {
|
|
132
|
+
name: 'postgres'
|
|
133
|
+
params: {
|
|
134
|
+
portalName: portalName
|
|
135
|
+
location: location
|
|
136
|
+
administratorLogin: postgresAdmin
|
|
137
|
+
administratorPassword: postgresAdminPassword
|
|
138
|
+
sku: postgresSku
|
|
139
|
+
storageGb: postgresStorageGb
|
|
140
|
+
keyVaultName: keyVault.outputs.vaultName
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Audit HMAC signing key, shared by backend + control galaxy.
|
|
145
|
+
module auditSecret 'modules/auditSecret.bicep' = {
|
|
146
|
+
name: 'auditSecret'
|
|
147
|
+
params: {
|
|
148
|
+
keyVaultName: keyVault.outputs.vaultName
|
|
149
|
+
auditSigningKey: auditSigningKey
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Entra app-registration client secret for connector OBO (control galaxy).
|
|
154
|
+
module entraClientSecretModule 'modules/entraClientSecret.bicep' = {
|
|
155
|
+
name: 'entraClientSecret'
|
|
156
|
+
params: {
|
|
157
|
+
keyVaultName: keyVault.outputs.vaultName
|
|
158
|
+
entraClientSecret: entraClientSecret
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- Compute ---------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
module caeEnv 'modules/containerAppsEnv.bicep' = {
|
|
165
|
+
name: 'containerAppsEnv'
|
|
166
|
+
params: {
|
|
167
|
+
portalName: portalName
|
|
168
|
+
location: location
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module backend 'modules/backendApp.bicep' = {
|
|
173
|
+
name: 'backendApp'
|
|
174
|
+
params: {
|
|
175
|
+
portalName: portalName
|
|
176
|
+
location: location
|
|
177
|
+
environmentId: caeEnv.outputs.environmentId
|
|
178
|
+
identityId: identity.outputs.identityId
|
|
179
|
+
acrLoginServer: registry.outputs.loginServer
|
|
180
|
+
backendImage: backendImage
|
|
181
|
+
keyVaultUri: keyVault.outputs.vaultUri
|
|
182
|
+
databaseSecretName: postgres.outputs.backendConnectionSecretName
|
|
183
|
+
auditSecretName: auditSecret.outputs.secretName
|
|
184
|
+
insightsConnectionString: insights.outputs.connectionString
|
|
185
|
+
entraTenantId: entraTenantId
|
|
186
|
+
entraAdminClientId: entraAdminClientId
|
|
187
|
+
portalOrgDid: portalOrgDid
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module controlGalaxy 'modules/controlGalaxyApp.bicep' = {
|
|
192
|
+
name: 'controlGalaxyApp'
|
|
193
|
+
params: {
|
|
194
|
+
portalName: portalName
|
|
195
|
+
location: location
|
|
196
|
+
environmentId: caeEnv.outputs.environmentId
|
|
197
|
+
identityId: identity.outputs.identityId
|
|
198
|
+
acrLoginServer: registry.outputs.loginServer
|
|
199
|
+
controlGalaxyImage: controlGalaxyImage
|
|
200
|
+
backendBaseUrl: 'https://${backend.outputs.fqdn}'
|
|
201
|
+
keyVaultUri: keyVault.outputs.vaultUri
|
|
202
|
+
databaseSecretName: postgres.outputs.controlConnectionSecretName
|
|
203
|
+
auditSecretName: auditSecret.outputs.secretName
|
|
204
|
+
entraClientSecretName: entraClientSecretModule.outputs.secretName
|
|
205
|
+
insightsConnectionString: insights.outputs.connectionString
|
|
206
|
+
entraTenantId: entraTenantId
|
|
207
|
+
entraAdminClientId: entraAdminClientId
|
|
208
|
+
portalOrgDid: portalOrgDid
|
|
209
|
+
portalOrgName: portalOrgName
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module innovationJob 'modules/innovationJob.bicep' = {
|
|
214
|
+
name: 'innovationJob'
|
|
215
|
+
params: {
|
|
216
|
+
portalName: portalName
|
|
217
|
+
location: location
|
|
218
|
+
environmentId: caeEnv.outputs.environmentId
|
|
219
|
+
identityId: identity.outputs.identityId
|
|
220
|
+
identityClientId: identity.outputs.clientId
|
|
221
|
+
keyVaultName: keyVault.outputs.vaultName
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- Outputs ---------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
output backendUrl string = 'https://${backend.outputs.fqdn}'
|
|
228
|
+
output controlGalaxyUrl string = 'https://${controlGalaxy.outputs.fqdn}'
|
|
229
|
+
output registryLoginServer string = registry.outputs.loginServer
|
|
230
|
+
output postgresFqdn string = postgres.outputs.fqdn
|
|
231
|
+
output keyVaultName string = keyVault.outputs.vaultName
|
|
232
|
+
output storageAccountName string = storage.outputs.accountName
|
|
233
|
+
output implementerJobName string = innovationJob.outputs.jobName
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Audit HMAC signing key, shared by the backend and the control galaxy.
|
|
2
|
+
// Written here (not in keyVault.bicep) so the @secure() value stays inside a
|
|
3
|
+
// module that references the vault by name param, keeping it out of the
|
|
4
|
+
// orchestration deployment graph as an output.
|
|
5
|
+
|
|
6
|
+
@description('Name of the existing Key Vault to write the secret into.')
|
|
7
|
+
param keyVaultName string
|
|
8
|
+
|
|
9
|
+
@secure()
|
|
10
|
+
@description('Audit HMAC signing key value.')
|
|
11
|
+
param auditSigningKey string
|
|
12
|
+
|
|
13
|
+
@description('Secret name.')
|
|
14
|
+
param secretName string = 'audit-signing-key'
|
|
15
|
+
|
|
16
|
+
resource vault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
|
|
17
|
+
name: keyVaultName
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
resource secret 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = {
|
|
21
|
+
parent: vault
|
|
22
|
+
name: secretName
|
|
23
|
+
properties: {
|
|
24
|
+
value: auditSigningKey
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
output secretName string = secret.name
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Backend kernel as a Container App. This is the headless control plane:
|
|
2
|
+
// it serves the AI gateway (/v1), the MCP hub (/mcp), the galaxy registry +
|
|
3
|
+
// provisioner, the durable audit, identity verification, and the control-plane
|
|
4
|
+
// config API (/admin/api/*). External ingress on 6101 because galaxies reach
|
|
5
|
+
// /v1 with a per-galaxy key, developer tools reach /mcp, and the control galaxy
|
|
6
|
+
// reaches /admin/api with the forwarded operator token, all across the network
|
|
7
|
+
// boundary. minReplicas is 1: the gateway is on the runtime hot path (ADR-0003).
|
|
8
|
+
//
|
|
9
|
+
// Secrets (DATABASE_URL, AUDIT_SIGNING_KEY) resolve from Key Vault over the
|
|
10
|
+
// shared user-assigned identity. Images pull from the customer ACR over the
|
|
11
|
+
// same identity (AcrPull granted in platformIdentity.bicep). No stored secret.
|
|
12
|
+
|
|
13
|
+
@description('Short, kebab-case portal name.')
|
|
14
|
+
param portalName string
|
|
15
|
+
|
|
16
|
+
@description('Azure region.')
|
|
17
|
+
param location string
|
|
18
|
+
|
|
19
|
+
@description('Container Apps managed environment id (containerAppsEnv.bicep).')
|
|
20
|
+
param environmentId string
|
|
21
|
+
|
|
22
|
+
@description('User-assigned managed identity resource id (platformIdentity.bicep).')
|
|
23
|
+
param identityId string
|
|
24
|
+
|
|
25
|
+
@description('ACR login server (e.g. acmeacr.azurecr.io).')
|
|
26
|
+
param acrLoginServer string
|
|
27
|
+
|
|
28
|
+
@description('Fully qualified backend image reference, including tag.')
|
|
29
|
+
param backendImage string
|
|
30
|
+
|
|
31
|
+
@description('Key Vault URI (https://<vault>.vault.azure.net/).')
|
|
32
|
+
param keyVaultUri string
|
|
33
|
+
|
|
34
|
+
@description('Key Vault secret name holding the backend database connection string.')
|
|
35
|
+
param databaseSecretName string
|
|
36
|
+
|
|
37
|
+
@description('Key Vault secret name holding the audit HMAC signing key.')
|
|
38
|
+
param auditSecretName string
|
|
39
|
+
|
|
40
|
+
@description('Application Insights connection string.')
|
|
41
|
+
param insightsConnectionString string
|
|
42
|
+
|
|
43
|
+
@description('Entra tenant id for token validation.')
|
|
44
|
+
param entraTenantId string
|
|
45
|
+
|
|
46
|
+
@description('Admin/config-plane Entra app registration client id (shared audience with the control galaxy).')
|
|
47
|
+
param entraAdminClientId string
|
|
48
|
+
|
|
49
|
+
@description('Portal organization DID (identity namespace for agents and audit).')
|
|
50
|
+
param portalOrgDid string
|
|
51
|
+
|
|
52
|
+
@description('CPU cores.')
|
|
53
|
+
param cpuCores string = '1.0'
|
|
54
|
+
|
|
55
|
+
@description('Memory.')
|
|
56
|
+
param memorySize string = '2Gi'
|
|
57
|
+
|
|
58
|
+
// Domain-specific secrets[] and env[]. These literal arrays stay here (rather
|
|
59
|
+
// than in containerApp.bicep) so the infra contract test can parse the env
|
|
60
|
+
// contract from this module. The shared containerApps scaffolding lives in
|
|
61
|
+
// containerApp.bicep.
|
|
62
|
+
var backendSecrets = [
|
|
63
|
+
{
|
|
64
|
+
name: 'database-url'
|
|
65
|
+
keyVaultUrl: '${keyVaultUri}secrets/${databaseSecretName}'
|
|
66
|
+
identity: identityId
|
|
67
|
+
}
|
|
68
|
+
{
|
|
69
|
+
name: 'audit-signing-key'
|
|
70
|
+
keyVaultUrl: '${keyVaultUri}secrets/${auditSecretName}'
|
|
71
|
+
identity: identityId
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
var backendEnv = [
|
|
76
|
+
{ name: 'NODE_ENV', value: 'production' }
|
|
77
|
+
{ name: 'HOST', value: '0.0.0.0' }
|
|
78
|
+
{ name: 'PORT', value: '6101' }
|
|
79
|
+
{ name: 'DATABASE_URL', secretRef: 'database-url' }
|
|
80
|
+
{ name: 'AUDIT_SIGNING_KEY', secretRef: 'audit-signing-key' }
|
|
81
|
+
{ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: insightsConnectionString }
|
|
82
|
+
{ name: 'ENTRA_TENANT_ID', value: entraTenantId }
|
|
83
|
+
{ name: 'BACKEND_API_AUDIENCE', value: 'api://${entraAdminClientId}' }
|
|
84
|
+
{ name: 'PORTAL_ORG_DID', value: portalOrgDid }
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
module backend 'containerApp.bicep' = {
|
|
88
|
+
name: 'backendContainerApp'
|
|
89
|
+
params: {
|
|
90
|
+
portalName: portalName
|
|
91
|
+
appSuffix: 'backend'
|
|
92
|
+
location: location
|
|
93
|
+
environmentId: environmentId
|
|
94
|
+
identityId: identityId
|
|
95
|
+
acrLoginServer: acrLoginServer
|
|
96
|
+
image: backendImage
|
|
97
|
+
port: 6101
|
|
98
|
+
maxReplicas: 3
|
|
99
|
+
cpuCores: cpuCores
|
|
100
|
+
memorySize: memorySize
|
|
101
|
+
secrets: backendSecrets
|
|
102
|
+
env: backendEnv
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
output appName string = backend.outputs.appName
|
|
107
|
+
output fqdn string = backend.outputs.fqdn
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Shared Container App scaffolding for the platform's HTTP surfaces (backend
|
|
2
|
+
// kernel, control galaxy). Both wrap this module: they assemble their own
|
|
3
|
+
// domain-specific secrets[] and env[] arrays and pass them in here, so the
|
|
4
|
+
// identity, ACR registry binding, Key-Vault secret wiring, external ingress,
|
|
5
|
+
// and single-revision scale shape live in exactly one place.
|
|
6
|
+
//
|
|
7
|
+
// What stays in the wrapper modules (backendApp.bicep, controlGalaxyApp.bicep):
|
|
8
|
+
// the literal env[] and secrets[] arrays. The infra contract test parses those
|
|
9
|
+
// arrays from the wrapper files, so keeping them there preserves both the
|
|
10
|
+
// env-contract guard and the per-app readability of the config.
|
|
11
|
+
|
|
12
|
+
@description('Short, kebab-case portal name.')
|
|
13
|
+
param portalName string
|
|
14
|
+
|
|
15
|
+
@description('Suffix appended to portalName for the app + container name (e.g. backend, control-galaxy).')
|
|
16
|
+
param appSuffix string
|
|
17
|
+
|
|
18
|
+
@description('Azure region.')
|
|
19
|
+
param location string
|
|
20
|
+
|
|
21
|
+
@description('Container Apps managed environment id (containerAppsEnv.bicep).')
|
|
22
|
+
param environmentId string
|
|
23
|
+
|
|
24
|
+
@description('User-assigned managed identity resource id (platformIdentity.bicep).')
|
|
25
|
+
param identityId string
|
|
26
|
+
|
|
27
|
+
@description('ACR login server (e.g. acmeacr.azurecr.io).')
|
|
28
|
+
param acrLoginServer string
|
|
29
|
+
|
|
30
|
+
@description('Fully qualified container image reference, including tag.')
|
|
31
|
+
param image string
|
|
32
|
+
|
|
33
|
+
@description('Ingress target port.')
|
|
34
|
+
param port int
|
|
35
|
+
|
|
36
|
+
@description('Maximum replica count. minReplicas is fixed at 1 (runtime hot path, ADR-0003).')
|
|
37
|
+
param maxReplicas int
|
|
38
|
+
|
|
39
|
+
@description('CPU cores.')
|
|
40
|
+
param cpuCores string
|
|
41
|
+
|
|
42
|
+
@description('Memory.')
|
|
43
|
+
param memorySize string
|
|
44
|
+
|
|
45
|
+
@description('Key-Vault-backed container secrets ({ name, keyVaultUrl, identity }).')
|
|
46
|
+
param secrets array
|
|
47
|
+
|
|
48
|
+
@description('Container env entries ({ name, value } or { name, secretRef }).')
|
|
49
|
+
param env array
|
|
50
|
+
|
|
51
|
+
resource app 'Microsoft.App/containerApps@2024-03-01' = {
|
|
52
|
+
name: '${portalName}-${appSuffix}'
|
|
53
|
+
location: location
|
|
54
|
+
identity: {
|
|
55
|
+
type: 'UserAssigned'
|
|
56
|
+
userAssignedIdentities: {
|
|
57
|
+
'${identityId}': {}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
properties: {
|
|
61
|
+
managedEnvironmentId: environmentId
|
|
62
|
+
configuration: {
|
|
63
|
+
activeRevisionsMode: 'Single'
|
|
64
|
+
ingress: {
|
|
65
|
+
external: true
|
|
66
|
+
targetPort: port
|
|
67
|
+
transport: 'auto'
|
|
68
|
+
allowInsecure: false
|
|
69
|
+
}
|
|
70
|
+
registries: [
|
|
71
|
+
{
|
|
72
|
+
server: acrLoginServer
|
|
73
|
+
identity: identityId
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
secrets: secrets
|
|
77
|
+
}
|
|
78
|
+
template: {
|
|
79
|
+
containers: [
|
|
80
|
+
{
|
|
81
|
+
name: appSuffix
|
|
82
|
+
image: image
|
|
83
|
+
resources: {
|
|
84
|
+
cpu: json(cpuCores)
|
|
85
|
+
memory: memorySize
|
|
86
|
+
}
|
|
87
|
+
env: env
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
scale: {
|
|
91
|
+
minReplicas: 1
|
|
92
|
+
maxReplicas: maxReplicas
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
output appName string = app.name
|
|
99
|
+
output fqdn string = app.properties.configuration.ingress.fqdn
|