feat: add Go workload deployment artifacts and split Ingress routing
This commit is contained in:
1 parent
ca019abb14
commit
4a8f2d56e2
13 files changed
+452
-26
No files matched your search
@@ -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
|
- 火山 Visual 签名 canonical request
|
||||||
- Next.js 生产构建
|
- Next.js 生产构建
|
||||||
|
- Go 后端全量测试与构建(`backend/`,契约 fixture 同步)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm test
|
npm test
|
||||||
npm run build
|
npm run build
|
||||||
npm run health
|
npm run health
|
||||||
|
npm run go:test
|
||||||
|
npm run go:build
|
||||||
```
|
```
|
||||||
+3
-2
@@ -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
|
- React 19
|
||||||
- TypeScript
|
- TypeScript
|
||||||
- GSAP
|
- GSAP
|
||||||
- PostgreSQL(生产)/本地 JSON(开发)
|
- PostgreSQL(生产)/本地 JSON(开发)
|
||||||
- Aliyun OSS 可选
|
- Aliyun OSS 可选
|
||||||
- Vitest
|
- Vitest
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
zhinian-api
|
||||||
|
coverage.out
|
||||||
|
*.test
|
||||||
|
.runtime/
|
||||||
@@ -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_IDLE_TIMEOUT_MS: "30000"
|
||||||
DATABASE_STATEMENT_TIMEOUT_MS: "30000"
|
DATABASE_STATEMENT_TIMEOUT_MS: "30000"
|
||||||
DATABASE_APPLICATION_NAME: zhinian-web
|
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
|
||||||
@@ -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
|
||||||
+25
-1
@@ -12,7 +12,7 @@ spec:
|
|||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
# Longest-prefix matching sends public internal-API traffic to the
|
# 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
|
- path: /api/internal/worker
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
@@ -20,6 +20,30 @@ spec:
|
|||||||
name: zhinian-public-deny
|
name: zhinian-public-deny
|
||||||
port:
|
port:
|
||||||
number: 80
|
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: /
|
- path: /
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# Example only. Replace every placeholder and keep the populated file out of Git.
|
# 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
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
@@ -6,6 +8,7 @@ metadata:
|
|||||||
namespace: zhinian
|
namespace: zhinian
|
||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
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
|
DATABASE_URL: postgresql://APP_USER:APP_PASSWORD@RDS_INTERNAL_HOST:5432/APP_DATABASE
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@@ -15,6 +18,7 @@ metadata:
|
|||||||
namespace: zhinian
|
namespace: zhinian
|
||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
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
|
DATABASE_URL: postgresql://MIGRATION_USER:MIGRATION_PASSWORD@RDS_INTERNAL_HOST:5432/APP_DATABASE
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@@ -24,6 +28,7 @@ metadata:
|
|||||||
namespace: zhinian
|
namespace: zhinian
|
||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
stringData:
|
||||||
|
# Local development only: the Node Worker is not deployed in production.
|
||||||
ZHINIAN_INTERNAL_WORKER_TOKEN: REPLACE_WITH_A_LONG_RANDOM_VALUE
|
ZHINIAN_INTERNAL_WORKER_TOKEN: REPLACE_WITH_A_LONG_RANDOM_VALUE
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@@ -33,4 +38,60 @@ metadata:
|
|||||||
namespace: zhinian
|
namespace: zhinian
|
||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
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
|
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
|
||||||
+9
-20
@@ -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
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -10,7 +15,7 @@ spec:
|
|||||||
strategy:
|
strategy:
|
||||||
type: RollingUpdate
|
type: RollingUpdate
|
||||||
rollingUpdate:
|
rollingUpdate:
|
||||||
maxSurge: 1 # Budget RDS connections for (replicas + maxSurge) * DATABASE_POOL_MAX.
|
maxSurge: 1
|
||||||
maxUnavailable: 0
|
maxUnavailable: 0
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
@@ -37,25 +42,13 @@ spec:
|
|||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: zhinian-runtime
|
name: zhinian-runtime
|
||||||
env:
|
env:
|
||||||
- name: DATABASE_URL
|
# The session secret must be the same value the Go backend uses so
|
||||||
valueFrom:
|
# the frontend middleware and the Go backend verify the same cookies.
|
||||||
secretKeyRef:
|
|
||||||
name: zhinian-web-db
|
|
||||||
key: DATABASE_URL
|
|
||||||
- name: ZHINIAN_INTERNAL_WORKER_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: zhinian-worker-auth
|
|
||||||
key: ZHINIAN_INTERNAL_WORKER_TOKEN
|
|
||||||
- name: ZHINIAN_AUTH_SESSION_SECRET
|
- name: ZHINIAN_AUTH_SESSION_SECRET
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: zhinian-web-auth
|
name: zhinian-web-auth
|
||||||
key: ZHINIAN_AUTH_SESSION_SECRET
|
key: ZHINIAN_AUTH_SESSION_SECRET
|
||||||
volumeMounts:
|
|
||||||
- name: rds-ca
|
|
||||||
mountPath: /etc/zhinian/rds
|
|
||||||
readOnly: true
|
|
||||||
startupProbe:
|
startupProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /api/health
|
path: /api/health
|
||||||
@@ -64,7 +57,7 @@ spec:
|
|||||||
failureThreshold: 24
|
failureThreshold: 24
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /api/ready
|
path: /api/health
|
||||||
port: http
|
port: http
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
@@ -89,7 +82,3 @@ spec:
|
|||||||
drop: ["ALL"]
|
drop: ["ALL"]
|
||||||
# The current image runs as root. Add a fixed non-root image user and
|
# The current image runs as root. Add a fixed non-root image user and
|
||||||
# verify /app/.runtime permissions before enabling runAsNonRoot.
|
# 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
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
+16
-1
@@ -15,10 +15,25 @@ kubectl -n zhinian create secret generic zhinian-rds-ca \
|
|||||||
--from-file=ca.pem=./path/to/downloaded-rds-ca.pem
|
--from-file=ca.pem=./path/to/downloaded-rds-ca.pem
|
||||||
kubectl apply -f deploy/ack/configmap.yaml
|
kubectl apply -f deploy/ack/configmap.yaml
|
||||||
kubectl apply -f deploy/ack/secrets.example.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
|
-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**(清单
|
数据库 schema 由部署负责人在发布前手工执行,**不部署迁移 Job Pod**(清单
|
||||||
`deploy/ack/migration-job.yaml` 已弃用保留):使用迁移角色账号依次执行
|
`deploy/ack/migration-job.yaml` 已弃用保留):使用迁移角色账号依次执行
|
||||||
`database/migrations/0001_initial_schema.sql`、`0002_generation_lifecycle_fencing.sql`,
|
`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");
|
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(/^\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");
|
const ingress = read("ingress.yaml");
|
||||||
assert(ingress.includes("path: /api/internal/worker"), "Ingress must intercept the internal worker prefix");
|
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");
|
const service = read("service.yaml");
|
||||||
assert(service.includes("name: zhinian-public-deny"), "selectorless deny Service is required");
|
assert(service.includes("name: zhinian-public-deny"), "selectorless deny Service is required");
|
||||||
|
|||||||
Reference in new issue
Block a user