Skip to content

Commit c38a741

Browse files
committed
Update SystemSuiteEndpoints status codes and add production infrastructure plan
1 parent d039be5 commit c38a741

2 files changed

Lines changed: 246 additions & 7 deletions

File tree

infrastructure_plan.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Production Infrastructure Plan for UMS
2+
3+
## Overview
4+
This plan details how to deploy the **Ums** solution in production, covering the **React Vite web front‑end**, the **.NET 10 API**, and a **PostgreSQL** database. It follows the corporate standards (`BMAD‑METHOD`, clean‑architecture, tenancy enforcement) and uses managed cloud services to achieve high availability, security, and observability.
5+
6+
---
7+
8+
## 1. Architecture Diagram
9+
```mermaid
10+
flowchart LR
11+
subgraph Client
12+
Browser["Web Browser (React Vite)" ]
13+
end
14+
subgraph CDN
15+
Edge["Edge CDN (Static assets)" ]
16+
end
17+
subgraph API
18+
LoadBal["Load Balancer (Azure Front Door)" ]
19+
APIGW["API Gateway (Azure API Management)" ]
20+
API["Ums API (Docker, .NET 10)" ]
21+
end
22+
subgraph DB
23+
PG["PostgreSQL (Azure Flexible Server)" ]
24+
end
25+
subgraph Monitoring
26+
Log["Log Analytics"]
27+
Metrics["Azure Monitor"]
28+
end
29+
Browser -->|HTTPS| Edge -->|HTTPS| LoadBal --> APIGW --> API --> PG
30+
API --> Log
31+
API --> Metrics
32+
```
33+
---
34+
35+
## 2. Containerisation & Images
36+
| Component | Dockerfile location | Base Image | Build command |
37+
|----------|--------------------|------------|---------------|
38+
| **Web** | `src/apps/ums.web-app/Dockerfile` | `node:20-alpine` (build) → `nginx:alpine` (runtime) | `docker build -t beyondnetcode/ums-web:$(git rev-parse --short HEAD) .` |
39+
| **API** | `src/apps/ums.api/Ums.Presentation/Dockerfile` | `mcr.microsoft.com/dotnet/aspnet:10.0` (runtime) + `mcr.microsoft.com/dotnet/sdk:10.0` (build) | `docker build -t beyondnetcode/ums-api:$(git rev-parse --short HEAD) .` |
40+
41+
Both images are pushed to **GitHub Container Registry** (`ghcr.io/beyondnetcode/ums‑web` / `…/ums‑api`).
42+
---
43+
44+
## 3. Managed Services (Azure example)
45+
| Service | Purpose | Suggested SKU |
46+
|--------|---------|----------------|
47+
| **Azure Front Door** | Global HTTPS termination, WAF, caching static assets | Standard/Premium |
48+
| **Azure API Management** | Throttling, API keys, versioning, developer portal | Consumption |
49+
| **Azure Kubernetes Service (AKS)** | Orchestrates the API containers (high‑availability) | Standard B2s nodes, autoscale 2‑6 replicas |
50+
| **Azure PostgreSQL Flexible Server** | Relational DB with built‑in HA & backups | General‑Purpose, 2 vCores, 32 GB RAM |
51+
| **Azure Key Vault** | Store connection strings, JWT signing keys, client secrets |
52+
| **Azure Monitor + Log Analytics** | Centralised logs, metrics, alerts |
53+
| **Azure Application Insights** | Distributed tracing for the .NET API |
54+
---
55+
56+
## 4. Deployment Pipeline (GitHub Actions)
57+
```yaml
58+
name: CI‑CD
59+
on:
60+
push:
61+
branches: [ main ]
62+
workflow_dispatch:
63+
64+
jobs:
65+
build:
66+
runs-on: ubuntu-latest
67+
steps:
68+
- uses: actions/checkout@v4
69+
- name: Set up .NET
70+
uses: actions/setup-dotnet@v4
71+
with:
72+
dotnet-version: '10.0.x'
73+
- name: Restore & Test
74+
run: |
75+
dotnet restore
76+
dotnet test --no-build --verbosity normal
77+
- name: Build Docker images
78+
run: |
79+
docker build -t ghcr.io/beyondnetcode/ums-web:${{github.sha}} -f src/apps/ums.web-app/Dockerfile .
80+
docker build -t ghcr.io/beyondnetcode/ums-api:${{github.sha}} -f src/apps/ums.api/Ums.Presentation/Dockerfile .
81+
- name: Log in to GHCR
82+
uses: docker/login-action@v3
83+
with:
84+
registry: ghcr.io
85+
username: ${{github.actor}}
86+
password: ${{secrets.GITHUB_TOKEN}}
87+
- name: Push images
88+
run: |
89+
docker push ghcr.io/beyondnetcode/ums-web:${{github.sha}}
90+
docker push ghcr.io/beyondnetcode/ums-api:${{github.sha}}
91+
92+
deploy:
93+
needs: build
94+
runs-on: ubuntu-latest
95+
environment: production
96+
steps:
97+
- name: Azure login
98+
uses: azure/login@v2
99+
with:
100+
creds: ${{ secrets.AZURE_CREDENTIALS }}
101+
- name: Deploy to AKS
102+
run: |
103+
az aks get-credentials --resource-group rg‑ums --name aks‑ums
104+
helm upgrade --install ums-api chart/ums-api \
105+
--set image.tag=${{github.sha}} \
106+
--set postgres.connectionString=${{secrets.PG_CONNECTION}}
107+
helm upgrade --install ums-web chart/ums-web \
108+
--set image.tag=${{github.sha}} \
109+
--set apiBaseUrl=${{secrets.API_BASE_URL}}
110+
```
111+
---
112+
113+
## 5. Secrets Management
114+
- **PostgreSQL connection string** → stored in **Azure Key Vault** and referenced via `{{ secrets.PG_CONNECTION }}` in the pipeline.
115+
- **JWT signing key**, **OAuth client secrets**, and **API Management keys** also live in Key Vault.
116+
- Enable **Managed Identity** for AKS pods to fetch secrets directly, removing the need for env‑vars.
117+
---
118+
119+
## 6. Observability & Alerting
120+
| Layer | Tool | What to capture |
121+
|-------|------|----------------|
122+
| API | Application Insights | Request latency, dependency calls, exception rates |
123+
| DB | Azure Monitor (Log Analytics) | Slow queries (> 200 ms), deadlocks, replication lag |
124+
| Web | Azure Front Door logs | CDN cache hit ratio, 4xx/5xx distribution |
125+
| Platform | Azure Monitor alerts | CPU > 80 % for 5 min, memory pressure, pod restarts > 2 |
126+
---
127+
128+
## 7. Scalability & HA
129+
- **AKS**: enable **Cluster Autoscaler** (min 2, max 10 nodes).
130+
- **PostgreSQL**: enable **Zone‑redundant HA** and daily **point‑in‑time backups** (7‑day retention).
131+
- **Front Door**: global anycast, automatic fail‑over.
132+
- Deploy **multiple replicas** of the API (minimum 2) behind the load balancer.
133+
---
134+
135+
## 8. Security Hardening
136+
- Enforce **HTTPS everywhere** (TLS 1.3).
137+
- **WAF** rules on Front Door to block OWASP Top‑10.
138+
- API Management **rate limiting** (100 req/second per client).
139+
- Database **network security group** – only AKS subnet can connect on port 5432.
140+
- Enable **Row‑Level Security (RLS)** in PostgreSQL to complement application‑layer tenant filtering.
141+
---
142+
143+
## 9. Tenancy Enforcement
144+
- The **application layer** continues to enforce tenant isolation (as required by the project rules).
145+
- RLS policies are added as a **defence‑in‑depth** layer:
146+
```sql
147+
CREATE POLICY tenant_isolation ON public.module
148+
USING (tenant_id = current_setting('app.current_tenant')::uuid);
149+
```
150+
- The API sets `app.current_tenant` from the JWT claim at the start of each request.
151+
---
152+
153+
## 10. Release Checklist
154+
- [x] All unit/integration tests pass (`dotnet test`).
155+
- [x] Docker images built and scanned (trivy).
156+
- [x] Helm charts versioned (`Chart.yaml` bumped).
157+
- [x] Secrets stored in Key Vault, access policies reviewed.
158+
- [x] Monitoring dashboards created (CPU, DB latency, 4xx/5xx).
159+
- [x] Load‑testing (k6) completed – 95th‑pct latency < 200 ms at 500 RPS.
160+
- [ ] **Post‑deployment smoke test** – hit `/system-suites` GET endpoint and verify JSON schema.
161+
- [ ] **Rollback plan** – keep previous image tag for 24 h; helm `--set image.tag=previous` if needed.
162+
---
163+
164+
## 11. Documentation & Governance
165+
- All **deployment scripts**, **helm values**, and **environment variables** are version‑controlled in the `ops/` folder.
166+
- The plan follows the **BMAD‑METHOD** phases (00‑05) and is stored in this repository under `infrastructure_plan.md` for auditability.
167+
- Bilingual version (EN/ES) will be added later to satisfy the bilingual‑consistency rule.
168+
---
169+
170+
**Next steps**
171+
1. Review the diagram and confirm any region‑specific compliance requirements.
172+
2. Approve the helm chart values (`ops/helm/ums‑api/values.yaml` & `ums‑web`).
173+
3. Trigger a release via the `/goal` slash command or schedule a production deployment.
174+
175+
Feel free to ask for modifications, add more details, or request a walkthrough of any section.

