feat: add Go workload deployment artifacts and split Ingress routing
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# Task: Build first-deployment artifacts for the Go stack
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260814-go-deploy-artifacts-2a5f8e1d
|
||||
- Mode: Feature
|
||||
- Branch: main
|
||||
- Worktree: /Users/brother7/Documents/AI/NianAIGC
|
||||
- Base commit: ca019abb14859f7413214b2b6a11a8a07cebf5f7
|
||||
- Owner: dsh
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Build the deployment artifacts for the first production deployment of the ADR-003 split topology: Go container image build, ACK Deployment/Service for the Go API workload, split-path Ingress routing, and workload configuration updates.
|
||||
- Human direction (2026-08-14): first production deployment runs Next.js (pages/static/SSR) plus Go (backend paths) directly; no Node Worker and no migration Job pod in production.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- Production Web workload holds no RDS/provider credentials and only needs the shared session secret for local cookie verification (its middleware already verifies the cookie with HMAC locally, no database access).
|
||||
- Go workload runs non-root, root filesystem read-only, with writable emptyDir mounts for runtime/logs/settings/temp.
|
||||
- Keep the manifest contract checker (`check-ack-manifests.mjs`) authoritative for the new topology.
|
||||
- Keep deprecated manifests (worker, migration Job) on disk with header comments.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Added `backend/Dockerfile` (multi-stage `golang:1.21-alpine` → `alpine:3.20`, static `CGO_ENABLED=0` build, non-root uid/gid 10001, ca-certificates + tzdata) and `backend/.dockerignore`.
|
||||
- Added `deploy/ack/go-api.yaml`: Deployment `zhinian-go-api` (1 replica, `/api/ready` database-aware readiness, runAsNonRoot, readOnlyRootFilesystem, RDS CA + data + tmp volumes, bootstrap/provider/webhook secrets, embedded WorkerLoop config) plus ClusterIP Service `zhinian-go-api:8080`.
|
||||
- Added `zhinian-go-runtime` ConfigMap to `deploy/ack/configmap.yaml` with the full Go runtime surface (DB/TLS settings, auth, embedded worker, billing, runtime/log/settings dirs).
|
||||
- Updated `deploy/ack/web.yaml`: removed RDS credentials, worker token, and RDS CA mount; readiness switched to process-level `/api/health` (Web is database-free in production).
|
||||
- Updated `deploy/ack/ingress.yaml`: `/api`, `/uploads`, `/generated-results` → `zhinian-go-api`; `/api/internal/worker` still → selectorless deny Service; pages/static → Web.
|
||||
- Updated `deploy/ack/secrets.example.yaml` with `zhinian-go-db`, `zhinian-go-bootstrap`, `zhinian-go-providers`, `zhinian-go-secrets` and notes that the session secret must match across workloads; marked local-only secrets.
|
||||
- Marked `deploy/ack/worker.yaml` deprecated (production uses the embedded WorkerLoop).
|
||||
- Updated `scripts/check-ack-manifests.mjs` assertions for the split topology (Web database-free, Go API non-root/database-aware readiness/bootstrap config, Ingress split routing).
|
||||
- Updated `docs/DEPLOYMENT.md`, `README.zh-CN.md`, and `README.md` deployment/tech-stack guidance (Go image build command, apply order, split topology).
|
||||
|
||||
## Verification
|
||||
|
||||
- `npm run deploy:check` — PASS (9 manifest files, new assertions).
|
||||
- All `deploy/ack/*.yaml` parse as valid multi-document YAML.
|
||||
- `CGO_ENABLED=0 go build ./cmd/zhinian-api` — PASS.
|
||||
- Docker image build itself must run on a machine with Docker; the Dockerfile is static-checked against the build steps in `scripts/run-go-command.mjs` conventions.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Build and push the `zhinian-go-api` image, then validate the manifests with `kubectl apply --dry-run=server` on the target ACK cluster.
|
||||
- Validate the full stack against non-production RDS/OSS/provider/Webhook dependencies before the first rollout.
|
||||
- Decide whether to delete the deprecated `worker.yaml` and `migration-job.yaml`.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Canonical memory (current-state Next Steps, commitments) still lists "build the Go workload deployment artifacts" as open; promote completion there in the next integration pass.
|
||||
@@ -314,9 +314,12 @@ ZHINIAN_INTERNAL_WORKER_TOKEN=change-me-worker-token
|
||||
- 分镜提示词与 `@素材` 引用编排
|
||||
- 火山 Visual 签名 canonical request
|
||||
- Next.js 生产构建
|
||||
- Go 后端全量测试与构建(`backend/`,契约 fixture 同步)
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
npm run health
|
||||
npm run go:test
|
||||
npm run go:build
|
||||
```
|
||||
|
||||
@@ -29,11 +29,12 @@
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Next.js 15
|
||||
- Next.js 15(前端:页面、静态资源、SSR)
|
||||
- Go 1.21(生产后端:独占 `/api`、`/uploads`、`/generated-results`,内嵌 WorkerLoop,见 `backend/` 与 [`docs/DEPLOYMENT.md`](./docs/DEPLOYMENT.md))
|
||||
- React 19
|
||||
- TypeScript
|
||||
- GSAP
|
||||
- PostgreSQL(生产)/本地 JSON(开发)
|
||||
- PostgreSQL(生产)/本地 JSON(开发)
|
||||
- Aliyun OSS 可选
|
||||
- Vitest
|
||||
|
||||
|
||||
4
backend/.dockerignore
Normal file
4
backend/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
zhinian-api
|
||||
coverage.out
|
||||
*.test
|
||||
.runtime/
|
||||
22
backend/Dockerfile
Normal file
22
backend/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build context: the repository backend/ directory.
|
||||
# docker build -f backend/Dockerfile -t REGISTRY/PROJECT/zhinian-go-api:TAG backend/
|
||||
|
||||
FROM golang:1.21-alpine AS build
|
||||
WORKDIR /src
|
||||
ENV CGO_ENABLED=0 GOOS=linux
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN go build -trimpath -ldflags="-s -w" -o /out/zhinian-api ./cmd/zhinian-api
|
||||
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache ca-certificates tzdata \
|
||||
&& addgroup -S -g 10001 zhinian \
|
||||
&& adduser -S -D -H -u 10001 -G zhinian zhinian \
|
||||
&& mkdir -p /var/lib/zhinian \
|
||||
&& chown -R zhinian:zhinian /var/lib/zhinian
|
||||
COPY --from=build /out/zhinian-api /usr/local/bin/zhinian-api
|
||||
USER 10001:10001
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["zhinian-api"]
|
||||
@@ -16,3 +16,31 @@ data:
|
||||
DATABASE_IDLE_TIMEOUT_MS: "30000"
|
||||
DATABASE_STATEMENT_TIMEOUT_MS: "30000"
|
||||
DATABASE_APPLICATION_NAME: zhinian-web
|
||||
---
|
||||
# Go API runtime settings. Provider endpoints/models use code defaults unless
|
||||
# overridden here; credentials always come from zhinian-go-providers.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: zhinian-go-runtime
|
||||
namespace: zhinian
|
||||
data:
|
||||
NODE_ENV: production
|
||||
GO_BACKEND_PORT: "8080"
|
||||
ZHINIAN_DATA_BACKEND: postgres
|
||||
ZHINIAN_AUTH_REQUIRED: "true"
|
||||
ZHINIAN_AUTH_COOKIE_SECURE: "true"
|
||||
ZHINIAN_PUBLIC_BASE_URL: https://REPLACE_WITH_PUBLIC_HOST
|
||||
DATABASE_SSL_MODE: verify-full
|
||||
DATABASE_CA_CERT_PATH: /etc/zhinian/rds/ca.pem
|
||||
DATABASE_POOL_MAX: "10"
|
||||
DATABASE_CONNECTION_TIMEOUT_MS: "5000"
|
||||
DATABASE_IDLE_TIMEOUT_MS: "30000"
|
||||
DATABASE_STATEMENT_TIMEOUT_MS: "30000"
|
||||
DATABASE_APPLICATION_NAME: zhinian-go-api
|
||||
ZHINIAN_GO_EMBEDDED_WORKER: "true"
|
||||
ZHINIAN_WORKER_ID: zhinian-go-api-embedded
|
||||
ZHINIAN_BILLING_REQUIRED: "1"
|
||||
ZHINIAN_RUNTIME_DIR: /var/lib/zhinian/runtime
|
||||
ZHINIAN_LOG_DIR: /var/lib/zhinian/logs
|
||||
ZHINIAN_SETTINGS_FILE: /var/lib/zhinian/settings.env
|
||||
|
||||
207
deploy/ack/go-api.yaml
Normal file
207
deploy/ack/go-api.yaml
Normal file
@@ -0,0 +1,207 @@
|
||||
# Go API workload: owns /api, /uploads, and /generated-results from the first
|
||||
# production deployment. The embedded WorkerLoop replaces the Node Worker; do
|
||||
# not deploy zhinian-worker in production.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: zhinian-go-api
|
||||
namespace: zhinian
|
||||
spec:
|
||||
# Keep one replica until generated assets live in shared OSS storage and the
|
||||
# RDS/provider connection budget is measured for more.
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: zhinian
|
||||
app.kubernetes.io/component: go-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: zhinian
|
||||
app.kubernetes.io/component: go-api
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: go-api
|
||||
image: REGISTRY/PROJECT/zhinian-go-api:REPLACE_TAG
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: zhinian-go-runtime
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-db
|
||||
key: DATABASE_URL
|
||||
# The session secret must be the same value Next.js uses so the
|
||||
# frontend middleware and the Go backend verify the same cookies.
|
||||
- name: ZHINIAN_AUTH_SESSION_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-web-auth
|
||||
key: ZHINIAN_AUTH_SESSION_SECRET
|
||||
- name: ZHINIAN_BOOTSTRAP_ADMIN_PHONE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-bootstrap
|
||||
key: ZHINIAN_BOOTSTRAP_ADMIN_PHONE
|
||||
- name: ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-bootstrap
|
||||
key: ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD
|
||||
- name: ZHINIAN_BOOTSTRAP_ADMIN_NAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-bootstrap
|
||||
key: ZHINIAN_BOOTSTRAP_ADMIN_NAME
|
||||
optional: true
|
||||
- name: VOLCENGINE_ACCESS_KEY_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-providers
|
||||
key: VOLCENGINE_ACCESS_KEY_ID
|
||||
optional: true
|
||||
- name: VOLCENGINE_SECRET_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-providers
|
||||
key: VOLCENGINE_SECRET_ACCESS_KEY
|
||||
optional: true
|
||||
- name: JIMENG_IMAGE_GENERATE_46_REQ_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-providers
|
||||
key: JIMENG_IMAGE_GENERATE_46_REQ_KEY
|
||||
optional: true
|
||||
- name: EVOLINK_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-providers
|
||||
key: EVOLINK_API_KEY
|
||||
optional: true
|
||||
- name: SEEDANCE_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-providers
|
||||
key: SEEDANCE_API_KEY
|
||||
optional: true
|
||||
- name: BAILIAN_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-providers
|
||||
key: BAILIAN_API_KEY
|
||||
optional: true
|
||||
- name: DASHSCOPE_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-providers
|
||||
key: DASHSCOPE_API_KEY
|
||||
optional: true
|
||||
- name: ALI_OSS_ACCESS_KEY_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-providers
|
||||
key: ALI_OSS_ACCESS_KEY_ID
|
||||
optional: true
|
||||
- name: ALI_OSS_ACCESS_KEY_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-providers
|
||||
key: ALI_OSS_ACCESS_KEY_SECRET
|
||||
optional: true
|
||||
- name: ZHINIAN_WEBHOOK_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-secrets
|
||||
key: ZHINIAN_WEBHOOK_SECRET
|
||||
- name: ZHINIAN_API_KEYS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-secrets
|
||||
key: ZHINIAN_API_KEYS
|
||||
optional: true
|
||||
- name: ZHINIAN_INTERNAL_WORKER_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-go-secrets
|
||||
key: ZHINIAN_INTERNAL_WORKER_TOKEN
|
||||
optional: true
|
||||
volumeMounts:
|
||||
- name: rds-ca
|
||||
mountPath: /etc/zhinian/rds
|
||||
readOnly: true
|
||||
- name: data
|
||||
mountPath: /var/lib/zhinian
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: http
|
||||
periodSeconds: 5
|
||||
failureThreshold: 24
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/ready
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 512Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
readOnlyRootFilesystem: true
|
||||
volumes:
|
||||
- name: rds-ca
|
||||
secret:
|
||||
secretName: zhinian-rds-ca
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: zhinian-go-api
|
||||
namespace: zhinian
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: zhinian
|
||||
app.kubernetes.io/component: go-api
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
@@ -12,7 +12,7 @@ spec:
|
||||
http:
|
||||
paths:
|
||||
# Longest-prefix matching sends public internal-API traffic to the
|
||||
# selectorless deny Service instead of the Web workload.
|
||||
# selectorless deny Service instead of any workload.
|
||||
- path: /api/internal/worker
|
||||
pathType: Prefix
|
||||
backend:
|
||||
@@ -20,6 +20,30 @@ spec:
|
||||
name: zhinian-public-deny
|
||||
port:
|
||||
number: 80
|
||||
# Backend paths belong to the Go API workload from the first
|
||||
# production deployment.
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: zhinian-go-api
|
||||
port:
|
||||
number: 8080
|
||||
- path: /uploads
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: zhinian-go-api
|
||||
port:
|
||||
number: 8080
|
||||
- path: /generated-results
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: zhinian-go-api
|
||||
port:
|
||||
number: 8080
|
||||
# Pages and static assets stay with Next.js.
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Example only. Replace every placeholder and keep the populated file out of Git.
|
||||
# Secrets marked "(local development only)" are not referenced by the first
|
||||
# production deployment; keep or drop them as your local workflow requires.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -6,6 +8,7 @@ metadata:
|
||||
namespace: zhinian
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Local development only: the production Web workload holds no RDS credentials.
|
||||
DATABASE_URL: postgresql://APP_USER:APP_PASSWORD@RDS_INTERNAL_HOST:5432/APP_DATABASE
|
||||
---
|
||||
apiVersion: v1
|
||||
@@ -15,6 +18,7 @@ metadata:
|
||||
namespace: zhinian
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Manual schema execution only: run database/migrations/*.sql with this role.
|
||||
DATABASE_URL: postgresql://MIGRATION_USER:MIGRATION_PASSWORD@RDS_INTERNAL_HOST:5432/APP_DATABASE
|
||||
---
|
||||
apiVersion: v1
|
||||
@@ -24,6 +28,7 @@ metadata:
|
||||
namespace: zhinian
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Local development only: the Node Worker is not deployed in production.
|
||||
ZHINIAN_INTERNAL_WORKER_TOKEN: REPLACE_WITH_A_LONG_RANDOM_VALUE
|
||||
---
|
||||
apiVersion: v1
|
||||
@@ -33,4 +38,60 @@ metadata:
|
||||
namespace: zhinian
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Shared by the Next.js middleware and the Go backend so both verify the
|
||||
# same session cookies. Keep identical across workloads.
|
||||
ZHINIAN_AUTH_SESSION_SECRET: REPLACE_WITH_A_DIFFERENT_LONG_RANDOM_VALUE
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: zhinian-go-db
|
||||
namespace: zhinian
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Application role (least privilege): grants applied manually after the SQL.
|
||||
DATABASE_URL: postgresql://APP_USER:APP_PASSWORD@RDS_INTERNAL_HOST:5432/APP_DATABASE
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: zhinian-go-bootstrap
|
||||
namespace: zhinian
|
||||
type: Opaque
|
||||
stringData:
|
||||
# First super administrator, created once at Go startup when no super
|
||||
# administrator exists. Password must be at least 8 characters.
|
||||
ZHINIAN_BOOTSTRAP_ADMIN_PHONE: REPLACE_WITH_ADMIN_PHONE
|
||||
ZHINIAN_BOOTSTRAP_ADMIN_PASSWORD: REPLACE_WITH_STRONG_PASSWORD
|
||||
ZHINIAN_BOOTSTRAP_ADMIN_NAME: 平台超级管理员
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: zhinian-go-providers
|
||||
namespace: zhinian
|
||||
type: Opaque
|
||||
stringData:
|
||||
# Provider credentials. Delete keys for providers you do not use; the Go
|
||||
# workload tolerates missing optional keys and fails closed when an enabled
|
||||
# engine has no credentials.
|
||||
VOLCENGINE_ACCESS_KEY_ID: REPLACE_OR_REMOVE
|
||||
VOLCENGINE_SECRET_ACCESS_KEY: REPLACE_OR_REMOVE
|
||||
JIMENG_IMAGE_GENERATE_46_REQ_KEY: REPLACE_OR_REMOVE
|
||||
EVOLINK_API_KEY: REPLACE_OR_REMOVE
|
||||
SEEDANCE_API_KEY: REPLACE_OR_REMOVE
|
||||
BAILIAN_API_KEY: REPLACE_OR_REMOVE
|
||||
DASHSCOPE_API_KEY: REPLACE_OR_REMOVE
|
||||
ALI_OSS_ACCESS_KEY_ID: REPLACE_OR_REMOVE
|
||||
ALI_OSS_ACCESS_KEY_SECRET: REPLACE_OR_REMOVE
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: zhinian-go-secrets
|
||||
namespace: zhinian
|
||||
type: Opaque
|
||||
stringData:
|
||||
ZHINIAN_WEBHOOK_SECRET: REPLACE_WITH_A_LONG_RANDOM_VALUE
|
||||
ZHINIAN_API_KEYS: REPLACE_WITH_PUBLIC_API_KEYS
|
||||
ZHINIAN_INTERNAL_WORKER_TOKEN: REPLACE_OR_REMOVE
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
# Next.js frontend workload for the first production deployment: serves pages,
|
||||
# static assets, and SSR only. All /api, /uploads, and /generated-results
|
||||
# traffic is routed to zhinian-go-api by the Ingress, so this workload holds no
|
||||
# RDS credentials and needs only the shared session secret for local cookie
|
||||
# verification in the middleware.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -10,7 +15,7 @@ spec:
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1 # Budget RDS connections for (replicas + maxSurge) * DATABASE_POOL_MAX.
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
@@ -37,25 +42,13 @@ spec:
|
||||
- configMapRef:
|
||||
name: zhinian-runtime
|
||||
env:
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-web-db
|
||||
key: DATABASE_URL
|
||||
- name: ZHINIAN_INTERNAL_WORKER_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-worker-auth
|
||||
key: ZHINIAN_INTERNAL_WORKER_TOKEN
|
||||
# The session secret must be the same value the Go backend uses so
|
||||
# the frontend middleware and the Go backend verify the same cookies.
|
||||
- name: ZHINIAN_AUTH_SESSION_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: zhinian-web-auth
|
||||
key: ZHINIAN_AUTH_SESSION_SECRET
|
||||
volumeMounts:
|
||||
- name: rds-ca
|
||||
mountPath: /etc/zhinian/rds
|
||||
readOnly: true
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
@@ -64,7 +57,7 @@ spec:
|
||||
failureThreshold: 24
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/ready
|
||||
path: /api/health
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
@@ -89,7 +82,3 @@ spec:
|
||||
drop: ["ALL"]
|
||||
# The current image runs as root. Add a fixed non-root image user and
|
||||
# verify /app/.runtime permissions before enabling runAsNonRoot.
|
||||
volumes:
|
||||
- name: rds-ca
|
||||
secret:
|
||||
secretName: zhinian-rds-ca
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# DEPRECATED (2026-08-14): production deploys the Go API workload with the
|
||||
# embedded WorkerLoop (ZHINIAN_GO_EMBEDDED_WORKER=true). The Node Worker is
|
||||
# local-development only; do not apply this manifest in production.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
|
||||
@@ -15,10 +15,25 @@ kubectl -n zhinian create secret generic zhinian-rds-ca \
|
||||
--from-file=ca.pem=./path/to/downloaded-rds-ca.pem
|
||||
kubectl apply -f deploy/ack/configmap.yaml
|
||||
kubectl apply -f deploy/ack/secrets.example.yaml # 仅作模板;先替换全部占位值
|
||||
kubectl apply -f deploy/ack/web.yaml -f deploy/ack/worker.yaml \
|
||||
kubectl apply -f deploy/ack/web.yaml -f deploy/ack/go-api.yaml \
|
||||
-f deploy/ack/service.yaml -f deploy/ack/ingress.yaml
|
||||
```
|
||||
|
||||
生产拓扑为 ADR-003 双工作负载:Next.js 只服务页面/静态资源/SSR(不持有任何
|
||||
RDS/服务商凭据,仅共享会话密钥);Go 工作负载 `zhinian-go-api` 独占
|
||||
`/api`、`/uploads`、`/generated-results`,内嵌 WorkerLoop(生产**不部署**
|
||||
Node Worker,`worker.yaml` 已弃用保留)。Ingress 按路径分流:页面 → Web,
|
||||
后端路径 → Go,`/api/internal/worker` → 无端点 deny Service。
|
||||
|
||||
Go 镜像构建:
|
||||
|
||||
```bash
|
||||
docker build -f backend/Dockerfile -t REGISTRY/PROJECT/zhinian-go-api:TAG backend/
|
||||
```
|
||||
|
||||
Go 首次启动时从 `zhinian-go-bootstrap` Secret 读取
|
||||
`ZHINIAN_BOOTSTRAP_ADMIN_*`,仅当不存在任何超级管理员时创建一次。
|
||||
|
||||
数据库 schema 由部署负责人在发布前手工执行,**不部署迁移 Job Pod**(清单
|
||||
`deploy/ack/migration-job.yaml` 已弃用保留):使用迁移角色账号依次执行
|
||||
`database/migrations/0001_initial_schema.sql`、`0002_generation_lifecycle_fencing.sql`,
|
||||
|
||||
@@ -17,11 +17,28 @@ assert(migrationJob.includes("secretName: zhinian-rds-ca"), "migration Job must
|
||||
|
||||
const web = read("web.yaml");
|
||||
assert(/^\s*replicas: 1\s*$/m.test(web), "Web must default to one replica until object storage is shared");
|
||||
assert(web.includes("path: /api/ready"), "Web must use database-aware readiness");
|
||||
assert(web.includes("path: /api/health"), "Web must use process-level readiness (it is database-free in production)");
|
||||
assert(!web.includes("zhinian-web-db"), "Web must not hold RDS credentials in production");
|
||||
assert(!web.includes("rds-ca"), "Web must not mount the RDS CA in production");
|
||||
|
||||
const goApi = read("go-api.yaml");
|
||||
assert(/^\s*replicas: 1\s*$/m.test(goApi), "Go API must default to one replica until object storage is shared");
|
||||
assert(goApi.includes("path: /api/ready"), "Go API must use database-aware readiness");
|
||||
assert(goApi.includes("runAsNonRoot: true"), "Go API must run as a non-root user");
|
||||
assert(goApi.includes("name: zhinian-go-runtime"), "Go API must consume the Go runtime ConfigMap");
|
||||
assert(goApi.includes("name: zhinian-go-bootstrap"), "Go API must receive bootstrap administrator credentials");
|
||||
assert(goApi.includes("secretName: zhinian-rds-ca"), "Go API must mount the RDS CA");
|
||||
|
||||
const configMap = read("configmap.yaml");
|
||||
assert(configMap.includes("ZHINIAN_GO_EMBEDDED_WORKER: \"true\""), "Go runtime ConfigMap must embed the WorkerLoop");
|
||||
|
||||
const ingress = read("ingress.yaml");
|
||||
assert(ingress.includes("path: /api/internal/worker"), "Ingress must intercept the internal worker prefix");
|
||||
assert(ingress.includes("name: zhinian-public-deny"), "Ingress must route the internal prefix away from Web");
|
||||
assert(ingress.includes("name: zhinian-public-deny"), "Ingress must route the internal prefix away from the workloads");
|
||||
assert(ingress.includes("name: zhinian-go-api"), "Ingress must route backend paths to the Go API Service");
|
||||
assert(ingress.includes("path: /uploads"), "Ingress must route /uploads to the Go API");
|
||||
assert(ingress.includes("path: /generated-results"), "Ingress must route /generated-results to the Go API");
|
||||
assert(ingress.includes("name: zhinian-web"), "Ingress must route pages/static paths to Web");
|
||||
|
||||
const service = read("service.yaml");
|
||||
assert(service.includes("name: zhinian-public-deny"), "selectorless deny Service is required");
|
||||
|
||||
Reference in New Issue
Block a user