Skip to content

第57天:Element Plus SSR 部署与运维

学习目标

  • 掌握 SSR 应用的部署策略和最佳实践
  • 学会配置生产环境的服务器和负载均衡
  • 了解 SSR 应用的监控和日志管理
  • 掌握自动化部署和持续集成流程

知识点概览

1. 部署架构设计

1.1 生产环境架构

typescript
// config/deployment.ts
export interface DeploymentConfig {
  environment: 'development' | 'staging' | 'production'
  
  // 服务器配置
  server: {
    host: string
    port: number
    workers: number
    maxMemory: string
    timeout: number
  }
  
  // 负载均衡配置
  loadBalancer: {
    enabled: boolean
    algorithm: 'round-robin' | 'least-connections' | 'ip-hash'
    healthCheck: {
      path: string
      interval: number
      timeout: number
      retries: number
    }
  }
  
  // 缓存配置
  cache: {
    redis: {
      cluster: boolean
      nodes: string[]
      password: string
      maxRetries: number
    }
    cdn: {
      enabled: boolean
      provider: 'cloudflare' | 'aws' | 'aliyun'
      domain: string
      ttl: number
    }
  }
  
  // 监控配置
  monitoring: {
    enabled: boolean
    apm: {
      provider: 'newrelic' | 'datadog' | 'elastic'
      apiKey: string
    }
    logging: {
      level: 'error' | 'warn' | 'info' | 'debug'
      transport: 'file' | 'elasticsearch' | 'cloudwatch'
    }
  }
  
  // 安全配置
  security: {
    https: {
      enabled: boolean
      cert: string
      key: string
      hsts: boolean
    }
    cors: {
      origin: string[]
      credentials: boolean
    }
    rateLimit: {
      windowMs: number
      max: number
    }
  }
}

// 环境配置
export const deploymentConfigs: Record<string, DeploymentConfig> = {
  development: {
    environment: 'development',
    server: {
      host: 'localhost',
      port: 3000,
      workers: 1,
      maxMemory: '512MB',
      timeout: 30000
    },
    loadBalancer: {
      enabled: false,
      algorithm: 'round-robin',
      healthCheck: {
        path: '/health',
        interval: 30000,
        timeout: 5000,
        retries: 3
      }
    },
    cache: {
      redis: {
        cluster: false,
        nodes: ['localhost:6379'],
        password: '',
        maxRetries: 3
      },
      cdn: {
        enabled: false,
        provider: 'cloudflare',
        domain: '',
        ttl: 3600
      }
    },
    monitoring: {
      enabled: true,
      apm: {
        provider: 'elastic',
        apiKey: ''
      },
      logging: {
        level: 'debug',
        transport: 'file'
      }
    },
    security: {
      https: {
        enabled: false,
        cert: '',
        key: '',
        hsts: false
      },
      cors: {
        origin: ['http://localhost:3000'],
        credentials: true
      },
      rateLimit: {
        windowMs: 60000,
        max: 1000
      }
    }
  },
  
  production: {
    environment: 'production',
    server: {
      host: '0.0.0.0',
      port: 3000,
      workers: 4,
      maxMemory: '2GB',
      timeout: 10000
    },
    loadBalancer: {
      enabled: true,
      algorithm: 'least-connections',
      healthCheck: {
        path: '/health',
        interval: 10000,
        timeout: 3000,
        retries: 3
      }
    },
    cache: {
      redis: {
        cluster: true,
        nodes: [
          'redis-1.internal:6379',
          'redis-2.internal:6379',
          'redis-3.internal:6379'
        ],
        password: process.env.REDIS_PASSWORD || '',
        maxRetries: 5
      },
      cdn: {
        enabled: true,
        provider: 'cloudflare',
        domain: 'cdn.example.com',
        ttl: 86400
      }
    },
    monitoring: {
      enabled: true,
      apm: {
        provider: 'newrelic',
        apiKey: process.env.NEW_RELIC_LICENSE_KEY || ''
      },
      logging: {
        level: 'info',
        transport: 'elasticsearch'
      }
    },
    security: {
      https: {
        enabled: true,
        cert: '/etc/ssl/certs/app.crt',
        key: '/etc/ssl/private/app.key',
        hsts: true
      },
      cors: {
        origin: ['https://example.com', 'https://www.example.com'],
        credentials: true
      },
      rateLimit: {
        windowMs: 60000,
        max: 100
      }
    }
  }
}

