Compare commits

...
37 Commits
Author SHA1 Message Date
github-actions[bot] c03996f0be pages: update evidence feed [skip ci] 2026-04-03 17:39:47 +00:00
github-actions[bot] 7d8cab609d pages: update evidence feed [skip ci] 2026-04-02 17:53:36 +00:00
github-actions[bot] 75723148ff pages: update evidence feed [skip ci] 2026-04-01 17:54:28 +00:00
github-actions[bot] 5b254b424c pages: update evidence feed [skip ci] 2026-03-31 17:58:41 +00:00
github-actions[bot] 291aeeef2f pages: update evidence feed [skip ci] 2026-03-30 17:57:16 +00:00
github-actions[bot] f8ca3f2e4e pages: update evidence feed [skip ci] 2026-03-29 17:37:42 +00:00
github-actions[bot] 880d47e92c pages: update evidence feed [skip ci] 2026-03-28 17:35:59 +00:00
github-actions[bot] 7354b26434 pages: update evidence feed [skip ci] 2026-03-27 17:53:01 +00:00
github-actions[bot] 3da1c2a4de pages: update evidence feed [skip ci] 2026-03-26 18:02:40 +00:00
github-actions[bot] 18e9396cf5 pages: update evidence feed [skip ci] 2026-03-25 18:02:31 +00:00
github-actions[bot] e3bd94330c pages: update evidence feed [skip ci] 2026-03-24 17:59:54 +00:00
github-actions[bot] 8bbd8bee1d pages: update evidence feed [skip ci] 2026-03-23 17:55:43 +00:00
github-actions[bot] 79f846bdf7 pages: update evidence feed [skip ci] 2026-03-22 17:30:51 +00:00
github-actions[bot] 3a63d7422b pages: update evidence feed [skip ci] 2026-03-21 17:31:42 +00:00
github-actions[bot] f6bb371d0c pages: update evidence feed [skip ci] 2026-03-20 17:39:37 +00:00
github-actions[bot] 65c864e97c pages: update evidence feed [skip ci] 2026-03-19 17:54:51 +00:00
github-actions[bot] d403be1089 pages: update evidence feed [skip ci] 2026-03-18 18:02:34 +00:00
github-actions[bot] b32843470d pages: update evidence feed [skip ci] 2026-03-17 18:02:13 +00:00
github-actions[bot] bcedcb2931 pages: update evidence feed [skip ci] 2026-03-16 18:02:45 +00:00
github-actions[bot] 1ea8e86112 pages: update evidence feed [skip ci] 2026-03-15 17:34:48 +00:00
github-actions[bot] 15bc920c82 pages: update evidence feed [skip ci] 2026-03-14 17:33:15 +00:00
github-actions[bot] 5d2f9d92b1 pages: update evidence feed [skip ci] 2026-03-13 17:40:54 +00:00
github-actions[bot] 9851e02574 pages: update evidence feed [skip ci] 2026-03-12 17:53:33 +00:00
github-actions[bot] 990a095f7d pages: update evidence feed [skip ci] 2026-03-11 17:52:51 +00:00
github-actions[bot] b1b4d041c9 pages: update evidence feed [skip ci] 2026-03-10 17:51:45 +00:00
github-actions[bot] 5d6acdddb8 pages: update evidence feed [skip ci] 2026-03-09 17:51:13 +00:00
github-actions[bot] f2f26ba434 pages: update evidence feed [skip ci] 2026-03-08 17:28:25 +00:00
github-actions[bot] d8d404a617 pages: update evidence feed [skip ci] 2026-03-07 17:27:02 +00:00
github-actions[bot] b7177392bf pages: update evidence feed [skip ci] 2026-03-06 17:39:52 +00:00
github-actions[bot] 024ab2ccd4 pages: update evidence feed [skip ci] 2026-03-05 18:48:17 +00:00
github-actions[bot] b0b0aa42b0 pages: update evidence feed [skip ci] 2026-03-04 17:44:32 +00:00
github-actions[bot] 2570e4d18f pages: update evidence feed [skip ci] 2026-03-03 17:49:35 +00:00
github-actions[bot] 2465fc77e1 pages: update evidence feed [skip ci] 2026-03-03 10:55:50 +00:00
github-actions[bot] 35827fe379 pages: update evidence feed [skip ci] 2026-03-03 06:43:53 +00:00
github-actions[bot] c8a059d671 pages: update evidence feed [skip ci] 2026-03-03 05:47:07 +00:00
github-actions[bot] 89c06e3f5b pages: update evidence feed [skip ci] 2026-03-02 14:46:47 +00:00
github-actions[bot] d134c11d44 pages: update evidence feed [skip ci] 2026-03-02 06:48:55 +00:00
139 changed files with 49 additions and 11759 deletions
-1
View File
@@ -1 +0,0 @@
client/data/mysql/mysql.sock
-66
View File
@@ -1,66 +0,0 @@
name: deploy-repo-gate
on:
pull_request:
branches-ignore: [gh-pages]
permissions:
contents: read
pull-requests: read
jobs:
gate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed files
id: changed
run: |
git fetch --no-tags --prune --depth=1 origin "${{ github.base_ref }}"
git diff --name-only "origin/${{ github.base_ref }}...HEAD" > .changed-files.txt
cat .changed-files.txt
- name: Enforce allowed directories (unless platform-admin label)
env:
GITHUB_EVENT_PATH: ${{ github.event_path }}
run: |
python3 - <<'PY'
import json, pathlib, sys
allowed = (
'envs/', 'release/', 'evidence/', 'docs/', 'scripts/', 'pages/', '.github/', 'k8s/'
)
changed = [line.strip() for line in pathlib.Path('.changed-files.txt').read_text(encoding='utf-8').splitlines() if line.strip()]
event = json.loads(pathlib.Path('${{ github.event_path }}').read_text(encoding='utf-8'))
labels = {label.get('name','') for label in event.get('pull_request', {}).get('labels', [])}
disallowed = [p for p in changed if not p.startswith(allowed)]
if disallowed and 'platform-admin' not in labels:
print('Disallowed changes detected (missing platform-admin label):')
for p in disallowed:
print('-', p)
sys.exit(1)
print('Path gate passed.')
PY
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install deps
run: pip install --disable-pip-version-check pyyaml jsonschema
- name: Validate release queue structure
run: python3 scripts/promoter/validate_queue.py
- name: Evidence validate + collect (dry run)
run: |
python3 scripts/evidence/validate.py
python3 scripts/evidence/collect.py --out /tmp/evidence-index.json --summary /tmp/evidence-latest.md
-36
View File
@@ -1,36 +0,0 @@
name: nightly-auto-repair
on:
push:
branches-ignore: [gh-pages]
paths-ignore: ['**']
schedule:
# 01:30 Asia/Tokyo (JST) = 16:30 UTC previous day
- cron: "30 16 * * *"
workflow_dispatch:
permissions:
contents: write
issues: write
jobs:
repair:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Run auto-repair loop
run: |
bash scripts/repair/run.sh \
--allow-main \
--check-cmd "bash scripts/ci/run_checks.sh" \
--max-attempts 3 \
--recipes repairs/recipes.yaml \
--issue-repo "${{ github.repository }}"
-82
View File
@@ -1,82 +0,0 @@
name: nightly-evidence
on:
push:
branches-ignore: [gh-pages]
paths-ignore: ['**']
schedule:
# 02:00 Asia/Tokyo (JST) = 17:00 UTC previous day
- cron: "0 17 * * *"
workflow_dispatch:
permissions:
contents: write
concurrency:
group: nightly-evidence-publish
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install deps
run: pip install --disable-pip-version-check pyyaml jsonschema
- name: Validate and collect evidence
run: |
python3 scripts/evidence/validate.py
python3 scripts/evidence/collect.py --out evidence/index.json --summary evidence/summary/latest.md
- name: Build publish payload (atomic staging)
run: |
rm -rf .publish-tmp
mkdir -p .publish-tmp/evidence/summary
cp pages/index.html pages/app.js .publish-tmp/
cp evidence/index.json .publish-tmp/evidence/index.json
cp evidence/summary/latest.md .publish-tmp/evidence/summary/latest.md
python3 - <<'PY'
import json, pathlib
feed = pathlib.Path('.publish-tmp/evidence/index.json')
data = json.loads(feed.read_text(encoding='utf-8'))
if not isinstance(data, list):
raise SystemExit('evidence/index.json must be an array')
print(f'staged records: {len(data)}')
PY
- name: Publish to gh-pages branch
run: |
set -euo pipefail
rm -rf .gh-pages-wt
if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then
git worktree add --force -B gh-pages .gh-pages-wt origin/gh-pages
else
git worktree add --force -B gh-pages .gh-pages-wt
fi
rsync -a --delete .publish-tmp/ .gh-pages-wt/
cd .gh-pages-wt
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "No changes to publish"
exit 0
fi
git commit -m "pages: update evidence feed [skip ci]"
git push origin HEAD:gh-pages
-78
View File
@@ -1,78 +0,0 @@
# End-to-End Dry Run (Bid-MVP Factory)
This dry run validates docs gates, evidence feed, promoter, smoke runner, and repair loop without secrets.
## 1. Docs Repo Checks (`ljwx-docs`)
```bash
cd ../ljwx-docs
test -f PLAN.md
rg --files docs/factory docs/templates .github/PULL_REQUEST_TEMPLATE
rg -n "Bid-MVP Factory|/factory/|/templates/" docs/.vitepress/config.ts
```
Note: `npm run docs:build` currently fails on existing repo dead links unrelated to this implementation.
## 2. Deploy Repo Checks (`ljwx-deploy`)
```bash
cd ../ljwx-deploy
test -f PLAN.md
python3 scripts/evidence/validate.py
python3 scripts/evidence/collect.py --out evidence/index.json
python3 -m json.tool evidence/index.json >/dev/null
```
## 3. Promoter Dry Run
```bash
python3 scripts/promoter/validate_queue.py
bash scripts/promoter/promote.sh --dry-run
```
Expected: script reports normalization/promotion simulation without commit/push.
## 4. Smoke Runner Dry Run
```bash
python3 scripts/smoke/run_smoke.py --help
python3 scripts/smoke/run_smoke.py --dry-run
```
Expected: skips targets when no promoted records exist yet.
## 5. Repair Loop Dry Run
```bash
python3 scripts/repair/run_repair.py --help
python3 scripts/repair/diagnose.py --help
python3 scripts/repair/run_repair.py \
--recipes repairs/recipes.yaml \
--check-cmd "bash scripts/ci/run_checks.sh" \
--max-attempts 1 \
--log-dir .factory/repair/dryrun \
--dry-run
bash scripts/repair/run.sh \
--allow-main \
--max-attempts 1 \
--check-cmd "bash scripts/ci/run_checks.sh" \
--recipes repairs/recipes.yaml \
--dry-run
```
Expected: check passes or repair actions/logs are produced in `.factory/repair/dryrun`.
## 6. Pages Publish Workflow (Manual)
- Trigger workflow: `evidence-pages` (`workflow_dispatch`).
- Confirm `gh-pages` branch is updated by bot commit.
- Open dashboard URL: `https://brunogaosz.github.io/ljwx-deploy/`.
- Verify `Smoke` and `Promotion` columns render from `evidence/index.json`.
## 7. Full Dev Autopilot (Optional)
```bash
bash scripts/factory/run_all_dev.sh
```
Requires authenticated `gh` CLI and permissions to create/merge PRs and issues.
+20 -7
View File
@@ -1,6 +1,8 @@
(function () {
const FEED_URL = "./evidence/index.json";
const METRICS_URL = "./evidence/metrics/queue-health.json";
let allRecords = [];
let queueMetrics = null;
const envFilter = document.getElementById("env-filter");
const testFilter = document.getElementById("test-filter");
@@ -11,11 +13,11 @@
return String(v ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}
function digestFromHarbor(harbor) {
if (!harbor) return "-";
const idx = harbor.indexOf("@sha256:");
if (idx === -1) return harbor;
return "sha256:" + harbor.slice(idx + 8, idx + 8 + 16);
function digestFromImage(imageRef) {
if (!imageRef) return "-";
const idx = imageRef.indexOf("@sha256:");
if (idx === -1) return imageRef;
return "sha256:" + imageRef.slice(idx + 8, idx + 8 + 16);
}
function smokeStatus(rec) {
@@ -57,14 +59,17 @@
<td><code>${text(r.evidenceId || "-")}</code></td>
<td>${text(r.service || "-")}</td>
<td>${text(r.env || "-")}</td>
<td><code>${text(digestFromHarbor(r?.image?.harbor || ""))}</code></td>
<td><code>${text(digestFromImage(r?.image?.deployed || r?.image?.harbor || ""))}</code></td>
<td>${text(r?.deploy?.syncedAt || r?.promotedAt || "-")}</td>
<td><span class="pill ${klass}">${text(smoke)}</span></td>
<td>${linksHtml(r)}</td>
</tr>`;
}).join("") || '<tr><td colspan="7">No records</td></tr>';
metaEl.textContent = `records: ${filtered.length} / ${allRecords.length}`;
const queueMeta = queueMetrics?.counts
? ` | queue pending=${queueMetrics.counts.pending}, failed=${queueMetrics.counts.failed}, superseded=${queueMetrics.counts.superseded}`
: "";
metaEl.textContent = `records: ${filtered.length} / ${allRecords.length}${queueMeta}`;
}
function fillEnvOptions() {
@@ -85,6 +90,14 @@
}
allRecords = await res.json();
if (!Array.isArray(allRecords)) allRecords = [];
const metricsRes = await fetch(METRICS_URL, { cache: "no-store" });
if (metricsRes.ok) {
const payload = await metricsRes.json();
if (payload && typeof payload === "object") {
queueMetrics = payload;
}
}
fillEnvOptions();
render();
}
-72
View File
@@ -1,72 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-platform
labels:
app: agent-platform
spec:
replicas: 1
selector:
matchLabels:
app: agent-platform
template:
metadata:
labels:
app: agent-platform
spec:
imagePullSecrets:
- name: regcred
containers:
- name: agent-platform
image: harbor.eu.lingjingwanxiang.cn/ljwx/agent-platform-api:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8000
name: http
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: agent-platform-db
key: DATABASE_URL
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: agent-platform-app
key: REDIS_URL
- name: ANTHROPIC_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: agent-platform-app
key: ANTHROPIC_AUTH_TOKEN
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: agent-platform-app
key: JWT_SECRET_KEY
- name: MILVUS_HOST
value: ""
- name: MILVUS_PORT
value: "19530"
- name: APP_ENV
value: "prod"
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 30
failureThreshold: 5
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
-25
View File
@@ -1,25 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: agent-platform
annotations:
cert-manager.io/cluster-issuer: dnspod-letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
tls:
- hosts:
- agent.lingjingwanxiang.cn
secretName: agent-platform-tls
rules:
- host: agent.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: agent-platform
port:
number: 8000
@@ -1,9 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
- secret.yaml
- regcred.yaml
-7
View File
@@ -1,7 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: regcred
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: eyJhdXRocyI6eyJoYXJib3IuZXUubGluZ2ppbmd3YW54aWFuZy5jbiI6eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJIYXJib3IxMjM0NSIsImF1dGgiOiJZV1J0YVc0NlNHRnlZbTl5TVRJek5EVT0ifX19
-17
View File
@@ -1,17 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: agent-platform-db
type: Opaque
stringData:
DATABASE_URL: postgresql+asyncpg://agent:ChangeMeInProduction@postgres-lb.infra.svc.cluster.local:5432/agent_platform
---
apiVersion: v1
kind: Secret
metadata:
name: agent-platform-app
type: Opaque
stringData:
REDIS_URL: redis://redis-lb.infra.svc.cluster.local:6379/0
ANTHROPIC_AUTH_TOKEN: CHANGE_ME
JWT_SECRET_KEY: CHANGE_ME
-14
View File
@@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: agent-platform
labels:
app: agent-platform
spec:
selector:
app: agent-platform
ports:
- name: http
port: 8000
targetPort: http
type: ClusterIP
@@ -1,8 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: agent-platform
resources:
- ../../base
images:
- name: harbor.eu.lingjingwanxiang.cn/ljwx/agent-platform-api
newTag: sha-dec280a
-62
View File
@@ -1,62 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
labels:
app: backend
app.kubernetes.io/name: backend
app.kubernetes.io/part-of: youngth-guard
component: api
spec:
replicas: 1
selector:
matchLabels:
app: backend
app.kubernetes.io/name: backend
app.kubernetes.io/part-of: youngth-guard
template:
metadata:
labels:
app: backend
app.kubernetes.io/name: backend
app.kubernetes.io/part-of: youngth-guard
component: api
spec:
imagePullSecrets:
- name: harbor-registry
containers:
- name: backend
image: harbor.omniverseai.net/ljwx/ljwx-shiti:sha-775861d
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8000
name: http
protocol: TCP
env:
- name: ENVIRONMENT
value: "dev"
- name: LOG_LEVEL
value: "info"
- name: DATABASE_URL
value: "postgresql+asyncpg://appuser:123456@postgres-infra-postgresql.infra.svc.cluster.local:5432/appdb"
- name: REDIS_URL
value: "redis://:123456@redis-infra-master.infra.svc.cluster.local:6379/0"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
-6
View File
@@ -1,6 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
-20
View File
@@ -1,20 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: backend
labels:
app: backend
app.kubernetes.io/name: backend
app.kubernetes.io/part-of: youngth-guard
component: api
spec:
type: ClusterIP
selector:
app: backend
app.kubernetes.io/name: backend
app.kubernetes.io/part-of: youngth-guard
ports:
- name: http
port: 8000
targetPort: http
protocol: TCP
@@ -1,7 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
- ../../base
@@ -1,8 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
namePrefix: gh-
resources:
- ../../base
-326
View File
@@ -1,326 +0,0 @@
# ljwx-bookstore GitOps 配置
智能小说阅读平台的 Kubernetes GitOps 部署配置,使用 Kustomize 管理。
## 📋 概览
- **应用名称**: ljwx-bookstore
- **代码仓库**: https://github.com/BrunoGaoSZ/ljwx-bookstore
- **GitOps 仓库**: https://github.com/BrunoGaoSZ/ljwx-deploy
- **命名空间**: bookstore
- **配置管理**: Kustomize
- **GitOps 控制器**: Argo CD
## 🚀 快速部署
### 前置条件
1. **Kubernetes 集群**: K3s 或其他 Kubernetes 集群
2. **Argo CD**: 已安装并配置
3. **基础设施**:
- MySQL 8.0 (infra namespace)
- Redis 7.2.12 (ljwx-health namespace)
- Traefik Ingress Controller
- cert-manager + Let's Encrypt
### 创建必需的 Secrets
```bash
# 1. 创建命名空间
kubectl create namespace bookstore
# 2. GHCR 镜像拉取凭证
kubectl create secret docker-registry ghcr-secret \
--docker-server=ghcr.io \
--docker-username=<github-username> \
--docker-password=<github-token> \
-n bookstore
# 3. Redis 连接配置
kubectl create secret generic redis-secret \
--from-literal=host=redis.ljwx-health.svc.cluster.local \
--from-literal=port=6379 \
--from-literal=password=<redis-password> \
-n bookstore
```
### 部署应用
```bash
# 方法 1: 使用 Argo CD Application CRD
kubectl apply -f argocd-apps/40-ljwx-bookstore.yaml
# 方法 2: 使用 Argo CD CLI
argocd app create ljwx-bookstore \
--repo https://github.com/BrunoGaoSZ/ljwx-deploy.git \
--path apps/ljwx-bookstore/overlays/bookstore \
--dest-server https://kubernetes.default.svc \
--dest-namespace bookstore \
--sync-policy automated \
--auto-prune \
--self-heal
# 查看部署状态
argocd app get ljwx-bookstore
argocd app sync ljwx-bookstore
```
## 📁 目录结构
```
apps/ljwx-bookstore/
├── base/ # 基础配置
│ ├── deployment.yaml # Deployment 定义
│ ├── service.yaml # Service 定义
│ ├── ingress.yaml # Ingress + Middleware(HTTPS + 根路径重定向)
│ ├── ghcr-secret.yaml # GHCR 拉取凭证引用
│ ├── redis-secret.yaml # Redis 连接配置引用
│ └── kustomization.yaml # Kustomize 基础配置
│
└── overlays/ # 环境特定配置
└── bookstore/ # bookstore 环境
└── kustomization.yaml # 镜像版本配置(自动更新)
```
## ⚙️ 配置说明
### Deployment 配置
**文件**: `base/deployment.yaml`
- **副本数**: 2
- **镜像**: `ghcr.io/brunogaosz/ljwx-bookstore/bookstore:bookstore-<commit>`
- **端口**: 8080
- **环境变量**:
- `SPRING_DATASOURCE_URL`: MySQL 连接(infra namespace)
- `SPRING_DATASOURCE_USERNAME`: bookstore
- `SPRING_DATASOURCE_PASSWORD`: 从 mysql-standalone Secret 获取
- `SPRING_DATA_REDIS_HOST`: 从 redis-secret 获取
- `SPRING_DATA_REDIS_PORT`: 从 redis-secret 获取
- `SPRING_DATA_REDIS_PASSWORD`: 从 redis-secret 获取
- **健康检查**:
- Liveness Probe: `/fiction/actuator/health/liveness`
- Readiness Probe: `/fiction/actuator/health/readiness`
### Service 配置
**文件**: `base/service.yaml`
- **类型**: ClusterIP
- **端口**: 8080 → 8080
### Ingress 配置
**文件**: `base/ingress.yaml`
- **Ingress Controller**: Traefik
- **域名**: bookstore.lingjingwanxiang.cn
- **功能**:
- ✅ HTTPS 自动重定向
- ✅ Let's Encrypt 证书自动签发(cert-manager)
- ✅ 根路径(`/`)自动重定向到 `/fiction/index`
- **TLS Secret**: bookstore-lingjingwanxiang-tls
- **ClusterIssuer**: dnspod-letsencrypt
**Traefik Middleware**: 实现根路径重定向
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-root
namespace: bookstore
spec:
redirectRegex:
regex: "^https?://[^/]+/$"
replacement: "https://bookstore.lingjingwanxiang.cn/fiction/index"
permanent: false
```
### Kustomize 配置
**基础配置** (`base/kustomization.yaml`):
```yaml
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
- ghcr-secret.yaml
- redis-secret.yaml
```
**环境配置** (`overlays/bookstore/kustomization.yaml`):
```yaml
resources:
- ../../base
namespace: bookstore
images:
- name: ghcr.io/brunogaosz/ljwx-bookstore/bookstore
newName: ghcr.io/brunogaosz/ljwx-bookstore/bookstore
newTag: "bookstore-<commit-sha>" # 由 CI 自动更新
```
## 🔄 GitOps 工作流程
### 自动化部署流程
1. **代码提交** → GitHub (ljwx-bookstore)
2. **GitHub Actions CI**:
- 构建 Docker 镜像(多架构:linux/amd64, linux/arm64)
- 推送到 GHCR: `ghcr.io/brunogaosz/ljwx-bookstore/bookstore:bookstore-<commit>`
3. **GitOps 更新**:
- CI 自动创建 PR 到 ljwx-deploy 仓库
- 更新 `overlays/bookstore/kustomization.yaml` 中的镜像标签
4. **PR 合并** → main 分支
5. **Argo CD 同步**:
- 检测 Git 仓库变更(轮询间隔:3 分钟)
- 自动同步到 Kubernetes 集群
6. **滚动更新**:
- Kubernetes 执行零停机滚动更新
- 新 Pod 启动 → 健康检查通过 → 旧 Pod 终止
### 手动更新配置
```bash
# 1. 克隆 GitOps 仓库
git clone https://github.com/BrunoGaoSZ/ljwx-deploy.git
cd ljwx-deploy
# 2. 修改配置
vim apps/ljwx-bookstore/base/deployment.yaml
# 3. 提交变更
git add .
git commit -m "feat: update bookstore configuration"
git push
# 4. Argo CD 自动同步(~3 分钟)
# 或手动触发同步
argocd app sync ljwx-bookstore
```
## 📊 监控和验证
### 查看部署状态
```bash
# Argo CD 应用状态
argocd app get ljwx-bookstore
# Kubernetes 资源状态
kubectl get all -n bookstore
# Pod 日志
kubectl logs -f -n bookstore -l app=bookstore
# Ingress 状态
kubectl get ingress -n bookstore
kubectl describe ingress bookstore-ingress -n bookstore
```
### 健康检查
```bash
# 应用健康检查
curl https://bookstore.lingjingwanxiang.cn/fiction/actuator/health
# 预期响应
{
"status": "UP",
"components": {
"db": {"status": "UP"},
"redis": {"status": "UP"},
"diskSpace": {"status": "UP"},
"livenessState": {"status": "UP"},
"readinessState": {"status": "UP"}
}
}
```
### 访问测试
```bash
# 测试根路径重定向
curl -I https://bookstore.lingjingwanxiang.cn/
# 预期: HTTP 307 重定向到 /fiction/index
# 访问应用
curl -L https://bookstore.lingjingwanxiang.cn/
# 预期: HTTP 200,返回首页 HTML
```
## 🔧 故障排查
### Pod 无法启动
```bash
# 查看 Pod 状态
kubectl get pods -n bookstore
# 查看 Pod 事件
kubectl describe pod <pod-name> -n bookstore
# 查看日志
kubectl logs <pod-name> -n bookstore
# 常见问题:
# 1. 镜像拉取失败 → 检查 ghcr-secret
# 2. 健康检查失败 → 检查 MySQL/Redis 连接
# 3. ConfigMap/Secret 不存在 → 检查依赖资源
```
### Ingress 无法访问
```bash
# 检查 Ingress 资源
kubectl get ingress -n bookstore
kubectl describe ingress bookstore-ingress -n bookstore
# 检查 TLS 证书
kubectl get certificate -n bookstore
kubectl describe certificate -n bookstore
# 检查 Traefik 日志
kubectl logs -n kube-system -l app.kubernetes.io/name=traefik
```
### Argo CD 同步失败
```bash
# 查看同步状态
argocd app get ljwx-bookstore
# 查看同步日志
argocd app logs ljwx-bookstore
# 手动同步
argocd app sync ljwx-bookstore --force
# 常见问题:
# 1. Git 仓库无法访问 → 检查网络和认证
# 2. Kustomize 配置错误 → 检查 YAML 语法
# 3. 资源冲突 → 使用 --force 强制同步
```
## 📖 相关文档
- **代码仓库**: https://github.com/BrunoGaoSZ/ljwx-bookstore
- **应用访问**: https://bookstore.lingjingwanxiang.cn
- **Argo CD 指南**: [docs/argocd-migration/README.md](../../docs/argocd-migration/README.md)
- **Flyway 迁移**: [docs/flyway-guide/README.md](../../docs/flyway-guide/README.md)
## 🎯 最佳实践
1. **永远不要直接修改 Kubernetes 资源** - 所有变更通过 Git 提交
2. **使用语义化提交信息** - 便于追踪变更历史
3. **环境隔离** - 使用 Kustomize overlays 管理不同环境
4. **自动化测试** - CI 流程包含镜像构建和推送验证
5. **监控告警** - 配置 Prometheus + Grafana 监控(计划中)
## 📝 变更历史
- **2025-12-20**: 配置 HTTPS Ingress + 根路径重定向
- **2025-12-19**: 完成 GitOps 自动部署流程
- **2025-12-18**: 配置 MySQL 和 Redis 连接
- **2025-12-17**: 初始化 Kustomize 配置
-79
View File
@@ -1,79 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: bookstore
labels:
app: bookstore
managed-by: argocd
project: ljwx-bookstore
spec:
replicas: 2
selector:
matchLabels:
app: bookstore
template:
metadata:
labels:
app: bookstore
project: ljwx-bookstore
version: v0.1.4
spec:
imagePullSecrets:
- name: ghcr-secret
containers:
- name: bookstore
image: ghcr.io/brunogaosz/ljwx-bookstore/bookstore:bookstore-latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: SERVER_PORT
value: "8080"
# Database configuration from infra Postgres
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: bookstore-db-credentials
key: SPRING_DATASOURCE_URL
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: bookstore-db-credentials
key: SPRING_DATASOURCE_USERNAME
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: bookstore-db-credentials
key: SPRING_DATASOURCE_PASSWORD
# Redis configuration
- name: SPRING_DATA_REDIS_HOST
value: "redis-service.ljwx-health.svc.cluster.local"
- name: SPRING_DATA_REDIS_PORT
value: "6379"
- name: SPRING_DATA_REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-credentials
key: REDIS_PASSWORD
- name: SPRING_DATA_REDIS_DATABASE
value: "0"
livenessProbe:
httpGet:
path: /fiction/actuator/health
port: 8080
initialDelaySeconds: 120
periodSeconds: 30
readinessProbe:
httpGet:
path: /fiction/actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
@@ -1,7 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: ghcr-secret
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: eyJhdXRocyI6eyJnaGNyLmlvIjp7InVzZXJuYW1lIjoiQnJ1bm9HYW9TWiIsInBhc3N3b3JkIjoiZ2hwX05UVVNwTW9kOURhcXAxeGxCTWliOGxnV09WMUJidTF5MjJ4cyIsImVtYWlsIjoieW91ci1lbWFpbEBleGFtcGxlLmNvbSIsImF1dGgiOiJRbkoxYm05SFlXOVRXanBuYUhCZlRsUlZVM0JOYjJRNVJHRnhjREY0YkVKTmFXSTRiR2RYVDFZeFFtSjFNWGt5TW5oeiJ9fX0=
-41
View File
@@ -1,41 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: bookstore-ingress
namespace: bookstore
annotations:
# Traefik annotations
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/redirect-to-https: "true"
# Redirect root path to /fiction/index
traefik.ingress.kubernetes.io/router.middlewares: bookstore-redirect-root@kubernetescrd
# cert-manager annotation for automatic TLS certificate
cert-manager.io/cluster-issuer: dnspod-letsencrypt
spec:
ingressClassName: traefik
tls:
- hosts:
- bookstore.lingjingwanxiang.cn
secretName: bookstore-lingjingwanxiang-tls
rules:
- host: bookstore.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: bookstore
port:
number: 8080
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-root
namespace: bookstore
spec:
redirectRegex:
regex: "^https?://[^/]+/$"
replacement: "https://bookstore.lingjingwanxiang.cn/fiction/index"
permanent: false
@@ -1,6 +0,0 @@
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
- ghcr-secret.yaml
- redis-secret.yaml
@@ -1,7 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: redis-credentials
type: Opaque
stringData:
REDIS_PASSWORD: "123456"
-12
View File
@@ -1,12 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: bookstore
spec:
selector:
app: bookstore
ports:
- protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
@@ -1,11 +0,0 @@
# ljwx-bookstore 通用配置(跨环境共享)
# 通用标签
commonLabels:
project: ljwx-bookstore
managed-by: argocd
app: bookstore
# 通用注解
commonAnnotations:
maintained-by: ljwx-team
@@ -1,7 +0,0 @@
resources:
- ../../base
namespace: bookstore
images:
- name: ghcr.io/brunogaosz/ljwx-bookstore/bookstore
newName: ghcr.io/brunogaosz/ljwx-bookstore/bookstore
newTag: "bookstore-d917045"
@@ -1,76 +0,0 @@
# Bookstore 环境配置
# 副本数
replicaCount: 2
# 镜像配置
image:
repository: ghcr.io/brunogaosz/ljwx-bookstore/bookstore
tag: "main-latest" # CI 会更新为 Git SHA
pullPolicy: IfNotPresent
# 服务配置
service:
type: ClusterIP
port: 8080
targetPort: 8080
# Ingress 配置
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
hosts:
- host: bookstore.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: bookstore-tls
hosts:
- bookstore.example.com
# 资源限制
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
# 健康检查
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 120
periodSeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
# 数据库配置(使用 infra postgres)
postgresql:
enabled: false # 使用外部数据库
# Redis 配置
redis:
enabled: false # 使用外部 Redis
# OpenSearch 配置
opensearch:
enabled: false # 使用外部 OpenSearch
# 环境变量(通过 Secret 注入)
env:
SPRING_PROFILES_ACTIVE: "prod"
SERVER_PORT: "8080"
# 数据库连接将通过 Secret 注入
-530
View File
@@ -1,530 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ljwx-health-device-approval-config
namespace: ljwx-health
labels:
app: ljwx-boot
component: device-approval
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
data:
DEVICE_APPROVAL_AUTO_CLEANUP_DAYS: '30'
DEVICE_APPROVAL_BATCH_SIZE_LIMIT: '100'
DEVICE_APPROVAL_TIMEOUT_HOURS: '72'
DEVICE_APPROVAL_EVENT_THREAD_POOL_SIZE: '10'
DEVICE_APPROVAL_EVENT_QUEUE_CAPACITY: '100'
DEVICE_APPROVAL_METRICS_ENABLED: 'true'
DEVICE_APPROVAL_LOG_LEVEL: INFO
DEVICE_APPROVAL_EVENT_LISTENER_LOG_LEVEL: INFO
DEVICE_APPROVAL_METRICS_SERVICE_LOG_LEVEL: DEBUG
DEVICE_APPROVAL_QUERY_CACHE_TTL: '300'
DEVICE_APPROVAL_HISTORY_QUERY_OPTIMIZATION: 'true'
application-device-approval.yml: "# Device V2.1 - \u8BBE\u5907\u7ED1\u5B9A\u5BA1\
\u6279\u6A21\u5757\u4E13\u7528\u914D\u7F6E\n# \u81EA\u52A8\u52A0\u8F7D\u5F53 spring.profiles.include=device-approval\
\ \u65F6\u751F\u6548\n\ndevice:\n approval:\n # \u5BA1\u6279\u6D41\u7A0B\u914D\
\u7F6E\n auto-cleanup-days: ${DEVICE_APPROVAL_AUTO_CLEANUP_DAYS:30}\n batch-size-limit:\
\ ${DEVICE_APPROVAL_BATCH_SIZE_LIMIT:100}\n timeout-hours: ${DEVICE_APPROVAL_TIMEOUT_HOURS:72}\n\
\n # \u4E8B\u4EF6\u53D1\u5E03\u914D\u7F6E\n events:\n async-thread-pool-size:\
\ ${DEVICE_APPROVAL_EVENT_THREAD_POOL_SIZE:10}\n async-queue-capacity: ${DEVICE_APPROVAL_EVENT_QUEUE_CAPACITY:100}\n\
\n # \u53EF\u89C2\u6D4B\u6027\u914D\u7F6E\n observability:\n metrics-enabled:\
\ ${DEVICE_APPROVAL_METRICS_ENABLED:true}\n log-level: ${DEVICE_APPROVAL_LOG_LEVEL:INFO}\n\
\ event-listener-log-level: ${DEVICE_APPROVAL_EVENT_LISTENER_LOG_LEVEL:INFO}\n\
\ metrics-service-log-level: ${DEVICE_APPROVAL_METRICS_SERVICE_LOG_LEVEL:DEBUG}\n\
\n # \u6027\u80FD\u4F18\u5316\u914D\u7F6E\n performance:\n query-cache-ttl:\
\ ${DEVICE_APPROVAL_QUERY_CACHE_TTL:300}\n history-query-optimization: ${DEVICE_APPROVAL_HISTORY_QUERY_OPTIMIZATION:true}\n\
\n# Logback \u65E5\u5FD7\u914D\u7F6E (\u8986\u76D6\u9ED8\u8BA4\u914D\u7F6E)\n\
logging:\n level:\n com.ljwx.modules.health.service.DeviceBindApprovalServiceImpl:\
\ ${DEVICE_APPROVAL_LOG_LEVEL:INFO}\n com.ljwx.modules.health.listener.DeviceBindApprovalEventListener:\
\ ${DEVICE_APPROVAL_EVENT_LISTENER_LOG_LEVEL:INFO}\n com.ljwx.modules.health.service.DeviceBindApprovalMetricsService:\
\ ${DEVICE_APPROVAL_METRICS_SERVICE_LOG_LEVEL:DEBUG}\n"
---
apiVersion: v1
kind: Service
metadata:
name: ljwx-health-admin
namespace: ljwx-health
labels:
app: ljwx-admin
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http
selector:
app: ljwx-admin
---
apiVersion: v1
kind: Service
metadata:
name: ljwx-health-bigscreen
namespace: ljwx-health
labels:
app: ljwx-bigscreen
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
ports:
- port: 5225
targetPort: 5225
protocol: TCP
name: http
selector:
app: ljwx-bigscreen
---
apiVersion: v1
kind: Service
metadata:
name: ljwx-health-boot
namespace: ljwx-health
labels:
app: ljwx-boot
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
spec:
type: ClusterIP
ports:
- port: 9998
targetPort: 9998
protocol: TCP
name: http
selector:
app: ljwx-boot
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-health-admin
namespace: ljwx-health
labels:
app: ljwx-admin
component: frontend
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-admin
template:
metadata:
labels:
app: ljwx-admin
component: frontend
spec:
imagePullSecrets:
- name: harbor-secret
containers:
- name: ljwx-admin
image: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-admin:dev-latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
restartPolicy: Always
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-health-bigscreen
namespace: ljwx-health
labels:
app: ljwx-bigscreen
component: dashboard
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-bigscreen
template:
metadata:
labels:
app: ljwx-bigscreen
component: dashboard
spec:
imagePullSecrets:
- name: harbor-secret
containers:
- name: ljwx-bigscreen
image: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-bigscreen:dev-latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 5225
protocol: TCP
env:
- name: ENV
value: production
- name: DB_HOST
value: mysql-lb.infra.svc.cluster.local
- name: DB_PORT
value: '3306'
- name: DB_NAME
value: ljwx_health
- name: REDIS_HOST
value: redis-lb.infra.svc.cluster.local
- name: REDIS_PORT
value: '6379'
- name: LOG_LEVEL
value: INFO
envFrom:
- secretRef:
name: ljwx-boot-secrets
livenessProbe:
httpGet:
path: /health
port: 5225
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 5225
initialDelaySeconds: 10
periodSeconds: 5
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 250m
memory: 512Mi
restartPolicy: Always
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-health-boot
namespace: ljwx-health
labels:
app: ljwx-boot
component: backend
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-boot
template:
metadata:
annotations:
prometheus.io/scrape: 'true'
prometheus.io/port: '9998'
prometheus.io/path: /actuator/prometheus
labels:
app: ljwx-boot
component: backend
spec:
imagePullSecrets:
- name: harbor-secret
containers:
- name: ljwx-boot
image: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-boot:dev-latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 9998
protocol: TCP
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: SPRING_DATASOURCE_URL
value: jdbc:mysql://mysql.infra.svc.cluster.local:3306/ljwx_health?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
- name: SPRING_DATASOURCE_DRIVER_CLASS_NAME
value: com.mysql.cj.jdbc.Driver
- name: SPRING_REDIS_HOST
value: redis-lb.infra.svc.cluster.local
- name: SPRING_REDIS_PORT
value: '6379'
- name: SPRING_REDIS_DATABASE
value: '1'
- name: SPRING_REDIS_PASSWORD
value: '123456'
- name: REDIS_HOST
value: redis-lb.infra.svc.cluster.local
- name: REDIS_PORT
value: '6379'
- name: REDIS_DB
value: '1'
- name: REDIS_PASSWORD
value: '123456'
- name: LOG_LEVEL
value: INFO
- name: SPRING_FLYWAY_ENABLED
value: 'false'
- name: SPRING_FLYWAY_BASELINE_ON_MIGRATE
value: 'true'
- name: SPRING_FLYWAY_LOCATIONS
value: classpath:db/baseline
- name: OTEL_SERVICE_NAME
value: ljwx-boot
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://tempo.infra.svc.cluster.local:4317
- name: OTEL_TRACES_SAMPLER
value: parentbased_traceidratio
- name: OTEL_TRACES_SAMPLER_ARG
value: '0.1'
- name: SPRING_PROFILES_INCLUDE
value: device-approval
envFrom:
- secretRef:
name: ljwx-boot-secrets
- configMapRef:
name: ljwx-health-device-approval-config
livenessProbe:
failureThreshold: 3
httpGet:
path: /actuator/health/liveness
port: 9998
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
failureThreshold: 3
httpGet:
path: /actuator/health/readiness
port: 9998
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 3
resources:
limits:
cpu: 2000m
memory: 2Gi
requests:
cpu: 500m
memory: 1Gi
volumeMounts:
- name: logs
mountPath: /app/logs
- name: device-approval-config
mountPath: /app/config/application-device-approval.yml
subPath: application-device-approval.yml
readOnly: true
volumes:
- name: logs
emptyDir: {}
- name: device-approval-config
configMap:
name: ljwx-health-device-approval-config
items:
- key: application-device-approval.yml
path: application-device-approval.yml
restartPolicy: Always
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ljwx-health-api-ingress
namespace: ljwx-health
labels:
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: 'true'
traefik.ingress.kubernetes.io/router.middlewares: ljwx-health-ljwx-health-strip-api-prefix@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- health-admin.lingjingwanxiang.cn
secretName: ljwx-health-admin-tls
rules:
- host: health-admin.lingjingwanxiang.cn
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: ljwx-health-boot
port:
number: 9998
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ljwx-health-ingress
namespace: ljwx-health
labels:
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: 'true'
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: 'true'
spec:
ingressClassName: traefik
tls:
- hosts:
- health-admin.lingjingwanxiang.cn
secretName: ljwx-health-admin-tls
- hosts:
- health-dashboard.lingjingwanxiang.cn
secretName: ljwx-health-bigscreen-tls
rules:
- host: health-admin.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ljwx-health-admin
port:
number: 80
- host: health-dashboard.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ljwx-health-bigscreen
port:
number: 5225
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: ljwx-health-strip-api-prefix
namespace: ljwx-health
labels:
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
spec:
stripPrefix:
prefixes:
- /api
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: ljwx-health-boot
namespace: ljwx-health
labels:
app: ljwx-boot
release: monitoring
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
spec:
selector:
matchLabels:
app: ljwx-boot
endpoints:
- port: http
path: /actuator/prometheus
interval: 30s
scrapeTimeout: 10s
relabelings:
- sourceLabels:
- __meta_kubernetes_service_name
targetLabel: service
- sourceLabels:
- __meta_kubernetes_namespace
targetLabel: namespace
- sourceLabels:
- __meta_kubernetes_pod_name
targetLabel: pod
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: ljwx-health-bigscreen
namespace: ljwx-health
labels:
app: ljwx-bigscreen
release: monitoring
helm.sh/chart: ljwx-health-0.1.0
app.kubernetes.io/name: ljwx-health
app.kubernetes.io/instance: ljwx-health
app.kubernetes.io/version: 1.0.6
app.kubernetes.io/managed-by: Helm
spec:
selector:
matchLabels:
app: ljwx-bigscreen
endpoints:
- port: http
path: /metrics
interval: 30s
scrapeTimeout: 10s
relabelings:
- sourceLabels:
- __meta_kubernetes_service_name
targetLabel: service
- sourceLabels:
- __meta_kubernetes_namespace
targetLabel: namespace
- sourceLabels:
- __meta_kubernetes_pod_name
targetLabel: pod
-2
View File
@@ -1,2 +0,0 @@
resources:
- all.yaml
@@ -1,13 +0,0 @@
resources:
- ../../base
namespace: ljwx-health
images:
- name: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-boot
newName: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-boot
newTag: "latest"
- name: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-admin
newName: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-admin
newTag: "latest"
- name: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-bigscreen
newName: harbor.eu.lingjingwanxiang.cn/ljwx-health/ljwx-bigscreen
newTag: "latest"
@@ -1,33 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: ljwx-platform-admin-ui-nginx
data:
default.conf: |
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ljwx-platform-screen-nginx
data:
default.conf: |
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
@@ -1,61 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-platform-admin-ui
labels:
app: ljwx-platform-admin-ui
project: ljwx-platform
component: frontend
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-platform-admin-ui
template:
metadata:
labels:
app: ljwx-platform-admin-ui
project: ljwx-platform
component: frontend
spec:
imagePullSecrets:
- name: regcred
containers:
- name: ljwx-platform-admin-ui
image: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-platform-admin-ui:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: http
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
readOnly: true
volumes:
- name: nginx-conf
configMap:
name: ljwx-platform-admin-ui-nginx
@@ -1,61 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-platform-screen
labels:
app: ljwx-platform-screen
project: ljwx-platform
component: screen
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-platform-screen
template:
metadata:
labels:
app: ljwx-platform-screen
project: ljwx-platform
component: screen
spec:
imagePullSecrets:
- name: regcred
containers:
- name: ljwx-platform-screen
image: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-platform-screen:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: http
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
readOnly: true
volumes:
- name: nginx-conf
configMap:
name: ljwx-platform-screen-nginx
-92
View File
@@ -1,92 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: ljwx-platform
labels:
app: ljwx-platform
project: ljwx-platform
spec:
replicas: 1
selector:
matchLabels:
app: ljwx-platform
template:
metadata:
labels:
app: ljwx-platform
project: ljwx-platform
spec:
imagePullSecrets:
- name: regcred
containers:
- name: ljwx-platform
image: harbor.eu.lingjingwanxiang.cn/ljwx-platform/ljwx-platform:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
volumeMounts:
- name: app-logs
mountPath: /workspace/logs
env:
- name: APP_PORT
value: "8080"
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: SPRING_FLYWAY_VALIDATE_ON_MIGRATE
value: "false"
- name: DB_URL
valueFrom:
secretKeyRef:
name: ljwx-platform-db
key: DB_URL
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: ljwx-platform-db
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: ljwx-platform-db
key: DB_PASSWORD
- name: SPRING_DATASOURCE_URL
valueFrom:
secretKeyRef:
name: ljwx-platform-db
key: DB_URL
- name: SPRING_DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: ljwx-platform-db
key: DB_USERNAME
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: ljwx-platform-db
key: DB_PASSWORD
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: ljwx-platform-app
key: JWT_SECRET
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 90
periodSeconds: 30
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: 45
periodSeconds: 10
resources:
requests:
cpu: "300m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
volumes:
- name: app-logs
emptyDir: {}
-51
View File
@@ -1,51 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ljwx-platform
annotations:
cert-manager.io/cluster-issuer: dnspod-letsencrypt
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
tls:
- hosts:
- platform.lingjingwanxiang.cn
secretName: ljwx-platform-tls
- hosts:
- platform-admin.lingjingwanxiang.cn
secretName: ljwx-platform-admin-tls
- hosts:
- platform-screen.lingjingwanxiang.cn
secretName: ljwx-platform-screen-tls
rules:
- host: platform.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ljwx-platform
port:
number: 8080
- host: platform-admin.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ljwx-platform-admin-ui
port:
number: 80
- host: platform-screen.lingjingwanxiang.cn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ljwx-platform-screen
port:
number: 80
@@ -1,15 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- deployment-admin-ui.yaml
- deployment-screen.yaml
- service.yaml
- service-admin-ui.yaml
- service-screen.yaml
- ingress.yaml
- configmap-nginx.yaml
- secret.yaml
- regcred.yaml
- networkpolicy.yaml
-160
View File
@@ -1,160 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ljwx-platform-default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# ── Backend ────────────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ljwx-platform-allow-ingress-from-traefik
spec:
podSelector:
matchLabels:
app: ljwx-platform
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ljwx-platform-allow-egress-dns-and-db
spec:
podSelector:
matchLabels:
app: ljwx-platform
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: infra
podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
---
# ── Admin UI ───────────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ljwx-platform-admin-ui-allow-ingress-from-traefik
spec:
podSelector:
matchLabels:
app: ljwx-platform-admin-ui
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
ports:
- protocol: TCP
port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ljwx-platform-admin-ui-allow-egress-dns
spec:
podSelector:
matchLabels:
app: ljwx-platform-admin-ui
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# ── Screen ─────────────────────────────────────────────────────────────────────
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ljwx-platform-screen-allow-ingress-from-traefik
spec:
podSelector:
matchLabels:
app: ljwx-platform-screen
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
ports:
- protocol: TCP
port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ljwx-platform-screen-allow-egress-dns
spec:
podSelector:
matchLabels:
app: ljwx-platform-screen
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
-8
View File
@@ -1,8 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: regcred
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: eyJhdXRocyI6eyJoYXJib3IuZXUubGluZ2ppbmd3YW54aWFuZy5jbiI6eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJIYXJib3IxMjM0NSIsImF1dGgiOiJZV1J0YVc0NlNHRnlZbTl5TVRJek5EVT0ifSwiaGFyYm9yLmV1LmxpbmdqaW5nd2FueGlhbmcuY246ODA4MCI6eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJIYXJib3IxMjM0NSIsImF1dGgiOiJZV1J0YVc0NlNHRnlZbTl5TVRJek5EVT0ifX19
-18
View File
@@ -1,18 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: ljwx-platform-db
type: Opaque
stringData:
DB_URL: jdbc:postgresql://postgres-lb.infra.svc.cluster.local:5432/postgres
DB_USERNAME: postgres
DB_PASSWORD: ChangeMeInProduction123!
---
apiVersion: v1
kind: Secret
metadata:
name: ljwx-platform-app
type: Opaque
stringData:
JWT_SECRET: ChangeThisToAtLeast32BytesBeforeProductionUse
@@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: ljwx-platform-admin-ui
labels:
app: ljwx-platform-admin-ui
project: ljwx-platform
spec:
selector:
app: ljwx-platform-admin-ui
ports:
- protocol: TCP
port: 80
targetPort: 80
name: http
type: ClusterIP
@@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: ljwx-platform-screen
labels:
app: ljwx-platform-screen
project: ljwx-platform
spec:
selector:
app: ljwx-platform-screen
ports:
- protocol: TCP
port: 80
targetPort: 80
name: http
type: ClusterIP
-14
View File
@@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: ljwx-platform
spec:
selector:
app: ljwx-platform
ports:
- protocol: TCP
port: 8080
targetPort: 8080
name: http
type: ClusterIP
@@ -1,15 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ljwx-platform
resources:
- ../../base
images:
- name: harbor.eu.lingjingwanxiang.cn/ljwx-platform/ljwx-platform
newName: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-platform
newTag: sha-9f1266d1
- name: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-platform-admin-ui
newName: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-platform-admin-ui
newTag: sha-b7766e9
- name: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-platform-screen
newName: harbor.eu.lingjingwanxiang.cn/ljwx/ljwx-platform-screen
newTag: sha-b7766e9
-256
View File
@@ -1,256 +0,0 @@
# ljwx-template GitOps 配置
此目录包含 ljwx-template 项目的 GitOps 部署配置,配合 Argo CD 使用。
## 架构说明
### 仓库分离策略
**代码仓库** (`ljwx-template`):
- 业务代码(FastAPI + Spring Boot)
- Helm Chart (`deploy/helm/app/`)
- CI Pipeline(GitHub Actions)
**GitOps 仓库** (`ljwx-deploy` - 本仓库):
- Argo CD Application 定义
- 环境特定配置
- AppProject 权限定义
**优势**:
- ✅ 权限分离:开发人员无需访问部署配置
- ✅ 审批独立:部署变更有独立的 PR 流程
- ✅ 回滚清晰:部署配置历史独立管理
- ✅ 安全性高:Secrets 不在代码仓库
## 目录结构
```
apps/ljwx-template/
├── base/
│ └── values-common.yaml # 所有环境共享配置
└── overlays/
├── eu-dev/
│ └── values.yaml # EU Dev 环境配置
├── mac-dev/
│ └── values.yaml # Mac Dev 环境配置
└── prod/
└── values.yaml # 生产环境配置
```
## 配置覆盖顺序
Argo CD 会按以下顺序合并配置:
1. **Helm Chart 默认值** (`ljwx-template/deploy/helm/app/values.yaml`)
2. **通用配置** (`base/values-common.yaml`)
3. **环境特定配置** (`overlays/{env}/values.yaml`)
后面的配置会覆盖前面的。
## 环境说明
### EU Dev (`overlays/eu-dev/`)
- **用途**: EU 团队开发环境
- **命名空间**: `dev`
- **特点**: DEBUG 日志,1 副本,资源限制较小
### Mac Dev (`overlays/mac-dev/`)
- **用途**: Mac k3s 本地开发环境
- **命名空间**: `dev`
- **特点**: DEBUG 日志,1 副本,本地数据库
### Prod (`overlays/prod/`)
- **用途**: 生产环境
- **命名空间**: `ljwx-template-prod`
- **特点**: INFO 日志,3 副本,HPA,HTTPS Ingress
## 使用方法
### 修改配置
1. 克隆本仓库:
```bash
git clone git@github.com:BrunoGaoSZ/ljwx-deploy.git
cd ljwx-deploy/apps/ljwx-template
```
2. 编辑环境特定配置:
```bash
# 修改 EU Dev 配置
vim overlays/eu-dev/values.yaml
# 例如:调整副本数
# apiFast:
# replicaCount: 2
```
3. 提交变更:
```bash
git add .
git commit -m "feat(ljwx-template): increase eu-dev replicas to 2"
git push
```
4. Argo CD 自动检测变更并部署(~30 秒)
### 查看部署状态
```bash
# CLI
argocd app get ljwx-template-eu-dev
# Web UI
open https://argocd.example.com
```
### 回滚配置
```bash
# Git 回滚
git revert HEAD
git push
# 或使用 Argo CD 回滚
argocd app rollback ljwx-template-eu-dev
```
## 常见配置修改
### 1. 调整资源限制
```yaml
# overlays/eu-dev/values.yaml
apiFast:
resources:
requests:
memory: "512Mi" # 从 128Mi 提升
cpu: "500m" # 从 100m 提升
limits:
memory: "1Gi"
cpu: "1000m"
```
### 2. 更新镜像版本
```yaml
# overlays/eu-dev/values.yaml
apiFast:
image:
tag: "main-abc1234" # 使用特定 Git SHA
```
### 3. 启用 HPA
```yaml
# overlays/eu-dev/values.yaml
apiFast:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 70
```
### 4. 更新数据库连接
```yaml
# overlays/eu-dev/values.yaml
database:
host: new-postgres-host.example.com
port: 5432
name: new_database
```
### 5. 修改日志级别
```yaml
# overlays/prod/values.yaml
apiFast:
env:
LOG_LEVEL: "WARNING" # 从 INFO 调整为 WARNING
```
## 安全最佳实践
### 1. Secrets 管理
**禁止在 Git 中存储 Secrets!**
使用以下方案之一:
- **ExternalSecrets Operator** (推荐)
- **SealedSecrets**
- **Vault**
- 手动创建 K8s Secrets
示例(ExternalSecrets):
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials-eu-dev
spec:
secretStoreRef:
name: vault-backend
target:
name: db-credentials-eu-dev
data:
- secretKey: DB_USER
remoteRef:
key: ljwx/eu-dev/database
property: username
```
### 2. 权限控制
- 使用 `ljwx:developer` 角色:只读权限
- 使用 `ljwx:operator` 角色:同步权限
- 生产环境需要额外审批
### 3. 变更审计
所有变更都通过 Git PR:
- 需要 Code Review
- 自动触发 CI 检查
- 可追溯历史
## 故障排查
### Application 无法同步
```bash
# 查看详细状态
argocd app get ljwx-template-eu-dev
# 查看差异
argocd app diff ljwx-template-eu-dev
# 强制刷新
argocd app sync ljwx-template-eu-dev --force
```
### 配置未生效
检查配置覆盖顺序:
```bash
# 查看最终渲染的 manifest
argocd app manifests ljwx-template-eu-dev
```
### PreSync Hook 失败
查看 Migration Job 日志:
```bash
kubectl logs -n dev job/ljwx-template-migration-xxx
```
## 相关文档
- **代码仓库**: https://github.com/BrunoGaoSZ/ljwx-template
- **Helm Chart 说明**: `ljwx-template/deploy/helm/app/README.md`
- **完整部署指南**: `ljwx-template/deploy/argo/README.md`
- **Argo CD 官方文档**: https://argo-cd.readthedocs.io/
## 联系方式
- 问题反馈: https://github.com/BrunoGaoSZ/ljwx-template/issues
- 团队: ljwx-team@example.com
@@ -1,11 +0,0 @@
# ljwx-template 通用配置(跨环境共享)
# 此文件定义所有环境的公共配置,具体环境在 overlays 中覆盖
# 通用标签
commonLabels:
project: ljwx-template
managed-by: argocd
# 通用注解
commonAnnotations:
maintained-by: ljwx-team
@@ -1,50 +0,0 @@
# EU Dev 环境配置覆盖
# FastAPI 配置
apiFast:
enabled: true
replicaCount: 1 # dev 环境资源有限
image:
tag: "main-5205c31" # CI 会自动更新为 Git SHA
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
env:
LOG_LEVEL: "DEBUG" # dev 环境开启 debug
WEB_CONCURRENCY: "1"
autoscaling:
enabled: false # dev 不启用自动扩展
# Spring Boot 配置
bootAdmin:
enabled: false # dev 环境暂不启用
# 数据库配置(指向 dev 集群的数据库)
database:
host: postgres-dev-eu.default.svc.cluster.local
port: 5432
name: app_dev_eu
secretName: db-credentials-eu-dev
# Redis 配置
redis:
host: redis-dev-eu.default.svc.cluster.local
port: 6379
secretName: redis-credentials-eu-dev
# Migration 配置
migration:
enabled: true
image:
tag: "main-latest"
# OpenTelemetry 配置
otel:
enabled: true
endpoint: "http://tempo-eu-dev:4317"
# Ingress 配置(dev 环境使用 NodePort 或禁用)
ingress:
enabled: false
# ServiceMonitor(如果有 Prometheus Operator)
serviceMonitor:
enabled: true
interval: 15s # dev 环境更频繁采集
@@ -1,61 +0,0 @@
# Mac Dev 环境配置覆盖
# FastAPI 配置
apiFast:
enabled: true
replicaCount: 1
image:
tag: "main-latest"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
env:
LOG_LEVEL: "DEBUG"
WEB_CONCURRENCY: "1"
autoscaling:
enabled: false
# Spring Boot 配置
bootAdmin:
enabled: false
# 数据库配置(Mac k3s 本地数据库)
database:
host: postgres-mac-dev.default.svc.cluster.local
port: 5432
name: app_dev_mac
secretName: db-credentials-mac-dev
# Redis 配置
redis:
host: redis-mac-dev.default.svc.cluster.local
port: 6379
secretName: redis-credentials-mac-dev
# Migration 配置
migration:
enabled: true
image:
tag: "main-latest"
# OpenTelemetry 配置
otel:
enabled: true
endpoint: "http://tempo-mac-dev:4317"
# Ingress 配置
ingress:
enabled: false
# ServiceMonitor
serviceMonitor:
enabled: true
interval: 15s
@@ -1,96 +0,0 @@
# 生产环境配置覆盖
# FastAPI 配置
apiFast:
enabled: true
replicaCount: 3 # 生产环境多副本
image:
tag: "v1.0.0" # 生产使用具体版本号
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
env:
LOG_LEVEL: "INFO" # 生产环境 INFO 级别
WEB_CONCURRENCY: "4"
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
# Spring Boot 配置
bootAdmin:
enabled: true
replicaCount: 2
image:
tag: "v1.0.0"
resources:
requests:
memory: "1Gi"
cpu: "1000m"
limits:
memory: "2Gi"
cpu: "2000m"
env:
SPRING_PROFILES_ACTIVE: "prod"
# 数据库配置(生产 RDS/云数据库)
database:
host: prod-db.example.com
port: 5432
name: app_prod
secretName: db-credentials-prod # 使用 ExternalSecrets 或 SealedSecrets
# Redis 配置(生产 ElastiCache/云 Redis)
redis:
host: prod-redis.example.com
port: 6379
secretName: redis-credentials-prod
# Migration 配置
migration:
enabled: true
image:
tag: "v1.0.0"
backoffLimit: 5
ttlSecondsAfterFinished: 3600 # 保留 1 小时
# OpenTelemetry 配置
otel:
enabled: true
endpoint: "http://tempo-prod:4317"
# Ingress 配置(生产环境启用 HTTPS)
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/rate-limit: "100"
hosts:
- host: api.ljwx.example.com
paths:
- path: /
pathType: Prefix
service: api-fast
tls:
- secretName: api-ljwx-tls
hosts:
- api.ljwx.example.com
# ServiceMonitor
serviceMonitor:
enabled: true
interval: 30s
@@ -1,70 +0,0 @@
# ljwx-website EU Development Environment Configuration
# This file is automatically updated by CI/CD pipeline
website:
enabled: true
replicaCount: 1
image:
repository: harbor.eu.lingjingwanxiang.cn:8080/ljwx/ljwx-website
tag: "main-latest" # Updated by CI
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 80
resources:
requests:
memory: "32Mi"
cpu: "25m"
limits:
memory: "64Mi"
cpu: "50m"
autoscaling:
enabled: false
probes:
liveness:
path: /
initialDelaySeconds: 10
periodSeconds: 10
readiness:
path: /
initialDelaySeconds: 5
periodSeconds: 5
# Ingress configuration
ingress:
enabled: true
className: traefik # k3s default ingress controller
# Fixed subdomain
host: "website.lingjingwanxiang.cn"
baseDomain: "lingjingwanxiang.cn"
path: /
pathType: Prefix
annotations:
# Traefik annotations
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
# cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
enabled: true
secretName: "website-tls"
# clusterIssuer: "letsencrypt-prod"
# ServiceMonitor (Prometheus Operator)
serviceMonitor:
enabled: false
interval: 30s
# Namespace labels
namespaceLabels:
project: ljwx-website
managed-by: argocd
environment: dev-eu
-17
View File
@@ -1,17 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 1
selector:
matchLabels: { app: nginx }
template:
metadata:
labels: { app: nginx }
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
-3
View File
@@ -1,3 +0,0 @@
resources:
- deployment.yaml
- service.yaml
-10
View File
@@ -1,10 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
selector:
app: nginx
ports:
- port: 80
targetPort: 80
@@ -1,6 +0,0 @@
resources:
- ../../base
images:
- name: nginx
newName: ghcr.io/nginxinc/nginx-unprivileged
newTag: "1.27-alpine"
-69
View File
@@ -1,69 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: youngth-guard-backend
spec:
replicas: 1
selector:
matchLabels:
app: youngth-guard-backend
template:
metadata:
labels:
app: youngth-guard-backend
spec:
imagePullSecrets:
- name: ghcr-secret
containers:
- name: backend
image: ghcr.io/brunogaosz/youngth-guard-backend:latest
ports:
- containerPort: 8000
env:
- name: ENV
value: "production"
- name: LOG_LEVEL
value: "info"
# Database configuration from infra Postgres
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: DATABASE_URL
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PORT
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PASSWORD
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
-7
View File
@@ -1,7 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: ghcr-secret
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: eyJhdXRocyI6eyJnaGNyLmlvIjp7InVzZXJuYW1lIjoiQnJ1bm9HYW9TWiIsInBhc3N3b3JkIjoiZ2hwX05UVVNwTW9kOURhcXAxeGxCTWliOGxnV09WMUJidTF5MjJ4cyIsImVtYWlsIjoieW91ci1lbWFpbEBleGFtcGxlLmNvbSIsImF1dGgiOiJRbkoxYm05SFlXOVRXanBuYUhCZlRsUlZVM0JOYjJRNVJHRnhjREY0YkVKTmFXSTRiR2RYVDFZeFFtSjFNWGt5TW5oeiJ9fX0=
@@ -1,4 +0,0 @@
resources:
- deployment.yaml
- service.yaml
- ghcr-secret.yaml
-12
View File
@@ -1,12 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: youngth-guard-backend
spec:
selector:
app: youngth-guard-backend
ports:
- protocol: TCP
port: 8000
targetPort: 8000
type: ClusterIP
@@ -1,163 +0,0 @@
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
annotations:
# ============================================
# ArgoCD PreSync Hook
# ============================================
# 这个 Job 会在应用部署前执行
# 如果失败,会阻止应用部署
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "-1"
labels:
app: youngth-guard-backend
component: migration
spec:
backoffLimit: 0
template:
metadata:
labels:
component: migration
spec:
restartPolicy: Never
# ============================================
# Init Container: 等待数据库就绪
# ============================================
initContainers:
- name: wait-for-db
image: postgres:16-alpine
command:
- /bin/sh
- -c
- |
echo "Waiting for database to be ready..."
until pg_isready -h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USER; do
echo "Database is unavailable - sleeping"
sleep 2
done
echo "Database is ready!"
env:
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PORT
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_USER
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PASSWORD
# ============================================
# Main Container: Flyway Migration
# ============================================
containers:
- name: flyway
image: flyway/flyway:10-alpine
command:
- /bin/sh
- -c
- |
set -e
# 构建 Flyway 连接 URL
export FLYWAY_URL="jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
echo "============================================"
echo "Database Migration with Flyway"
echo "============================================"
echo "Database: $FLYWAY_URL"
echo "User: $FLYWAY_USER"
echo "Migrations: /flyway/sql"
echo "============================================"
# 列出迁移文件
echo "Available migrations:"
ls -la /flyway/sql/ || echo "No migrations found"
# 执行迁移
echo ""
echo "Running migrations..."
flyway -connectRetries=3 migrate
# 显示迁移历史
echo ""
echo "Migration history:"
flyway info
echo ""
echo "✅ Migration completed successfully"
env:
# Flyway 配置
- name: FLYWAY_USER
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_USER
- name: FLYWAY_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PASSWORD
# 从 Secret 获取连接信息
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PORT
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_DB
# Flyway 选项
- name: FLYWAY_BASELINE_ON_MIGRATE
value: "true"
- name: FLYWAY_BASELINE_VERSION
value: "0"
- name: FLYWAY_OUT_OF_ORDER
value: "false"
- name: FLYWAY_VALIDATE_ON_MIGRATE
value: "true"
volumeMounts:
- name: migrations
mountPath: /flyway/sql
readOnly: true
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
# ============================================
# Volumes: 挂载迁移文件
# ============================================
volumes:
- name: migrations
configMap:
name: db-migrations
@@ -1,140 +0,0 @@
apiVersion: v1
data:
V1__initial_schema.sql: |
-- ============================================
-- Migration: V1__initial_schema
-- Description: 初始化 youngth-guard 数据库 Schema
-- Author: System
-- Date: 2025-01-18
-- ============================================
-- Description:
-- 创建 youngth-guard 应用的初始数据库结构
-- 包括用户表、评估记录表等核心表
--
-- Rollback Strategy:
-- 如需回滚,创建 V2 迁移删除这些表
--
-- Performance Impact:
-- 预估执行时间:< 5 秒
-- 无锁表操作
-- 对应用无影响(首次创建)
-- ============================================
-- ============================================
-- Step 1: 创建用户表
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
COMMENT ON TABLE users IS '用户表';
COMMENT ON COLUMN users.id IS '用户 ID(主键)';
COMMENT ON COLUMN users.username IS '用户名(唯一)';
COMMENT ON COLUMN users.email IS '邮箱(唯一)';
COMMENT ON COLUMN users.created_at IS '创建时间';
COMMENT ON COLUMN users.updated_at IS '更新时间';
-- ============================================
-- Step 2: 创建心理评估记录表
-- ============================================
CREATE TABLE IF NOT EXISTS assessments (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
score INTEGER NOT NULL CHECK (score >= 0 AND score <= 100),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
assessment_type VARCHAR(50) NOT NULL DEFAULT 'general',
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_assessments_user_id ON assessments(user_id);
CREATE INDEX IF NOT EXISTS idx_assessments_status ON assessments(status);
CREATE INDEX IF NOT EXISTS idx_assessments_type ON assessments(assessment_type);
CREATE INDEX IF NOT EXISTS idx_assessments_created_at ON assessments(created_at);
COMMENT ON TABLE assessments IS '心理评估记录表';
COMMENT ON COLUMN assessments.id IS '评估 ID(主键)';
COMMENT ON COLUMN assessments.user_id IS '用户 ID(外键)';
COMMENT ON COLUMN assessments.score IS '评估分数(0-100)';
COMMENT ON COLUMN assessments.status IS '评估状态:pending/completed/cancelled';
COMMENT ON COLUMN assessments.assessment_type IS '评估类型:general/anxiety/depression等';
COMMENT ON COLUMN assessments.notes IS '评估备注';
COMMENT ON COLUMN assessments.created_at IS '创建时间';
COMMENT ON COLUMN assessments.updated_at IS '更新时间';
-- ============================================
-- Step 3: 创建审计日志表
-- ============================================
CREATE TABLE IF NOT EXISTS audit_logs (
id BIGSERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(50) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id INTEGER,
changes JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
CREATE INDEX IF NOT EXISTS idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);
COMMENT ON TABLE audit_logs IS '审计日志表';
COMMENT ON COLUMN audit_logs.id IS '日志 ID(主键)';
COMMENT ON COLUMN audit_logs.user_id IS '操作用户 ID';
COMMENT ON COLUMN audit_logs.action IS '操作类型:create/update/delete/login等';
COMMENT ON COLUMN audit_logs.entity_type IS '实体类型:user/assessment等';
COMMENT ON COLUMN audit_logs.entity_id IS '实体 ID';
COMMENT ON COLUMN audit_logs.changes IS '变更内容(JSON 格式)';
COMMENT ON COLUMN audit_logs.ip_address IS '操作 IP 地址';
COMMENT ON COLUMN audit_logs.user_agent IS '用户代理';
COMMENT ON COLUMN audit_logs.created_at IS '创建时间';
-- ============================================
-- Verification
-- ============================================
-- 验证表已创建
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'users') THEN
RAISE EXCEPTION 'Table users was not created!';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'assessments') THEN
RAISE EXCEPTION 'Table assessments was not created!';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'audit_logs') THEN
RAISE EXCEPTION 'Table audit_logs was not created!';
END IF;
RAISE NOTICE 'All tables created successfully!';
END $$;
-- ============================================
-- Notes:
-- - 所有表都使用 IF NOT EXISTS 确保幂等性
-- - 索引已根据常用查询场景优化
-- - 审计日志表使用 BIGSERIAL 应对大量日志
-- - 使用 JSONB 类型存储灵活的变更数据
-- ============================================
kind: ConfigMap
metadata:
creationTimestamp: null
name: db-migrations
@@ -1,11 +0,0 @@
resources:
- ../../base
- db-migration-job.yaml
- db-migrations-configmap.yaml
namespace: dev
images:
- name: ghcr.io/brunogaosz/youngth-guard-backend
newName: ghcr.io/brunogaosz/youngth-guard-backend
newTag: "dev-b90d0d9"
-20
View File
@@ -1,20 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cluster-bootstrap
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: cluster
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
-18
View File
@@ -1,18 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: nginx-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/nginx/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
-20
View File
@@ -1,20 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: youngth-guard-backend
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/youngth-guard/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
-20
View File
@@ -1,20 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infra-arc
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: infra/arc
destination:
server: https://kubernetes.default.svc
namespace: arc-system
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
-20
View File
@@ -1,20 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infra-data
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: infra/data
destination:
server: https://kubernetes.default.svc
namespace: infra
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
-60
View File
@@ -1,60 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-template-eu-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
labels:
environment: eu-dev
project: ljwx-template
annotations:
argocd.argoproj.io/manifest-generate-paths: .
spec:
project: ljwx
# 多源配置:从两个 repo 拉取配置
sources:
# 源 1: Helm Chart(代码仓库)
- repoURL: 'https://github.com/BrunoGaoSZ/ljwx-template.git'
targetRevision: main
path: deploy/helm/app
helm:
valueFiles:
# 引用源 2 的 values 文件
- $values/apps/ljwx-template/base/values-common.yaml
- $values/apps/ljwx-template/overlays/eu-dev/values.yaml
# 源 2: Values 文件(GitOps 仓库)
- repoURL: 'https://github.com/BrunoGaoSZ/ljwx-deploy.git'
targetRevision: main
ref: values # 定义引用名称
# 目标配置:部署到哪个集群和命名空间
destination:
server: 'https://kubernetes.default.svc'
namespace: dev # 使用已有的 dev 命名空间
# 同步策略
syncPolicy:
automated:
prune: true # 自动删除 Git 中不存在的资源
selfHeal: true # 自动修复手动更改
allowEmpty: false
syncOptions:
- CreateNamespace=false # dev 命名空间已存在
- PrunePropagationPolicy=foreground
- PruneLast=true # 先部署新资源,再删除旧资源
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# 忽略某些字段的差异(避免误报)
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # 如果使用 HPA,忽略 replicas 差异
-56
View File
@@ -1,56 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-template-mac-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
labels:
environment: mac-dev
project: ljwx-template
annotations:
argocd.argoproj.io/manifest-generate-paths: .
spec:
project: ljwx
# 多源配置:从两个 repo 拉取配置
sources:
# 源 1: Helm Chart(代码仓库)
- repoURL: 'https://github.com/BrunoGaoSZ/ljwx-template.git'
targetRevision: main
path: deploy/helm/app
helm:
valueFiles:
- $values/apps/ljwx-template/base/values-common.yaml
- $values/apps/ljwx-template/overlays/mac-dev/values.yaml
# 源 2: Values 文件(GitOps 仓库)
- repoURL: 'https://github.com/BrunoGaoSZ/ljwx-deploy.git'
targetRevision: main
ref: values
destination:
server: 'https://kubernetes.default.svc'
namespace: dev # Mac k3s 也使用 dev 命名空间
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=false
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
-34
View File
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-bookstore
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
labels:
environment: bookstore
project: ljwx-bookstore
spec:
project: ljwx
source:
repoURL: 'https://github.com/BrunoGaoSZ/ljwx-deploy.git'
targetRevision: main
path: apps/ljwx-bookstore/overlays/bookstore
destination:
server: 'https://kubernetes.default.svc'
namespace: bookstore
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
-34
View File
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-platform
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
labels:
environment: prod
project: ljwx-platform
spec:
project: ljwx
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/ljwx-platform/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: ljwx-platform
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
-34
View File
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: agent-platform
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
labels:
environment: prod
project: agent-platform
spec:
project: ljwx
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/agent-platform/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: agent-platform
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
-34
View File
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-health-k3s-eu
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
labels:
environment: k3s-eu
project: ljwx-health
spec:
project: ljwx
source:
repoURL: "https://github.com/BrunoGaoSZ/ljwx-deploy.git"
targetRevision: main
path: apps/ljwx-health/overlays/k3s-eu
destination:
server: "https://kubernetes.default.svc"
namespace: ljwx-health
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
-62
View File
@@ -1,62 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ljwx-website-eu-dev
namespace: argocd
labels:
project: ljwx-website
environment: dev-eu
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default # Or use 'ljwx-project' if you have a specific AppProject
source:
# Source: ljwx-website business repository (contains Helm chart)
repoURL: https://github.com/BrunoGaoSZ/ljwx-website.git
targetRevision: main
path: deploy/helm/app
helm:
# Use both base values and environment-specific overlay
valueFiles:
- values.yaml # Base configuration
- ../../environments/dev-eu.yaml # Environment overlay
# Additional runtime overrides (managed by CI/CD)
# These values override everything above
values: |
website:
image:
tag: "main-latest" # Will be updated by CI/CD
ingress:
enabled: true
className: traefik
host: "website.lingjingwanxiang.cn"
destination:
server: https://kubernetes.default.svc
namespace: ljwx-website-dev
syncPolicy:
automated:
prune: true # Auto-delete resources when removed from git
selfHeal: true # Auto-sync when cluster state drifts from git
allowEmpty: false
syncOptions:
- CreateNamespace=true # Auto-create namespace if it doesn't exist
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# Health assessment
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Ignore if HPA manages replicas
-66
View File
@@ -1,66 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: ljwx
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: LJWX Template Applications
# 允许的源代码仓库(分离后需要两个 repo)
sourceRepos:
# 代码仓库(Helm chart)
- 'https://github.com/BrunoGaoSZ/ljwx-template.git'
- 'git@github.com:BrunoGaoSZ/ljwx-template.git'
- 'https://github.com/BrunoGaoSZ/ljwx-bookstore.git'
- 'git@github.com:BrunoGaoSZ/ljwx-bookstore.git'
# GitOps 仓库(部署配置)
- 'https://github.com/BrunoGaoSZ/ljwx-deploy.git'
- 'git@github.com:BrunoGaoSZ/ljwx-deploy.git'
# 允许部署的目标集群和命名空间
destinations:
- namespace: 'ljwx-*'
server: 'https://kubernetes.default.svc'
- namespace: 'argocd'
server: 'https://kubernetes.default.svc'
- namespace: 'dev' # 已有的 dev 命名空间
server: 'https://kubernetes.default.svc'
- namespace: 'bookstore' # bookstore 命名空间
server: 'https://kubernetes.default.svc'
- namespace: 'agent-platform'
server: 'https://kubernetes.default.svc'
# 允许创建的集群级资源(谨慎开放)
clusterResourceWhitelist:
- group: ''
kind: Namespace
# 允许的命名空间级资源
namespaceResourceWhitelist:
- group: '*'
kind: '*'
# 孤立资源策略(删除 Application 时的行为)
orphanedResources:
warn: true
# 角色(可选,用于细粒度权限控制)
roles:
- name: developer
description: Developer role with read-only access
policies:
- p, proj:ljwx:developer, applications, get, ljwx/*, allow
- p, proj:ljwx:developer, applications, sync, ljwx/*, deny
groups:
- ljwx-developers
- name: operator
description: Operator role with sync permissions
policies:
- p, proj:ljwx:operator, applications, get, ljwx/*, allow
- p, proj:ljwx:operator, applications, sync, ljwx/*, allow
- p, proj:ljwx:operator, applications, override, ljwx/*, allow
groups:
- ljwx-operators
-7
View File
@@ -1,7 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: dev
imagePullSecrets:
- name: regcred
-4
View File
@@ -1,4 +0,0 @@
resources:
- namespace-dev.yaml
- regcred.yaml
- default-sa.yaml
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: dev
-8
View File
@@ -1,8 +0,0 @@
apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJoYXJib3IuZXUubGluZ2ppbmd3YW54aWFuZy5jbjo4MDgwIjp7InVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IkhhcmJvcjEyMzQ1IiwiYXV0aCI6IllXUnRhVzQ2U0dGeVltOXlNVEl6TkRVPSJ9fX0=
kind: Secret
metadata:
name: regcred
namespace: dev
type: kubernetes.io/dockerconfigjson
-146
View File
@@ -1,146 +0,0 @@
# 平台目标架构说明
## 📋 概述
本平台采用 **GitOps + 基础设施复用** 的架构模式,实现应用的自动化发布和统一的数据库管理。
---
## 🔄 CI/CD 职责边界
### GitHub Actions(CI 层)
**职责:构建和推送**
- 代码提交后自动触发
- 构建 Docker 镜像
- 推送镜像到容器仓库(GHCR)
- 更新 GitOps 仓库的镜像版本
**不负责:** 实际部署、数据库管理、环境配置
### ArgoCD(CD 层)
**职责:部署和同步**
- 监听 GitOps 仓库变化
- 自动将声明式配置同步到 Kubernetes
- 执行数据库迁移(PreSync Hook)
- 保证集群状态与 Git 一致
**不负责:** 构建镜像、修改代码
---
## 🏃 GitHub Runner 定位
### 默认:GitHub Hosted Runner
**适用场景:** 所有标准构建任务
- 无需维护
- 按使用付费
- 适合 95% 的场景
### ARC (Actions Runner Controller)
**仅在以下场景启用:**
- 需要访问内网资源
- 构建时间超过 6 小时
- 需要 GPU 等特殊硬件
**原则:** 不使用则不部署,避免维护成本
---
## 🗄️ Infra Postgres 角色
### 定位:平台级共享数据库服务
**为什么不是每个项目独立部署?**
- 减少资源浪费(内存、存储)
- 统一备份和恢复策略
- 集中监控和运维
### 使用方式
```
一个 Postgres 实例 → 多个独立数据库
├── youngth_guard_dev (项目1 - 开发环境)
├── youngth_guard_prod (项目1 - 生产环境)
├── other_project_dev (项目2 - 开发环境)
└── ...
```
### 隔离保证
- **数据库级别隔离**:每个项目有独立的数据库和用户
- **权限最小化**:应用用户只能访问自己的数据库
- **网络隔离**:仅集群内访问,不对外暴露
---
## 🔗 应用、数据库、迁移、回滚关系
### 正常发布流程
```
1. 开发者 git push
↓
2. GitHub Actions 构建镜像
↓
3. 更新 GitOps 仓库
↓
4. ArgoCD 检测到变化
↓
5. 执行数据库迁移(PreSync Hook)
✓ 成功 → 继续部署应用
✗ 失败 → 阻止部署
↓
6. 部署新版本应用
```
### 回滚策略
#### 应用回滚(快速)
- **方式:** Git revert → ArgoCD 自动回滚
- **时间:** 2-5 分钟
- **场景:** 应用逻辑问题、性能问题
#### 数据库回滚(谨慎)
- **方式 1:** 兼容性迁移(推荐)
- 新旧版本共存的 Schema 设计
- 不需要真正回滚
- **方式 2:** PITR(Point-In-Time Recovery)
- 恢复到指定时间点
- 用于严重数据错误
- **注意:** 会丢失恢复点之后的数据
---
## ✅ 核心原则
1. **不中断服务**
- 数据库迁移必须向后兼容
- 应用先部署新版本,再清理旧 Schema
2. **可回滚**
- 应用随时可以回到上一个版本
- 数据库设计支持新旧版本共存
3. **自动化优先**
- 减少人工操作
- 所有变更通过 Git 触发
4. **基础设施复用**
- 共享 Postgres、监控、日志
- 降低运维复杂度
---
## 🎯 对团队的意义
**对开发者:**
- 只需要 `git push`,无需关心部署细节
- 数据库自动创建和迁移
- 专注业务逻辑开发
**对运维:**
- 统一的备份和恢复流程
- 集中监控和告警
- 减少重复劳动
**对业务:**
- 更快的发布速度
- 更高的系统稳定性
- 更低的运维成本
-459
View File
@@ -1,459 +0,0 @@
# ArgoCD PreSync Hook - 数据库迁移
## 📋 概述
本文档说明如何在 ArgoCD 中使用 **PreSync Hook** 自动执行数据库迁移。
**工作流程:**
```
1. git push → CI 构建镜像
2. CI 更新 GitOps 仓库
3. ArgoCD 检测到变化
4. 执行 PreSync Hook(数据库迁移)
✓ 成功 → 继续部署应用
✗ 失败 → 阻止部署
5. 部署新版本应用
```
---
## 🎯 PreSync Hook 机制
### Hook 类型
| Hook | 执行时机 | 用途 |
|------|---------|------|
| **PreSync** | 同步前 | 数据库迁移、配置预检 |
| Sync | 同步中 | 标准资源部署 |
| PostSync | 同步后 | 数据初始化、通知 |
| SyncFail | 失败时 | 清理、回滚、告警 |
### PreSync 注解
```yaml
metadata:
annotations:
# 标记为 PreSync Hook
argocd.argoproj.io/hook: PreSync
# 删除策略:在新 Hook 执行前删除旧的
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
# Sync Wave:-1 表示最先执行
argocd.argoproj.io/sync-wave: "-1"
```
---
## 📁 文件组织
### 方式 1:ConfigMap(适合少量迁移)
```yaml
# 迁移文件存储在 ConfigMap
apps/youngth-guard/overlays/dev/
├── db-migrations-configmap.yaml # 包含 SQL 文件
└── db-migration-job.yaml # PreSync Job
```
**优点:** 简单直接
**缺点:** ConfigMap 大小限制 1MB,不适合大量迁移
### 方式 2:打包到镜像(推荐)
```dockerfile
# Dockerfile
FROM flyway/flyway:10-alpine
# 复制迁移文件
COPY backend-fastapi/migrations /flyway/sql
# 复制配置
COPY flyway.conf /flyway/conf/
```
**优点:**
- 迁移文件和代码版本一致
- 无大小限制
- 便于审计
**缺点:** 需要构建专门的迁移镜像
### 方式 3:从 Git 拉取(灵活)
```yaml
initContainers:
- name: fetch-migrations
image: alpine/git
command:
- /bin/sh
- -c
- |
git clone --depth=1 --branch=main \
https://github.com/BrunoGaoSZ/youngth-guard.git /repo
cp -r /repo/backend-fastapi/migrations/* /migrations/
volumeMounts:
- name: migrations
mountPath: /migrations
```
**优点:**
- 灵活,可以指定 branch/tag
- 不需要重新构建镜像
**缺点:**
- 需要 Git 访问权限
- 依赖外部服务
---
## 🔒 并发保护
### 问题:多个 Sync 同时触发
如果多个开发者同时部署,可能导致多个迁移 Job 并发执行。
### 解决方案:Flyway 内置锁
Flyway 使用数据库表锁防止并发:
```sql
-- Flyway 自动创建的锁表
CREATE TABLE flyway_schema_history (
installed_rank INT NOT NULL,
version VARCHAR(50),
description VARCHAR(200),
type VARCHAR(20),
script VARCHAR(1000),
checksum INT,
installed_by VARCHAR(100),
installed_on TIMESTAMP DEFAULT NOW(),
execution_time INT,
success BOOLEAN NOT NULL,
PRIMARY KEY (installed_rank)
);
-- 执行迁移时 Flyway 会锁定此表
```
**机制:**
1. Job A 开始迁移 → 锁定 `flyway_schema_history`
2. Job B 尝试迁移 → 等待锁释放(超时后失败)
3. Job A 完成 → 释放锁
4. Job B 检测到已执行,跳过
### 额外保护:BackoffLimit
```yaml
spec:
backoffLimit: 0 # 失败后不重试
```
避免失败后反复重试导致资源浪费。
---
## ❌ 失败路径说明
### 场景 1:迁移 SQL 错误
```
PreSync Job 失败 (Exit Code 1)
↓
ArgoCD 标记 Sync 失败
↓
应用部署被阻止(Deployment 不会更新)
↓
旧版本应用继续运行
```
**查看错误:**
```bash
# 查看 Job 日志
kubectl -n dev logs -l component=migration --tail=100
# 查看 ArgoCD Application 状态
kubectl -n argocd get application youngth-guard-backend -o yaml
```
**修复步骤:**
1. 检查迁移 SQL 语法
2. 修复 Git 仓库中的迁移文件
3. 手动触发 ArgoCD Sync,或等待自动同步
4. 验证迁移成功
### 场景 2:数据库连接失败
```
Init Container (wait-for-db) 失败
↓
Job 一直 Pending
↓
ArgoCD 等待超时(默认 5 分钟)
↓
Sync 失败
```
**原因:**
- Secret 不存在或错误
- 数据库服务不可用
- 网络策略阻止连接
**修复步骤:**
```bash
# 1. 检查 Secret
kubectl -n dev get secret postgres-youngth-guard-dev
# 2. 测试连接
kubectl run -it --rm psql-test \
--image=postgres:16-alpine \
--namespace=dev \
--restart=Never \
-- sh -c 'export $(kubectl get secret postgres-youngth-guard-dev -o jsonpath="{.data.DATABASE_URL}" | base64 -d) && pg_isready -d $DATABASE_URL'
# 3. 检查数据库 Pod
kubectl -n infra get pods -l app=postgres
```
### 场景 3:迁移文件缺失
```
Flyway 启动
↓
未找到迁移文件(/flyway/sql 为空)
↓
Flyway 报错或跳过
↓
Job 成功(但实际没有执行迁移)
```
**预防:**
```yaml
# 在 Job 中添加验证
command:
- /bin/sh
- -c
- |
# 检查迁移文件
if [ ! "$(ls -A /flyway/sql)" ]; then
echo "ERROR: No migration files found in /flyway/sql"
exit 1
fi
# 执行迁移
flyway migrate
```
### 场景 4:迁移部分成功
```
V1 迁移成功
V2 迁移成功
V3 迁移失败(SQL 错误)
↓
Flyway 记录 V3 失败
↓
Job 失败
```
**状态:**
- V1, V2 已应用到数据库
- V3 标记为失败,未应用
**修复步骤:**
```bash
# 1. 连接数据库查看状态
kubectl -n infra exec -it postgres-0 -- psql -U postgres -d youngth_guard_dev
# 2. 检查 Flyway 历史
SELECT * FROM flyway_schema_history ORDER BY installed_rank;
# 3. 如果需要手动修复
# 修复数据库状态,然后标记迁移为成功
UPDATE flyway_schema_history SET success = true WHERE version = '3';
# 或者删除失败记录,修复 SQL 后重新运行
DELETE FROM flyway_schema_history WHERE version = '3';
```
---
## ✅ 验证迁移成功
### 检查清单
```bash
# 1. 查看 Job 状态
kubectl -n dev get job -l component=migration
# 预期输出:
# NAME COMPLETIONS DURATION AGE
# db-migration 1/1 15s 2m
# 2. 查看 Job 日志
kubectl -n dev logs -l component=migration --tail=50
# 预期包含:
# ✅ Migration completed successfully
# 3. 检查 Flyway 历史
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT version, description, installed_on, success FROM flyway_schema_history ORDER BY installed_rank;"
# 4. 验证应用部署
kubectl -n dev get pods -l app=youngth-guard-backend
# 5. 检查应用日志(应该能连接数据库)
kubectl -n dev logs -l app=youngth-guard-backend --tail=20
```
---
## 🔄 完整集成示例
### 目录结构
```
apps/youngth-guard/overlays/dev/
├── kustomization.yaml
├── deployment.yaml
├── service.yaml
├── db-migration-job.yaml # PreSync Hook
└── db-migrations-configmap.yaml # 迁移文件(或使用镜像)
```
### kustomization.yaml
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
- ../../base
- db-migration-job.yaml
- db-migrations-configmap.yaml
images:
- name: ghcr.io/brunogaosz/youngth-guard-backend
newName: ghcr.io/brunogaosz/youngth-guard-backend
newTag: "dev-abc1234"
```
### ArgoCD Application
```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: youngth-guard-backend
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/BrunoGaoSZ/ljwx-deploy.git
targetRevision: main
path: apps/youngth-guard/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
# 重要:失败时不继续
retry:
limit: 2
backoff:
duration: 5s
factor: 2
maxDuration: 1m
```
---
## 🐛 调试技巧
### 手动运行迁移 Job
```bash
# 1. 从 PreSync Job 创建手动 Job
kubectl -n dev get job db-migration -o yaml | \
sed 's/argocd.argoproj.io\/hook: PreSync//' | \
sed 's/name: db-migration/name: db-migration-manual/' | \
kubectl apply -f -
# 2. 查看日志
kubectl -n dev logs -f job/db-migration-manual
# 3. 清理
kubectl -n dev delete job db-migration-manual
```
### 跳过迁移(紧急情况)
```yaml
# 临时移除 PreSync Hook
metadata:
annotations:
# argocd.argoproj.io/hook: PreSync # 注释掉
argocd.argoproj.io/hook: Skip
```
⚠️ **危险操作!** 仅在紧急情况使用,之后必须手动执行迁移。
### 查看 ArgoCD 事件
```bash
# 查看 Application 事件
kubectl -n argocd describe application youngth-guard-backend
# 查看最近的 Sync
kubectl -n argocd get application youngth-guard-backend -o jsonpath='{.status.operationState}'
```
---
## 📚 最佳实践
1. **迁移文件版本化**
- 迁移文件提交到 Git
- 和代码一起审查
2. **测试迁移**
- 在本地数据库测试
- 在测试环境验证
- 使用 Flyway validate 检查
3. **监控迁移**
- 设置告警(迁移失败)
- 记录迁移时间
- 定期审查 flyway_schema_history
4. **备份数据**
- 迁移前自动备份(通过 PreSync 触发)
- 保留最近 7 天备份
5. **文档化**
- 每个迁移都有清晰注释
- 记录回滚步骤
- 说明业务影响
---
## 📖 相关文档
- `docs/flyway-guide/README.md` - Flyway 规范和示例
- `docs/db-isolation-spec.md` - 数据库隔离规范
- `docs/architecture-overview.md` - 平台架构说明
- `infra/postgres/README.md` - Postgres 部署文档
- ArgoCD Hooks 官方文档:https://argo-cd.readthedocs.io/en/stable/user-guide/resource_hooks/
-175
View File
@@ -1,175 +0,0 @@
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
annotations:
# ============================================
# ArgoCD PreSync Hook
# ============================================
# 这个 Job 会在应用部署前执行
# 如果失败,会阻止应用部署
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "-1"
labels:
app: youngth-guard-backend
component: migration
managed-by: argocd
spec:
# 不自动清理,便于查看日志
# ttlSecondsAfterFinished: 3600
# 失败后不重试(避免并发问题)
backoffLimit: 0
template:
metadata:
labels:
app: youngth-guard-backend
component: migration
spec:
restartPolicy: Never
# ============================================
# Init Container: 等待数据库就绪
# ============================================
initContainers:
- name: wait-for-db
image: postgres:16-alpine
command:
- /bin/sh
- -c
- |
echo "Waiting for database to be ready..."
until pg_isready -h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USER; do
echo "Database is unavailable - sleeping"
sleep 2
done
echo "Database is ready!"
env:
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PORT
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_USER
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PASSWORD
# ============================================
# Main Container: Flyway Migration
# ============================================
containers:
- name: flyway
image: flyway/flyway:10-alpine
command:
- /bin/sh
- -c
- |
set -e
echo "============================================"
echo "Database Migration with Flyway"
echo "============================================"
echo "Database: $FLYWAY_URL"
echo "User: $FLYWAY_USER"
echo "Migrations: /flyway/sql"
echo "============================================"
# 列出迁移文件
echo "Available migrations:"
ls -la /flyway/sql/ || echo "No migrations found"
# 执行迁移
echo ""
echo "Running migrations..."
flyway -connectRetries=3 migrate
# 显示迁移历史
echo ""
echo "Migration history:"
flyway info
echo ""
echo "✅ Migration completed successfully"
env:
# Flyway 配置
- name: FLYWAY_URL
value: "jdbc:postgresql://$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DB)"
- name: FLYWAY_USER
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_USER
- name: FLYWAY_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PASSWORD
# 从 Secret 获取连接信息
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PORT
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_DB
# Flyway 选项
- name: FLYWAY_BASELINE_ON_MIGRATE
value: "true"
- name: FLYWAY_BASELINE_VERSION
value: "0"
- name: FLYWAY_OUT_OF_ORDER
value: "false"
- name: FLYWAY_VALIDATE_ON_MIGRATE
value: "true"
volumeMounts:
- name: migrations
mountPath: /flyway/sql
readOnly: true
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
# ============================================
# Volumes: 挂载迁移文件
# ============================================
volumes:
- name: migrations
configMap:
name: db-migrations
# 如果迁移文件很多,建议使用 initContainer 从 Git 拉取
# 或者打包到应用镜像中
@@ -1,59 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: db-migrations
labels:
app: youngth-guard-backend
component: migration
managed-by: argocd
data:
# ============================================
# 迁移文件示例
# ============================================
# 实际项目中,建议:
# 1. 将迁移文件打包到应用镜像中
# 2. 或使用 initContainer 从 Git 拉取
# 3. 这里仅作为示例
# ============================================
V1__initial_schema.sql: |
-- ============================================
-- Migration: V1__initial_schema
-- Description: 初始化数据库 Schema
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
CREATE TABLE IF NOT EXISTS assessments (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
score INTEGER NOT NULL CHECK (score >= 0 AND score <= 100),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_assessments_user_id ON assessments(user_id);
CREATE INDEX IF NOT EXISTS idx_assessments_status ON assessments(status);
COMMENT ON TABLE users IS '用户表';
COMMENT ON TABLE assessments IS '心理评估记录表';
V2__add_phone_column.sql: |
-- ============================================
-- Migration: V2__add_phone_column
-- Description: 添加手机号列
-- ============================================
ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR(20);
CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone) WHERE phone IS NOT NULL;
COMMENT ON COLUMN users.phone IS '用户手机号(可选)';
-594
View File
@@ -1,594 +0,0 @@
# CI/Runner 策略说明
## 📋 概述
本文档说明 GitHub Actions Runner 的选择策略,帮助团队在 **GitHub-hosted runner** 和 **Self-hosted runner (ARC)** 之间做出正确决策。
**核心策略:**
- ✅ **默认使用 GitHub-hosted runner**(免费额度 + 零维护)
- ✅ **按需启用 ARC**(特殊场景:内网访问、大量构建、特殊硬件)
- ✅ **避免过早优化**(不要一开始就自建 runner)
---
## 🎯 快速决策树
```
你的项目需要 CI/CD 吗?
↓
是
↓
是否需要访问内网资源?
├─ 是 → 使用 ARC (Self-hosted)
│ (例如:内网数据库、私有 API)
│
└─ 否 → 构建时间是否 > 10 分钟?
├─ 否 → 使用 GitHub-hosted runner ✅
│ (大多数项目)
│
└─ 是 → 月构建次数是否 > 500 次?
├─ 否 → 使用 GitHub-hosted runner ✅
│ (成本仍可接受)
│
└─ 是 → 考虑启用 ARC
(评估成本 vs 维护成本)
```
---
## 🔄 GitHub-hosted vs Self-hosted (ARC)
### 对比表
| 维度 | GitHub-hosted Runner | Self-hosted Runner (ARC) |
|------|---------------------|-------------------------|
| **成本** | 免费额度:2000 分钟/月<br>超出:$0.008/分钟 | K8s 节点成本:$50-200/月<br>维护成本:人力 |
| **维护** | ✅ 零维护(GitHub 管理) | ❌ 需要维护(更新、监控、扩缩容) |
| **安全性** | ✅ 每次构建全新环境<br>✅ 自动清理 | ⚠️ 需要配置安全策略<br>⚠️ 需要定期更新镜像 |
| **性能** | 标准:2 核 7GB<br>Large:4 核 16GB | 可自定义(2-16 核) |
| **启动速度** | 🟡 中等(10-30 秒) | 🟢 快速(2-10 秒) |
| **网络访问** | ✅ 公网<br>❌ 无法访问内网 | ✅ 公网 + 内网(K8s 集群内) |
| **并发构建** | ✅ 无限(GitHub 管理) | ⚠️ 受限于 K8s 节点资源 |
| **缓存** | ✅ 支持 actions/cache | ✅ 支持 + 可自建缓存层 |
| **特殊硬件** | ❌ 无 GPU/特殊设备 | ✅ 可配置 GPU/特殊设备 |
| **适用场景** | 🎯 大多数项目 | 🎯 内网访问、大量构建、特殊硬件 |
---
## ✅ 默认策略:使用 GitHub-hosted Runner
### 为什么默认选择 GitHub-hosted?
#### 1. **免费额度充足**
GitHub 提供慷慨的免费额度:
- **公共仓库:** 无限免费分钟
- **私有仓库(个人账户):** 2000 分钟/月
- **私有仓库(团队账户):** 3000 分钟/月
- **私有仓库(企业账户):** 50000 分钟/月
**实际案例:**
```
项目:youngth-guard-backend
构建时间:5 分钟/次
每日提交:10 次
月构建次数:300 次
月消耗分钟:1500 分钟
结论:免费额度完全够用 ✅
```
#### 2. **零维护成本**
GitHub-hosted runner 完全由 GitHub 管理:
- ✅ 自动更新操作系统和工具链
- ✅ 自动扩缩容(并发构建)
- ✅ 自动清理(每次构建全新环境)
- ✅ 无需监控和告警
- ✅ 无需担心资源不足
**对比 Self-hosted:**
- ❌ 需要维护 ARC 控制器
- ❌ 需要维护 runner 镜像
- ❌ 需要配置扩缩容策略
- ❌ 需要监控资源使用
- ❌ 需要定期更新依赖
#### 3. **安全性最佳**
每次构建都是全新的虚拟机:
- ✅ 完全隔离(无交叉污染)
- ✅ 无状态(自动清理密钥/缓存)
- ✅ 防止密钥泄露(构建结束即销毁)
#### 4. **快速上手**
无需配置,开箱即用:
```yaml
# .github/workflows/build.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest # 就这么简单!
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t myapp .
- name: Push to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
```
---
## 🚀 何时启用 ARC (Self-hosted Runner)
### 场景 1:需要访问内网资源 ✅ 推荐
**问题:** GitHub-hosted runner 运行在公网,无法访问内网数据库/API。
**示例:**
- 需要连接内网 Postgres 数据库进行集成测试
- 需要调用内网 API 进行验证
- 需要访问内网 NFS/S3 存储
**解决方案:** 使用 ARC,runner 运行在 K8s 集群内,可访问内网服务。
```yaml
# .github/workflows/integration-test.yml
jobs:
test:
runs-on: self-hosted # 使用 ARC
steps:
- name: Run integration tests
run: |
# 可以直接访问内网数据库
export DB_HOST=postgres-lb.infra.svc.cluster.local
pytest tests/integration/
```
### 场景 2:大量构建,成本优化 ⚠️ 需评估
**问题:** 超出免费额度,GitHub-hosted runner 费用高。
**成本对比:**
| 方案 | 月构建次数 | 月构建时间 | 月成本 |
|------|----------|----------|--------|
| GitHub-hosted | 1000 次 | 5000 分钟 | $24 (超出 3000 分钟) |
| GitHub-hosted | 5000 次 | 25000 分钟 | $176 |
| ARC (1 节点) | 无限 | 无限 | $50-100 (K8s 节点) + 维护 |
**决策:**
- 月构建 < 5000 分钟 → GitHub-hosted 更划算 ✅
- 月构建 > 10000 分钟 → 考虑 ARC
- 月构建 > 50000 分钟 → 强烈推荐 ARC
### 场景 3:特殊硬件需求 ✅ 推荐
**问题:** 需要 GPU、特殊 CPU 架构(ARM)、大内存。
**示例:**
- 机器学习模型训练(需要 GPU)
- 构建 ARM 架构镜像(Apple Silicon / Raspberry Pi)
- 大型 monorepo 构建(需要 32GB+ 内存)
**解决方案:** 使用 ARC,配置专用节点池。
```yaml
# ARC 配置:GPU 节点池
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: gpu-runner
spec:
replicas: 1
template:
spec:
nodeSelector:
cloud.google.com/gke-accelerator: nvidia-tesla-t4
tolerations:
- key: nvidia.com/gpu
operator: Exists
containers:
- name: runner
resources:
limits:
nvidia.com/gpu: 1
```
### 场景 4:构建速度优化 🟡 可选
**问题:** GitHub-hosted runner 启动慢(10-30 秒)。
**对比:**
- GitHub-hosted:10-30 秒启动
- ARC (预热):2-10 秒启动
**决策:**
- 构建频率低(< 50 次/天)→ 启动时间不重要 ✅
- 构建频率高(> 200 次/天)→ 考虑 ARC
- 需要快速反馈(如 PR 验证)→ 考虑 ARC
### 场景 5:合规性要求 ✅ 推荐
**问题:** 企业安全政策禁止使用公网 runner。
**示例:**
- 金融行业(需要数据本地化)
- 政府项目(需要在指定区域运行)
- 内部工具(禁止代码离开内网)
**解决方案:** 使用 ARC,确保所有构建在内网完成。
---
## 📊 成本分析
### GitHub-hosted Runner 成本
**定价(2025 年):**
- Linux:$0.008/分钟
- Windows:$0.016/分钟
- macOS:$0.08/分钟
**实际案例:**
| 项目 | 构建时间 | 月构建次数 | 月消耗分钟 | 免费额度 | 超出分钟 | 月成本 |
|------|---------|----------|----------|---------|---------|--------|
| 小型项目 | 3 分钟 | 200 次 | 600 分钟 | 3000 | 0 | **$0** |
| 中型项目 | 8 分钟 | 500 次 | 4000 分钟 | 3000 | 1000 | **$8** |
| 大型项目 | 15 分钟 | 1000 次 | 15000 分钟 | 3000 | 12000 | **$96** |
### ARC (Self-hosted) 成本
**基础成本:**
- K8s 节点:$50-200/月(取决于云厂商和规格)
- 存储(缓存):$10-20/月
- 网络流量:$5-15/月
**维护成本:**
- 初始搭建:4-8 小时
- 月度维护:2-4 小时/月
- 按工程师时薪 $50 计算:$100-200/月
**总成本:** $165-435/月
**适用场景:**
- 月构建分钟 > 20000 → ARC 成本更低
- 需要内网访问 → ARC 是唯一选择
- 团队规模 > 10 人 → 维护成本分摊
---
## 🔧 ARC 配置示例
### 前提条件
当前 `ljwx-deploy` 仓库已经配置了 ARC:
- ✅ ARC 控制器已部署(`infra/arc/`)
- ✅ RunnerDeployment 已配置(弹性伸缩)
- ✅ GHCR 认证已配置(imagePullSecrets)
### 查看当前 ARC 状态
```bash
# 查看 ARC 控制器
kubectl -n arc-systems get pods
# 查看 Runner 状态
kubectl -n arc-runners get runners
# 查看 RunnerDeployment
kubectl -n arc-runners get runnerdeployments
```
### 使用 ARC Runner
在 GitHub Actions workflow 中指定:
```yaml
# .github/workflows/build-with-arc.yml
name: Build with ARC
on:
push:
branches: [main]
jobs:
build:
# 使用 self-hosted runner
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Build in Kubernetes
run: |
echo "Running in K8s cluster"
echo "Can access internal services:"
curl postgres-lb.infra.svc.cluster.local:5432
- name: Build Docker image
run: |
docker build -t myapp .
docker push ghcr.io/myorg/myapp:${{ github.sha }}
```
### 配置多个 Runner 类型
```yaml
# infra/arc/runner-deployment-standard.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: arc-runner-standard
namespace: arc-runners
spec:
replicas: 2
template:
spec:
labels:
- self-hosted
- linux
- x64
- standard # 自定义标签
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
---
# infra/arc/runner-deployment-large.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: arc-runner-large
namespace: arc-runners
spec:
replicas: 1
template:
spec:
labels:
- self-hosted
- linux
- x64
- large # 自定义标签
resources:
requests:
memory: "8Gi"
cpu: "4000m"
limits:
memory: "16Gi"
cpu: "8000m"
```
在 workflow 中使用:
```yaml
jobs:
small-build:
runs-on: [self-hosted, standard] # 使用标准 runner
steps:
- run: npm run build
large-build:
runs-on: [self-hosted, large] # 使用大内存 runner
steps:
- run: mvn clean install
```
---
## 📋 决策 Checklist
### 选择 GitHub-hosted Runner(推荐 ✅)
满足以下**所有**条件时,使用 GitHub-hosted:
- [ ] 无需访问内网资源
- [ ] 月构建分钟 < 10000(企业账户 < 50000)
- [ ] 无特殊硬件需求(GPU/大内存)
- [ ] 无合规性要求(数据可离开内网)
- [ ] 构建时间 < 30 分钟/次
- [ ] 团队规模较小(< 5 人)
**优势:**
- ✅ 零成本(免费额度内)
- ✅ 零维护
- ✅ 最高安全性
- ✅ 无限并发
### 选择 ARC (Self-hosted Runner)
满足以下**任一**条件时,考虑 ARC:
- [ ] 需要访问内网服务(数据库、API、存储)
- [ ] 月构建分钟 > 20000
- [ ] 需要 GPU 或特殊硬件
- [ ] 需要大内存(> 16GB)
- [ ] 合规性要求(数据不能离开内网)
- [ ] 需要自定义 runner 环境(特殊依赖)
- [ ] 团队规模较大(> 10 人,维护成本分摊)
**权衡:**
- ✅ 可访问内网
- ✅ 成本可控(大量构建时)
- ✅ 性能可定制
- ❌ 需要维护
- ❌ 初始搭建成本
- ❌ 需要安全加固
---
## 🚦 推荐配置策略
### 阶段 1:项目启动(0-3 个月)
**策略:100% GitHub-hosted**
```yaml
# .github/workflows/build.yml
jobs:
build:
runs-on: ubuntu-latest # 默认
```
**原因:**
- 快速验证 CI/CD 流程
- 无需维护基础设施
- 免费额度足够
### 阶段 2:快速迭代(3-6 个月)
**策略:GitHub-hosted 为主,ARC 为辅**
```yaml
# .github/workflows/build.yml
jobs:
# 普通构建:GitHub-hosted
build:
runs-on: ubuntu-latest
# 集成测试(需要内网):ARC
integration-test:
runs-on: self-hosted
steps:
- name: Test with internal DB
run: pytest tests/integration/
```
**原因:**
- 大部分构建仍使用 GitHub-hosted
- 仅特殊场景使用 ARC
- 逐步积累 ARC 运维经验
### 阶段 3:规模化(6 个月+)
**策略:按需选择,精细化管理**
```yaml
# .github/workflows/matrix-build.yml
jobs:
# 轻量级任务:GitHub-hosted
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
# 标准构建:ARC (成本优化)
build:
runs-on: [self-hosted, standard]
steps:
- run: docker build .
# 大型构建:ARC large
build-monorepo:
runs-on: [self-hosted, large]
steps:
- run: nx run-many --target=build --all
# macOS 构建:GitHub-hosted (无 ARC macOS)
build-ios:
runs-on: macos-latest
steps:
- run: xcodebuild
```
**原因:**
- 成本最优
- 性能最优
- 维护成本可控
---
## 🔍 监控和优化
### GitHub-hosted Runner 监控
查看使用量:
1. GitHub 仓库 → Settings → Billing → Usage this month
2. 查看每个 workflow 的耗时:Actions → Workflow → Timing
**优化建议:**
- 使用 actions/cache 缓存依赖
- 并行执行独立 jobs
- 避免不必要的构建(配置 paths filter)
```yaml
# 优化示例:仅在代码变更时构建
on:
push:
paths:
- 'src/**'
- 'Dockerfile'
- '.github/workflows/build.yml'
```
### ARC Runner 监控
```bash
# 查看 runner 资源使用
kubectl -n arc-runners top pods
# 查看 runner 日志
kubectl -n arc-runners logs -l app=runner
# 查看 HPA 状态(如果配置了自动扩缩容)
kubectl -n arc-runners get hpa
```
**优化建议:**
- 配置 HPA 根据队列长度扩缩容
- 使用 PVC 缓存 Docker 层
- 定期清理旧镜像
---
## 📖 相关文档
- `infra/arc/README.md` - ARC 部署和配置详解
- `docs/architecture-overview.md` - 平台架构说明
- `.github/workflows/` - 现有 workflow 示例
- GitHub Actions 官方文档:https://docs.github.com/en/actions
- ARC 项目:https://github.com/actions/actions-runner-controller
---
## 🎯 总结
**默认策略:** 使用 GitHub-hosted runner ✅
**例外场景:**
1. 需要访问内网 → ARC ✅
2. 月构建 > 20000 分钟 → 评估 ARC
3. 需要 GPU/特殊硬件 → ARC ✅
4. 合规性要求 → ARC ✅
**避免:**
- ❌ 过早优化(项目初期就自建 runner)
- ❌ 盲目跟风(看到别人用 ARC 就用)
- ❌ 忽视维护成本(只看节点成本,忽略人力)
**记住:**
> 最好的 runner 策略是**不需要维护的 runner**。
>
> 优先使用 GitHub-hosted,仅在必要时启用 ARC。
-516
View File
@@ -1,516 +0,0 @@
# 数据库复用与隔离规范
## 📋 规范目标
本规范定义了如何在共享的 infra Postgres 实例中安全地隔离多个项目的数据,确保:
1. **数据隔离** - 项目之间无法访问彼此的数据
2. **权限最小化** - 应用只能访问自己需要的数据库
3. **命名统一** - 便于管理和审计
4. **可扩展** - 支持多项目、多环境
---
## 🏷️ 命名规范
### 数据库命名规范
**格式:** `{project}_{env}`
**规则:**
- 全部小写
- 使用下划线分隔
- 项目名简短清晰(推荐 2-4 个单词)
- 环境名固定为:`dev` / `staging` / `prod`
**示例:**
```
youngth_guard_dev ✅ 正确
youngth_guard_staging ✅ 正确
youngth_guard_prod ✅ 正确
YoungthGuard_Dev ❌ 错误(大小写混合)
youngth-guard-dev ❌ 错误(使用连字符)
yg_dev ❌ 不推荐(缩写不清晰)
```
---
### 用户命名规范
**格式:** `{project}_{env}_user`
**规则:**
- 与数据库名对应,添加 `_user` 后缀
- 全部小写
- 每个数据库有独立的用户
- 不使用超级用户或 postgres 用户
**示例:**
```
youngth_guard_dev_user ✅ 正确
youngth_guard_staging_user ✅ 正确
youngth_guard_prod_user ✅ 正确
youngth_guard_user ❌ 错误(缺少环境标识)
admin ❌ 错误(不清晰)
postgres ❌ 禁止(超级用户)
```
---
### Secret 命名规范
**格式:** `postgres-{project}-{env}`
**规则:**
- 使用连字符分隔(Kubernetes Secret 命名约定)
- 创建在应用的 namespace 中
- 包含完整的连接信息
**示例:**
```
postgres-youngth-guard-dev ✅ 正确
postgres-youngth-guard-staging ✅ 正确
postgres-youngth-guard-prod ✅ 正确
youngth-guard-db ❌ 错误(缺少 postgres 前缀)
db-secret ❌ 错误(不清晰)
```
---
## 🔒 权限最小化原则
### 应用用户权限清单
**允许的权限:**
- ✅ 连接到指定数据库:`CONNECT`
- ✅ 创建 Schema:`CREATE`(在自己的数据库内)
- ✅ 在 public schema 中操作:`USAGE`, `CREATE`
- ✅ 表的所有权限:`SELECT`, `INSERT`, `UPDATE`, `DELETE`
- ✅ 序列权限:`USAGE`, `SELECT`, `UPDATE`
**禁止的权限:**
- ❌ 超级用户权限:`SUPERUSER`
- ❌ 创建角色:`CREATEROLE`
- ❌ 创建数据库:`CREATEDB`
- ❌ 复制权限:`REPLICATION`
- ❌ 访问其他数据库
### 标准权限 SQL 模板
```sql
-- 1. 创建数据库
CREATE DATABASE {project}_{env};
-- 2. 创建用户(密码应该使用强密码)
CREATE USER {project}_{env}_user WITH PASSWORD 'SecurePassword123!';
-- 3. 撤销默认权限
REVOKE ALL ON DATABASE {project}_{env} FROM PUBLIC;
-- 4. 授予连接权限
GRANT CONNECT ON DATABASE {project}_{env} TO {project}_{env}_user;
-- 5. 连接到目标数据库
\c {project}_{env}
-- 6. 授予 Schema 权限
GRANT USAGE, CREATE ON SCHEMA public TO {project}_{env}_user;
-- 7. 授予表权限(包括未来创建的表)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO {project}_{env}_user;
-- 8. 授予序列权限
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO {project}_{env}_user;
-- 9. 授予函数执行权限
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT EXECUTE ON FUNCTIONS TO {project}_{env}_user;
```
---
## 🔐 Secret 内容规范
### Secret 必须包含的字段
```yaml
apiVersion: v1
kind: Secret
metadata:
name: postgres-{project}-{env}
namespace: {app-namespace}
labels:
app: {project}
env: {env}
db-provider: infra-postgres
type: Opaque
stringData:
# 必须字段
POSTGRES_HOST: postgres-lb.infra.svc.cluster.local
POSTGRES_PORT: "5432"
POSTGRES_DB: {project}_{env}
POSTGRES_USER: {project}_{env}_user
POSTGRES_PASSWORD: {secure-password}
# 可选:完整连接字符串
DATABASE_URL: postgresql://{project}_{env}_user:{password}@postgres-lb.infra.svc.cluster.local:5432/{project}_{env}
```
### 密码要求
**强密码规则:**
- 至少 16 字符
- 包含大小写字母
- 包含数字
- 包含特殊字符
- 不包含项目名或常见词汇
- 每个环境使用不同密码
**生成密码示例:**
```bash
# 生成安全随机密码
openssl rand -base64 24
```
---
## 📝 完整示例:youngth-guard 项目
### 场景说明
项目名称:youngth-guard(留守儿童心理风险感知平台)
环境:dev, staging, prod
应用 Namespace:dev, staging, prod
---
### 示例 1:创建 dev 环境数据库
#### Step 1: 在 Postgres 中创建数据库和用户
```bash
# 连接到 Postgres
kubectl -n infra exec -it postgres-0 -- psql -U postgres
# 执行以下 SQL
```
```sql
-- 1. 创建数据库
CREATE DATABASE youngth_guard_dev;
-- 2. 创建用户(使用强密码)
CREATE USER youngth_guard_dev_user WITH PASSWORD 'YgDev2025!SecureP@ssw0rd';
-- 3. 撤销 PUBLIC 权限
REVOKE ALL ON DATABASE youngth_guard_dev FROM PUBLIC;
-- 4. 授予连接权限
GRANT CONNECT ON DATABASE youngth_guard_dev TO youngth_guard_dev_user;
-- 5. 连接到新数据库
\c youngth_guard_dev
-- 6. 授予 Schema 权限
GRANT USAGE, CREATE ON SCHEMA public TO youngth_guard_dev_user;
-- 7. 授予表权限(包括未来创建的表)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO youngth_guard_dev_user;
-- 8. 授予序列权限
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO youngth_guard_dev_user;
-- 9. 授予函数执行权限
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT EXECUTE ON FUNCTIONS TO youngth_guard_dev_user;
-- 10. 验证权限
\l youngth_guard_dev
\du youngth_guard_dev_user
```
#### Step 2: 创建 Kubernetes Secret
```bash
# 在 dev namespace 创建 Secret
kubectl create secret generic postgres-youngth-guard-dev \
--namespace=dev \
--from-literal=POSTGRES_HOST=postgres-lb.infra.svc.cluster.local \
--from-literal=POSTGRES_PORT=5432 \
--from-literal=POSTGRES_DB=youngth_guard_dev \
--from-literal=POSTGRES_USER=youngth_guard_dev_user \
--from-literal=POSTGRES_PASSWORD='YgDev2025!SecureP@ssw0rd' \
--from-literal=DATABASE_URL='postgresql://youngth_guard_dev_user:YgDev2025!SecureP@ssw0rd@postgres-lb.infra.svc.cluster.local:5432/youngth_guard_dev'
# 添加标签
kubectl label secret postgres-youngth-guard-dev \
-n dev \
app=youngth-guard \
env=dev \
db-provider=infra-postgres
```
#### Step 3: 在应用中使用 Secret
**Deployment 配置示例:**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: youngth-guard-backend
namespace: dev
spec:
template:
spec:
containers:
- name: backend
image: ghcr.io/brunogaosz/youngth-guard-backend:latest
env:
# 方式 1:单独环境变量
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PORT
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PASSWORD
# 方式 2:完整连接字符串
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: DATABASE_URL
```
#### Step 4: 验证连接
```bash
# 测试从应用 Pod 连接数据库
kubectl run -it --rm psql-test \
--image=postgres:16-alpine \
--namespace=dev \
--env="PGHOST=postgres-lb.infra.svc.cluster.local" \
--env="PGPORT=5432" \
--env="PGDATABASE=youngth_guard_dev" \
--env="PGUSER=youngth_guard_dev_user" \
--env="PGPASSWORD=YgDev2025!SecureP@ssw0rd" \
--restart=Never \
-- psql -c "SELECT current_database(), current_user;"
# 预期输出:
# current_database | current_user
# -------------------+--------------------------
# youngth_guard_dev | youngth_guard_dev_user
```
---
### 示例 2:staging 和 prod 环境
**遵循相同模式,只需修改环境标识:**
```sql
-- Staging
CREATE DATABASE youngth_guard_staging;
CREATE USER youngth_guard_staging_user WITH PASSWORD 'YgStaging2025!DifferentP@ss';
-- ... 授权步骤相同 ...
-- Production
CREATE DATABASE youngth_guard_prod;
CREATE USER youngth_guard_prod_user WITH PASSWORD 'YgProd2025!AnotherSecureP@ss';
-- ... 授权步骤相同 ...
```
```bash
# Staging Secret
kubectl create secret generic postgres-youngth-guard-staging -n staging ...
# Production Secret
kubectl create secret generic postgres-youngth-guard-prod -n prod ...
```
---
## ✅ 规范检查清单
### 创建新项目数据库前必须确认
- [ ] 数据库名称符合 `{project}_{env}` 格式
- [ ] 用户名称符合 `{project}_{env}_user` 格式
- [ ] 密码符合强密码要求(16+ 字符,包含大小写数字特殊字符)
- [ ] 已撤销 PUBLIC 权限
- [ ] 仅授予必要的权限(CONNECT, USAGE, CRUD)
- [ ] 未授予 SUPERUSER / CREATEROLE / CREATEDB 权限
- [ ] Secret 名称符合 `postgres-{project}-{env}` 格式
- [ ] Secret 创建在正确的 namespace
- [ ] Secret 包含所有必需字段
- [ ] 已验证应用可以连接
---
## 🔍 权限审计
### 定期审计命令
```sql
-- 1. 列出所有数据库和所有者
SELECT datname, pg_catalog.pg_get_userbyid(datdba) as owner
FROM pg_catalog.pg_database
WHERE datname NOT IN ('postgres', 'template0', 'template1')
ORDER BY datname;
-- 2. 列出所有非系统用户
SELECT usename, usesuper, usecreatedb, usecreaterole
FROM pg_catalog.pg_user
WHERE usename NOT IN ('postgres', 'replication')
ORDER BY usename;
-- 3. 检查危险权限(应该为空)
SELECT usename
FROM pg_catalog.pg_user
WHERE (usesuper = true OR usecreatedb = true OR usecreaterole = true)
AND usename != 'postgres';
-- 4. 查看特定用户权限
SELECT grantee, privilege_type
FROM information_schema.table_privileges
WHERE grantee = 'youngth_guard_dev_user'
AND table_schema = 'public'
LIMIT 10;
```
---
## 🚫 禁止行为
以下操作严格禁止:
1. ❌ **使用 postgres 超级用户连接应用**
- 原因:权限过大,安全风险
2. ❌ **应用用户访问其他数据库**
- 原因:破坏隔离性
3. ❌ **在代码中硬编码密码**
- 原因:安全风险,密码泄露
4. ❌ **多个项目共享同一个数据库**
- 原因:无法独立管理,迁移困难
5. ❌ **在 public schema 外创建对象**
- 原因:权限管理复杂化
6. ❌ **手动修改 Secret(绕过 GitOps)**
- 原因:配置漂移,无法追踪
---
## 📊 多项目示例矩阵
| 项目 | 环境 | 数据库名 | 用户名 | Secret 名 | Namespace |
|------|------|---------|--------|----------|-----------|
| youngth-guard | dev | youngth_guard_dev | youngth_guard_dev_user | postgres-youngth-guard-dev | dev |
| youngth-guard | staging | youngth_guard_staging | youngth_guard_staging_user | postgres-youngth-guard-staging | staging |
| youngth-guard | prod | youngth_guard_prod | youngth_guard_prod_user | postgres-youngth-guard-prod | prod |
| other-project | dev | other_project_dev | other_project_dev_user | postgres-other-project-dev | dev |
| api-gateway | prod | api_gateway_prod | api_gateway_prod_user | postgres-api-gateway-prod | prod |
---
## 🛠️ 故障排查
### 常见问题
#### 问题 1:应用无法连接数据库
**排查步骤:**
```bash
# 1. 检查 Secret 是否存在
kubectl -n dev get secret postgres-youngth-guard-dev
# 2. 验证 Secret 内容
kubectl -n dev get secret postgres-youngth-guard-dev -o yaml
# 3. 测试网络连接
kubectl run -it --rm nettest \
--image=postgres:16-alpine \
--namespace=dev \
--restart=Never \
-- pg_isready -h postgres-lb.infra.svc.cluster.local -p 5432
# 4. 测试认证
kubectl run -it --rm psql-test \
--image=postgres:16-alpine \
--namespace=dev \
--restart=Never \
-- psql -h postgres-lb.infra.svc.cluster.local -U youngth_guard_dev_user -d youngth_guard_dev -c "SELECT 1;"
```
#### 问题 2:权限不足错误
**排查步骤:**
```sql
-- 连接到数据库
\c youngth_guard_dev
-- 检查当前用户
SELECT current_user, current_database();
-- 检查 Schema 权限
SELECT schema_name, schema_owner
FROM information_schema.schemata
WHERE schema_name = 'public';
-- 检查表权限
\dp
```
**解决方案:**
```sql
-- 重新授予权限
GRANT USAGE, CREATE ON SCHEMA public TO youngth_guard_dev_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO youngth_guard_dev_user;
```
---
## 📚 参考资料
- PostgreSQL 官方文档:权限管理
https://www.postgresql.org/docs/16/user-manag.html
- Kubernetes Secrets 最佳实践
https://kubernetes.io/docs/concepts/configuration/secret/
- 本平台相关文档:
- `docs/architecture-overview.md` - 平台架构说明
- `infra/postgres/README.md` - Postgres 部署文档
- `infra/postgres/backup/README.md` - 备份恢复文档
-743
View File
@@ -1,743 +0,0 @@
# 数据库升级 & 回滚操作手册
## 📋 概述
本文档提供数据库迁移的标准操作流程,确保升级和回滚操作的安全性和可逆性。
**核心原则:**
- ✅ 所有操作都必须可逆
- ✅ 优先选择向前修复(Rollforward),而非回滚(Rollback)
- ✅ 回滚应用 ≠ 回滚数据库
- ✅ PITR 是最后手段,不是常规操作
---
## 🎯 决策树:出现问题时该怎么办
```
检测到问题
↓
是否是数据库迁移导致?
├─ 否 → 回滚应用代码(K8s Deployment)
│ 数据库保持不变
│
└─ 是 → 迁移是否已部分成功?
├─ 完全失败 → 修复迁移 SQL → 重新执行
│ 数据库状态未变
│
└─ 部分成功 → 数据是否已损坏/丢失?
├─ 否 → 编写修复迁移(Rollforward)
│ ↓
│ V{N+1}__fix_migration_vN.sql
│
└─ 是 → 触发 PITR 恢复
⚠️ 需要审批 + 数据丢失风险
```
---
## 📦 STEP 1: 升级前准备
### 1.1 Pre-Flight Checklist
```bash
# ============================================
# 升级前检查清单
# ============================================
# 1. 确认迁移文件已合并到主分支
git log --oneline -5
git diff main origin/main
# 2. 检查 Flyway 迁移文件命名
ls -la backend-fastapi/migrations/
# 预期:V1__, V2__, V3__ ... 连续且符合规范
# 3. 在本地/测试环境验证迁移
cd backend-fastapi
flyway -url=jdbc:postgresql://localhost:5432/test_db \
-user=test_user \
-password=test_pass \
-locations=filesystem:./migrations \
migrate
# 4. 检查迁移是否幂等
# 再次执行,应该显示 "No migrations to apply"
flyway migrate
# 5. 检查生产数据库状态
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT version, description, installed_on, success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;"
# 6. 确认备份最新
kubectl -n infra get cronjob
kubectl -n infra logs -l app=pgbackrest --tail=50
# 7. 查看最近的备份时间
kubectl -n infra exec postgres-0 -- \
pgbackrest --stanza=main info
# 8. 确认当前没有长时间运行的事务(可能导致锁等待)
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT pid, usename, application_name, state, query_start, query
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY query_start;"
```
**通过标准:**
- ✅ 迁移在测试环境成功执行
- ✅ 备份在最近 24 小时内完成
- ✅ 无长时间运行的查询(> 30 分钟)
- ✅ 迁移执行时间 < 5 分钟(如果 > 5 分钟,考虑维护窗口)
### 1.2 手动触发完整备份(推荐)
```bash
# 在升级前手动触发一次完整备份
kubectl -n infra create job --from=cronjob/pgbackrest-full-backup \
manual-backup-before-migration-$(date +%Y%m%d%H%M%S)
# 等待备份完成
kubectl -n infra wait --for=condition=complete --timeout=600s \
job/manual-backup-before-migration-$(date +%Y%m%d%H%M%S)
# 检查备份状态
kubectl -n infra logs job/manual-backup-before-migration-$(date +%Y%m%d%H%M%S)
```
### 1.3 通知相关人员
- [ ] 通知团队即将执行数据库迁移
- [ ] 预估迁移时间窗口
- [ ] 准备回滚预案(如果需要)
---
## 🚀 STEP 2: 执行升级
### 2.1 自动执行(通过 ArgoCD PreSync)
**标准流程:**
```bash
# 1. CI 构建新镜像并推送
# 2. CI 更新 GitOps 仓库中的镜像 tag
git pull origin main
# 3. ArgoCD 检测到变化,自动触发 Sync
# 4. PreSync Hook 执行数据库迁移
# 5. 迁移成功 → 部署新应用
# 6. 迁移失败 → 阻止部署
# 监控 ArgoCD Sync 进度
kubectl -n argocd get application youngth-guard-backend -w
# 查看 PreSync Job 日志
kubectl -n dev logs -l component=migration -f --tail=100
# 查看迁移历史
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT version, description, installed_on, success FROM flyway_schema_history ORDER BY installed_rank;"
```
### 2.2 手动执行(紧急情况/测试)
```bash
# 1. 手动创建迁移 Job(移除 PreSync Hook 注解)
kubectl -n dev apply -f - <<EOF
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-manual-$(date +%Y%m%d%H%M%S)
namespace: dev
spec:
backoffLimit: 0
template:
metadata:
labels:
app: youngth-guard-backend
component: migration
spec:
restartPolicy: Never
initContainers:
- name: wait-for-db
image: postgres:16-alpine
command:
- /bin/sh
- -c
- |
echo "Waiting for database..."
until pg_isready -h \$POSTGRES_HOST -p \$POSTGRES_PORT -U \$POSTGRES_USER; do
sleep 2
done
echo "Database ready!"
env:
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PORT
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_USER
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PASSWORD
containers:
- name: flyway
image: flyway/flyway:10-alpine
command:
- /bin/sh
- -c
- |
set -e
echo "Running migrations..."
flyway -connectRetries=3 migrate
echo "Migration completed!"
flyway info
env:
- name: FLYWAY_URL
value: "jdbc:postgresql://\$(POSTGRES_HOST):\$(POSTGRES_PORT)/\$(POSTGRES_DB)"
- name: FLYWAY_USER
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_USER
- name: FLYWAY_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PASSWORD
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_PORT
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-youngth-guard-dev
key: POSTGRES_DB
- name: FLYWAY_BASELINE_ON_MIGRATE
value: "true"
- name: FLYWAY_BASELINE_VERSION
value: "0"
- name: FLYWAY_OUT_OF_ORDER
value: "false"
- name: FLYWAY_VALIDATE_ON_MIGRATE
value: "true"
volumeMounts:
- name: migrations
mountPath: /flyway/sql
readOnly: true
volumes:
- name: migrations
configMap:
name: db-migrations
EOF
# 2. 查看 Job 日志
kubectl -n dev logs -f job/db-migration-manual-$(date +%Y%m%d%H%M%S)
# 3. 验证迁移成功
kubectl -n dev get job db-migration-manual-$(date +%Y%m%d%H%M%S)
```
### 2.3 升级后验证
```bash
# 1. 检查迁移 Job 状态
kubectl -n dev get job -l component=migration
# 预期输出:
# NAME COMPLETIONS DURATION AGE
# db-migration 1/1 15s 2m
# 2. 验证 Flyway 历史
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT version, description, installed_on, success FROM flyway_schema_history ORDER BY installed_rank;"
# 预期:所有迁移 success = true
# 3. 检查应用 Pod 状态
kubectl -n dev get pods -l app=youngth-guard-backend
# 预期:所有 Pod Running,Ready 1/1
# 4. 查看应用日志
kubectl -n dev logs -l app=youngth-guard-backend --tail=50
# 预期:无数据库连接错误
# 5. 执行冒烟测试
curl -X GET https://dev.youngth-guard.example.com/health
# 预期:{"status": "ok"}
# 6. 验证数据库表结构
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"\dt" # 列出所有表
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"\d users" # 查看 users 表结构
```
**验证通过标准:**
- ✅ 迁移 Job 成功完成(COMPLETIONS 1/1)
- ✅ Flyway 历史中所有迁移 success = true
- ✅ 应用 Pod 全部 Running
- ✅ 应用日志无错误
- ✅ 健康检查接口返回正常
---
## ⏮️ STEP 3: 回滚策略
### 3.1 场景 1:应用代码有 Bug,数据库迁移成功
**问题:** 新版本应用有功能性 Bug,但数据库迁移已成功。
**策略:回滚应用,保留数据库**
```bash
# ============================================
# 回滚应用到上一个版本
# ============================================
# 方法 1:通过 ArgoCD 回滚
kubectl -n argocd patch application youngth-guard-backend \
--type merge \
-p '{"operation": {"sync": {"revision": "previous-commit-sha"}}}'
# 方法 2:通过 Kubectl 回滚 Deployment
kubectl -n dev rollout undo deployment/youngth-guard-backend
# 方法 3:手动更新镜像 tag
cd ljwx-deploy
git revert HEAD # 撤销最新的镜像 tag 更新
git push origin main
# ArgoCD 自动检测并回滚应用
# 验证回滚
kubectl -n dev rollout status deployment/youngth-guard-backend
kubectl -n dev get pods -l app=youngth-guard-backend
```
**重要:**
- ❌ **不要回滚数据库迁移**
- ✅ 旧版本应用代码必须兼容新数据库 Schema(这就是 Expand/Contract 的意义)
- ✅ 如果旧代码不兼容,编写修复迁移(Rollforward)
### 3.2 场景 2:数据库迁移失败,数据库状态未变
**问题:** Flyway 迁移执行失败,数据库保持原状态。
**策略:修复迁移 SQL,重新执行**
```bash
# ============================================
# 场景:V5 迁移失败
# ============================================
# 1. 查看失败的迁移
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT * FROM flyway_schema_history WHERE success = false;"
# 输出示例:
# installed_rank | version | description | success
# 5 | 5 | add_email_column | false
# 2. 查看错误日志
kubectl -n dev logs -l component=migration --tail=100
# 3. 修复迁移文件
# 编辑 backend-fastapi/migrations/V5__add_email_column.sql
# 修复 SQL 语法错误
# 4. 删除失败记录(让 Flyway 重新尝试)
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"DELETE FROM flyway_schema_history WHERE version = '5' AND success = false;"
# 5. 重新执行迁移
kubectl -n dev delete job db-migration # 删除旧 Job
# ArgoCD 会自动重新创建 PreSync Job
# 或手动触发
argocd app sync youngth-guard-backend --prune
# 6. 验证迁移成功
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT * FROM flyway_schema_history WHERE version = '5';"
```
### 3.3 场景 3:数据库迁移部分成功,数据未损坏
**问题:** V5 迁移执行了一部分后失败,数据库处于中间状态但数据未丢失。
**策略:Rollforward(编写修复迁移)**
```bash
# ============================================
# 场景:V5 添加了列但未创建索引就失败了
# ============================================
# 1. 评估当前数据库状态
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"\d users"
# 发现:email 列已添加,但缺少索引
# 2. 标记 V5 为失败(如果未自动标记)
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"UPDATE flyway_schema_history SET success = false WHERE version = '5';"
# 3. 编写修复迁移
# 创建 backend-fastapi/migrations/V6__fix_v5_migration.sql
cat > backend-fastapi/migrations/V6__fix_v5_migration.sql <<'EOF'
-- ============================================
-- Migration: V6__fix_v5_migration
-- Description: 修复 V5 迁移中断导致的缺失索引
-- ============================================
-- 创建缺失的索引(幂等)
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- 验证数据完整性
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_users_email') THEN
RAISE EXCEPTION 'Index idx_users_email still missing!';
END IF;
END $$;
EOF
# 4. 删除 V5 失败记录
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"DELETE FROM flyway_schema_history WHERE version = '5';"
# 5. 提交修复并部署
git add backend-fastapi/migrations/V6__fix_v5_migration.sql
git commit -m "fix: V6 修复 V5 迁移中断"
git push origin main
# 6. ArgoCD 自动执行 V6 迁移
```
**原则:**
- ✅ **优先选择 Rollforward**(向前修复)
- ✅ 编写 V{N+1} 修复 V{N} 的问题
- ❌ **不要直接修改已执行的迁移文件**
- ❌ **不要手动修改数据库 Schema**
### 3.4 场景 4:数据损坏或丢失 → 触发 PITR
**问题:** 迁移导致数据损坏或意外删除。
**⚠️ 警告:这是最后手段,会丢失 PITR 时间点之后的数据!**
**前提条件:**
- [ ] 数据已损坏且无法通过 SQL 修复
- [ ] 业务方批准数据丢失风险
- [ ] 已评估 PITR 后需要手动补录的数据量
- [ ] 确认 PITR 目标时间点的备份存在
**执行步骤:**
```bash
# ============================================
# PITR 恢复到迁移前的时间点
# ============================================
# 1. 确定目标时间点(迁移开始前)
TARGET_TIME="2025-01-18 10:30:00"
# 2. 停止应用(防止新数据写入)
kubectl -n dev scale deployment/youngth-guard-backend --replicas=0
# 3. 停止 Postgres StatefulSet
kubectl -n infra scale statefulset postgres --replicas=0
# 4. 创建 PITR 恢复 Job
cat > /tmp/pitr-restore.yaml <<EOF
apiVersion: batch/v1
kind: Job
metadata:
name: postgres-pitr-restore
namespace: infra
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: pgbackrest
image: pgbackrest/pgbackrest:latest
command:
- /bin/bash
- -c
- |
set -e
echo "Starting PITR restore to ${TARGET_TIME}"
# 恢复到指定时间点
pgbackrest --stanza=main \
--type=time \
--target="${TARGET_TIME}" \
--delta \
restore
echo "PITR restore completed!"
env:
- name: TARGET_TIME
value: "${TARGET_TIME}"
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
- name: backup
mountPath: /backup
- name: pgbackrest-config
mountPath: /etc/pgbackrest
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-data-postgres-0
- name: backup
persistentVolumeClaim:
claimName: pgbackrest-backup
- name: pgbackrest-config
configMap:
name: pgbackrest-config
EOF
kubectl apply -f /tmp/pitr-restore.yaml
# 5. 等待恢复完成
kubectl -n infra wait --for=condition=complete --timeout=1800s job/postgres-pitr-restore
# 6. 查看恢复日志
kubectl -n infra logs job/postgres-pitr-restore
# 7. 启动 Postgres
kubectl -n infra scale statefulset postgres --replicas=1
# 8. 等待 Postgres 就绪
kubectl -n infra wait --for=condition=ready pod/postgres-0 --timeout=300s
# 9. 验证数据库状态
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT version, description, installed_on FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 5;"
# 10. 验证业务数据
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT count(*) FROM users;"
# 11. 清理失败的迁移记录
# 根据 Flyway 历史决定是否需要删除失败的迁移记录
# 12. 重新启动应用(使用旧版本镜像)
kubectl -n dev scale deployment/youngth-guard-backend --replicas=3
# 13. 验证应用功能
kubectl -n dev get pods -l app=youngth-guard-backend
curl https://dev.youngth-guard.example.com/health
```
**PITR 后操作:**
- [ ] 评估丢失的数据(PITR 时间点之后的写入)
- [ ] 手动补录关键数据
- [ ] 修复导致问题的迁移文件
- [ ] 在测试环境重新验证修复后的迁移
- [ ] 部署修复后的迁移
---
## 🚫 禁止行为清单
### 迁移期间绝对禁止的操作
1. **❌ 直接在生产数据库执行 DDL**
```sql
-- 禁止!
psql -U postgres -d youngth_guard_prod
ALTER TABLE users ADD COLUMN ...;
```
**正确做法:** 所有 Schema 变更必须通过 Flyway 迁移文件
2. **❌ 修改已执行的迁移文件**
```bash
# 禁止!
vim backend-fastapi/migrations/V3__add_column.sql # V3 已执行
git commit -m "fix V3"
```
**正确做法:** 创建新的 V4 修复 V3 的问题
3. **❌ 手动修改 `flyway_schema_history` 表(非紧急情况)**
```sql
-- 禁止!(除非明确知道后果)
UPDATE flyway_schema_history SET success = true WHERE version = '5';
```
**正确做法:** 删除失败记录,修复 SQL,重新执行
4. **❌ 在迁移中执行大规模数据修改(无批量处理)**
```sql
-- 禁止!会锁表很久
UPDATE users SET status = 'active'; -- 100万行
```
**正确做法:** 使用批量处理(参考 `docs/flyway-guide/README.md`)
5. **❌ 在迁移中删除表/列(无 Expand/Contract)**
```sql
-- 禁止!
DROP TABLE old_table;
ALTER TABLE users DROP COLUMN old_field;
```
**正确做法:** 先重命名为 `_deprecated`,等待几个版本后再删除
6. **❌ 回滚数据库迁移(非 PITR 场景)**
```bash
# 禁止!
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"ALTER TABLE users DROP COLUMN email;" # 试图"撤销" V5
```
**正确做法:** 使用 Expand/Contract 模式,或编写修复迁移
7. **❌ 在高峰期执行耗时迁移**
```bash
# 禁止!
# 14:00 (业务高峰) 执行需要 10 分钟的迁移
```
**正确做法:** 在凌晨低峰期执行,或使用在线 Schema 变更工具
8. **❌ 未备份就执行高风险迁移**
```bash
# 禁止!
# 没有检查备份就执行 DROP/TRUNCATE
```
**正确做法:** 先验证最近备份,必要时手动触发完整备份
9. **❌ 跳过测试环境直接在生产执行**
```bash
# 禁止!
# 新迁移文件直接部署到生产
```
**正确做法:** dev → staging → prod 逐级验证
10. **❌ 在迁移失败时继续部署应用**
```bash
# 禁止!
kubectl -n dev set image deployment/youngth-guard-backend \
backend=ghcr.io/brunogaosz/youngth-guard-backend:v2 # 迁移失败时强制部署
```
**正确做法:** 修复迁移后再部署,或回滚应用到旧版本
---
## 📋 快速参考卡
### 常见场景速查表
| 场景 | 策略 | 操作 |
|------|------|------|
| 应用 Bug,DB 正常 | 回滚应用 | `kubectl rollout undo deployment/xxx` |
| 迁移完全失败 | 修复 SQL 重试 | 删除失败记录 → 修复文件 → 重新执行 |
| 迁移部分成功 | Rollforward | 编写 V{N+1} 修复迁移 |
| 数据损坏 | PITR | 恢复到迁移前时间点 |
| 迁移太慢 | 批量处理 | 参考 Flyway 指南中的批量更新示例 |
| 需要回滚 Schema | Expand/Contract | 先部署代码兼容 → 再删除旧 Schema |
### 故障排查命令速查
```bash
# 查看迁移历史
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT * FROM flyway_schema_history ORDER BY installed_rank;"
# 查看迁移 Job 日志
kubectl -n dev logs -l component=migration --tail=100
# 检查应用状态
kubectl -n dev get pods -l app=youngth-guard-backend
kubectl -n dev logs -l app=youngth-guard-backend --tail=50
# 查看最近备份
kubectl -n infra exec postgres-0 -- pgbackrest --stanza=main info
# 检查长时间运行的查询
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c \
"SELECT pid, usename, query_start, state, query FROM pg_stat_activity WHERE state != 'idle';"
# 检查表结构
kubectl -n infra exec postgres-0 -- \
psql -U postgres -d youngth_guard_dev -c "\d table_name"
# 回滚应用
kubectl -n dev rollout undo deployment/youngth-guard-backend
# 手动触发备份
kubectl -n infra create job --from=cronjob/pgbackrest-full-backup manual-backup-$(date +%Y%m%d%H%M%S)
```
---
## 📞 升级失败联系流程
1. **立即操作:**
- 停止继续部署(如果尚未完成)
- 截图错误日志
- 记录时间点
2. **评估影响:**
- 业务功能是否受影响?
- 数据是否损坏?
- 用户是否受影响?
3. **根据决策树选择策略:**
- 应用 Bug → 回滚应用
- 迁移失败 → 修复重试
- 数据损坏 → 联系 DBA + PITR
4. **事后复盘:**
- 记录故障原因
- 更新测试用例
- 改进迁移流程
---
## 📖 相关文档
- `docs/flyway-guide/README.md` - Flyway 规范和 Expand/Contract 模式
- `docs/argocd-migration/README.md` - PreSync Hook 工作流程
- `infra/postgres/backup/README.md` - 备份和 PITR 详细步骤
- `docs/architecture-overview.md` - 平台架构说明
-548
View File
@@ -1,548 +0,0 @@
# Flyway 数据库版本化规范
## 📋 概述
Flyway 是数据库迁移管理工具,用于:
- 版本化数据库 Schema
- 自动化数据库升级
- 跟踪迁移历史
- 支持回滚(通过向后兼容设计)
**核心原则:**
- ✅ 所有 Schema 变更必须通过 Flyway
- ✅ 迁移必须向后兼容(支持新旧代码共存)
- ✅ 禁止直接修改数据库
- ✅ 每个迁移都有明确的版本号
---
## 📁 标准目录结构
### 项目中的位置
```
youngth-guard/
└── backend-fastapi/
└── migrations/ # Flyway 迁移目录
├── V1__initial_schema.sql
├── V2__add_users_table.sql
├── V3__add_email_column.sql
├── V4__add_email_index.sql
└── V5__remove_deprecated_column.sql
```
### 多模块项目
```
project-root/
├── service-a/
│ └── migrations/
│ ├── V1__service_a_init.sql
│ └── V2__service_a_update.sql
└── service-b/
└── migrations/
├── V1__service_b_init.sql
└── V2__service_b_update.sql
```
**注意:** 每个服务使用独立的数据库,因此版本号独立管理。
---
## 🏷️ 命名规范
### 文件命名格式
```
V{version}__{description}.sql
```
**规则:**
1. **V(大写)** - 固定前缀,表示 Versioned migration
2. **{version}** - 版本号,格式:
- 单数字:`V1`, `V2`, `V3` ...
- 点分版本:`V1.1`, `V1.2`, `V2.1` ...
- 时间戳:`V20251218101500` (不推荐,难以理解)
3. **__(双下划线)** - 分隔符
4. **{description}** - 描述,使用下划线分隔单词
5. **.sql** - 文件扩展名
### 正确示例
```
✅ V1__initial_schema.sql
✅ V2__add_users_table.sql
✅ V3__add_email_column.sql
✅ V4__migrate_old_data.sql
✅ V5__add_index_on_email.sql
✅ V6__remove_deprecated_field.sql
✅ V1.1__hotfix_user_constraint.sql
```
### 错误示例
```
❌ v1__initial.sql # 小写 v
❌ V1_initial_schema.sql # 单下划线
❌ 1__initial_schema.sql # 缺少 V
❌ V1-initial-schema.sql # 使用连字符
❌ V1__Initial Schema.sql # 包含空格
❌ V1__添加用户表.sql # 非 ASCII 字符
```
---
## 📝 迁移内容规范
### 每个迁移文件应该
- ✅ 完成一个独立的功能
- ✅ 包含清晰的注释
- ✅ 幂等(使用 `IF NOT EXISTS` / `IF EXISTS`)
- ✅ 可逆(通过后续迁移回滚)
- ✅ 测试过(在本地和测试环境)
### 迁移文件模板
```sql
-- ============================================
-- Migration: V{version}__{description}
-- Author: {your-name}
-- Date: {date}
-- Description:
-- {详细描述这个迁移做什么}
-- {为什么需要这个变更}
-- ============================================
-- 开始事务(可选,Flyway 默认会包裹在事务中)
BEGIN;
-- 你的 SQL 语句
-- ...
-- 提交
COMMIT;
```
---
## 🔄 Expand/Contract 模式
**Expand/Contract** 是实现零停机升级的关键模式。
### 模式说明
1. **Expand(扩展)** - 添加新结构,保留旧结构
2. **Deploy(部署)** - 部署新代码,同时支持新旧结构
3. **Contract(收缩)** - 删除旧结构
### 示例:添加新列并重命名
**场景:** 将 `users.name` 列重命名为 `users.full_name`
#### ❌ 错误做法(会导致停机)
```sql
-- V2__rename_column.sql
ALTER TABLE users RENAME COLUMN name TO full_name;
```
**问题:** 旧代码仍在使用 `name` 列,会立即报错。
#### ✅ 正确做法(Expand/Contract)
**Step 1: Expand - 添加新列**
```sql
-- V2__add_full_name_column.sql
-- Expand: 添加新列,保留旧列
ALTER TABLE users ADD COLUMN IF NOT EXISTS full_name VARCHAR(255);
-- 复制数据到新列
UPDATE users SET full_name = name WHERE full_name IS NULL;
-- 创建触发器保持同步(可选)
CREATE OR REPLACE FUNCTION sync_user_name()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.name IS DISTINCT FROM OLD.name THEN
NEW.full_name := NEW.name;
END IF;
IF NEW.full_name IS DISTINCT FROM OLD.full_name THEN
NEW.name := NEW.full_name;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER sync_user_name_trigger
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION sync_user_name();
```
**Step 2: Deploy - 部署新代码**
应用代码更新为使用 `full_name`,但数据库同时支持两列。
**Step 3: Contract - 删除旧列**
```sql
-- V3__remove_name_column.sql
-- Contract: 删除旧列(确认新代码已稳定运行)
-- 删除触发器
DROP TRIGGER IF EXISTS sync_user_name_trigger ON users;
DROP FUNCTION IF EXISTS sync_user_name();
-- 删除旧列
ALTER TABLE users DROP COLUMN IF EXISTS name;
```
---
## 🚫 禁止使用的 SQL
以下 SQL 操作**禁止**直接在 Flyway 迁移中使用:
### 1. 破坏性操作(生产环境)
```sql
-- ❌ 禁止:直接删除表
DROP TABLE users;
-- ✅ 推荐:重命名表为 _deprecated,稍后删除
ALTER TABLE users RENAME TO users_deprecated_20251218;
-- 在后续迁移中删除:
-- DROP TABLE IF EXISTS users_deprecated_20251218;
```
### 2. 直接删除列(无过渡期)
```sql
-- ❌ 禁止:直接删除列
ALTER TABLE users DROP COLUMN email;
-- ✅ 推荐:使用 Expand/Contract
-- V2: 标记为废弃(添加注释)
-- V3: 应用不再使用
-- V4: 删除列
```
### 3. 修改列类型(无向后兼容)
```sql
-- ❌ 禁止:直接修改类型
ALTER TABLE users ALTER COLUMN age TYPE INTEGER;
-- ✅ 推荐:
-- V2: 添加新列 age_int
-- V3: 迁移数据
-- V4: 应用使用新列
-- V5: 删除旧列
```
### 4. 添加 NOT NULL 约束(无默认值)
```sql
-- ❌ 禁止:直接添加 NOT NULL
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
-- ✅ 推荐:
-- V2: 添加列(允许 NULL)
-- V3: 填充数据
-- V4: 添加 NOT NULL 约束
```
### 5. 删除索引(无影响评估)
```sql
-- ❌ 禁止:直接删除索引
DROP INDEX idx_users_email;
-- ✅ 推荐:
-- 1. 评估性能影响
-- 2. 在低峰期执行
-- 3. 监控查询性能
-- 4. 准备回滚计划
```
### 6. 大规模数据更新
```sql
-- ❌ 禁止:一次性更新所有数据
UPDATE users SET status = 'active';
-- ✅ 推荐:批量更新
DO $$
DECLARE
batch_size INT := 1000;
total_rows INT;
BEGIN
LOOP
UPDATE users
SET status = 'active'
WHERE id IN (
SELECT id FROM users
WHERE status IS NULL
LIMIT batch_size
);
GET DIAGNOSTICS total_rows = ROW_COUNT;
EXIT WHEN total_rows = 0;
PERFORM pg_sleep(0.1); -- 避免长时间锁表
END LOOP;
END $$;
```
---
## 📚 迁移示例
### 示例 1:初始化 Schema
```sql
-- V1__initial_schema.sql
-- ============================================
-- 初始化数据库 Schema
-- ============================================
-- 用户表
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
-- 评估记录表
CREATE TABLE IF NOT EXISTS assessments (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
score INTEGER NOT NULL CHECK (score >= 0 AND score <= 100),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_assessments_user_id ON assessments(user_id);
CREATE INDEX IF NOT EXISTS idx_assessments_status ON assessments(status);
CREATE INDEX IF NOT EXISTS idx_assessments_created_at ON assessments(created_at);
-- 审计日志表
CREATE TABLE IF NOT EXISTS audit_logs (
id BIGSERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(50) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id INTEGER,
changes JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
COMMENT ON TABLE users IS '用户表';
COMMENT ON TABLE assessments IS '心理评估记录表';
COMMENT ON TABLE audit_logs IS '审计日志表';
```
### 示例 2:添加新列(Expand)
```sql
-- V2__add_phone_column.sql
-- ============================================
-- 添加手机号列
-- 使用 Expand 模式,保证向后兼容
-- ============================================
-- 添加列(允许 NULL)
ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR(20);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone) WHERE phone IS NOT NULL;
-- 添加注释
COMMENT ON COLUMN users.phone IS '用户手机号(可选)';
```
### 示例 3:数据迁移(Expand/Contract - Part 1)
```sql
-- V3__add_profile_data_column.sql
-- ============================================
-- 添加 profile_data JSON 列
-- 准备将结构化数据迁移到 JSON
-- ============================================
-- Expand: 添加新列
ALTER TABLE users ADD COLUMN IF NOT EXISTS profile_data JSONB DEFAULT '{}'::jsonb;
-- 迁移现有数据
UPDATE users
SET profile_data = jsonb_build_object(
'email', email,
'phone', COALESCE(phone, ''),
'created_at', created_at::text
)
WHERE profile_data = '{}'::jsonb;
-- 创建 GIN 索引支持 JSON 查询
CREATE INDEX IF NOT EXISTS idx_users_profile_data ON users USING GIN (profile_data);
-- 注意:此时 email 和 phone 列仍然存在
-- 应用代码需要逐步迁移到使用 profile_data
```
### 示例 4:删除旧列(Expand/Contract - Part 2)
```sql
-- V4__remove_old_profile_columns.sql
-- ============================================
-- 删除已迁移到 JSON 的旧列
-- 前提:应用代码已完全切换到 profile_data
-- ============================================
-- Contract: 删除旧列
-- 注意:执行前确认应用已不再使用这些列
-- 先删除依赖的索引
DROP INDEX IF EXISTS idx_users_email;
DROP INDEX IF EXISTS idx_users_phone;
-- 删除列
-- ALTER TABLE users DROP COLUMN IF EXISTS email; -- 保留 email 作为主要联系方式
ALTER TABLE users DROP COLUMN IF EXISTS phone;
COMMENT ON COLUMN users.profile_data IS '用户扩展信息(JSON 格式)';
```
### 示例 5:添加约束
```sql
-- V5__add_user_status_constraint.sql
-- ============================================
-- 添加用户状态列和约束
-- ============================================
-- 添加状态列
ALTER TABLE users ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'active';
-- 填充现有数据
UPDATE users SET status = 'active' WHERE status IS NULL;
-- 添加 CHECK 约束
ALTER TABLE users ADD CONSTRAINT check_user_status
CHECK (status IN ('active', 'inactive', 'suspended', 'deleted'));
-- 添加索引
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
COMMENT ON COLUMN users.status IS '用户状态:active|inactive|suspended|deleted';
```
---
## ✅ 最佳实践 Checklist
### 编写迁移前
- [ ] 确认变更的业务需求
- [ ] 设计向后兼容的方案
- [ ] 评估对现有代码的影响
- [ ] 考虑回滚策略
- [ ] 评估性能影响(特别是大表操作)
### 编写迁移时
- [ ] 使用正确的命名格式
- [ ] 添加清晰的注释
- [ ] 使用 `IF EXISTS` / `IF NOT EXISTS`
- [ ] 遵循 Expand/Contract 模式
- [ ] 避免大规模数据更新(或使用批量处理)
- [ ] 添加适当的索引
### 测试迁移
- [ ] 在本地数据库测试
- [ ] 在测试环境验证
- [ ] 验证旧代码仍能运行
- [ ] 验证新代码正常工作
- [ ] 检查性能影响
- [ ] 准备回滚迁移
### 部署迁移
- [ ] 在低峰期执行
- [ ] 备份数据库
- [ ] 监控执行进度
- [ ] 验证应用功能
- [ ] 准备紧急回滚
---
## 🔍 故障排查
### 问题 1:迁移失败
```bash
# 查看 Flyway 历史
SELECT * FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 10;
# 查看失败的迁移
SELECT * FROM flyway_schema_history WHERE success = false;
```
**解决方案:**
1. 检查错误日志
2. 手动修复数据库状态
3. 标记迁移为已解决:
```sql
UPDATE flyway_schema_history
SET success = true
WHERE version = 'X' AND success = false;
```
### 问题 2:需要跳过某个迁移
**不推荐!** 但紧急情况下:
```sql
-- 手动标记迁移为已执行
INSERT INTO flyway_schema_history (
installed_rank, version, description, type, script, checksum,
installed_by, installed_on, execution_time, success
) VALUES (
(SELECT COALESCE(MAX(installed_rank), 0) + 1 FROM flyway_schema_history),
'5', 'skip_this_migration', 'SQL', 'V5__skip_this.sql', NULL,
'admin', NOW(), 0, true
);
```
### 问题 3:迁移顺序错误
**原因:** 版本号不连续或重复
**解决:**
- 使用点分版本:`V2.1`, `V2.2`
- 或重命名文件(未执行的迁移)
---
## 📖 相关文档
- Flyway 官方文档:https://flywaydb.org/documentation/
- 本平台相关:
- `docs/architecture-overview.md` - 平台架构
- `docs/db-isolation-spec.md` - 数据库隔离规范
- `infra/postgres/README.md` - Postgres 部署
-55
View File
@@ -1,55 +0,0 @@
-- ============================================
-- Migration: V{VERSION}__{DESCRIPTION}
-- Author: {YOUR_NAME}
-- Date: {YYYY-MM-DD}
-- Jira: {TICKET_ID} (optional)
-- ============================================
-- Description:
-- {详细描述这个迁移做什么}
-- {为什么需要这个变更}
-- {对现有系统的影响}
--
-- Rollback Strategy:
-- {如何回滚这个变更}
-- {是否需要后续迁移来清理}
--
-- Performance Impact:
-- {预估执行时间}
-- {是否会锁表}
-- {对应用的影响}
-- ============================================
-- 可选:开始事务
-- BEGIN;
-- ============================================
-- Step 1: {步骤描述}
-- ============================================
-- 你的 SQL 语句
-- ...
-- ============================================
-- Step 2: {步骤描述}
-- ============================================
-- 你的 SQL 语句
-- ...
-- ============================================
-- Verification (可选)
-- ============================================
-- 验证脚本(在迁移后检查)
-- SELECT count(*) FROM your_table;
-- SELECT * FROM your_table LIMIT 1;
-- 可选:提交事务
-- COMMIT;
-- ============================================
-- Notes:
-- {额外说明}
-- {已知问题}
-- {后续计划}
-- ============================================
File diff suppressed because it is too large Load Diff
-29
View File
@@ -1,29 +0,0 @@
# GitHub Pages Setup for Evidence Feed
## Enable Pages
1. Open repository `Settings`.
2. Go to `Pages`.
3. Under `Build and deployment`, choose:
- Source: `Deploy from a branch`
- Branch: `gh-pages`
- Folder: `/ (root)`
4. Save.
## Evidence Feed URL
After first successful publish, the URLs are typically:
- Dashboard: `https://<owner>.github.io/<repo>/`
- Feed JSON: `https://<owner>.github.io/<repo>/evidence/index.json`
Example for `BrunoGaoSZ/ljwx-deploy`:
- `https://brunogaosz.github.io/ljwx-deploy/`
- `https://brunogaosz.github.io/ljwx-deploy/evidence/index.json`
## Access Control Notes
- Public repo: feed is public.
- Private/internal repo: GitHub Pages visibility depends on plan/policy.
- Keep records free of secrets/internal-only endpoints.
-4
View File
@@ -1,4 +0,0 @@
service: <svc>
image: harbor.omniverseai.net/app/<svc>@sha256:__DIGEST__
replicas: 1
resourcesProfile: mvp-small
-55
View File
@@ -1,55 +0,0 @@
# Evidence Model
`evidence/records/*.yaml` is the source-of-truth evidence feed for promotion and verification.
## Required fields
- `evidenceId`
- `service`
- `env`
- `source.repo`
- `source.commit`
- `image.harbor`
- `deploy.deployRepoCommit`
## Optional fields
- `source.workflowRun`
- `image.ghcr`
- `deploy.argocdApp`
- `deploy.syncedAt`
- `tests.smoke`
- `tests.e2e`
- `approvals.*`
## Lifecycle
1. Queue promoter writes/updates evidence records in `evidence/records/`.
2. Validation script checks structure and required fields.
3. Collection script builds `evidence/index.json` and `evidence/summary/latest.md`.
4. Nightly workflow publishes dashboard and feed to `gh-pages` branch root.
## Example record (YAML)
```yaml
evidenceId: 2026-03-02-frontend-sha1234
service: frontend
env: dev
source:
repo: ghcr.io/example/frontend
commit: sha1234
workflowRun: https://github.com/example/frontend/actions/runs/123
image:
ghcr: ghcr.io/example/frontend@sha256:1111111111111111111111111111111111111111111111111111111111111111
harbor: harbor.omniverseai.net/app/frontend@sha256:2222222222222222222222222222222222222222222222222222222222222222
deploy:
deployRepoCommit: abcdef1234567890
argocdApp: frontend-dev
syncedAt: 2026-03-02T02:10:00Z
tests:
smoke:
status: pass
url: https://github.com/example/deploy/actions/runs/456
approvals:
releasePr: https://github.com/example/deploy/pull/789
```
+27
View File
@@ -0,0 +1,27 @@
{
"generatedAt": "2026-04-03T17:39:46Z",
"counts": {
"pending": 0,
"promoted": 0,
"failed": 0,
"superseded": 0
},
"pending": {
"oldestCreatedAt": "",
"maxAgeSeconds": 0,
"staleThresholdSeconds": 1800,
"staleIds": [],
"byServiceEnv": {}
},
"failed": {
"last24h": 0,
"byServiceEnv": {}
},
"retries": {
"pendingMaxAttempts": 0,
"failedMaxAttempts": 0
},
"ratios": {
"supersededVsPromoted": 0.0
}
}
View File
-88
View File
@@ -1,88 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/evidence.schema.json",
"title": "Bid-MVP Evidence Record",
"type": "object",
"additionalProperties": true,
"required": [
"evidenceId",
"service",
"env",
"source",
"image",
"deploy"
],
"properties": {
"evidenceId": { "type": "string", "minLength": 3 },
"service": { "type": "string", "minLength": 1 },
"env": { "type": "string", "minLength": 1 },
"source": {
"type": "object",
"required": ["repo", "commit"],
"additionalProperties": true,
"properties": {
"repo": { "type": "string", "minLength": 3 },
"commit": { "type": "string", "minLength": 1 },
"workflowRun": { "type": ["string", "null"] }
}
},
"image": {
"type": "object",
"required": ["harbor"],
"additionalProperties": true,
"properties": {
"ghcr": { "type": ["string", "null"] },
"harbor": { "type": "string", "minLength": 3 }
}
},
"deploy": {
"type": "object",
"required": ["deployRepoCommit"],
"additionalProperties": true,
"properties": {
"deployRepoCommit": { "type": "string", "minLength": 1 },
"argocdApp": { "type": ["string", "null"] },
"syncedAt": { "type": ["string", "null"] }
}
},
"promotedAt": { "type": ["string", "null"] },
"tests": {
"type": "object",
"additionalProperties": true,
"properties": {
"smoke": {
"type": "object",
"additionalProperties": true,
"properties": {
"status": { "type": ["string", "null"] },
"details": { "type": ["string", "null"] },
"url": { "type": ["string", "null"] }
}
},
"e2e": {
"type": "object",
"additionalProperties": true,
"properties": {
"status": { "type": ["string", "null"] },
"url": { "type": ["string", "null"] }
}
}
}
},
"approvals": {
"type": "object",
"additionalProperties": true,
"properties": {
"specPr": { "type": ["string", "null"] },
"archPr": { "type": ["string", "null"] },
"demoPr": { "type": ["string", "null"] },
"uatPr": { "type": ["string", "null"] },
"releasePr": { "type": ["string", "null"] },
"prs": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
# Latest Evidence Summary
| service | env | harbor digest | syncedAt | smoke | links |
| service | env | deployed digest | syncedAt | smoke | links |
| --- | --- | --- | --- | --- | --- |
+1 -1
View File
@@ -29,7 +29,7 @@
<th>evidenceId</th>
<th>service</th>
<th>env</th>
<th>harbor digest</th>
<th>deployed digest</th>
<th>syncedAt</th>
<th>smoke</th>
<th>links</th>
@@ -1,16 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: arc-github-token
namespace: arc-system
type: Opaque
stringData:
# Option A: Personal Access Token
github_token: "ghp_xxx_replace_me"
# Option B: GitHub App (use these keys instead of github_token)
# github_app_id: "123456"
# github_app_installation_id: "789012"
# github_app_private_key: |
# -----BEGIN RSA PRIVATE KEY-----
# ...
# -----END RSA PRIVATE KEY-----
-66
View File
@@ -1,66 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: arc-controller
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
project: default
source:
repoURL: https://actions-runner-controller.github.io/actions-runner-controller
chart: actions-runner-controller
targetRevision: 0.23.3
helm:
values: |
# Secret naming strategy:
# - ARC controller always reads credentials from secret `arc-github-token`.
# - `arc-github-token` must contain one of:
# - github_token (PAT), or
# - github_app_id + github_app_installation_id + github_app_private_key.
# - Do not rename this secret without updating this Application.
authSecret:
enabled: true
create: false
name: arc-github-token
destination:
server: https://kubernetes.default.svc
namespace: arc-system
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
ignoreDifferences:
- group: apiextensions.k8s.io
kind: CustomResourceDefinition
name: horizontalrunnerautoscalers.actions.summerwind.dev
jsonPointers:
- /spec
- /metadata/annotations
- group: apiextensions.k8s.io
kind: CustomResourceDefinition
name: runnerdeployments.actions.summerwind.dev
jsonPointers:
- /spec
- /metadata/annotations
- group: apiextensions.k8s.io
kind: CustomResourceDefinition
name: runnerreplicasets.actions.summerwind.dev
jsonPointers:
- /spec
- /metadata/annotations
- group: apiextensions.k8s.io
kind: CustomResourceDefinition
name: runners.actions.summerwind.dev
jsonPointers:
- /spec
- /metadata/annotations
- group: apiextensions.k8s.io
kind: CustomResourceDefinition
name: runnersets.actions.summerwind.dev
jsonPointers:
- /spec
- /metadata/annotations
-3
View File
@@ -1,3 +0,0 @@
resources:
- controller.yaml
- runner-set.yaml
-51
View File
@@ -1,51 +0,0 @@
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: org-runner
namespace: arc-system
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
replicas: 1
template:
spec:
organization: BrunoGaoSZ
labels:
- arc
- docker
- linux
- self-hosted
dockerdWithinRunnerContainer: true
image: summerwind/actions-runner-dind:ubuntu-22.04
env:
- name: DOCKER_HOST
value: unix:///var/run/docker.sock
dockerEnv:
- name: DOCKER_TLS_CERTDIR
value: ""
resources:
limits:
cpu: "2"
memory: "4Gi"
requests:
cpu: "500m"
memory: "1Gi"
---
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
name: org-runner-autoscaler
namespace: arc-system
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
scaleTargetRef:
name: org-runner
minReplicas: 1
maxReplicas: 5
metrics:
- type: PercentageRunnersBusy
scaleUpThreshold: "0.75"
scaleDownThreshold: "0.25"
scaleUpFactor: "2"
scaleDownFactor: "0.5"

Some files were not shown because too many files have changed in this diff Show More