diff --git a/.gitea/workflows/ci-cd.yaml b/.gitea/workflows/ci-cd.yaml new file mode 100644 index 0000000..7309b62 --- /dev/null +++ b/.gitea/workflows/ci-cd.yaml @@ -0,0 +1,156 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + +env: + HARBOR_URL: 192.168.1.83:8080 + HARBOR_PROJECT: pager + BACKEND_IMAGE: pager-backend + FRONTEND_IMAGE: pager-frontend + K8S_NAMESPACE: learn + +jobs: + build-backend: + name: Build Backend Image + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Harbor + uses: docker/login-action@v2 + with: + registry: ${{ env.HARBOR_URL }} + username: admin + password: ${{ secrets.HARBOR_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.HARBOR_URL }}/${{ env.HARBOR_PROJECT }}/${{ env.BACKEND_IMAGE }} + tags: | + type=sha,prefix={{branch}}- + type=ref,event=branch + type=semver,pattern={{version}} + latest + + - name: Build and push Backend + uses: docker/build-push-action@v4 + with: + context: ./backend + file: ./backend/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.HARBOR_URL }}/${{ env.HARBOR_PROJECT }}/${{ env.BACKEND_IMAGE }}:buildcache + cache-to: type=registry,ref=${{ env.HARBOR_URL }}/${{ env.HARBOR_PROJECT }}/${{ env.BACKEND_IMAGE }}:buildcache,mode=max + + build-frontend: + name: Build Frontend Image + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Harbor + uses: docker/login-action@v2 + with: + registry: ${{ env.HARBOR_URL }} + username: admin + password: ${{ secrets.HARBOR_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.HARBOR_URL }}/${{ env.HARBOR_PROJECT }}/${{ env.FRONTEND_IMAGE }} + tags: | + type=sha,prefix={{branch}}- + type=ref,event=branch + type=semver,pattern={{version}} + latest + + - name: Build and push Frontend + uses: docker/build-push-action@v4 + with: + context: ./frontend + file: ./frontend/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.HARBOR_URL }}/${{ env.HARBOR_PROJECT }}/${{ env.FRONTEND_IMAGE }}:buildcache + cache-to: type=registry,ref=${{ env.HARBOR_URL }}/${{ env.HARBOR_PROJECT }}/${{ env.FRONTEND_IMAGE }}:buildcache,mode=max + + deploy: + name: Deploy to Kubernetes + needs: [build-backend, build-frontend] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + + - name: Configure kubectl + env: + KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG }} + run: | + mkdir -p ~/.kube + echo "$KUBECONFIG_CONTENT" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Get short SHA + id: vars + run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Update image tags in K8s manifests + run: | + sed -i "s|IMAGE_TAG|${{ github.ref_name }}-${{ steps.vars.outputs.SHORT_SHA }}|g" k8s/deployment.yaml + + - name: Deploy to Kubernetes + run: | + kubectl apply -f k8s/namespace.yaml + kubectl apply -f k8s/configmap.yaml -n ${{ env.K8S_NAMESPACE }} + kubectl apply -f k8s/secret.yaml -n ${{ env.K8S_NAMESPACE }} + kubectl apply -f k8s/postgres.yaml -n ${{ env.K8S_NAMESPACE }} + kubectl apply -f k8s/deployment.yaml -n ${{ env.K8S_NAMESPACE }} + kubectl apply -f k8s/service.yaml -n ${{ env.K8S_NAMESPACE }} + kubectl apply -f k8s/ingress.yaml -n ${{ env.K8S_NAMESPACE }} + + - name: Wait for deployment + run: | + kubectl rollout status deployment/pager-backend -n ${{ env.K8S_NAMESPACE }} --timeout=5m + kubectl rollout status deployment/pager-frontend -n ${{ env.K8S_NAMESPACE }} --timeout=5m + + - name: Verify deployment + run: | + kubectl get pods -n ${{ env.K8S_NAMESPACE }} + kubectl get svc -n ${{ env.K8S_NAMESPACE }} + kubectl get ingress -n ${{ env.K8S_NAMESPACE }} + + - name: Deployment notification + if: always() + run: | + if [ $? -eq 0 ]; then + echo "✅ Deployment successful to namespace: ${{ env.K8S_NAMESPACE }}" + else + echo "❌ Deployment failed" + exit 1 + fi diff --git a/CICD_DEPLOY.md b/CICD_DEPLOY.md new file mode 100644 index 0000000..06dbb3b --- /dev/null +++ b/CICD_DEPLOY.md @@ -0,0 +1,396 @@ +# CI/CD 部署指南 + +本文档介绍如何配置和使用 CI/CD 流程将 Pager 应用部署到 Kubernetes 集群,并使用 Grafana 监控。 + +## 📋 目录 + +- [环境要求](#环境要求) +- [配置步骤](#配置步骤) +- [部署流程](#部署流程) +- [监控配置](#监控配置) +- [故障排查](#故障排查) + +## 🔧 环境要求 + +### 基础设施 + +- **Kubernetes 集群**: v1.24+ +- **Harbor 镜像仓库**: http://192.168.1.83:8080 (admin/admin123) +- **Gitea**: http://192.168.1.83:33000 +- **Namespace**: learn +- **Grafana**: 用于应用监控 +- **Prometheus**: 用于指标采集 + +### 工具 + +- kubectl v1.24+ +- docker 或 podman +- git + +## 🚀 配置步骤 + +### 1. 配置 Gitea Secrets + +在 Gitea 仓库中配置以下 secrets: + +**路径**: http://192.168.1.83:33000/gao/pager/settings/secrets + +| Secret 名称 | 说明 | 值 | +|------------|------|-----| +| `HARBOR_PASSWORD` | Harbor 密码 | `admin123` | +| `KUBECONFIG` | K8s 配置文件 (base64 编码) | 见下方说明 | + +#### 生成 KUBECONFIG Secret + +```bash +# 将 kubeconfig 文件编码为 base64 +cat ~/.kube/config | base64 -w 0 +``` + +将输出的 base64 字符串设置为 `KUBECONFIG` secret。 + +### 2. 创建 Harbor 项目 + +```bash +# 登录 Harbor +docker login 192.168.1.83:8080 -u admin -p admin123 + +# Harbor 已经自动创建 pager 项目(通过 UI 或 API) +``` + +或通过 Harbor UI 创建: +- 访问 http://192.168.1.83:8080 +- 登录 (admin/admin123) +- 创建项目 `pager` + +### 3. 配置 Kubernetes + +#### 创建 Namespace 和基础资源 + +```bash +cd /Users/brunogao/work/codes/pingan/pager + +# 创建 namespace +kubectl apply -f k8s/namespace.yaml + +# 创建 secrets 和 configmap +kubectl apply -f k8s/secret.yaml +kubectl apply -f k8s/configmap.yaml + +# 部署 PostgreSQL +kubectl apply -f k8s/postgres.yaml + +# 等待 PostgreSQL 就绪 +kubectl wait --for=condition=ready pod -l app=pager-postgres -n learn --timeout=300s +``` + +#### 验证基础设施 + +```bash +# 检查 namespace +kubectl get namespace learn + +# 检查 secrets +kubectl get secret -n learn + +# 检查 PostgreSQL +kubectl get pods -n learn +kubectl logs -l app=pager-postgres -n learn +``` + +### 4. 配置 Gitea Actions Runner + +确保 Gitea Actions Runner 已配置并运行: + +**查看 Runners**: http://192.168.1.83:33000/gao/pager/settings/actions/runners + +如果没有 runner,需要注册一个: + +```bash +# 下载 act_runner +wget https://dl.gitea.com/act_runner/0.2.6/act_runner-0.2.6-linux-amd64 + +# 注册 runner +./act_runner register --instance http://192.168.1.83:33000 --token YOUR_REGISTRATION_TOKEN + +# 运行 runner +./act_runner daemon +``` + +## 📦 部署流程 + +### 自动部署(推荐) + +当代码推送到 `master` 或 `main` 分支时,CI/CD 流程会自动触发: + +```bash +git add . +git commit -m "feat: your changes" +git push origin master +``` + +CI/CD 流程包括: + +1. **构建阶段**: + - 构建 Backend Docker 镜像 + - 构建 Frontend Docker 镜像 + - 推送镜像到 Harbor + +2. **部署阶段**: + - 更新 K8s Deployment + - 滚动更新应用 + - 健康检查 + +3. **验证阶段**: + - 检查 Pod 状态 + - 验证服务可达性 + +### 手动部署 + +如需手动部署,可以执行: + +```bash +cd k8s + +# 部署应用 +kubectl apply -f deployment.yaml -n learn +kubectl apply -f service.yaml -n learn +kubectl apply -f ingress.yaml -n learn + +# 检查部署状态 +kubectl rollout status deployment/pager-backend -n learn +kubectl rollout status deployment/pager-frontend -n learn + +# 查看 Pod +kubectl get pods -n learn -l app=pager-backend +kubectl get pods -n learn -l app=pager-frontend +``` + +### 访问应用 + +添加 hosts 记录: + +```bash +echo "192.168.1.83 pager.learn.local" | sudo tee -a /etc/hosts +``` + +访问应用: +- **前端**: http://pager.learn.local +- **后端 API**: http://pager.learn.local/api +- **API 文档**: http://pager.learn.local/api/docs + +## 📊 监控配置 + +### 安装 ServiceMonitor + +```bash +# 部署 ServiceMonitor (需要 Prometheus Operator) +kubectl apply -f k8s/monitoring/servicemonitor.yaml +``` + +### 安装 Grafana Dashboard + +```bash +cd k8s/monitoring + +# 设置 Grafana 访问信息 +export GRAFANA_URL="http://grafana.your-domain.com" +export GRAFANA_USER="admin" +export GRAFANA_PASSWORD="admin" + +# 安装 Dashboard +./install-dashboard.sh +``` + +### 监控指标 + +Dashboard 包含以下监控面板: + +- **Backend HTTP Requests**: API 请求速率 +- **Backend Response Time**: P95/P99 响应时间 +- **Pod CPU Usage**: 容器 CPU 使用率 +- **Pod Memory Usage**: 容器内存使用 +- **Database Connections**: 数据库连接数 +- **Error Rate**: 4xx/5xx 错误率 +- **Pod Restart Count**: Pod 重启次数 +- **Disk Usage**: 磁盘使用率 + +## 🔍 故障排查 + +### 查看日志 + +```bash +# Backend 日志 +kubectl logs -l app=pager-backend -n learn --tail=100 -f + +# Frontend 日志 +kubectl logs -l app=pager-frontend -n learn --tail=100 -f + +# PostgreSQL 日志 +kubectl logs -l app=pager-postgres -n learn --tail=100 -f +``` + +### 调试 Pod + +```bash +# 进入 Backend Pod +kubectl exec -it deployment/pager-backend -n learn -- /bin/bash + +# 进入 PostgreSQL Pod +kubectl exec -it deployment/pager-postgres -n learn -- psql -U postgres -d wordmaster +``` + +### 常见问题 + +#### 1. 镜像拉取失败 + +**症状**: `ImagePullBackOff` 或 `ErrImagePull` + +**解决方案**: +```bash +# 检查 secret +kubectl get secret harbor-registry -n learn -o yaml + +# 重新创建 secret +kubectl delete secret harbor-registry -n learn +kubectl create secret docker-registry harbor-registry \ + --docker-server=192.168.1.83:8080 \ + --docker-username=admin \ + --docker-password=admin123 \ + --namespace=learn +``` + +#### 2. Pod 启动失败 + +**症状**: `CrashLoopBackOff` + +**解决方案**: +```bash +# 查看 Pod 事件 +kubectl describe pod -l app=pager-backend -n learn + +# 查看日志 +kubectl logs -l app=pager-backend -n learn --previous +``` + +#### 3. 数据库连接失败 + +**症状**: Backend 日志显示数据库连接错误 + +**解决方案**: +```bash +# 检查 PostgreSQL 是否运行 +kubectl get pods -l app=pager-postgres -n learn + +# 测试数据库连接 +kubectl run -it --rm debug --image=postgres:15-alpine --restart=Never -n learn -- \ + psql -h pager-postgres -U postgres -d wordmaster +``` + +#### 4. Ingress 无法访问 + +**症状**: 无法通过域名访问应用 + +**解决方案**: +```bash +# 检查 Ingress 状态 +kubectl get ingress -n learn +kubectl describe ingress pager-ingress -n learn + +# 检查 Ingress Controller +kubectl get pods -n ingress-nginx +``` + +### 回滚部署 + +如果部署出现问题,可以回滚到上一个版本: + +```bash +# Backend 回滚 +kubectl rollout undo deployment/pager-backend -n learn + +# Frontend 回滚 +kubectl rollout undo deployment/pager-frontend -n learn + +# 查看回滚历史 +kubectl rollout history deployment/pager-backend -n learn +``` + +## 🔄 更新部署 + +### 更新环境变量 + +```bash +# 编辑 ConfigMap +kubectl edit configmap pager-config -n learn + +# 编辑 Secret +kubectl edit secret pager-secrets -n learn + +# 重启 Pod 使配置生效 +kubectl rollout restart deployment/pager-backend -n learn +``` + +### 更新镜像 + +```bash +# 手动更新镜像 +kubectl set image deployment/pager-backend \ + backend=192.168.1.83:8080/pager/pager-backend:new-tag \ + -n learn + +# 查看更新状态 +kubectl rollout status deployment/pager-backend -n learn +``` + +## 📈 性能优化 + +### 水平扩容 + +```bash +# 扩容 Backend +kubectl scale deployment pager-backend --replicas=3 -n learn + +# 扩容 Frontend +kubectl scale deployment pager-frontend --replicas=3 -n learn + +# 配置自动扩容 (HPA) +kubectl autoscale deployment pager-backend \ + --cpu-percent=70 \ + --min=2 \ + --max=10 \ + -n learn +``` + +### 资源调整 + +编辑 `k8s/deployment.yaml` 中的资源限制: + +```yaml +resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "1000m" +``` + +## 📝 最佳实践 + +1. **使用版本标签**: 避免使用 `latest` 标签,使用具体的版本号或 Git SHA +2. **健康检查**: 配置 `livenessProbe` 和 `readinessProbe` +3. **资源限制**: 设置合理的 CPU 和内存限制 +4. **日志管理**: 配置日志收集和轮转 +5. **备份数据库**: 定期备份 PostgreSQL 数据 +6. **监控告警**: 配置 Grafana 告警规则 +7. **安全加固**: 使用非 root 用户运行容器,扫描镜像漏洞 + +## 🆘 支持 + +如遇问题,请: + +1. 查看 Gitea Actions 日志:http://192.168.1.83:33000/gao/pager/actions +2. 查看 Pod 日志:`kubectl logs -n learn` +3. 查看 Grafana 监控面板 +4. 检查本文档的故障排查章节 diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..99d894a --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,46 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Git +.git/ +.gitignore + +# Logs +*.log +backend.log* +*.pid + +# Development files +test_*.py +*_test.py +*.bak +*.bak2 + +# Documentation +*.md +!README.md + +# Temporary files +tmp/ +temp/ +test_results/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..918f58c --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,34 @@ +# Backend Dockerfile +FROM python:3.11-slim + +# 设置工作目录 +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# 复制依赖文件 +COPY requirements.txt . + +# 安装 Python 依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 复制应用代码 +COPY . . + +# 创建上传目录 +RUN mkdir -p uploads + +# 暴露端口 +EXPOSE 8888 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:8888/health', timeout=5)" + +# 启动命令 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8888"] diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..445d9e7 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# Quick deployment script for Pager application + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +NAMESPACE="learn" +HARBOR_URL="192.168.1.83:8080" +HARBOR_PROJECT="pager" + +echo -e "${GREEN}🚀 Pager Application Deployment Script${NC}" +echo "============================================" +echo "" + +# Function to print section headers +section() { + echo "" + echo -e "${YELLOW}>>> $1${NC}" +} + +# Function to check command exists +check_command() { + if ! command -v $1 &> /dev/null; then + echo -e "${RED}❌ Error: $1 is not installed${NC}" + exit 1 + fi +} + +# Check prerequisites +section "Checking prerequisites" +check_command kubectl +check_command docker +echo -e "${GREEN}✅ All prerequisites met${NC}" + +# Check kubectl connectivity +section "Checking Kubernetes connectivity" +if ! kubectl cluster-info &> /dev/null; then + echo -e "${RED}❌ Error: Cannot connect to Kubernetes cluster${NC}" + exit 1 +fi +echo -e "${GREEN}✅ Connected to Kubernetes cluster${NC}" + +# Create namespace if not exists +section "Creating namespace: $NAMESPACE" +if kubectl get namespace $NAMESPACE &> /dev/null; then + echo -e "${YELLOW}⚠️ Namespace $NAMESPACE already exists${NC}" +else + kubectl apply -f k8s/namespace.yaml + echo -e "${GREEN}✅ Namespace created${NC}" +fi + +# Deploy PostgreSQL +section "Deploying PostgreSQL" +kubectl apply -f k8s/postgres.yaml +echo "⏳ Waiting for PostgreSQL to be ready..." +kubectl wait --for=condition=ready pod -l app=pager-postgres -n $NAMESPACE --timeout=300s || true +echo -e "${GREEN}✅ PostgreSQL deployed${NC}" + +# Deploy secrets and config +section "Deploying secrets and configuration" +kubectl apply -f k8s/secret.yaml +kubectl apply -f k8s/configmap.yaml +echo -e "${GREEN}✅ Secrets and ConfigMaps deployed${NC}" + +# Check if we should deploy application +read -p "Do you want to deploy application pods? (requires images in Harbor) [y/N]: " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + section "Deploying application" + + # Get current git commit + GIT_SHA=$(git rev-parse --short HEAD) + IMAGE_TAG="master-${GIT_SHA}" + + echo "Using image tag: $IMAGE_TAG" + + # Update deployment with image tag + sed "s|IMAGE_TAG|$IMAGE_TAG|g" k8s/deployment.yaml > /tmp/deployment-temp.yaml + + kubectl apply -f /tmp/deployment-temp.yaml + kubectl apply -f k8s/service.yaml + kubectl apply -f k8s/ingress.yaml + + rm /tmp/deployment-temp.yaml + + echo "⏳ Waiting for deployments to be ready..." + kubectl rollout status deployment/pager-backend -n $NAMESPACE --timeout=300s || true + kubectl rollout status deployment/pager-frontend -n $NAMESPACE --timeout=300s || true + + echo -e "${GREEN}✅ Application deployed${NC}" +else + echo -e "${YELLOW}⏭️ Skipping application deployment${NC}" +fi + +# Deploy monitoring +read -p "Do you want to deploy monitoring (ServiceMonitor)? [y/N]: " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + section "Deploying monitoring" + kubectl apply -f k8s/monitoring/servicemonitor.yaml + echo -e "${GREEN}✅ Monitoring deployed${NC}" +fi + +# Show deployment status +section "Deployment Status" +echo "" +echo "📦 Pods:" +kubectl get pods -n $NAMESPACE + +echo "" +echo "🌐 Services:" +kubectl get svc -n $NAMESPACE + +echo "" +echo "🔌 Ingress:" +kubectl get ingress -n $NAMESPACE + +echo "" +echo -e "${GREEN}🎉 Deployment Complete!${NC}" +echo "" +echo "Next steps:" +echo "1. Add to /etc/hosts: 192.168.1.83 pager.learn.local" +echo "2. Access frontend: http://pager.learn.local" +echo "3. Access API docs: http://pager.learn.local/api/docs" +echo "4. View logs: kubectl logs -f -l app=pager-backend -n $NAMESPACE" +echo "5. Monitor in Grafana" +echo "" diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..2d09132 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Production +dist/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Git +.git/ +.gitignore + +# Environment +.env.local +.env.*.local + +# Logs +*.log +frontend.log* +*.pid + +# Testing +coverage/ + +# Temporary files +.DS_Store +Thumbs.db diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..e06829e --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,36 @@ +# Frontend Dockerfile - Multi-stage build +FROM node:18-alpine AS builder + +# 设置工作目录 +WORKDIR /app + +# 复制 package 文件 +COPY package*.json ./ + +# 安装依赖 +RUN npm ci + +# 复制源代码 +COPY . . + +# 构建生产版本 +RUN npm run build + +# 生产镜像 - 使用 nginx +FROM nginx:alpine + +# 复制构建产物 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 复制 nginx 配置 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 暴露端口 +EXPOSE 80 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +# 启动 nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..eb9c872 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,37 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; + + # 前端路由支持 + location / { + try_files $uri $uri/ /index.html; + } + + # API 代理(开发环境可选) + location /api { + proxy_pass http://pager-backend:8888; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 静态资源缓存 + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # 安全头 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; +} diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..4f48535 --- /dev/null +++ b/k8s/configmap.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: pager-config + namespace: learn +data: + # Database configuration + DATABASE_URL: postgresql://postgres:postgres@pager-postgres:5432/wordmaster + + # JWT configuration + ACCESS_TOKEN_EXPIRE_MINUTES: "30" + + # AI Model configuration + AI_MODEL: google/gemini-3-pro-preview + USE_CLAUDE_PARSER: "true" + + # Application configuration + ENVIRONMENT: production + LOG_LEVEL: INFO diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..f6ae2f4 --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,157 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pager-backend + namespace: learn + labels: + app: pager-backend + component: api +spec: + replicas: 2 + selector: + matchLabels: + app: pager-backend + template: + metadata: + labels: + app: pager-backend + component: api + version: IMAGE_TAG + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8888" + prometheus.io/path: "/metrics" + spec: + imagePullSecrets: + - name: harbor-registry + containers: + - name: backend + image: 192.168.1.83:8080/pager/pager-backend:IMAGE_TAG + imagePullPolicy: Always + ports: + - containerPort: 8888 + name: http + protocol: TCP + env: + - name: DATABASE_URL + valueFrom: + configMapKeyRef: + name: pager-config + key: DATABASE_URL + - name: SECRET_KEY + valueFrom: + secretKeyRef: + name: pager-secrets + key: SECRET_KEY + - name: ALGORITHM + valueFrom: + secretKeyRef: + name: pager-secrets + key: ALGORITHM + - name: ACCESS_TOKEN_EXPIRE_MINUTES + valueFrom: + configMapKeyRef: + name: pager-config + key: ACCESS_TOKEN_EXPIRE_MINUTES + - name: OPENROUTER_API_KEY + valueFrom: + secretKeyRef: + name: pager-secrets + key: OPENROUTER_API_KEY + - name: AI_MODEL + valueFrom: + configMapKeyRef: + name: pager-config + key: AI_MODEL + - name: USE_CLAUDE_PARSER + valueFrom: + configMapKeyRef: + name: pager-config + key: USE_CLAUDE_PARSER + - name: ENVIRONMENT + valueFrom: + configMapKeyRef: + name: pager-config + key: ENVIRONMENT + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /health + port: 8888 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8888 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumeMounts: + - name: uploads + mountPath: /app/uploads + volumes: + - name: uploads + emptyDir: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pager-frontend + namespace: learn + labels: + app: pager-frontend + component: web +spec: + replicas: 2 + selector: + matchLabels: + app: pager-frontend + template: + metadata: + labels: + app: pager-frontend + component: web + version: IMAGE_TAG + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "80" + spec: + imagePullSecrets: + - name: harbor-registry + containers: + - name: frontend + image: 192.168.1.83:8080/pager/pager-frontend:IMAGE_TAG + imagePullPolicy: Always + ports: + - containerPort: 80 + name: http + protocol: TCP + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "200m" + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..83ed951 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pager-ingress + namespace: learn + labels: + app: pager + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/proxy-body-size: "50m" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" +spec: + ingressClassName: nginx + rules: + - host: pager.learn.local + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: pager-backend + port: + number: 8888 + - path: / + pathType: Prefix + backend: + service: + name: pager-frontend + port: + number: 80 diff --git a/k8s/monitoring/grafana-dashboard.json b/k8s/monitoring/grafana-dashboard.json new file mode 100644 index 0000000..c6496f0 --- /dev/null +++ b/k8s/monitoring/grafana-dashboard.json @@ -0,0 +1,126 @@ +{ + "dashboard": { + "title": "Pager Application Metrics", + "tags": ["pager", "kubernetes", "application"], + "timezone": "browser", + "schemaVersion": 16, + "version": 1, + "refresh": "30s", + "panels": [ + { + "id": 1, + "title": "Backend HTTP Requests", + "type": "graph", + "gridPos": {"x": 0, "y": 0, "w": 12, "h": 8}, + "targets": [ + { + "expr": "rate(http_requests_total{namespace=\"learn\",app=\"pager-backend\"}[5m])", + "legendFormat": "{{method}} {{path}}" + } + ] + }, + { + "id": 2, + "title": "Backend Response Time", + "type": "graph", + "gridPos": {"x": 12, "y": 0, "w": 12, "h": 8}, + "targets": [ + { + "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{namespace=\"learn\",app=\"pager-backend\"}[5m]))", + "legendFormat": "P95" + }, + { + "expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{namespace=\"learn\",app=\"pager-backend\"}[5m]))", + "legendFormat": "P99" + } + ] + }, + { + "id": 3, + "title": "Pod CPU Usage", + "type": "graph", + "gridPos": {"x": 0, "y": 8, "w": 12, "h": 8}, + "targets": [ + { + "expr": "rate(container_cpu_usage_seconds_total{namespace=\"learn\",pod=~\"pager.*\"}[5m])", + "legendFormat": "{{pod}}" + } + ] + }, + { + "id": 4, + "title": "Pod Memory Usage", + "type": "graph", + "gridPos": {"x": 12, "y": 8, "w": 12, "h": 8}, + "targets": [ + { + "expr": "container_memory_usage_bytes{namespace=\"learn\",pod=~\"pager.*\"}", + "legendFormat": "{{pod}}" + } + ] + }, + { + "id": 5, + "title": "Database Connections", + "type": "graph", + "gridPos": {"x": 0, "y": 16, "w": 12, "h": 8}, + "targets": [ + { + "expr": "pg_stat_database_numbackends{namespace=\"learn\"}", + "legendFormat": "Active Connections" + } + ] + }, + { + "id": 6, + "title": "Error Rate", + "type": "graph", + "gridPos": {"x": 12, "y": 16, "w": 12, "h": 8}, + "targets": [ + { + "expr": "rate(http_requests_total{namespace=\"learn\",status=~\"5..\"}[5m])", + "legendFormat": "5xx Errors" + }, + { + "expr": "rate(http_requests_total{namespace=\"learn\",status=~\"4..\"}[5m])", + "legendFormat": "4xx Errors" + } + ] + }, + { + "id": 7, + "title": "Pod Restart Count", + "type": "singlestat", + "gridPos": {"x": 0, "y": 24, "w": 6, "h": 4}, + "targets": [ + { + "expr": "sum(kube_pod_container_status_restarts_total{namespace=\"learn\",pod=~\"pager.*\"})" + } + ] + }, + { + "id": 8, + "title": "Pod Count", + "type": "singlestat", + "gridPos": {"x": 6, "y": 24, "w": 6, "h": 4}, + "targets": [ + { + "expr": "count(kube_pod_info{namespace=\"learn\",pod=~\"pager.*\"})" + } + ] + }, + { + "id": 9, + "title": "Disk Usage", + "type": "graph", + "gridPos": {"x": 12, "y": 24, "w": 12, "h": 8}, + "targets": [ + { + "expr": "kubelet_volume_stats_used_bytes{namespace=\"learn\"} / kubelet_volume_stats_capacity_bytes{namespace=\"learn\"} * 100", + "legendFormat": "{{persistentvolumeclaim}}" + } + ] + } + ] + } +} diff --git a/k8s/monitoring/install-dashboard.sh b/k8s/monitoring/install-dashboard.sh new file mode 100755 index 0000000..4d770c6 --- /dev/null +++ b/k8s/monitoring/install-dashboard.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Install Grafana Dashboard for Pager Application + +set -e + +# Configuration +GRAFANA_URL="${GRAFANA_URL:-http://localhost:3000}" +GRAFANA_USER="${GRAFANA_USER:-admin}" +GRAFANA_PASSWORD="${GRAFANA_PASSWORD:-admin}" +DASHBOARD_FILE="grafana-dashboard.json" + +echo "📊 Installing Grafana Dashboard..." +echo "Grafana URL: $GRAFANA_URL" + +# Check if Grafana is accessible +if ! curl -sf "$GRAFANA_URL/api/health" > /dev/null; then + echo "❌ Error: Cannot connect to Grafana at $GRAFANA_URL" + echo "Please check if Grafana is running and accessible" + exit 1 +fi + +# Create or update dashboard +echo "📤 Uploading dashboard..." +RESPONSE=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + -u "$GRAFANA_USER:$GRAFANA_PASSWORD" \ + -d @"$DASHBOARD_FILE" \ + "$GRAFANA_URL/api/dashboards/db") + +# Check response +if echo "$RESPONSE" | grep -q '"status":"success"'; then + DASHBOARD_URL=$(echo "$RESPONSE" | grep -o '"url":"[^"]*' | cut -d'"' -f4) + echo "✅ Dashboard installed successfully!" + echo "📍 Dashboard URL: $GRAFANA_URL$DASHBOARD_URL" +else + echo "❌ Failed to install dashboard" + echo "Response: $RESPONSE" + exit 1 +fi + +echo "" +echo "🎉 Grafana Dashboard setup complete!" +echo "" +echo "Next steps:" +echo "1. Open Grafana: $GRAFANA_URL" +echo "2. Login with: $GRAFANA_USER / $GRAFANA_PASSWORD" +echo "3. Navigate to the 'Pager Application Metrics' dashboard" +echo "4. Ensure Prometheus data source is configured" diff --git a/k8s/monitoring/servicemonitor.yaml b/k8s/monitoring/servicemonitor.yaml new file mode 100644 index 0000000..3310461 --- /dev/null +++ b/k8s/monitoring/servicemonitor.yaml @@ -0,0 +1,51 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: pager-backend + namespace: learn + labels: + app: pager-backend + prometheus: kube-prometheus +spec: + selector: + matchLabels: + app: pager-backend + endpoints: + - port: http + path: /metrics + interval: 30s + scrapeTimeout: 10s +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: pager-frontend + namespace: learn + labels: + app: pager-frontend + prometheus: kube-prometheus +spec: + selector: + matchLabels: + app: pager-frontend + endpoints: + - port: http + interval: 30s + scrapeTimeout: 10s +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: pager-postgres + namespace: learn + labels: + app: pager-postgres + prometheus: kube-prometheus +spec: + selector: + matchLabels: + app: pager-postgres + endpoints: + - port: postgres + interval: 30s + scrapeTimeout: 10s diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 0000000..5623140 --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: learn + labels: + name: learn + env: production + app: pager + monitoring: enabled diff --git a/k8s/postgres.yaml b/k8s/postgres.yaml new file mode 100644 index 0000000..9e61734 --- /dev/null +++ b/k8s/postgres.yaml @@ -0,0 +1,103 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-pvc + namespace: learn +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: standard +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: pager-postgres + namespace: learn + labels: + app: pager-postgres + component: database +spec: + replicas: 1 + selector: + matchLabels: + app: pager-postgres + template: + metadata: + labels: + app: pager-postgres + component: database + spec: + containers: + - name: postgres + image: postgres:15-alpine + ports: + - containerPort: 5432 + name: postgres + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: pager-secrets + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: pager-secrets + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: pager-secrets + key: POSTGRES_DB + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + exec: + command: + - pg_isready + - -U + - postgres + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - pg_isready + - -U + - postgres + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: postgres-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: pager-postgres + namespace: learn + labels: + app: pager-postgres +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + protocol: TCP + name: postgres + selector: + app: pager-postgres diff --git a/k8s/secret.yaml b/k8s/secret.yaml new file mode 100644 index 0000000..0de896e --- /dev/null +++ b/k8s/secret.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Secret +metadata: + name: pager-secrets + namespace: learn +type: Opaque +stringData: + # Database credentials + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: wordmaster + + # Application secrets + SECRET_KEY: your-super-secret-and-secure-key-change-me-in-production + ALGORITHM: HS256 + + # OpenRouter AI API Key + OPENROUTER_API_KEY: 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 + + # Harbor Registry Secret (for pulling images) +--- +apiVersion: v1 +kind: Secret +metadata: + name: harbor-registry + namespace: learn +type: kubernetes.io/dockerconfigjson +data: + # Base64 encoded: {"auths":{"192.168.1.83:8080":{"username":"admin","password":"admin123","auth":"YWRtaW46YWRtaW4xMjM="}}} + .dockerconfigjson: eyJhdXRocyI6eyIxOTIuMTY4LjEuODM6ODA4MCI6eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbjEyMyIsImF1dGgiOiJZV1J0YVc0NllXUnRhVzR4TWpNPSJ9fX0= diff --git a/k8s/service.yaml b/k8s/service.yaml new file mode 100644 index 0000000..670a403 --- /dev/null +++ b/k8s/service.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: Service +metadata: + name: pager-backend + namespace: learn + labels: + app: pager-backend + component: api +spec: + type: ClusterIP + ports: + - port: 8888 + targetPort: 8888 + protocol: TCP + name: http + selector: + app: pager-backend +--- +apiVersion: v1 +kind: Service +metadata: + name: pager-frontend + namespace: learn + labels: + app: pager-frontend + component: web +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + selector: + app: pager-frontend