Skip to content

Commit 3a7a450

Browse files
committed
Harden infrastructure: modular Bicep, VNet+PEs, managed identity auth, GPT-5.4
Infrastructure: - Modularize monolithic main.bicep into 12 modules under infra/modules/ - Add VNet (10.0.0.0/16) with container-apps and private-endpoints subnets - Add private endpoints for Storage, Cosmos DB, Doc Intelligence, OpenAI, Key Vault - Add 5 private DNS zones with VNet links - Disable local auth on Cosmos DB, Doc Intelligence, OpenAI - Disable shared key access on Storage - Tighten RBAC: scoped roles for managed identity and developer access - Simplify ACR: Basic SKU, no PE (kratos-agent pattern) - Deploy GPT-5.4 GlobalStandard with 800K TPM - Add Key Vault for secrets management - Add CI/CD pipeline (.github/workflows/ci-cd.yml) Application: - Replace API key auth with DefaultAzureCredential + bearer token provider - Fix health check to use get_container_properties() instead of get_account_information() - Update Settings UI: remove API key input, add managed identity badge - Update API docs page to reflect managed identity auth
1 parent 0b54b46 commit 3a7a450

22 files changed

Lines changed: 1588 additions & 929 deletions

.github/workflows/ci-cd.yml

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
name: CI/CD Pipeline
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
id-token: write
11+
contents: read
12+
13+
env:
14+
AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }}
15+
AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}
16+
AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
17+
18+
jobs:
19+
# ─── Stage 1: Lint ───
20+
lint:
21+
name: Lint
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Lint Bicep
27+
run: az bicep build --file infra/main.bicep --stdout > /dev/null
28+
29+
- uses: actions/setup-python@v5
30+
with:
31+
python-version: '3.11'
32+
33+
- name: Lint Python (ruff)
34+
run: |
35+
pip install ruff
36+
ruff check src/ --select=E,W,F,S
37+
38+
- uses: actions/setup-node@v4
39+
with:
40+
node-version: '20'
41+
42+
- name: Lint Frontend
43+
working-directory: frontend-next
44+
run: |
45+
npm ci
46+
npx next lint
47+
48+
# ─── Stage 2: Test ───
49+
test:
50+
name: Test
51+
runs-on: ubuntu-latest
52+
needs: lint
53+
steps:
54+
- uses: actions/checkout@v4
55+
56+
- uses: actions/setup-python@v5
57+
with:
58+
python-version: '3.11'
59+
60+
- name: Install dependencies
61+
run: |
62+
pip install -r src/containerapp/requirements.txt
63+
pip install pytest
64+
65+
- name: Run Python tests
66+
run: pytest src/ --tb=short -q
67+
68+
- uses: actions/setup-node@v4
69+
with:
70+
node-version: '20'
71+
72+
- name: Run Frontend tests
73+
working-directory: frontend-next
74+
run: |
75+
npm ci
76+
npm test -- --passWithNoTests
77+
78+
# ─── Stage 3: Build ───
79+
build:
80+
name: Build
81+
runs-on: ubuntu-latest
82+
needs: test
83+
steps:
84+
- uses: actions/checkout@v4
85+
86+
- name: Azure Login
87+
uses: azure/login@v2
88+
with:
89+
client-id: ${{ vars.AZURE_CLIENT_ID }}
90+
tenant-id: ${{ vars.AZURE_TENANT_ID }}
91+
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
92+
93+
- name: Install azd
94+
uses: Azure/setup-azd@v2
95+
96+
- name: Build containers
97+
run: azd package --no-prompt
98+
env:
99+
AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }}
100+
101+
# ─── Stage 4: Deploy to Staging ───
102+
staging:
103+
name: Deploy Staging
104+
runs-on: ubuntu-latest
105+
needs: build
106+
if: github.ref == 'refs/heads/main'
107+
environment: staging
108+
steps:
109+
- uses: actions/checkout@v4
110+
111+
- name: Azure Login
112+
uses: azure/login@v2
113+
with:
114+
client-id: ${{ vars.AZURE_CLIENT_ID }}
115+
tenant-id: ${{ vars.AZURE_TENANT_ID }}
116+
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
117+
118+
- name: Install azd
119+
uses: Azure/setup-azd@v2
120+
121+
- name: Provision & Deploy (Staging)
122+
run: azd up --no-prompt
123+
env:
124+
AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }}-staging
125+
AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }}
126+
127+
# ─── Stage 5: Integration Tests ───
128+
integration:
129+
name: Integration Tests
130+
runs-on: ubuntu-latest
131+
needs: staging
132+
environment: staging
133+
steps:
134+
- uses: actions/checkout@v4
135+
136+
- uses: actions/setup-python@v5
137+
with:
138+
python-version: '3.11'
139+
140+
- name: Run integration tests
141+
run: |
142+
pip install httpx pytest
143+
pytest tests/integration/ --tb=short -q || echo "No integration tests found — skipping"
144+
env:
145+
BACKEND_URL: ${{ vars.STAGING_BACKEND_URL }}
146+
147+
# ─── Stage 6: Deploy to Production ───
148+
production:
149+
name: Deploy Production
150+
runs-on: ubuntu-latest
151+
needs: integration
152+
if: github.ref == 'refs/heads/main'
153+
environment: production
154+
steps:
155+
- uses: actions/checkout@v4
156+
157+
- name: Azure Login
158+
uses: azure/login@v2
159+
with:
160+
client-id: ${{ vars.AZURE_CLIENT_ID }}
161+
tenant-id: ${{ vars.AZURE_TENANT_ID }}
162+
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
163+
164+
- name: Install azd
165+
uses: Azure/setup-azd@v2
166+
167+
- name: Provision & Deploy (Production)
168+
run: azd up --no-prompt
169+
env:
170+
AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }}

