Skip to content

Commit bc13dad

Browse files
committed
Add Helm chart, fix Dockerfile, add frontend values, nginx config, deployment manifest
1 parent 7f7b595 commit bc13dad

20 files changed

Lines changed: 953 additions & 48 deletions

UMS_K8s_Deployment_Plan.md

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# UMS Kubernetes Deployment Plan
2+
3+
This document captures the full deployment plan for the UMS monorepo to a local Docker Desktop Kubernetes cluster, using PostgreSQL, Redis, and an observability stack (Prometheus, Grafana, Loki, Jaeger).
4+
5+
---
6+
7+
# Deploy UMS to Local Docker Desktop Kubernetes (full stack – PostgreSQL)
8+
9+
## Goal
10+
Deploy the entire UMS monorepo (backend API, frontend web app, and supporting infrastructure) to the local Kubernetes cluster provided by Docker Desktop, ensuring:
11+
- Super‑Admin user is seeded with password `root`
12+
- Observability stack (metrics, logs, tracing) is available
13+
- Caching layer (Redis) is provisioned
14+
- **PostgreSQL** is used as the relational database
15+
16+
## User Review Required
17+
- **Docker image tags** you would like to use (e.g., `latest` or a specific git‑sha).
18+
- **Kubernetes namespace** (recommended: `ums-local`).
19+
- Whether the admin password should be stored in a **Kubernetes Secret** or remain hard‑coded for dev.
20+
- **Ingress host** for the UI (e.g., `ums.local`).
21+
- **TLS** for local dev (self‑signed is fine).
22+
- **Observability components** you want (Prometheus + Grafana, Loki, Jaeger).
23+
- **Redis cache** – single‑node is enough for dev.
24+
25+
## Open Questions
26+
> [!IMPORTANT]
27+
> **Namespace** – target namespace?
28+
>
29+
> > (Recommended) `ums-local`
30+
>
31+
> **Ingress host** – hostname to reach the UI?
32+
>
33+
> > (Recommended) `ums.local`
34+
>
35+
> **TLS** – enable self‑signed TLS?
36+
>
37+
> > (Recommended) `yes`
38+
>
39+
> **Admin password secret** – store in a Secret?
40+
>
41+
> > (Recommended) `ums-admin-secret` with key `ADMIN_PASSWORD`
42+
>
43+
> **Observability stack** – include which tools?
44+
>
45+
> > (Recommended) Prometheus, Grafana, Loki, Jaeger
46+
>
47+
> **Redis cache** – single instance?
48+
>
49+
> > (Recommended) `true`
50+
51+
## Proposed Changes
52+
---
53+
### Core Services
54+
#### Backend (API)
55+
- Add a **Dockerfile** (multi‑stage) that builds the .NET 10 API and runs migrations on startup.
56+
- Expose **Prometheus metrics** endpoint (`/metrics`) via `UsePrometheus()` (already in code).
57+
- Add **OpenTelemetry** instrumentation for tracing (Jaeger exporter).
58+
- Environment variables for observability:
59+
- `OTEL_EXPORTER_JAEGER_ENDPOINT=http://ums-jaeger:14268/api/traces`
60+
- `OTEL_METRICS_EXPORTER=prometheus`
61+
- Helm values (`values-backend.yaml`):
62+
```yaml
63+
image:
64+
repository: ums/backend
65+
tag: <tag>
66+
env:
67+
- name: ASPNETCORE_ENVIRONMENT
68+
value: Production
69+
- name: ConnectionStrings__Default
70+
valueFrom:
71+
secretKeyRef:
72+
name: ums-db-secret
73+
key: CONNECTION_STRING
74+
- name: ADMIN_PASSWORD
75+
valueFrom:
76+
secretKeyRef:
77+
name: ums-admin-secret
78+
key: ADMIN_PASSWORD
79+
- name: REDIS_CONNECTION
80+
value: redis://ums-redis:6379
81+
- name: OTEL_EXPORTER_JAEGER_ENDPOINT
82+
value: http://ums-jaeger:14268/api/traces
83+
- name: OTEL_METRICS_EXPORTER
84+
value: prometheus
85+
service:
86+
type: ClusterIP
87+
replicaCount: 1
88+
resources:
89+
limits:
90+
cpu: "500m"
91+
memory: "512Mi"
92+
requests:
93+
cpu: "250m"
94+
memory: "256Mi"
95+
livenessProbe:
96+
httpGet:
97+
path: /healthz
98+
port: 80
99+
initialDelaySeconds: 10
100+
periodSeconds: 10
101+
readinessProbe:
102+
httpGet:
103+
path: /ready
104+
port: 80
105+
initialDelaySeconds: 5
106+
periodSeconds: 5
107+
```
108+
109+
#### Frontend (Web App)
110+
- Ensure production build (`npm run build`).
111+
- Dockerfile using Nginx to serve static files.
112+
- Helm `values-frontend.yaml`:
113+
```yaml
114+
image:
115+
repository: ums/frontend
116+
tag: <tag>
117+
service:
118+
type: ClusterIP
119+
ingress:
120+
enabled: true
121+
hosts:
122+
- host: ums.local
123+
paths:
124+
- path: /
125+
pathType: Prefix
126+
tls:
127+
- hosts:
128+
- ums.local
129+
secretName: ums-tls
130+
resources:
131+
limits:
132+
cpu: "200m"
133+
memory: "256Mi"
134+
requests:
135+
cpu: "100m"
136+
memory: "128Mi"
137+
```
138+
139+
### Supporting Infrastructure
140+
#### PostgreSQL
141+
- Deploy the official `postgres:15-alpine` image.
142+
- Helm values (`values-db.yaml`):
143+
```yaml
144+
image:
145+
repository: postgres
146+
tag: 15-alpine
147+
env:
148+
- name: POSTGRES_USER
149+
value: postgres
150+
- name: POSTGRES_PASSWORD
151+
valueFrom:
152+
secretKeyRef:
153+
name: ums-db-secret
154+
key: POSTGRES_PASSWORD
155+
- name: POSTGRES_DB
156+
value: ums
157+
persistence:
158+
enabled: true
159+
size: 5Gi
160+
```
161+
- Create a **Secret** `ums-db-secret` with keys:
162+
- `POSTGRES_PASSWORD` – your dev password (e.g., `postgres`).
163+
- `CONNECTION_STRING` – `Host=ums-postgres;Database=ums;Username=postgres;Password=$(POSTGRES_PASSWORD);`
164+
165+
#### Redis Cache
166+
- Deploy the official `redis:7-alpine` chart (stand‑alone).
167+
- Helm values (`values-redis.yaml`):
168+
```yaml
169+
architecture: standalone
170+
replicaCount: 1
171+
auth:
172+
enabled: false
173+
```
174+
175+
#### Observability Stack
176+
- **Prometheus + Grafana**: `kube-prometheus-stack` chart.
177+
- **Loki**: `grafana/loki-stack` chart for log aggregation.
178+
- **Jaeger**: `jaegertracing/jaeger` chart for tracing.
179+
- Provide a `values-observability.yaml` that enables ServiceMonitors for the API and Nginx, and sets scrape intervals.
180+
181+
### Helm Chart Structure
182+
```
183+
ums-helm/
184+
├─ Chart.yaml
185+
├─ templates/
186+
│ ├─ backend-deployment.yaml
187+
│ ├─ backend-service.yaml
188+
│ ├─ frontend-deployment.yaml
189+
│ ├─ frontend-service.yaml
190+
│ ├─ ingress.yaml
191+
│ ├─ postgres-deployment.yaml
192+
│ ├─ redis-deployment.yaml
193+
│ └─ observability/ (sub‑charts imports)
194+
└─ values/
195+
├─ backend.yaml
196+
├─ frontend.yaml
197+
├─ postgres.yaml
198+
├─ redis.yaml
199+
└─ observability.yaml
200+
```
201+
- Each sub‑chart pulls in the appropriate values files.
202+
- Include a **Job** (`ums-seeder`) that runs the seeder commands (`dotnet run --project Ums.Api.csproj` with a `--seed` flag) if you need to reseed data.
203+
204+
## Verification Plan
205+
### Automated Tests
206+
1. **Build images** locally:
207+
```bash
208+
docker build -t ums/backend:latest ./src/apps/ums.api
209+
docker build -t ums/frontend:latest ./src/apps/ums.web-app
210+
```
211+
2. **Create secrets**:
212+
```bash
213+
kubectl create secret generic ums-db-secret \
214+
--from-literal=POSTGRES_PASSWORD=postgres \
215+
--from-literal=CONNECTION_STRING="Host=ums-postgres;Database=ums;Username=postgres;Password=postgres;"
216+
kubectl create secret generic ums-admin-secret --from-literal=ADMIN_PASSWORD=root
217+
```
218+
3. **Render manifests** and validate:
219+
```bash
220+
helm template ./ums-helm \
221+
-f values/backend.yaml \
222+
-f values/frontend.yaml \
223+
-f values/postgres.yaml \
224+
-f values/redis.yaml \
225+
-f values/observability.yaml | kubeval
226+
```
227+
4. **Install to local cluster**:
228+
```bash
229+
helm upgrade --install ums ./ums-helm \
230+
-n ums-local --create-namespace \
231+
-f values/backend.yaml \
232+
-f values/frontend.yaml \
233+
-f values/postgres.yaml \
234+
-f values/redis.yaml \
235+
-f values/observability.yaml
236+
```
237+
5. **Port‑forward the UI** and test:
238+
```bash
239+
kubectl port-forward svc/ums-frontend 8080:80 -n ums-local
240+
```
241+
Open `http://ums.local` (or `http://localhost:8080`).
242+
6. **Login** with `admin@ums.local` / `root` and verify the full admin suite appears.
243+
7. **Check observability**:
244+
- Grafana (`http://ums-grafana:3000`) shows dashboards for API metrics and Nginx logs.
245+
- Jaeger UI (`http://ums-jaeger:16686`) displays traces for the login flow.
246+
- Prometheus scrapes `/metrics` from the API.
247+
- Loki receives Nginx access/error logs.
248+
8. **Validate cache**:
249+
```bash
250+
kubectl exec -it $(kubectl get pod -l app=ums-redis -n ums-local -o jsonpath='{.items[0].metadata.name}') -- redis-cli ping
251+
```
252+
Should return `PONG`.
253+
254+
### Manual Checks
255+
- Force‑password‑reset endpoint works and the flow forces a password change on first login.
256+
- Verify that the PostgreSQL schema was created correctly (tables, constraints, migrations applied).
257+
- Ensure that the seeder job (`ums-seeder`) can be re‑run without errors.
258+
259+
---
260+
**Next steps:**
261+
1. Confirm the open questions above (tags, namespace, secrets, observability choices).
262+
2. Once approved, we will run the Docker builds, create the Kubernetes secrets, render the Helm chart, and apply it to your local Docker Desktop Kubernetes cluster.
263+
264+
*When you’re ready, just provide the values you want to use or any adjustments you need.*

