diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f72387e..98c652e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: context: . file: Dockerfile target: app - platforms: linux/amd64,linux/arm64 + platforms: linux/arm64 push: true provenance: false tags: ${{ env.REGISTRY_IMAGE }}:${{ gitea.sha }} @@ -89,7 +89,7 @@ jobs: context: . file: Dockerfile target: migration - platforms: linux/amd64,linux/arm64 + platforms: linux/arm64 push: true provenance: false tags: ${{ env.REGISTRY_IMAGE }}:migrate-${{ gitea.sha }} diff --git a/Plan.md b/Plan.md index ea3a5a0..5dc860d 100644 --- a/Plan.md +++ b/Plan.md @@ -149,6 +149,7 @@ flowchart LR - ClusterIP service on port 3000 - Traefik ingress for `sheet.sudloh.com` - Cloudflare edge TLS with the standard HTTP Traefik origin route + - ARM64 workload scheduling matching the production image architecture - Readiness/liveness health endpoint - Requests of 500m CPU/512 MiB and limits of 1 CPU/1 GiB - HPA from 2–6 replicas at 70% CPU diff --git a/README.md b/README.md index 2dc528f..d10a6fc 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,7 @@ Kustomize resources live in `k8s/` and define: - 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. - Non-root, read-only containers with dropped capabilities and seccomp. +- ARM64 scheduling constraints matching the production hosts and images. - A versioned migration Job and namespace-scoped CI deployer permissions. PostgreSQL, Redis, and S3 are external services. The manifests deliberately do diff --git a/k8s/base/deployment.yaml b/k8s/base/deployment.yaml index f3acbe6..c0821c0 100644 --- a/k8s/base/deployment.yaml +++ b/k8s/base/deployment.yaml @@ -25,6 +25,8 @@ spec: spec: automountServiceAccountToken: false terminationGracePeriodSeconds: 30 + nodeSelector: + kubernetes.io/arch: arm64 securityContext: runAsNonRoot: true runAsUser: 1000 diff --git a/k8s/base/ingress.yaml b/k8s/base/ingress.yaml index c3940e1..9471c13 100644 --- a/k8s/base/ingress.yaml +++ b/k8s/base/ingress.yaml @@ -4,6 +4,8 @@ metadata: name: buzz-sheet labels: app.kubernetes.io/name: buzz-sheet + annotations: + traefik.ingress.kubernetes.io/read-timeout: "200" spec: ingressClassName: traefik rules: diff --git a/k8s/base/worker-deployment.yaml b/k8s/base/worker-deployment.yaml index 826841b..968bbd0 100644 --- a/k8s/base/worker-deployment.yaml +++ b/k8s/base/worker-deployment.yaml @@ -25,6 +25,8 @@ spec: spec: automountServiceAccountToken: false terminationGracePeriodSeconds: 35 + nodeSelector: + kubernetes.io/arch: arm64 securityContext: runAsNonRoot: true runAsUser: 1000 diff --git a/k8s/migration/job.yaml b/k8s/migration/job.yaml index 25fec83..c7a680f 100644 --- a/k8s/migration/job.yaml +++ b/k8s/migration/job.yaml @@ -17,6 +17,8 @@ spec: spec: restartPolicy: Never automountServiceAccountToken: false + nodeSelector: + kubernetes.io/arch: arm64 securityContext: runAsNonRoot: true runAsUser: 1000 diff --git a/tests/deployment-contract.test.ts b/tests/deployment-contract.test.ts index 8242e7c..780aba7 100644 --- a/tests/deployment-contract.test.ts +++ b/tests/deployment-contract.test.ts @@ -15,6 +15,7 @@ describe("production deployment contract", () => { expect(deployment).toContain("maxUnavailable: 0"); expect(deployment).toContain("path: /api/health?ready=1"); expect(deployment).toContain("path: /api/health"); + expect(deployment).toContain("kubernetes.io/arch: arm64"); expect(deployment).toMatch( /requests:\s+cpu: 500m\s+memory: 512Mi\s+limits:\s+cpu: "1"\s+memory: 1Gi/u, ); @@ -36,6 +37,9 @@ describe("production deployment contract", () => { expect(service).toContain("targetPort: http"); expect(ingress).toContain("ingressClassName: traefik"); expect(ingress).toContain("host: sheet.sudloh.com"); + expect(ingress).toContain( + 'traefik.ingress.kubernetes.io/read-timeout: "200"', + ); expect(ingress).not.toContain("secretName:"); expect(ingress).not.toContain("router.tls"); }); @@ -96,6 +100,7 @@ describe("production deployment contract", () => { expect(workflow).toContain("kubectl kustomize k8s/"); expect(workflow).toContain("target: app"); expect(workflow).toContain("target: migration"); + expect(workflow.match(/platforms: linux\/arm64/gmu)).toHaveLength(2); expect(workflow).toContain( "registry.neko-piranha.ts.net/astral/buzz-sheet", );