1.2 Docker 容器化

dockerfile
# Dockerfile
# 多阶段构建
FROM node:18-alpine AS builder

# 设置工作目录
WORKDIR /app

# 复制 package 文件
COPY package*.json ./
COPY pnpm-lock.yaml ./

# 安装 pnpm
RUN npm install -g pnpm

# 安装依赖
RUN pnpm install --frozen-lockfile

# 复制源代码
COPY . .

# 构建应用
RUN pnpm run build

# 生产阶段
FROM node:18-alpine AS production

# 安装 dumb-init
RUN apk add --no-cache dumb-init

# 创建应用用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# 设置工作目录
WORKDIR /app

# 复制 package 文件
COPY package*.json ./
COPY pnpm-lock.yaml ./

# 安装 pnpm
RUN npm install -g pnpm

# 只安装生产依赖
RUN pnpm install --prod --frozen-lockfile

# 从构建阶段复制构建产物
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

# 复制启动脚本
COPY --chown=nextjs:nodejs scripts/start.sh ./
RUN chmod +x start.sh

# 切换到非 root 用户
USER nextjs

# 暴露端口
EXPOSE 3000

# 设置环境变量
ENV NODE_ENV=production
ENV PORT=3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

# 启动应用
ENTRYPOINT ["dumb-init", "--"]
CMD ["./start.sh"]
bash
#!/bin/bash
# scripts/start.sh
set -e

# 等待依赖服务
echo "Waiting for dependencies..."

# 等待 Redis
until nc -z ${REDIS_HOST:-localhost} ${REDIS_PORT:-6379}; do
  echo "Waiting for Redis..."
  sleep 2
done

# 等待数据库(如果需要)
if [ -n "$DATABASE_URL" ]; then
  until nc -z ${DB_HOST:-localhost} ${DB_PORT:-5432}; do
    echo "Waiting for database..."
    sleep 2
  done
fi

echo "Dependencies are ready!"

# 启动应用
if [ "$NODE_ENV" = "production" ]; then
  # 生产模式:使用 PM2 集群
  exec pnpm start:cluster
else
  # 开发模式:单进程
  exec pnpm start
fi
javascript
// healthcheck.js
const http = require('http')

const options = {
  host: 'localhost',
  port: process.env.PORT || 3000,
  path: '/health',
  timeout: 2000
}

const request = http.request(options, (res) => {
  console.log(`Health check status: ${res.statusCode}`)
  if (res.statusCode === 200) {
    process.exit(0)
  } else {
    process.exit(1)
  }
})

request.on('error', (err) => {
  console.error('Health check failed:', err)
  process.exit(1)
})

request.end()

1.3 Kubernetes 部署配置

yaml
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: element-plus-ssr
  labels:
    name: element-plus-ssr

---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: element-plus-ssr
data:
  NODE_ENV: "production"
  PORT: "3000"
  REDIS_HOST: "redis-service"
  REDIS_PORT: "6379"
  LOG_LEVEL: "info"

---
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: element-plus-ssr
type: Opaque
data:
  REDIS_PASSWORD: <base64-encoded-password>
  NEW_RELIC_LICENSE_KEY: <base64-encoded-key>
  DATABASE_URL: <base64-encoded-url>

---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: element-plus-ssr
  namespace: element-plus-ssr
  labels:
    app: element-plus-ssr
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: element-plus-ssr
  template:
    metadata:
      labels:
        app: element-plus-ssr
    spec:
      containers:
      - name: app
        image: element-plus-ssr:latest
        ports:
        - containerPort: 3000
        envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: app-secrets
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 15"]
      terminationGracePeriodSeconds: 30

---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: element-plus-ssr-service
  namespace: element-plus-ssr
  labels:
    app: element-plus-ssr