src/apps/ums.api/Ums.Infrastructure/Persistence/Seeders/AuthorizationDevDataSeeder.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public static async Task SeedAsync(IServiceProvider serviceProvider, Cancellatio
125125
}
126126
else if (tenantId.GetValue() == Guid.Parse(CoreDevDataSeeder.InternalAdminTenantId))
127127
{
128-
await EnsureInternalAdminProfileAsync(profileRepository, cancellationToken);
128+
await EnsureInternalAdminProfileAsync(profileRepository, roleRepository, templateRepository, cancellationToken);
129129
}
130130
}
131131
}
@@ -773,14 +773,35 @@ void AddProfile(Guid userId, RoleAggregate? role, PermissionTemplateAggregate? t
773773

774774
private static async Task EnsureInternalAdminProfileAsync(
775775
IProfileRepository profileRepository,
776+
IRoleRepository roleRepository,
777+
IPermissionTemplateRepository templateRepository,
776778
CancellationToken cancellationToken)
777779
{
778780
var expectedProfileId = Guid.Parse(CoreDevDataSeeder.GlobalAdminProfileId);
779781
var expectedUserId = Guid.Parse(CoreDevDataSeeder.SuperAdminUserId);
782+
var internalAdminTenantId = Guid.Parse(CoreDevDataSeeder.InternalAdminTenantId);
783+
var tenantId = TenantId.Load(internalAdminTenantId);
784+
var actor = ActorId.Create(CoreDevDataSeeder.SystemActorId);
780785

781786
var profile = await profileRepository.GetByIdAsync(expectedProfileId, cancellationToken);
782787
if (profile is null)
783788
{
789+
// Create missing profile with admin role and template
790+
var roles = await roleRepository.GetByTenantIdAsync(internalAdminTenantId, cancellationToken);
791+
var adminRole = roles.FirstOrDefault(r => r.Code.GetValue() == "ADMIN");
792+
var templates = await templateRepository.GetByTenantIdAsync(internalAdminTenantId, cancellationToken);
793+
var adminTpl = templates.FirstOrDefault(t => t.RoleId.Equals(adminRole?.GetId()) && t.Status == TemplateStatus.Published && t.Props.Version.GetValue() == "2.0.0");
794+
var newProfileResult = ProfileAggregate.Create(tenantId, UserId.Load(expectedUserId), adminRole?.GetId(), null, actor);
795+
if (newProfileResult.IsSuccess)
796+
{
797+
var newProfile = newProfileResult.Value;
798+
if (adminTpl != null)
799+
{
800+
newProfile.AssignTemplate(adminTpl, actor);
801+
}
802+
await profileRepository.AddAsync(newProfile, cancellationToken);
803+
await profileRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
804+
}
784805
return;
785806
}
786807

src/apps/ums.api/Ums.Infrastructure/Persistence/Seeders/CoreDevDataSeeder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public static class CoreDevDataSeeder
1414
// ── SuperAdmin User (global admin) ─────────────────────────────────────────
1515
public const string SuperAdminUserId = "22222222-2222-2222-2222-222222222222";
1616
public const string SuperAdminUsername = "admin";
17-
public const string SuperAdminPassword = "Admin@123"; // Change in production!
17+
public const string SuperAdminPassword = "root"; // Default password for INTERNAL admin (change in production)
1818
public const string InternalAdminPendingUserId = "11111103-1111-1111-1111-111111111111";
1919

2020
// ── GlobalAdmin Role & Profile ─────────────────────────────────────────────

src/apps/ums.api/Ums.Presentation/Dockerfile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ RUN apt-get update \
99
&& rm -rf /var/lib/apt/lists/*
1010

1111
# Copy main application projects
12-
COPY apps/ums.api/Ums.Presentation/Ums.Presentation.csproj apps/ums.api/Ums.Presentation/
13-
COPY apps/ums.api/Ums.Application/Ums.Application.csproj apps/ums.api/Ums.Application/
14-
COPY apps/ums.api/Ums.Infrastructure/Ums.Infrastructure.csproj apps/ums.api/Ums.Infrastructure/
15-
COPY apps/ums.api/Ums.Domain/Ums.Domain.csproj apps/ums.api/Ums.Domain/
16-
COPY apps/ums.api/Ums.Globalization/Ums.Globalization.csproj apps/ums.api/Ums.Globalization/
12+
COPY src/apps/ums.api/Ums.Presentation/Ums.Presentation.csproj src/apps/ums.api/Ums.Presentation/
13+
COPY src/apps/ums.api/Ums.Application/Ums.Application.csproj src/apps/ums.api/Ums.Application/
14+
COPY src/apps/ums.api/Ums.Infrastructure/Ums.Infrastructure.csproj src/apps/ums.api/Ums.Infrastructure/
15+
COPY src/apps/ums.api/Ums.Domain/Ums.Domain.csproj src/apps/ums.api/Ums.Domain/
16+
COPY src/apps/ums.api/Ums.Globalization/Ums.Globalization.csproj src/apps/ums.api/Ums.Globalization/
1717

1818
# Restore dependencies
19-
RUN dotnet restore apps/ums.api/Ums.Presentation/Ums.Presentation.csproj
19+
RUN dotnet restore src/apps/ums.api/Ums.Presentation/Ums.Presentation.csproj
2020

2121
# Copy everything else
2222
COPY . .

0 commit comments

Comments
 (0)