第十二章 容器化与部署

第十二章 容器化与部署

容器化是微服务架构的基石。没有容器,微服务的独立部署、环境一致性和快速扩展都将无从谈起。这一章,我想分享一些实战经验,帮助你掌握.NET微服务的容器化艺术。

12.1 Docker实战:从入门到精通

12.1.1 Docker的核心价值

很多人对Docker的理解还停留在"轻量级虚拟机"的层面,但这远远低估了Docker的价值。Docker真正解决的是"在我电脑上能跑"的经典难题。

# Dockerfile示例 - 多层构建优化
# 阶段1:构建阶段
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
WORKDIR /source

# 复制项目文件
COPY *.sln .
COPY src/OrderService/OrderService.csproj ./src/OrderService/
COPY src/OrderService.Domain/OrderService.Domain.csproj ./src/OrderService.Domain/
COPY src/OrderService.Infrastructure/OrderService.Infrastructure.csproj ./src/OrderService.Infrastructure/

# 恢复依赖
RUN dotnet restore

# 复制源代码
COPY src/OrderService/ ./src/OrderService/
COPY src/OrderService.Domain/ ./src/OrderService.Domain/
COPY src/OrderService.Infrastructure/ ./src/OrderService.Infrastructure/

# 构建应用
WORKDIR /source/src/OrderService
RUN dotnet publish -c Release -o /app --no-restore

# 阶段2:运行时阶段
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine
WORKDIR /app

# 创建非root用户
RUN addgroup -g 1000 -S appuser && \
    adduser -u 1000 -S appuser -G appuser

# 安装必要的包
RUN apk add --no-cache \
    tzdata \
    ca-certificates \
    && update-ca-certificates

# 设置时区
ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# 复制应用
COPY --from=build /app .

# 设置文件权限
RUN chown -R appuser:appuser /app
USER appuser

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:80/health || exit 1

# 暴露端口
EXPOSE 80

# 启动应用
ENTRYPOINT ["dotnet", "OrderService.dll"]

12.1.2 Docker Compose编排

# docker-compose.yml - 本地开发环境
version: '3.8'

services:
  # API网关
  api-gateway:
    build:
      context: .
      dockerfile: src/ApiGateway/Dockerfile
    ports:
      - "8080:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:80
    depends_on:
      - order-service
      - product-service
      - user-service
    networks:
      - microservices-network

  # 订单服务
  order-service:
    build:
      context: .
      dockerfile: src/Services/OrderService/Dockerfile
    ports:
      - "5001:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:80
      - ConnectionStrings__DefaultConnection=Host=order-db;Database=OrderDB;Username=postgres;Password=postgres
      - RabbitMQ__HostName=rabbitmq
      - Redis__ConnectionString=redis:6379
      - Jaeger__AgentHost=jaeger-agent
    depends_on:
      - order-db
      - rabbitmq
      - redis
      - jaeger-agent
    networks:
      - microservices-network

  # 产品服务
  product-service:
    build:
      context: .
      dockerfile: src/Services/ProductService/Dockerfile
    ports:
      - "5002:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:80
      - ConnectionStrings__DefaultConnection=Host=product-db;Database=ProductDB;Username=postgres;Password=postgres
      - Elasticsearch__Uri=http://elasticsearch:9200
    depends_on:
      - product-db
      - elasticsearch
    networks:
      - microservices-network

  # 用户服务
  user-service:
    build:
      context: .
      dockerfile: src/Services/UserService/Dockerfile
    ports:
      - "5003:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:80
      - ConnectionStrings__DefaultConnection=Host=user-db;Database=UserDB;Username=postgres;Password=postgres
      - Jwt__SecretKey=your-secret-key-here
    depends_on:
      - user-db
    networks:
      - microservices-network

  # PostgreSQL数据库 - 订单服务
  order-db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=OrderDB
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    ports:
      - "5432:5432"
    volumes:
      - order-db-data:/var/lib/postgresql/data
      - ./docker/postgres/init-order-db.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - microservices-network

  # PostgreSQL数据库 - 产品服务
  product-db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=ProductDB
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    ports:
      - "5433:5432"
    volumes:
      - product-db-data:/var/lib/postgresql/data
      - ./docker/postgres/init-product-db.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - microservices-network

  # PostgreSQL数据库 - 用户服务
  user-db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=UserDB
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    ports:
      - "5434:5432"
    volumes:
      - user-db-data:/var/lib/postgresql/data
      - ./docker/postgres/init-user-db.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - microservices-network

  # Redis缓存
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
      - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
    command: redis-server /usr/local/etc/redis/redis.conf
    networks:
      - microservices-network

  # RabbitMQ消息队列
  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "5672:5672"
      - "15672:15672"  # 管理界面
    environment:
      - RABBITMQ_DEFAULT_USER=admin
      - RABBITMQ_DEFAULT_PASS=admin
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
      - ./docker/rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins
    networks:
      - microservices-network

  # Elasticsearch搜索引擎
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
    volumes:
      - elasticsearch-data:/usr/share/elasticsearch/data
    networks:
      - microservices-network

  # Kibana日志可视化
  kibana:
    image: docker.elastic.co/kibana/kibana:8.11.0
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch
    networks:
      - microservices-network

  # Jaeger分布式追踪
  jaeger-all-in-one:
    image: jaegertracing/all-in-one:1.50
    ports:
      - "16686:16686"  # Jaeger UI
      - "14268:14268"  # Jaeger collector
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    networks:
      - microservices-network

  # Prometheus监控
  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--storage.tsdb.retention.time=200h'
      - '--web.enable-lifecycle'
      - '--web.enable-admin-api'
    networks:
      - microservices-network

  # Grafana仪表盘
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana
      - ./docker/grafana/dashboards:/etc/grafana/provisioning/dashboards
      - ./docker/grafana/datasources:/etc/grafana/provisioning/datasources
    depends_on:
      - prometheus
    networks:
      - microservices-network

