From a4707391900ba95c117e7471b05e7c20fd72ac88 Mon Sep 17 00:00:00 2001 From: ROOG Date: Sun, 22 Mar 2026 17:28:45 +0800 Subject: [PATCH] ci: add gitea deploy workflow and k3s manifests with cert-manager tls --- .gitea/workflows/deploy.yml | 62 +++++++++++++++++++++++++++++++++++ k8s/README.md | 44 +++++++++++++++++++++++++ k8s/cluster-issuer.yaml | 14 ++++++++ k8s/deployment.yaml | 65 +++++++++++++++++++++++++++++++++++++ k8s/ingress.yaml | 28 ++++++++++++++++ k8s/namespace.yaml | 6 ++++ k8s/pvc.yaml | 14 ++++++++ k8s/service.yaml | 16 +++++++++ 8 files changed, 249 insertions(+) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 k8s/README.md create mode 100644 k8s/cluster-issuer.yaml create mode 100644 k8s/deployment.yaml create mode 100644 k8s/ingress.yaml create mode 100644 k8s/namespace.yaml create mode 100644 k8s/pvc.yaml create mode 100644 k8s/service.yaml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..7ff930c --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,62 @@ +name: Deploy Noval to K3s + +on: + push: + branches: + - main + - master + +jobs: + deploy: + runs-on: self-hosted + steps: + - name: Deploy to K3s + run: | + set -e + + APP_NAME="noval" + NAMESPACE="noval-roog-code" + IMAGE_NAME="noval-app" + + echo "🚀 开始部署 ${APP_NAME}..." + cd /root/k8syaml/NovalRedot + + echo "📥 拉取最新代码..." + git pull origin main + + NEW_COMMIT_SHA=$(git rev-parse --short HEAD) + NEW_COMMIT_MSG=$(git log -1 --oneline) + FULL_IMAGE="${IMAGE_NAME}:${NEW_COMMIT_SHA}" + + echo "📦 构建镜像 ${FULL_IMAGE}" + docker build -t "${FULL_IMAGE}" . + docker tag "${FULL_IMAGE}" "${IMAGE_NAME}:latest" + + if command -v k3s >/dev/null 2>&1; then + echo "📤 导入镜像到 k3s containerd..." + docker save "${FULL_IMAGE}" | k3s ctr images import - + docker save "${IMAGE_NAME}:latest" | k3s ctr images import - + fi + + echo "🧱 应用 K8s 基础资源..." + kubectl apply -f k8s/namespace.yaml + kubectl apply -f k8s/cluster-issuer.yaml + kubectl apply -f k8s/pvc.yaml + kubectl apply -f k8s/service.yaml + kubectl apply -f k8s/ingress.yaml + + if kubectl get deployment "${APP_NAME}" -n "${NAMESPACE}" >/dev/null 2>&1; then + echo "🔄 检测到现有 deployment,执行滚动更新..." + kubectl set image deployment/${APP_NAME} ${APP_NAME}=${FULL_IMAGE} -n "${NAMESPACE}" + else + echo "🆕 首次部署,创建 deployment..." + kubectl apply -f k8s/deployment.yaml + kubectl set image deployment/${APP_NAME} ${APP_NAME}=${FULL_IMAGE} -n "${NAMESPACE}" + fi + + echo "⏳ 等待 rollout 完成..." + kubectl rollout status deployment/${APP_NAME} -n "${NAMESPACE}" --timeout=300s + + echo "✅ 部署完成" + echo "📌 版本: ${NEW_COMMIT_SHA} - ${NEW_COMMIT_MSG}" + kubectl get pods -n "${NAMESPACE}" -l app=${APP_NAME} -o wide diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..6316405 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,44 @@ +# Noval K3s 部署说明 + +## 文件列表 + +- `namespace.yaml`:命名空间 +- `pvc.yaml`:SQLite 数据持久化存储 +- `deployment.yaml`:应用部署(2 副本,滚动更新) +- `service.yaml`:集群内服务 +- `cluster-issuer.yaml`:cert-manager ClusterIssuer(Let's Encrypt) +- `ingress.yaml`:Traefik 入口(域名 + TLS) + +## 一次性手动部署 + +```bash +cd /root/k8syaml/NovalRedot +kubectl apply -f k8s/namespace.yaml +kubectl apply -f k8s/cluster-issuer.yaml +kubectl apply -f k8s/pvc.yaml +kubectl apply -f k8s/deployment.yaml +kubectl apply -f k8s/service.yaml +kubectl apply -f k8s/ingress.yaml +``` + +## CI/CD 自动部署 + +已配置 Gitea Actions:`.gitea/workflows/deploy.yml` + +触发条件:`main` 分支 push。 + +流水线逻辑: +1. 在 Runner 所在机器拉取最新代码 +2. 构建 Docker 镜像并打 tag(`noval-app:` + `latest`) +3. 导入镜像到 k3s containerd +4. 应用 K8s 资源并滚动更新 Deployment +5. 等待 rollout 完成 + +## 你需要确认/调整 + +1. **域名**:当前写的是 `writing-reddot.roog-code.cn`(`k8s/ingress.yaml`) +2. **TLS Secret**:当前是 `writing-reddot-roog-code-tls` +3. **ClusterIssuer**:当前是 `letsencrypt-prod`(`k8s/cluster-issuer.yaml`) +4. **ClusterIssuer 邮箱**:当前是 `roog-code@outlook.com`(可改成你常用邮箱) +5. **Ingress 类**:当前按 Traefik 配置(k3s 默认) +6. **StorageClass**:`local-path`(k3s 默认) diff --git a/k8s/cluster-issuer.yaml b/k8s/cluster-issuer.yaml new file mode 100644 index 0000000..cdc8b34 --- /dev/null +++ b/k8s/cluster-issuer.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: roog-code@outlook.com + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: traefik diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..b8348c5 --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: noval + namespace: noval-roog-code + labels: + app: noval +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: noval + template: + metadata: + labels: + app: noval + spec: + containers: + - name: noval + image: noval-app:latest + imagePullPolicy: Never + ports: + - containerPort: 8000 + name: http + env: + - name: TZ + value: Asia/Shanghai + - name: NOVAL_DATA_DIR + value: /app/data + volumeMounts: + - name: noval-data + mountPath: /app/data + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" + livenessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 8 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumes: + - name: noval-data + persistentVolumeClaim: + claimName: noval-data-pvc + terminationGracePeriodSeconds: 30 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..ce7eecd --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,28 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: noval-ingress + namespace: noval-roog-code + labels: + app: noval + annotations: + kubernetes.io/ingress.class: "traefik" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + traefik.ingress.kubernetes.io/router.entrypoints: web,websecure + traefik.ingress.kubernetes.io/router.tls: "true" +spec: + rules: + - host: writing-reddot.roog-code.cn + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: noval-service + port: + number: 80 + tls: + - hosts: + - writing-reddot.roog-code.cn + secretName: writing-reddot-roog-code-tls diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 0000000..334e00d --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: noval-roog-code + labels: + app: noval diff --git a/k8s/pvc.yaml b/k8s/pvc.yaml new file mode 100644 index 0000000..57dad20 --- /dev/null +++ b/k8s/pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: noval-data-pvc + namespace: noval-roog-code + labels: + app: noval +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + storageClassName: local-path diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..473df04 --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: noval-service + namespace: noval-roog-code + labels: + app: noval +spec: + type: ClusterIP + selector: + app: noval + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 8000