第十二章 容器化与部署
第十二章 容器化与部署
容器化是微服务架构的基石。没有容器,微服务的独立部署、环境一致性和快速扩展都将无从谈起。这一章,我想分享一些实战经验,帮助你掌握.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 小结
容器化和部署是微服务架构的最后一公里。记住几个关键原则:
- 镜像要小:使用Alpine基础镜像,多阶段构建
- 安全至上:非root用户,最小权限原则
- 配置外部化:使用ConfigMap和Secret管理配置
- 健康检查:实现就绪、存活探针
- 自动化部署:CI/CD流水线,GitOps工作流
- 监控集成:日志、指标、追踪的容器化集成
最重要的是,容器化不是目的,而是手段。选择适合你团队和项目的部署策略,不要为了技术而技术。
在下一章中,我们将探讨安全实践,确保微服务系统的安全性。

浙公网安备 33010602011771号