fix(deploy): align runtime identity and ingress
CI / Verify (push) Successful in 1m9s
CI / Build immutable images and deploy (push) Has been skipped

This commit is contained in:
2026-08-29 06:52:32 +00:00 Unverified
parent cd552a8b04
commit 50dce9eaf7
8 changed files with 27 additions and 18 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ COPY --chown=bun:bun db ./db
COPY --chown=bun:bun drizzle ./drizzle COPY --chown=bun:bun drizzle ./drizzle
COPY --chown=bun:bun scripts/migrate.ts ./scripts/migrate.ts COPY --chown=bun:bun scripts/migrate.ts ./scripts/migrate.ts
COPY --chown=bun:bun tsconfig.json ./tsconfig.json COPY --chown=bun:bun tsconfig.json ./tsconfig.json
USER bun USER 1000:1000
ENTRYPOINT ["bun", "scripts/migrate.ts"] ENTRYPOINT ["bun", "scripts/migrate.ts"]
FROM oven/bun:${BUN_VERSION} AS app FROM oven/bun:${BUN_VERSION} AS app
@@ -52,6 +52,6 @@ COPY --from=next-builder --chown=bun:bun /app/.next/standalone ./
COPY --from=next-builder --chown=bun:bun /app/.next/static ./.next/static COPY --from=next-builder --chown=bun:bun /app/.next/static ./.next/static
COPY --from=next-builder --chown=bun:bun /app/public ./public COPY --from=next-builder --chown=bun:bun /app/public ./public
COPY --from=worker-builder --chown=bun:bun /app/dist/outbox-worker.js ./worker/outbox-worker.js COPY --from=worker-builder --chown=bun:bun /app/dist/outbox-worker.js ./worker/outbox-worker.js
USER bun USER 1000:1000
EXPOSE 3000 EXPOSE 3000
CMD ["bun", "server.js"] CMD ["bun", "server.js"]
+1 -1
View File
@@ -148,7 +148,7 @@ flowchart LR
- Two application replicas with rolling updates - Two application replicas with rolling updates
- ClusterIP service on port 3000 - ClusterIP service on port 3000
- Traefik ingress for `sheet.sudloh.com` - Traefik ingress for `sheet.sudloh.com`
- Externally provisioned `sheet-sudloh-com-tls` secret - Cloudflare edge TLS with the standard HTTP Traefik origin route
- Readiness/liveness health endpoint - Readiness/liveness health endpoint
- Requests of 500m CPU/512 MiB and limits of 1 CPU/1 GiB - Requests of 500m CPU/512 MiB and limits of 1 CPU/1 GiB
- HPA from 2–6 replicas at 70% CPU - HPA from 2–6 replicas at 70% CPU
+6 -5
View File
@@ -163,8 +163,9 @@ Without `REDIS_INTEGRATION_URL`, that external-service test is skipped.
## Containers ## Containers
The application image compiles Next.js in a Node builder stage, then runs the The application image compiles Next.js in a Node builder stage, then runs the
standalone server and bundled outbox worker on Bun as the unprivileged `bun` standalone server and bundled outbox worker on Bun as the unprivileged numeric
user. The migration image runs the committed Drizzle migrations on Bun. UID/GID `1000:1000`. The migration image runs the committed Drizzle migrations
with the same identity.
```bash ```bash
docker build \ docker build \
@@ -190,7 +191,7 @@ Kustomize resources live in `k8s/` and define:
- Namespace `buzz-sheet`. - Namespace `buzz-sheet`.
- Two rolling web replicas and one outbox worker. - Two rolling web replicas and one outbox worker.
- ClusterIP port 3000 and Traefik ingress for `sheet.sudloh.com`. - ClusterIP port 3000 and Traefik ingress for `sheet.sudloh.com`.
- Externally provisioned `sheet-sudloh-com-tls`. - Cloudflare edge TLS with the cluster's standard HTTP Traefik origin route.
- Startup, liveness, and dependency-aware readiness probes. - Startup, liveness, and dependency-aware readiness probes.
- Web requests of 500m CPU/512 MiB and limits of 1 CPU/1 GiB. - Web requests of 500m CPU/512 MiB and limits of 1 CPU/1 GiB.
- HPA from 2 to 6 web replicas at 70% CPU and a PDB with one available. - HPA from 2 to 6 web replicas at 70% CPU and a PDB with one available.
@@ -202,8 +203,8 @@ not create in-cluster data stores or credentials.
### External prerequisites ### External prerequisites
Before the first rollout, a cluster administrator must provision the TLS secret Before the first rollout, a cluster administrator must provision a
and a `buzz-sheet-env` Secret in the `buzz-sheet` namespace containing: `buzz-sheet-env` Secret in the `buzz-sheet` namespace containing:
```text ```text
DATABASE_URL DATABASE_URL
+4
View File
@@ -27,6 +27,10 @@ spec:
terminationGracePeriodSeconds: 30 terminationGracePeriodSeconds: 30
securityContext: securityContext:
runAsNonRoot: true runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
containers: containers:
-7
View File
@@ -4,15 +4,8 @@ metadata:
name: buzz-sheet name: buzz-sheet
labels: labels:
app.kubernetes.io/name: buzz-sheet app.kubernetes.io/name: buzz-sheet
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec: spec:
ingressClassName: traefik ingressClassName: traefik
tls:
- hosts:
- sheet.sudloh.com
secretName: sheet-sudloh-com-tls
rules: rules:
- host: sheet.sudloh.com - host: sheet.sudloh.com
http: http:
+4
View File
@@ -27,6 +27,10 @@ spec:
terminationGracePeriodSeconds: 35 terminationGracePeriodSeconds: 35
securityContext: securityContext:
runAsNonRoot: true runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
containers: containers:
+4
View File
@@ -19,6 +19,10 @@ spec:
automountServiceAccountToken: false automountServiceAccountToken: false
securityContext: securityContext:
runAsNonRoot: true runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
containers: containers:
+6 -3
View File
@@ -19,11 +19,13 @@ describe("production deployment contract", () => {
/requests:\s+cpu: 500m\s+memory: 512Mi\s+limits:\s+cpu: "1"\s+memory: 1Gi/u, /requests:\s+cpu: 500m\s+memory: 512Mi\s+limits:\s+cpu: "1"\s+memory: 1Gi/u,
); );
expect(deployment).toContain("runAsNonRoot: true"); expect(deployment).toContain("runAsNonRoot: true");
expect(deployment).toContain("runAsUser: 1000");
expect(deployment).toContain("runAsGroup: 1000");
expect(deployment).toContain("readOnlyRootFilesystem: true"); expect(deployment).toContain("readOnlyRootFilesystem: true");
expect(deployment).toContain('drop: ["ALL"]'); expect(deployment).toContain('drop: ["ALL"]');
}); });
it("exposes only the app through the requested service and TLS host", async () => { it("exposes only the app through the requested edge-TLS host", async () => {
const [service, ingress] = await Promise.all([ const [service, ingress] = await Promise.all([
repositoryFile("k8s/base/service.yaml"), repositoryFile("k8s/base/service.yaml"),
repositoryFile("k8s/base/ingress.yaml"), repositoryFile("k8s/base/ingress.yaml"),
@@ -34,7 +36,8 @@ describe("production deployment contract", () => {
expect(service).toContain("targetPort: http"); expect(service).toContain("targetPort: http");
expect(ingress).toContain("ingressClassName: traefik"); expect(ingress).toContain("ingressClassName: traefik");
expect(ingress).toContain("host: sheet.sudloh.com"); expect(ingress).toContain("host: sheet.sudloh.com");
expect(ingress).toContain("secretName: sheet-sudloh-com-tls"); expect(ingress).not.toContain("secretName:");
expect(ingress).not.toContain("router.tls");
}); });
it("keeps availability and scaling bounds explicit", async () => { it("keeps availability and scaling bounds explicit", async () => {
@@ -78,7 +81,7 @@ describe("production deployment contract", () => {
expect(dockerfile).toContain("FROM dependencies AS migration"); expect(dockerfile).toContain("FROM dependencies AS migration");
expect(dockerfile).toContain("FROM oven/bun:${BUN_VERSION} AS app"); expect(dockerfile).toContain("FROM oven/bun:${BUN_VERSION} AS app");
expect(dockerfile.match(/^USER bun$/gmu)).toHaveLength(2); expect(dockerfile.match(/^USER 1000:1000$/gmu)).toHaveLength(2);
expect(dockerfile).toContain('ENTRYPOINT ["bun", "scripts/migrate.ts"]'); expect(dockerfile).toContain('ENTRYPOINT ["bun", "scripts/migrate.ts"]');
expect(dockerfile).toContain('CMD ["bun", "server.js"]'); expect(dockerfile).toContain('CMD ["bun", "server.js"]');
}); });