新增 k8s 部署文件
CI — Docker Build & Push / Build & Push Image (push) Failing after 14m47s

This commit is contained in:
2026-04-24 11:30:08 +09:00
parent f3bd3f68a5
commit bd1acebcf3
9 changed files with 491 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# ═══════════════════════════════════════════════════════════════
# Namespace — 所有资源统一放在 resource-library 命名空间
# ═══════════════════════════════════════════════════════════════
apiVersion: v1
kind: Namespace
metadata:
name: resource-library
labels:
app.kubernetes.io/name: resource-library
app.kubernetes.io/part-of: resource-library
+27
View File
@@ -0,0 +1,27 @@
# ═══════════════════════════════════════════════════════════════
# Secret — 敏感凭证
# ───────────────────────────────────────────────────────────────
# 部署前请务必修改下面所有占位密码!
# 推荐用 kubectl 在集群内生成(避免把明文提交到 git):
# kubectl -n resource-library create secret generic resource-library-secret \
# --from-literal=MYSQL_ROOT_PASSWORD='...' \
# --from-literal=MYSQL_PASSWORD='...' \
# --from-literal=SECRET_KEY="$(python -c 'import secrets;print(secrets.token_hex(32))')" \
# --from-literal=ADMIN_PASSWORD='...'
# 或使用 sealed-secrets / external-secrets 管理。
# ═══════════════════════════════════════════════════════════════
apiVersion: v1
kind: Secret
metadata:
name: resource-library-secret
namespace: resource-library
type: Opaque
stringData:
# MySQL root 密码(仅 init 容器和 DBA 使用)
MYSQL_ROOT_PASSWORD: "CHANGE_ME_root_password"
# 应用使用的业务账号密码
MYSQL_PASSWORD: "CHANGE_ME_app_password"
# Flask SECRET_KEY(用于 session/签名),至少 32 字节随机
SECRET_KEY: "CHANGE_ME_please_run_python_secrets_token_hex_32"
# 首次启动自动创建的管理员密码
ADMIN_PASSWORD: "CHANGE_ME_Admin@123456"
+31
View File
@@ -0,0 +1,31 @@
# ═══════════════════════════════════════════════════════════════
# ConfigMap — 非敏感环境变量
# ═══════════════════════════════════════════════════════════════
apiVersion: v1
kind: ConfigMap
metadata:
name: resource-library-config
namespace: resource-library
data:
# MySQL
MYSQL_DATABASE: "resource_library"
MYSQL_USER: "resource_library"
# Flask
FLASK_ENV: "production"
LOG_LEVEL: "info"
# Gunicorn
GUNICORN_WORKERS: "4"
GUNICORN_TIMEOUT: "120"
GUNICORN_BIND: "0.0.0.0:5000"
# 上传限制
MAX_UPLOAD_SIZE_MB: "500"
# 管理员账号(首次启动时由 init_db.py 创建)
ADMIN_USERNAME: "admin"
ADMIN_EMAIL: "admin@example.com"
# 数据库等待超时(秒)
DB_WAIT_SECONDS: "120"
+119
View File
@@ -0,0 +1,119 @@
# ═══════════════════════════════════════════════════════════════
# MySQL 8.0 — StatefulSet + Headless Service
# ───────────────────────────────────────────────────────────────
# - 使用 StatefulSet 保证 Pod 名和存储稳定(mysql-0
# - Headless Service 提供稳定 DNSmysql.resource-library.svc.cluster.local
# - 数据持久化到 PVC(20Gi,按需修改)
# ═══════════════════════════════════════════════════════════════
---
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: resource-library
labels:
app.kubernetes.io/name: mysql
app.kubernetes.io/part-of: resource-library
spec:
clusterIP: None # Headless
selector:
app.kubernetes.io/name: mysql
ports:
- name: mysql
port: 3306
targetPort: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: resource-library
labels:
app.kubernetes.io/name: mysql
app.kubernetes.io/part-of: resource-library
spec:
serviceName: mysql
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: mysql
template:
metadata:
labels:
app.kubernetes.io/name: mysql
app.kubernetes.io/part-of: resource-library
spec:
securityContext:
fsGroup: 999 # mysql 用户组
containers:
- name: mysql
image: mysql:8.0
imagePullPolicy: IfNotPresent
args:
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_unicode_ci"
- "--default-authentication-plugin=mysql_native_password"
- "--innodb-buffer-pool-size=256M"
ports:
- name: mysql
containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: resource-library-secret
key: MYSQL_ROOT_PASSWORD
- name: MYSQL_DATABASE
valueFrom:
configMapKeyRef:
name: resource-library-config
key: MYSQL_DATABASE
- name: MYSQL_USER
valueFrom:
configMapKeyRef:
name: resource-library-config
key: MYSQL_USER
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: resource-library-secret
key: MYSQL_PASSWORD
volumeMounts:
- name: data
mountPath: /var/lib/mysql
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
livenessProbe:
exec:
command:
- /bin/sh
- -c
- mysqladmin ping -h 127.0.0.1 -uroot -p"${MYSQL_ROOT_PASSWORD}"
initialDelaySeconds: 60
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 5
readinessProbe:
exec:
command:
- /bin/sh
- -c
- mysqladmin ping -h 127.0.0.1 -uroot -p"${MYSQL_ROOT_PASSWORD}"
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
# storageClassName: "standard" # 取消注释并改为你集群实际的 StorageClass
resources:
requests:
storage: 20Gi
+23
View File
@@ -0,0 +1,23 @@
# ═══════════════════════════════════════════════════════════════
# PVC — 上传文件存储
# ───────────────────────────────────────────────────────────────
# 注意:
# - 若 App Deployment replicas > 1accessModes 必须是 ReadWriteMany
# (需要 NFS / CephFS / cloud-file 等支持 RWX 的 StorageClass
# - 单副本场景下 ReadWriteOnce 即可
# ═══════════════════════════════════════════════════════════════
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: resource-library-uploads
namespace: resource-library
labels:
app.kubernetes.io/name: resource-library-app
app.kubernetes.io/part-of: resource-library
spec:
accessModes:
- ReadWriteOnce # 多副本改为 ReadWriteMany
# storageClassName: "standard" # 按集群情况填写
resources:
requests:
storage: 50Gi
+102
View File
@@ -0,0 +1,102 @@
# ═══════════════════════════════════════════════════════════════
# Flask 应用 — Deployment + ClusterIP Service
# ───────────────────────────────────────────────────────────────
# - 镜像请替换为你实际推送到镜像仓库的 tag
# - 健康检查路径 /auth/login 复用 Dockerfile 中的 HEALTHCHECK
# - 单副本;多副本需要 RWX PVC,并把 init_db 逻辑迁移到 Job
# ═══════════════════════════════════════════════════════════════
---
apiVersion: v1
kind: Service
metadata:
name: resource-library-app
namespace: resource-library
labels:
app.kubernetes.io/name: resource-library-app
app.kubernetes.io/part-of: resource-library
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: resource-library-app
ports:
- name: http
port: 80
targetPort: 5000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: resource-library-app
namespace: resource-library
labels:
app.kubernetes.io/name: resource-library-app
app.kubernetes.io/part-of: resource-library
spec:
replicas: 1
strategy:
type: Recreate # 单副本 + RWO PVC 时避免滚动卡住
selector:
matchLabels:
app.kubernetes.io/name: resource-library-app
template:
metadata:
labels:
app.kubernetes.io/name: resource-library-app
app.kubernetes.io/part-of: resource-library
spec:
securityContext:
fsGroup: 1000
containers:
- name: app
# TODO: 替换为实际仓库地址,例如 ghcr.io/you/resource-library:1.0.0
image: resource-library:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 5000
env:
# 数据库连接串由多个片段拼接(使用 MySQL Service 的 DNS 名)
- name: DATABASE_URL
value: "mysql+pymysql://$(MYSQL_USER):$(MYSQL_PASSWORD)@mysql.resource-library.svc.cluster.local:3306/$(MYSQL_DATABASE)"
envFrom:
- configMapRef:
name: resource-library-config
- secretRef:
name: resource-library-secret
volumeMounts:
- name: uploads
mountPath: /app/app/static/uploads
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "2"
memory: "2Gi"
startupProbe:
# entrypoint 先等 DB、再 init_db,首次启动可能较久
httpGet:
path: /auth/login
port: 5000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 60 # 最多 5min
readinessProbe:
httpGet:
path: /auth/login
port: 5000
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
livenessProbe:
httpGet:
path: /auth/login
port: 5000
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
volumes:
- name: uploads
persistentVolumeClaim:
claimName: resource-library-uploads
+39
View File
@@ -0,0 +1,39 @@
# ═══════════════════════════════════════════════════════════════
# Ingress — 外部访问入口
# ───────────────────────────────────────────────────────────────
# 需要集群已安装 Ingress 控制器(nginx-ingress / traefik 等)。
# 若使用 cert-manager 自动签发 TLS,取消相关注解与 tls 段。
# 需支持大文件上传,因此显式放宽 proxy body size / 超时。
# ═══════════════════════════════════════════════════════════════
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: resource-library
namespace: resource-library
labels:
app.kubernetes.io/name: resource-library-app
app.kubernetes.io/part-of: resource-library
annotations:
# nginx-ingress
nginx.ingress.kubernetes.io/proxy-body-size: "512m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
# cert-manager(可选):自动申请 Let's Encrypt 证书
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx # 按集群实际 IngressClass 调整
tls:
- hosts:
- prl.example.com # TODO: 改成你的域名
secretName: resource-library-tls
rules:
- host: prl.example.com # TODO: 改成你的域名
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: resource-library-app
port:
number: 80
+109
View File
@@ -0,0 +1,109 @@
# Kubernetes 部署说明
## 文件结构
```
k8s/
├── 00-namespace.yaml # 命名空间
├── 01-secret.yaml # 敏感凭证(部署前必改)
├── 02-configmap.yaml # 非敏感配置
├── 10-mysql.yaml # MySQL StatefulSet + Headless Service
├── 20-app-pvc.yaml # 上传文件 PVC
├── 21-app.yaml # Flask Deployment + Service
├── 30-ingress.yaml # 外部访问入口
└── kustomization.yaml # Kustomize 汇总
```
## 部署前准备
1. **构建并推送镜像**
```bash
docker build -t ghcr.io/<org>/resource-library:1.0.0 .
docker push ghcr.io/<org>/resource-library:1.0.0
```
然后修改 `21-app.yaml` 中的 `image:` 字段,或在 `kustomization.yaml` 里用
`images:` 字段覆盖 tag。
2. **修改 Secret**
编辑 `01-secret.yaml`,把所有 `CHANGE_ME_*` 替换成真实强密码。
推荐直接用 `kubectl` 创建,避免明文入库:
```bash
kubectl create namespace resource-library
kubectl -n resource-library create secret generic resource-library-secret \
--from-literal=MYSQL_ROOT_PASSWORD="$(openssl rand -base64 24)" \
--from-literal=MYSQL_PASSWORD="$(openssl rand -base64 24)" \
--from-literal=SECRET_KEY="$(python -c 'import secrets;print(secrets.token_hex(32))')" \
--from-literal=ADMIN_PASSWORD='StrongAdmin@2026'
```
同时从 `kustomization.yaml` 的 `resources:` 中移除 `01-secret.yaml`,避免被覆盖。
3. **确认 StorageClass**
`10-mysql.yaml` 和 `20-app-pvc.yaml` 中 `storageClassName` 默认注释掉,
会走集群默认 StorageClass。可用下面的命令确认:
```bash
kubectl get sc
```
4. **修改域名与 Ingress**
编辑 `30-ingress.yaml`,把 `prl.example.com` 改成真实域名。
如不使用 cert-manager,请手工准备 TLS secret
```bash
kubectl -n resource-library create secret tls resource-library-tls \
--cert=fullchain.pem --key=privkey.pem
```
## 部署
```bash
kubectl apply -k k8s/
```
查看状态:
```bash
kubectl -n resource-library get pods,svc,pvc,ingress
kubectl -n resource-library logs -f deploy/resource-library-app
```
## 常见运维
- **重启应用**
```bash
kubectl -n resource-library rollout restart deploy/resource-library-app
```
- **进入 MySQL 执行 SQL**(例如修复 `resources.folder_id` 缺失)
```bash
kubectl -n resource-library exec -it mysql-0 -- \
mysql -uroot -p"$MYSQL_ROOT_PASSWORD" resource_library
```
- **备份数据库**
```bash
kubectl -n resource-library exec mysql-0 -- \
sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" resource_library' \
> backup.sql
```
## 多副本扩容注意事项
默认为 1 个应用副本。如要扩容:
1. PVC `accessModes` 必须改成 `ReadWriteMany`,且集群需要支持 RWX 的
StorageClassNFS/CephFS/云厂商 file 存储)。
2. `entrypoint.sh` 中的 `init_db` 在多副本并发执行时会重复,建议把初始化逻辑
拆成独立的 Kubernetes **Job**,在 Deployment 启动前一次性跑完。
3. Gunicorn 本身无状态,Session 存在 cookie 中,水平扩容无额外依赖。
+31
View File
@@ -0,0 +1,31 @@
# ═══════════════════════════════════════════════════════════════
# Kustomization — 一键部署入口
# ───────────────────────────────────────────────────────────────
# 用法:
# kubectl apply -k k8s/
# 卸载:
# kubectl delete -k k8s/
# kubectl delete pvc -n resource-library --all # 如需清理数据
# ═══════════════════════════════════════════════════════════════
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: resource-library
resources:
- 00-namespace.yaml
- 01-secret.yaml
- 02-configmap.yaml
- 10-mysql.yaml
- 20-app-pvc.yaml
- 21-app.yaml
- 30-ingress.yaml
commonLabels:
app.kubernetes.io/part-of: resource-library
# 如需给应用镜像打 tag,可用如下写法覆盖:
# images:
# - name: resource-library
# newName: ghcr.io/your-org/resource-library
# newTag: "1.0.0"