spec:
  selector:
    app: element-plus-ssr
  ports:
  - port: 80
    targetPort: 3000
    protocol: TCP
  type: ClusterIP

---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: element-plus-ssr-ingress
  namespace: element-plus-ssr
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/rate-limit-window: "1m"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - example.com
    - www.example.com
    secretName: app-tls
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: element-plus-ssr-service
            port:
              number: 80
  - host: www.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: element-plus-ssr-service
            port:
              number: 80

---
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: element-plus-ssr-hpa
  namespace: element-plus-ssr
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: element-plus-ssr
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60

2. 监控和日志系统

2.1 应用性能监控

typescript
// utils/monitoring.ts
import { Request, Response, NextFunction } from 'express'
import { performance } from 'perf_hooks'

export class ApplicationMonitor {
  private metrics: Map<string, MetricData> = new Map()
  private alerts: AlertRule[] = []
  
  constructor(private config: MonitoringConfig) {
    this.setupMetricsCollection()
    this.setupAlerts()
  }
  
  // 设置指标收集
  private setupMetricsCollection() {
    // 收集系统指标
    setInterval(() => {
      this.collectSystemMetrics()
    }, this.config.metricsInterval)
    
    // 收集应用指标
    setInterval(() => {
      this.collectApplicationMetrics()
    }, this.config.metricsInterval)
  }
  
  // 收集系统指标
  private collectSystemMetrics() {
    const memUsage = process.memoryUsage()
    const cpuUsage = process.cpuUsage()
    
    this.recordMetric('system.memory.used', memUsage.heapUsed / 1024 / 1024)
    this.recordMetric('system.memory.total', memUsage.heapTotal / 1024 / 1024)
    this.recordMetric('system.cpu.user', cpuUsage.user / 1000)
    this.recordMetric('system.cpu.system', cpuUsage.system / 1000)
    
    // 检查告警
    this.checkAlerts()
  }
  
  // 收集应用指标
  private collectApplicationMetrics() {
    // 这里可以收集应用特定的指标
    const activeConnections = this.getActiveConnections()
    const cacheHitRate = this.getCacheHitRate()
    
    this.recordMetric('app.connections.active', activeConnections)
    this.recordMetric('app.cache.hit_rate', cacheHitRate)
  }
  
  // 记录指标
  private recordMetric(name: string, value: number) {
    const now = Date.now()
    const existing = this.metrics.get(name)
    
    if (existing) {
      existing.values.push({ timestamp: now, value })
      // 保持最近 1000 个数据点
      if (existing.values.length > 1000) {
        existing.values.shift()
      }
      existing.lastUpdated = now
    } else {
      this.metrics.set(name, {
        name,
        values: [{ timestamp: now, value }],
        lastUpdated: now
      })
    }
  }
  
  // 请求监控中间件
  requestMonitor() {
    return (req: Request, res: Response, next: NextFunction) => {
      const startTime = performance.now()
      const startMemory = process.memoryUsage().heapUsed
      
      // 记录请求开始
      this.recordMetric('http.requests.total', 1)
      
      // 监听响应完成
      res.on('finish', () => {
        const duration = performance.now() - startTime
        const memoryDelta = process.memoryUsage().heapUsed - startMemory
        
        // 记录响应时间
        this.recordMetric('http.response_time', duration)
        
        // 记录内存使用变化
        this.recordMetric('http.memory_delta', memoryDelta / 1024 / 1024)
        
        // 记录状态码
        this.recordMetric(`http.status.${res.statusCode}`, 1)
        
        // 记录路由指标
        const route = req.route?.path || req.path
        this.recordMetric(`http.routes.${route}.requests`, 1)
        this.recordMetric(`http.routes.${route}.response_time`, duration)
        
        // 检查慢请求
        if (duration > this.config.slowRequestThreshold) {
          this.recordSlowRequest(req, res, duration)
        }
      })
      
      next()
    }
  }
  
