Install on Kubernetes (Kustomize)¶
The deploy/kustomize/ tree in the repository ships base manifests plus three overlays (dev, staging, production). The base manifests are deliberately strict — runAsNonRoot, readOnlyRootFilesystem, allowPrivilegeEscalation: false, capabilities.drop: [ALL] — so the overlays only need to add what is environment-specific.
What you get out of the box¶
| Manifest | Purpose |
|---|---|
namespace.yaml | ampora namespace |
serviceaccount.yaml | Pod identity, no token automount |
deployment.yaml | 2 replicas by default, topology-spread, lifecycle drain |
service.yaml | ClusterIP for the HTTP + WebSocket port |
ingress.yaml | Nginx-compatible Ingress with WebSocket annotations |
hpa.yaml | HorizontalPodAutoscaler |
pdb.yaml | PodDisruptionBudget |
configmap.yaml | non-secret settings |
secret.yaml | placeholder; replace via your secret backend |
networkpolicy.yaml | egress to Postgres + DNS only by default |
servicemonitor.yaml | Prometheus Operator scrape config |
Step 1 — pick or create an overlay¶
The production overlay is the right starting point for a real cluster. Copy it once and treat the copy as your environment's source of truth:
Edit deploy/kustomize/overlays/acme-prod/kustomization.yaml:
namespace: ampora
nameSuffix: ""
images:
- name: ampora/web
newName: registry.acme.io/ampora/web
newTag: 1.4.2 # pin a real version, not "latest"
configMapGenerator:
- name: ampora-config
behavior: merge
literals:
- Authentication__Oidc__Authority=https://login.acme.io/realms/ampora
- OpenTelemetry__OtlpEndpoint=http://opentelemetry-collector.observability.svc.cluster.local:4317
secretGenerator:
- name: ampora-secrets
behavior: replace
envs:
- secrets.env # never commit; produced by SOPS / sealed-secrets / ESO
patchesStrategicMerge:
- deployment-patch.yaml
- ingress-patch.yaml
Step 2 — provision the database¶
Ampora expects a reachable PostgreSQL with:
- a dedicated database (
ampora), - a dedicated role (
ampora) with full DDL on that database, - TLS enabled — Ampora's connection string must be
SSL Mode=Requirein production.
A managed offering (RDS, Cloud SQL, Azure Database, Aiven, …) is the recommended path. If you prefer in-cluster, use a tested operator (CrunchyData PGO, Zalando, CNPG) — Ampora does not ship a Postgres operator of its own.
Apply migrations on first start either by:
- letting the pod do it (
AMPORA_AUTO_MIGRATE=1, the default in the base ConfigMap), or - running them out-of-band as a
Jobthat exec's the same image withdotnet Ampora.Web.dll migrate. This is the safer choice for tightly controlled clusters.
Step 3 — configure secrets¶
The base Secret is a placeholder annotated ampora.io/placeholder=true so review tooling can refuse to ship it. Replace it via your real secret backend:
- External Secrets Operator — easiest for managed-secrets users.
- Sealed Secrets — git-friendly, no extra runtime dependency.
- SOPS + Kustomize — works without a controller.
- HashiCorp Vault Agent Injector — when you already run Vault.
The shape of the Secret expected by the deployment:
ConnectionStrings__Ampora: "Host=db.acme.io;Port=5432;Database=ampora;Username=ampora;Password=...;SSL Mode=Require;Trust Server Certificate=false"
Authentication__Oidc__Authority: "https://login.acme.io/realms/ampora"
Authentication__Oidc__ClientId: "ampora-web"
Authentication__Oidc__ClientSecret: "..."
KeyProtection__MasterKey: "<base64 of a 32-byte CSPRNG key>"
KeyProtection__MasterKey wraps every encryption-at-rest field. Generate once with openssl rand 32 | base64 and rotate by setting a new key plus KeyProtection__PreviousMasterKey for one cycle, then drop the previous.
Step 4 — wire the Ingress¶
The default ingress.yaml is Nginx-Ingress-shaped:
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/affinity-mode: "persistent"
nginx.ingress.kubernetes.io/session-cookie-name: "ampora-affinity"
The session-cookie affinity is mandatory for HA: Blazor Server's SignalR circuit must stick to the same instance. See Operations → High availability for the equivalent Traefik / Application Gateway configurations.
Override host and tls via the overlay's ingress-patch.yaml. Use cert-manager or your own ACME / corporate-CA flow to provision the TLS certificate.
Step 5 — apply¶
kubectl apply -k deploy/kustomize/overlays/acme-prod
kubectl -n ampora rollout status deploy/ampora-web
kubectl -n ampora get pods,svc,ingress
The deployment is ready when:
- both pods are
RunningandReady, /health/liveand/health/readyreturn 200,kubectl logs -l app.kubernetes.io/name=amporashowsHosting environment: ProductionandNow listening on: ....
Step 6 — first login¶
Visit https://AMPORA_HOST in a browser. You should be bounced to your OIDC provider, log in, and land on the Dashboard.
If the first user has no role, Ampora bootstraps them as Admin. From there on, role assignment follows the OIDC group / claim mapping you configured.
Image pinning by digest in CI¶
Pinning by tag is fine for humans; CI should pin by digest:
cd deploy/kustomize/overlays/acme-prod
kustomize edit set image \
registry.acme.io/ampora/web=registry.acme.io/ampora/web@sha256:<digest>
The release stage of .gitlab-ci.yml produces signed digests via cosign; your CD pipeline should use those digests directly.
NetworkPolicy¶
The base networkpolicy.yaml denies everything except:
- DNS to
kube-system, - TCP egress to the Postgres service,
- TCP egress to the OTel collector,
- TCP ingress on
8080from the Ingress controller's namespace.
Add an extra policy for outbound to your Git host, OIDC provider, HSM/KMS endpoint, and federation peers as you turn those features on.
Where to next¶
- HA wire-up: Operations → High availability.
- Hardening: Security → Hardening checklist.
- Day-2: Operations → Upgrades.