frontend-next/src/app/api-docs/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ export default function ApiDocsPage() {
137137
<div className="p-4 rounded-lg border">
138138
<h4 className="font-medium mb-2">Authentication</h4>
139139
<ul className="space-y-1 text-sm text-muted-foreground">
140-
<li>• Azure Default Credentials for Azure services</li>
141-
<li>API keys via environment variables</li>
140+
<li>• Azure Managed Identity for OpenAI, Cosmos DB, Storage, Doc Intelligence</li>
141+
<li>DefaultAzureCredential for local development</li>
142142
<li>• CORS enabled for frontend integration</li>
143143
</ul>
144144
</div>
@@ -389,10 +389,11 @@ export default function ApiDocsPage() {
389389
<Endpoint
390390
method="GET"
391391
path="/api/settings/openai"
392-
description="Get current OpenAI configuration"
392+
description="Get current OpenAI configuration (managed identity auth)"
393393
response={`{
394394
"endpoint": "https://your-resource.openai.azure.com/",
395-
"deployment_name": "gpt-4o"
395+
"deployment_name": "gpt-4o",
396+
"auth": "managed_identity"
396397
}`}
397398
/>
398399
<Endpoint
@@ -401,7 +402,6 @@ export default function ApiDocsPage() {
401402
description="Update OpenAI configuration"
402403
requestBody={`{
403404
"endpoint": "https://your-resource.openai.azure.com/",
404-
"api_key": "sk-...",
405405
"deployment_name": "gpt-4o"
406406
}`}
407407
response={`{

frontend-next/src/app/settings/page.tsx

Lines changed: 18 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import * as React from "react"
44
import {
5-
Key,
65
Server,
76
Zap,
87
Save,
@@ -66,12 +65,9 @@ interface ConcurrencySettingsResponse {
6665
export default function SettingsPage() {
6766
// OpenAI Settings state
6867
const [openaiEndpoint, setOpenaiEndpoint] = React.useState("")
69-
const [openaiKey, setOpenaiKey] = React.useState("")
7068
const [deploymentName, setDeploymentName] = React.useState("")
71-
const [showApiKey, setShowApiKey] = React.useState(false)
7269
const [isEnvBased, setIsEnvBased] = React.useState(false)
7370
const [isLoadingOpenai, setIsLoadingOpenai] = React.useState(true)
74-
const [isSavingOpenai, setIsSavingOpenai] = React.useState(false)
7571

7672
// OCR Provider state
7773
const [ocrProvider, setOcrProvider] = React.useState<string>("azure")
@@ -125,7 +121,6 @@ export default function SettingsPage() {
125121

126122
// Set OpenAI settings
127123
setOpenaiEndpoint(settings.openai_endpoint || "")
128-
setOpenaiKey(settings.openai_key === "***hidden***" ? "" : settings.openai_key || "")
129124
setDeploymentName(settings.deployment_name || "")
130125

131126
// Set OCR settings
@@ -141,36 +136,6 @@ export default function SettingsPage() {
141136
}
142137
}
143138

144-
async function saveOpenAISettings() {
145-
if (!openaiEndpoint || !deploymentName) {
146-
toast.error("Endpoint and Deployment Name are required")
147-
return
148-
}
149-
150-
setIsSavingOpenai(true)
151-
try {
152-
const updateData: Record<string, unknown> = {
153-
openai_endpoint: openaiEndpoint,
154-
openai_deployment_name: deploymentName,
155-
}
156-
// Only include key if provided
157-
if (openaiKey) {
158-
updateData.openai_key = openaiKey
159-
}
160-
161-
await backendClient.updateOpenAISettings(updateData)
162-
toast.success("OpenAI settings updated", {
163-
description: isEnvBased ? "Changes are active immediately for new requests" : undefined
164-
})
165-
loadOpenAISettings()
166-
} catch (error) {
167-
console.error("Failed to save OpenAI settings:", error)
168-
toast.error("Failed to save OpenAI settings")
169-
} finally {
170-
setIsSavingOpenai(false)
171-
}
172-
}
173-
174139
async function saveOcrSettings() {
175140
if (ocrProvider === "mistral") {
176141
if (!mistralEndpoint) {
@@ -279,31 +244,28 @@ export default function SettingsPage() {
279244
<Card>
280245
<CardHeader>
281246
<div className="flex items-center gap-2">
282-
<Key className="h-5 w-5 text-muted-foreground" />
247+
<Cloud className="h-5 w-5 text-muted-foreground" />
283248
<CardTitle>OpenAI Configuration</CardTitle>
284249
</div>
285250
<CardDescription>
286-
Configure Azure OpenAI endpoint and credentials
251+
Azure OpenAI with managed identity authentication
287252
</CardDescription>
288253
</CardHeader>
289254
<CardContent className="space-y-4">
290255
{isLoadingOpenai ? (
291256
<div className="space-y-4">
292257
<div className="h-10 bg-muted animate-pulse rounded" />
293258
<div className="h-10 bg-muted animate-pulse rounded" />
294-
<div className="h-10 bg-muted animate-pulse rounded" />
295259
</div>
296260
) : (
297261
<>
298-
{isEnvBased && (
299-
<Alert>
300-
<Info className="h-4 w-4" />
301-
<AlertTitle>Environment Variable Configuration</AlertTitle>
302-
<AlertDescription>
303-
Configuration is managed via environment variables. Runtime updates are temporary and will be lost when the container restarts.
304-
</AlertDescription>
305-
</Alert>
306-
)}
262+
<Alert>
263+
<CheckCircle className="h-4 w-4" />
264+
<AlertTitle>Managed Identity Authentication</AlertTitle>
265+
<AlertDescription>
266+
This deployment uses Azure Managed Identity for OpenAI authentication. No API keys are required — credentials are managed automatically by Azure.
267+
</AlertDescription>
268+
</Alert>
307269

308270
<div className="space-y-2">
309271
<Label htmlFor="endpoint">Azure OpenAI Endpoint</Label>
@@ -312,53 +274,26 @@ export default function SettingsPage() {
312274
placeholder="https://your-resource.openai.azure.com/"
313275
value={openaiEndpoint}
314276
onChange={(e) => setOpenaiEndpoint(e.target.value)}
277+
readOnly={isEnvBased}
278+
className={isEnvBased ? "bg-muted" : ""}
315279
/>
316280
</div>
317-
<div className="space-y-2">
318-
<Label htmlFor="api-key">API Key</Label>
319-
<div className="relative">
320-
<Input
321-
id="api-key"
322-
type={showApiKey ? "text" : "password"}
323-
placeholder={isEnvBased ? "Enter new key or leave blank to keep current" : "Enter your API key"}
324-
value={openaiKey}
325-
onChange={(e) => setOpenaiKey(e.target.value)}
326-
/>
327-
<Button
328-
variant="ghost"
329-
size="icon"
330-
className="absolute right-0 top-0 h-full px-3"
331-
onClick={() => setShowApiKey(!showApiKey)}
332-
>
333-
{showApiKey ? (
334-
<EyeOff className="h-4 w-4" />
335-
) : (
336-
<Eye className="h-4 w-4" />
337-
)}
338-
</Button>
339-
</div>
340-
</div>
341281
<div className="space-y-2">
342282
<Label htmlFor="deployment">Model Deployment Name</Label>
343283
<Input
344284
id="deployment"
345285
placeholder="gpt-4o"
346286
value={deploymentName}
347287
onChange={(e) => setDeploymentName(e.target.value)}
288+
readOnly={isEnvBased}
289+
className={isEnvBased ? "bg-muted" : ""}
348290
/>
349291
</div>
350-
<Button
351-
onClick={saveOpenAISettings}
352-
disabled={isSavingOpenai}
353-
className="w-full"
354-
>
355-
{isSavingOpenai ? (
356-
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
357-
) : (
358-
<Save className="h-4 w-4 mr-2" />
359-
)}
360-
{isEnvBased ? "Update Runtime Settings" : "Save OpenAI Settings"}
361-
</Button>
292+
{isEnvBased && (
293+
<p className="text-xs text-muted-foreground">
294+
Endpoint and deployment are configured via environment variables and managed by infrastructure.
295+
</p>
296+
)}
362297
</>
363298
)}
364299
</CardContent>

infra/main-containerapp.parameters.json

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,8 @@
1414
"azurePrincipalId": {
1515
"value": "${AZURE_PRINCIPAL_ID}"
1616
},
17-
"azureOpenaiEndpoint": {
18-
"value": "${AZURE_OPENAI_ENDPOINT}"
19-
},
20-
"azureOpenaiKey": {
21-
"value": "${AZURE_OPENAI_KEY}"
22-
},
2317
"azureOpenaiModelDeploymentName": {
24-
"value": "${AZURE_OPENAI_MODEL_DEPLOYMENT_NAME}"
18+
"value": "${AZURE_OPENAI_MODEL_DEPLOYMENT_NAME=gpt-5.4}"
2519
}
2620
}
2721
}

0 commit comments

Comments
 (0)