fix(deploy): align runtime identity and ingress
This commit is contained in:
+2
-2
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"]');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user