  // 记录慢请求
  private recordSlowRequest(req: Request, res: Response, duration: number) {
    const slowRequest = {
      url: req.url,
      method: req.method,
      duration,
      statusCode: res.statusCode,
      userAgent: req.get('User-Agent'),
      ip: req.ip,
      timestamp: new Date().toISOString()
    }
    
    console.warn('Slow request detected:', slowRequest)
    
    // 发送到监控服务
    this.sendToMonitoringService('slow_request', slowRequest)
  }
  
  // 设置告警规则
  private setupAlerts() {
    this.alerts = [
      {
        name: 'High Memory Usage',
        metric: 'system.memory.used',
        condition: 'greater_than',
        threshold: 1500, // MB
        duration: 300000, // 5 minutes
        severity: 'warning'
      },
      {
        name: 'Critical Memory Usage',
        metric: 'system.memory.used',
        condition: 'greater_than',
        threshold: 1800, // MB
        duration: 60000, // 1 minute
        severity: 'critical'
      },
      {
        name: 'High Response Time',
        metric: 'http.response_time',
        condition: 'average_greater_than',
        threshold: 1000, // ms
        duration: 300000, // 5 minutes
        severity: 'warning'
      },
      {
        name: 'Low Cache Hit Rate',
        metric: 'app.cache.hit_rate',
        condition: 'less_than',
        threshold: 0.8, // 80%
        duration: 600000, // 10 minutes
        severity: 'warning'
      }
    ]
  }
  
  // 检查告警
  private checkAlerts() {
    this.alerts.forEach(alert => {
      const metric = this.metrics.get(alert.metric)
      if (!metric) return
      
      const recentValues = this.getRecentValues(metric, alert.duration)
      if (recentValues.length === 0) return
      
      let triggered = false
      
      switch (alert.condition) {
        case 'greater_than':
          triggered = recentValues.every(v => v.value > alert.threshold)
          break
        case 'less_than':
          triggered = recentValues.every(v => v.value < alert.threshold)
          break
        case 'average_greater_than':
          const avg = recentValues.reduce((sum, v) => sum + v.value, 0) / recentValues.length
          triggered = avg > alert.threshold
          break
      }
      
      if (triggered) {
        this.triggerAlert(alert, recentValues)
      }
    })
  }
  
  // 获取最近的值
  private getRecentValues(metric: MetricData, duration: number): MetricValue[] {
    const cutoff = Date.now() - duration
    return metric.values.filter(v => v.timestamp > cutoff)
  }
  
  // 触发告警
  private triggerAlert(alert: AlertRule, values: MetricValue[]) {
    const alertData = {
      alert: alert.name,
      metric: alert.metric,
      severity: alert.severity,
      threshold: alert.threshold,
      currentValue: values[values.length - 1]?.value,
      timestamp: new Date().toISOString()
    }
    
    console.error('Alert triggered:', alertData)
    
    // 发送告警通知
    this.sendAlert(alertData)
  }
  
  // 发送告警
  private async sendAlert(alertData: any) {
    // 发送到 Slack
    if (this.config.slack?.webhook) {
      await this.sendSlackAlert(alertData)
    }
    
    // 发送邮件
    if (this.config.email?.enabled) {
      await this.sendEmailAlert(alertData)
    }
    
    // 发送到监控服务
    this.sendToMonitoringService('alert', alertData)
  }
  
  // 发送 Slack 告警
  private async sendSlackAlert(alertData: any) {
    try {
      const response = await fetch(this.config.slack!.webhook, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: `🚨 Alert: ${alertData.alert}`,
          attachments: [{
            color: alertData.severity === 'critical' ? 'danger' : 'warning',
            fields: [
              { title: 'Metric', value: alertData.metric, short: true },
              { title: 'Current Value', value: alertData.currentValue, short: true },
              { title: 'Threshold', value: alertData.threshold, short: true },
              { title: 'Severity', value: alertData.severity, short: true }
            ],
            timestamp: Math.floor(Date.now() / 1000)
          }]
        })
      })
      
      if (!response.ok) {
        console.error('Failed to send Slack alert:', response.statusText)
      }
    } catch (error) {
      console.error('Error sending Slack alert:', error)
    }
  }
  
  // 发送到监控服务
  private sendToMonitoringService(type: string, data: any) {
    // 这里可以集成 New Relic、DataDog 等监控服务
    if (typeof window !== 'undefined' && window.newrelic) {
      window.newrelic.addPageAction(type, data)
    }
  }
  
  // 获取指标数据
  getMetrics(): MetricData[] {
    return Array.from(this.metrics.values())
  }
  
  // 获取特定指标
  getMetric(name: string): MetricData | undefined {
    return this.metrics.get(name)
  }
  
  // 获取活跃连接数(示例)
  private getActiveConnections(): number {
    // 这里应该实现实际的连接计数逻辑
    return Math.floor(Math.random() * 100)
  }
  
  // 获取缓存命中率(示例)
  private getCacheHitRate(): number {
    // 这里应该实现实际的缓存命中率计算
    return 0.85 + Math.random() * 0.1
  }
}