volumes:
  order-db-data:
  product-db-data:
  user-db-data:
  redis-data:
  rabbitmq-data:
  elasticsearch-data:
  prometheus-data:
  grafana-data:

networks:
  microservices-network:
    driver: bridge

12.2 Kubernetes部署:生产级别的容器编排

12.2.1 Kubernetes基础概念

# Deployment配置 - 声明式部署
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: ecommerce
  labels:
    app: order-service
    version: v1
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
        version: v1
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9090"
        prometheus.io/path: "/metrics"
    spec:
      serviceAccountName: order-service
      containers:
      - name: order-service
        image: your-registry.com/ecommerce/order-service:1.0.0
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 80
          protocol: TCP
        - name: metrics
          containerPort: 9090
          protocol: TCP
        env:
        - name: ASPNETCORE_ENVIRONMENT
          value: "Production"
        - name: ASPNETCORE_URLS
          value: "http://+:80"
        - name: ConnectionStrings__DefaultConnection
          valueFrom:
            secretKeyRef:
              name: order-service-secrets
              key: database-connection
        - name: RabbitMQ__HostName
          value: "rabbitmq.ecommerce.svc.cluster.local"
        - name: Redis__ConnectionString
          value: "redis:6379"
        - name: Jaeger__AgentHost
          value: "jaeger-agent.istio-system.svc.cluster.local"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health/live
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3
        volumeMounts:
        - name: config
          mountPath: /app/config
          readOnly: true
        - name: temp
          mountPath: /tmp
      volumes:
      - name: config
        configMap:
          name: order-service-config
      - name: temp
        emptyDir: {}
      nodeSelector:
        kubernetes.io/os: linux
      tolerations:
      - key: "app"
        operator: "Equal"
        value: "ecommerce"
        effect: "NoSchedule"
---
# Service配置 - 服务发现和负载均衡
apiVersion: v1
kind: Service
metadata:
  name: order-service
  namespace: ecommerce
  labels:
    app: order-service
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 80
    targetPort: http
    protocol: TCP
  - name: metrics
    port: 9090
    targetPort: metrics
    protocol: TCP
  selector:
    app: order-service
---
# ConfigMap配置 - 应用配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-config
  namespace: ecommerce
data:
  appsettings.json: |
    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      },
      "AllowedHosts": "*",
      "FeatureFlags": {
        "EnableNewOrderFlow": true,
        "EnableCache": true
      }
    }
---
# Secret配置 - 敏感信息
apiVersion: v1
kind: Secret
metadata:
  name: order-service-secrets
  namespace: ecommerce