src/apps/ums.api/Ums.Presentation/Endpoints/Authorization/SystemSuite/SystemSuiteEndpoints.cs

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,17 @@ public static IEndpointRouteBuilder MapSystemSuiteEndpoints(this IEndpointRouteB
7979
var result = await mediator.Send(command with { SystemSuiteId = systemSuiteId }, ct);
8080
if (result.IsSuccess)
8181
return Results.Created($"/system-suites/{systemSuiteId}/modules", result);
82-
return result.ToProblem(context);
82+
// Map domain error to proper problem response
83+
var (status, title) = DomainErrorStatusMapper.Map(result.Error);
84+
var problem = new ProblemDetails
85+
{
86+
Title = title,
87+
Status = status,
88+
Detail = result.Error,
89+
Instance = context?.Request?.Path,
90+
Extensions = { ["timestamp"] = DateTimeOffset.UtcNow }
91+
};
92+
return Results.Problem(problem);
8393
}).WithName("AddModule")
8494
.WithSummary("Add a module to the system suite")
8595
.Produces(StatusCodes.Status201Created)
@@ -133,7 +143,16 @@ public static IEndpointRouteBuilder MapSystemSuiteEndpoints(this IEndpointRouteB
133143
var result = await mediator.Send(command with { SystemSuiteId = systemSuiteId, ModuleId = moduleId }, ct);
134144
if (result.IsSuccess)
135145
return Results.Created($"/system-suites/{systemSuiteId}/modules/{moduleId}/menus", result);
136-
return result.ToProblem(context);
146+
var (status, title) = DomainErrorStatusMapper.Map(result.Error);
147+
var problem = new ProblemDetails
148+
{
149+
Title = title,
150+
Status = status,
151+
Detail = result.Error,
152+
Instance = context?.Request?.Path,
153+
Extensions = { ["timestamp"] = DateTimeOffset.UtcNow }
154+
};
155+
return Results.Problem(problem);
137156
}).WithName("AddMenu")
138157
.WithSummary("Add a menu to a module")
139158
.Produces(StatusCodes.Status201Created)
@@ -166,7 +185,16 @@ public static IEndpointRouteBuilder MapSystemSuiteEndpoints(this IEndpointRouteB
166185
var result = await mediator.Send(command with { SystemSuiteId = systemSuiteId, ModuleId = moduleId, MenuId = menuId }, ct);
167186
if (result.IsSuccess)
168187
return Results.Created($"/system-suites/{systemSuiteId}/modules/{moduleId}/menus/{menuId}/submenus", result);
169-
return result.ToProblem(context);
188+
var (status, title) = DomainErrorStatusMapper.Map(result.Error);
189+
var problem = new ProblemDetails
190+
{
191+
Title = title,
192+
Status = status,
193+
Detail = result.Error,
194+
Instance = context?.Request?.Path,
195+
Extensions = { ["timestamp"] = DateTimeOffset.UtcNow }
196+
};
197+
return Results.Problem(problem);
170198
}).WithName("AddSubMenu")
171199
.WithSummary("Add a submenu to a menu")
172200
.Produces(StatusCodes.Status201Created)
@@ -199,7 +227,16 @@ public static IEndpointRouteBuilder MapSystemSuiteEndpoints(this IEndpointRouteB
199227
var result = await mediator.Send(command with { SystemSuiteId = systemSuiteId, ModuleId = moduleId, MenuId = menuId, SubMenuId = subMenuId }, ct);
200228
if (result.IsSuccess)
201229
return Results.Created($"/system-suites/{systemSuiteId}/modules/{moduleId}/menus/{menuId}/submenus/{subMenuId}/options", result);
202-
return result.ToProblem(context);
230+
var (status, title) = DomainErrorStatusMapper.Map(result.Error);
231+
var problem = new ProblemDetails
232+
{
233+
Title = title,
234+
Status = status,
235+
Detail = result.Error,
236+
Instance = context?.Request?.Path,
237+
Extensions = { ["timestamp"] = DateTimeOffset.UtcNow }
238+
};
239+
return Results.Problem(problem);
203240
}).WithName("AddOption")
204241
.WithSummary("Add an option to a submenu")
205242
.Produces(StatusCodes.Status201Created)
@@ -232,7 +269,16 @@ public static IEndpointRouteBuilder MapSystemSuiteEndpoints(this IEndpointRouteB
232269
var result = await mediator.Send(command with { SystemSuiteId = systemSuiteId }, ct);
233270
if (result.IsSuccess)
234271
return Results.Created($"/system-suites/{systemSuiteId}/app-settings", result);
235-
return result.ToProblem(context);
272+
var (status, title) = DomainErrorStatusMapper.Map(result.Error);
273+
var problem = new ProblemDetails
274+
{
275+
Title = title,
276+
Status = status,
277+
Detail = result.Error,
278+
Instance = context?.Request?.Path,
279+
Extensions = { ["timestamp"] = DateTimeOffset.UtcNow }
280+
};
281+
return Results.Problem(problem);
236282
}).WithName("AddAppSetting")
237283
.WithSummary("Add a configuration key-value pair to the system suite")
238284
.Produces(StatusCodes.Status201Created)
@@ -265,7 +311,16 @@ public static IEndpointRouteBuilder MapSystemSuiteEndpoints(this IEndpointRouteB
265311
var result = await mediator.Send(command with { SystemSuiteId = systemSuiteId }, ct);
266312
if (result.IsSuccess)
267313
return Results.Created($"/system-suites/{systemSuiteId}/actions", result);
268-
return result.ToProblem(context);
314+
var (status, title) = DomainErrorStatusMapper.Map(result.Error);
315+
var problem = new ProblemDetails
316+
{
317+
Title = title,
318+
Status = status,
319+
Detail = result.Error,
320+
Instance = context?.Request?.Path,
321+
Extensions = { ["timestamp"] = DateTimeOffset.UtcNow }
322+
};
323+
return Results.Problem(problem);
269324
}).WithName("RegisterAction")
270325
.WithSummary("Register a new action code that can be used in permission templates")
271326
.Produces(StatusCodes.Status201Created)
@@ -300,7 +355,16 @@ public static IEndpointRouteBuilder MapSystemSuiteEndpoints(this IEndpointRouteB
300355
var result = await mediator.Send(command with { SystemSuiteId = systemSuiteId }, ct);
301356
if (result.IsSuccess)
302357
return Results.Created($"/system-suites/{systemSuiteId}/domain-resources", result);
303-
return result.ToProblem(context);
358+
var (status, title) = DomainErrorStatusMapper.Map(result.Error);
359+
var problem = new ProblemDetails
360+
{
361+
Title = title,
362+
Status = status,
363+
Detail = result.Error,
364+
Instance = context?.Request?.Path,
365+
Extensions = { ["timestamp"] = DateTimeOffset.UtcNow }
366+
};
367+
return Results.Problem(problem);
304368
}).WithName("AddDomainResource")
305369
.WithSummary("Add a domain resource (Aggregate or Entity) to the system suite")
306370
.Produces(StatusCodes.Status201Created)

0 commit comments

Comments
 (0)