// 类型定义
interface MonitoringConfig {
  metricsInterval: number
  slowRequestThreshold: number
  slack?: {
    webhook: string
  }
  email?: {
    enabled: boolean
    smtp: {
      host: string
      port: number
      user: string
      pass: string
    }
  }
}

interface MetricData {
  name: string
  values: MetricValue[]
  lastUpdated: number
}

interface MetricValue {
  timestamp: number
  value: number
}

interface AlertRule {
  name: string
  metric: string
  condition: 'greater_than' | 'less_than' | 'average_greater_than'
  threshold: number
  duration: number
  severity: 'warning' | 'critical'
}

2.2 结构化日志系统

typescript
// utils/logger.ts
import winston from 'winston'
import { ElasticsearchTransport } from 'winston-elasticsearch'

export class StructuredLogger {
  private logger: winston.Logger
  private requestId: string = ''
  
  constructor(private config: LoggerConfig) {
    this.logger = this.createLogger()
  }
  
  // 创建日志器
  private createLogger(): winston.Logger {
    const transports: winston.transport[] = []
    
    // 控制台输出
    if (this.config.console.enabled) {
      transports.push(new winston.transports.Console({
        format: winston.format.combine(
          winston.format.colorize(),
          winston.format.timestamp(),
          winston.format.printf(({ timestamp, level, message, ...meta }) => {
            return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}`
          })
        )
      }))
    }
    
    // 文件输出
    if (this.config.file.enabled) {
      transports.push(new winston.transports.File({
        filename: this.config.file.path,
        format: winston.format.combine(
          winston.format.timestamp(),
          winston.format.json()
        ),
        maxsize: this.config.file.maxSize,
        maxFiles: this.config.file.maxFiles
      }))
    }
    
    // Elasticsearch 输出
    if (this.config.elasticsearch.enabled) {
      transports.push(new ElasticsearchTransport({
        level: this.config.level,
        clientOpts: {
          node: this.config.elasticsearch.node,
          auth: this.config.elasticsearch.auth
        },
        index: this.config.elasticsearch.index,
        transformer: (logData) => {
          return {
            '@timestamp': new Date().toISOString(),
            level: logData.level,
            message: logData.message,
            meta: logData.meta,
            service: 'element-plus-ssr',
            environment: process.env.NODE_ENV,
            requestId: this.requestId
          }
        }
      }))
    }
    
    return winston.createLogger({
      level: this.config.level,
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
      ),
      transports,
      exceptionHandlers: [
        new winston.transports.File({ filename: 'exceptions.log' })
      ],
      rejectionHandlers: [
        new winston.transports.File({ filename: 'rejections.log' })
      ]
    })
  }
  
  // 设置请求 ID
  setRequestId(requestId: string) {
    this.requestId = requestId
  }
  
  // 日志方法
  error(message: string, meta?: any) {
    this.logger.error(message, { ...meta, requestId: this.requestId })
  }
  
  warn(message: string, meta?: any) {
    this.logger.warn(message, { ...meta, requestId: this.requestId })
  }
  
  info(message: string, meta?: any) {
    this.logger.info(message, { ...meta, requestId: this.requestId })
  }
  
  debug(message: string, meta?: any) {
    this.logger.debug(message, { ...meta, requestId: this.requestId })
  }
  
  // 性能日志
  performance(operation: string, duration: number, meta?: any) {
    this.logger.info('Performance metric', {
      type: 'performance',
      operation,
      duration,
      ...meta,
      requestId: this.requestId
    })
  }
  
  // 业务日志
  business(event: string, data?: any) {
    this.logger.info('Business event', {
      type: 'business',
      event,
      data,
      requestId: this.requestId
    })
  }
  
  // 安全日志
  security(event: string, details?: any) {
    this.logger.warn('Security event', {
      type: 'security',
      event,
      details,
      requestId: this.requestId
    })
  }
  
  // 请求日志中间件
  requestLogger() {
    return (req: any, res: any, next: any) => {
      const requestId = req.headers['x-request-id'] || this.generateRequestId()
      req.requestId = requestId
      this.setRequestId(requestId)
      
      const startTime = Date.now()
      
      // 记录请求开始
      this.info('Request started', {
        method: req.method,
        url: req.url,
        userAgent: req.get('User-Agent'),
        ip: req.ip,
        headers: this.sanitizeHeaders(req.headers)
      })
      
      // 监听响应完成
      res.on('finish', () => {
        const duration = Date.now() - startTime
        
        this.info('Request completed', {
          method: req.method,
          url: req.url,
          statusCode: res.statusCode,
          duration,
          contentLength: res.get('Content-Length')
        })
        
        // 记录性能指标
        this.performance('http_request', duration, {
          method: req.method,
          url: req.url,
          statusCode: res.statusCode
        })
      })
      
      next()
    }
  }
  
  // 生成请求 ID
  private generateRequestId(): string {
    return Math.random().toString(36).substring(2, 15) + 
           Math.random().toString(36).substring(2, 15)
  }
  
  // 清理敏感头信息
  private sanitizeHeaders(headers: any): any {
    const sanitized = { ...headers }
    const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key']
    
    sensitiveHeaders.forEach(header => {
      if (sanitized[header]) {
        sanitized[header] = '[REDACTED]'
      }
    })
    
    return sanitized
  }
}

// 类型定义
interface LoggerConfig {
  level: string
  console: {
    enabled: boolean
  }
  file: {
    enabled: boolean
    path: string
    maxSize: number
    maxFiles: number
  }
  elasticsearch: {
    enabled: boolean
    node: string
    auth?: {
      username: string
      password: string
    }
    index: string
  }
}

3. 自动化部署流程

3.1 CI/CD 管道

yaml
# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'pnpm'
    
    - name: Install pnpm
      run: npm install -g pnpm
    
    - name: Install dependencies
      run: pnpm install --frozen-lockfile
    
    - name: Run linting
      run: pnpm run lint
    
    - name: Run type checking
      run: pnpm run type-check
    
    - name: Run unit tests
      run: pnpm run test:unit
    
    - name: Run integration tests
      run: pnpm run test:integration
    
    - name: Run E2E tests
      run: pnpm run test:e2e
    
    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info

  security:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Run security audit
      run: npm audit --audit-level high
    
    - name: Run Snyk security scan
      uses: snyk/actions/node@master
      env:
        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
    
    - name: Run SAST scan
      uses: github/codeql-action/init@v2
      with:
        languages: javascript
    
    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v2

  build:
    needs: [test, security]
    runs-on: ubuntu-latest
    outputs:
      image-digest: ${{ steps.build.outputs.digest }}
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Log in to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=sha,prefix={{branch}}-
          type=raw,value=latest,enable={{is_default_branch}}
    
    - name: Build and push Docker image
      id: build
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        platforms: linux/amd64,linux/arm64

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    environment: staging
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to staging
      run: |
        echo "Deploying to staging environment"
        # 这里添加部署到 staging 环境的脚本
    
    - name: Run smoke tests
      run: |
        echo "Running smoke tests on staging"
        # 这里添加冒烟测试脚本

  deploy-production:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
    - uses: actions/checkout@v3
    
    - name: Configure kubectl
      uses: azure/k8s-set-context@v1
      with:
        method: kubeconfig
        kubeconfig: ${{ secrets.KUBE_CONFIG }}
    
    - name: Deploy to production
      run: |
        # 更新 Kubernetes 部署
        kubectl set image deployment/element-plus-ssr \
          app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
          -n element-plus-ssr
        
        # 等待部署完成
        kubectl rollout status deployment/element-plus-ssr -n element-plus-ssr
    
    - name: Verify deployment
      run: |
        # 验证部署是否成功
        kubectl get pods -n element-plus-ssr
        
        # 运行健康检查
        curl -f https://example.com/health || exit 1
    
    - name: Notify deployment
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        channel: '#deployments'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}
      if: always()

3.2 部署脚本

bash
#!/bin/bash
# scripts/deploy.sh
set -e

# 配置
ENVIRONMENT=${1:-staging}
IMAGE_TAG=${2:-latest}
NAMESPACE="element-plus-ssr"
DEPLOYMENT_NAME="element-plus-ssr"

echo "Deploying to $ENVIRONMENT environment..."

# 验证环境
if [[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]]; then
  echo "Error: Environment must be 'staging' or 'production'"
  exit 1
fi

# 检查 kubectl 连接
if ! kubectl cluster-info &> /dev/null; then
  echo "Error: Cannot connect to Kubernetes cluster"
  exit 1
fi

# 检查命名空间
if ! kubectl get namespace $NAMESPACE &> /dev/null; then
  echo "Creating namespace $NAMESPACE..."
  kubectl create namespace $NAMESPACE
fi

# 应用配置
echo "Applying Kubernetes configurations..."
kubectl apply -f k8s/ -n $NAMESPACE

# 更新镜像
echo "Updating deployment image to $IMAGE_TAG..."
kubectl set image deployment/$DEPLOYMENT_NAME \
  app=ghcr.io/your-org/element-plus-ssr:$IMAGE_TAG \
  -n $NAMESPACE

# 等待部署完成
echo "Waiting for deployment to complete..."
kubectl rollout status deployment/$DEPLOYMENT_NAME -n $NAMESPACE --timeout=600s

# 验证部署
echo "Verifying deployment..."
kubectl get pods -n $NAMESPACE

# 运行健康检查
echo "Running health checks..."
SERVICE_URL=$(kubectl get ingress -n $NAMESPACE -o jsonpath='{.items[0].spec.rules[0].host}')
if [[ -n "$SERVICE_URL" ]]; then
  for i in {1..5}; do
    if curl -f "https://$SERVICE_URL/health" &> /dev/null; then
      echo "Health check passed"
      break
    else
      echo "Health check failed, retrying in 10 seconds..."
      sleep 10
    fi
    
    if [[ $i -eq 5 ]]; then
      echo "Health check failed after 5 attempts"
      exit 1
    fi
  done
fi

echo "Deployment completed successfully!"

# 发送通知
if [[ -n "$SLACK_WEBHOOK" ]]; then
  curl -X POST -H 'Content-type: application/json' \
    --data "{\"text\":\"🚀 Deployment to $ENVIRONMENT completed successfully!\"}" \
    $SLACK_WEBHOOK
fi

4. 实践练习

  1. 容器化部署实践

    • 创建 Docker 镜像
    • 配置 Kubernetes 部署
    • 设置负载均衡和自动扩缩容
  2. 监控系统实践

    • 集成 APM 监控
    • 配置告警规则
    • 设置日志聚合
  3. CI/CD 流程实践

    • 配置自动化测试
    • 实现自动部署
    • 设置部署回滚机制

5. 学习资源

6. 作业

  • 设计完整的生产环境部署架构
  • 实现自动化监控和告警系统
  • 配置 CI/CD 管道和部署流程
  • 创建运维手册和故障排除指南

总结

通过第57天的学习,我们全面掌握了:

  1. 部署架构:设计了完整的生产环境部署方案
  2. 容器化:实现了 Docker 容器化和 Kubernetes 编排
  3. 监控系统:建立了全面的应用监控和日志系统
  4. 自动化部署:配置了 CI/CD 管道和自动化部署流程

这些技能将帮助我们构建可靠、可扩展的生产环境。

Element Plus Study Guide