type: Opaque
data:
  database-connection: <base64-encoded-connection-string>
  jwt-secret: <base64-encoded-secret>
---
# ServiceAccount配置 - 权限管理
apiVersion: v1
kind: ServiceAccount
metadata:
  name: order-service
  namespace: ecommerce
---
# RBAC配置 - 细粒度权限控制
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: order-service-role
  namespace: ecommerce
rules:
- apiGroups: [""]
  resources: ["configmaps", "secrets"]
  verbs: ["get", "list"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: order-service-rolebinding
  namespace: ecommerce
subjects:
- kind: ServiceAccount
  name: order-service
  namespace: ecommerce
roleRef:
  kind: Role
  name: order-service-role
  apiGroup: rbac.authorization.k8s.io

12.2.2 Helm Chart打包

# Chart.yaml
apiVersion: v2
name: order-service
description: A Helm chart for Order Service
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
  - microservices
  - order
home: https://github.com/yourcompany/ecommerce
sources:
  - https://github.com/yourcompany/ecommerce/tree/main/src/Services/OrderService
maintainers:
  - name: DevOps Team
    email: devops@yourcompany.com

# values.yaml - 可配置参数
replicaCount: 3

image:
  repository: your-registry.com/ecommerce/order-service
  pullPolicy: IfNotPresent
  tag: "1.0.0"

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  create: true
  annotations: {}
  name: ""

podAnnotations: {}

podSecurityContext:
  fsGroup: 1000

securityContext:
  capabilities:
    drop:
    - ALL
  readOnlyRootFilesystem: false
  runAsNonRoot: true
  runAsUser: 1000

service:
  type: ClusterIP
  port: 80
  targetPort: 80
  annotations: {}

ingress:
  enabled: false
  className: ""
  annotations: {}
  hosts:
    - host: order-service.local
      paths:
        - path: /
          pathType: Prefix
  tls: []

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
  targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - order-service
        topologyKey: kubernetes.io/hostname

config:
  environment: "Production"
  logLevel: "Information"
  
secrets:
  databaseConnection: ""
  jwtSecret: ""

12.2.3 GitOps工作流

# .github/workflows/deploy.yml
name: Deploy to Kubernetes

on:
  push:
    branches: [ main ]
    paths:
      - 'src/Services/OrderService/**'
      - 'k8s/order-service/**'

env:
  REGISTRY: your-registry.com
  IMAGE_NAME: ecommerce/order-service
  KUBECONFIG: ${{ secrets.KUBECONFIG }}

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
      
    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ secrets.REGISTRY_USERNAME }}
        password: ${{ secrets.REGISTRY_PASSWORD }}
        
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=sha,prefix=main-
          type=raw,value=latest,enable={{is_default_branch}}
          
    - name: Build and push Docker image
      uses: docker/build-push-action@v5
      with:
        context: .
        file: ./src/Services/OrderService/Dockerfile
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
        
    - name: Set up Helm
      uses: azure/setup-helm@v3
      with:
        version: '3.12.0'
        
    - name: Deploy to Kubernetes
      run: |
        helm upgrade --install order-service ./k8s/order-service \
          --namespace ecommerce \
          --create-namespace \
          --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
          --set image.tag=${{ github.sha }} \
          --set config.environment="Production" \
          --wait \
          --timeout=10m
          
    - name: Verify deployment
      run: |
        kubectl rollout status deployment/order-service -n ecommerce
        kubectl get pods -n ecommerce -l app=order-service

12.3 小结

容器化和部署是微服务架构的最后一公里。记住几个关键原则:

  1. 镜像要小:使用Alpine基础镜像,多阶段构建
  2. 安全至上:非root用户,最小权限原则
  3. 配置外部化:使用ConfigMap和Secret管理配置
  4. 健康检查:实现就绪、存活探针
  5. 自动化部署:CI/CD流水线,GitOps工作流
  6. 监控集成:日志、指标、追踪的容器化集成

最重要的是,容器化不是目的,而是手段。选择适合你团队和项目的部署策略,不要为了技术而技术。

在下一章中,我们将探讨安全实践,确保微服务系统的安全性。

posted @ 2026-01-22 21:42  高宏顺  阅读(2)  评论(0)    收藏  举报