你已经写好了一个 ASP.NET Core 船舶管理系统,在本地跑得好好的。现在老板说"上云吧,用 K8s"。于是你开始看教程——Pod、Service、Deployment、Ingress、ConfigMap……概念看了一堆,还是不知道怎么把自己的应用真正部署上去。本文不堆概念,直接从一个真实的 ASP.NET Core PMS 项目出发,从 Dockerfile 写到 Helm Chart,每一步都有完整 YAML 和踩过的坑。


目录

  1. 部署架构总览
  2. Dockerfile 优化:从 800MB 到 80MB
  3. Deployment:你的第一个 Pod
  4. Service 与 Ingress:让外部能访问
  5. ConfigMap 与 Secret:配置外置
  6. 健康检查:livenessProbe 与 readinessProbe
  7. 资源管理:requests 与 limits
  8. 滚动更新与回滚
  9. 持久化存储:数据库与文件
  10. 多环境管理:Namespace 与 Kustomize
  11. Helm Chart:打包你的应用
  12. PMS 项目完整部署清单
  13. Checklist

文章摘要

本文详细介绍了将 ASP.NET Core 船舶管理系统(PMS)完整部署到 Kubernetes 的实战指南,涵盖从 Docker 镜像优化到 Helm Chart 打包的全流程。主要内容包括:

🔧 核心优化

  • Dockerfile 优化:通过多阶段构建将镜像从 800MB 缩减到 80MB
  • 资源配置:合理设置 requests/limits 和 QoS 等级保障稳定性
  • 健康检查:正确配置 liveness/readiness/startup 探针避免常见陷阱

🚀 部署架构

  • 完整微服务架构:API、前端、后台服务、数据库、缓存
  • Service 与 Ingress 配置实现外部访问
  • ConfigMap 与 Secret 管理敏感配置

🔄 运维保障

  • 滚动更新与回滚策略实现零停机部署
  • 持久化存储方案(数据库 StatefulSet + 文件存储)
  • 多环境管理(Namespace + Kustomize/Helm)

📋 生产清单

  • 提供完整的一键部署脚本和验证命令
  • 详细的 Checklist 涵盖 Dockerfile、Deployment、配置、网络、存储、安全、运维等七大维度

💡 关键经验

  • 健康检查分离:liveness 只检查进程存活,依赖检查放 readiness
  • 资源管理:必须设置 requests/limits,避免 OOMKill
  • 优雅关闭:配置 preStop 钩子确保服务平滑下线
  • 配置热更新:使用 Reloader 实现配置变更自动滚动

本文以真实项目为背景,每个步骤都提供可运行的 YAML 和代码示例,并分享了实际踩坑经验,帮助开发者从零到一完成 ASP.NET Core 应用的 K8s 生产级部署。


1. 部署架构总览

先看最终要搭出来的架构:

                    ┌─────────────┐
                    │   Ingress   │  ← Nginx Ingress Controller
                    │  (443/80)   │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
        ┌─────┴─────┐ ┌───┴───┐ ┌─────┴─────┐
        │  pms-api  │ │pms-web│ │  pms-sync │
        │ (2 Pods)  │ │(2 Pods)│ │ (1 Pod)   │
        │  .NET 8   │ │ NGINX │ │  .NET 8   │
        └─────┬─────┘ └───────┘ └─────┬─────┘
              │                        │
        ┌─────┴────────────────────────┴─────┐
        │         ClusterIP Services          │
        └─────┬──────────────┬────────────────┘
              │              │
        ┌─────┴─────┐ ┌─────┴─────┐
        │ SQL Server│ │  Redis    │
        │ (StatefulSet)│ (Deployment)│
        └───────────┘ └───────────┘
  • pms-api:ASP.NET Core 后端 API,2 副本
  • pms-web:NGINX 托管的前端静态文件,2 副本
  • pms-sync:船岸同步后台服务,1 副本(定时任务)
  • SQL Server:主数据库,StatefulSet + 持久化卷
  • Redis:缓存和会话状态

2. Dockerfile 优化:从 800MB 到 80MB

2.1 第一版:能跑但很胖

# ❌ 第一版:基于完整 SDK 镜像,800MB+
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "Pms.Api.dll"]

问题:

  • 构建镜像和运行时镜像混在一起
  • 没有利用分层缓存
  • 镜像里有源代码和编译工具
  • 以 root 用户运行

2.2 优化版:多阶段构建

# ✅ 优化版:多阶段构建 + 分层缓存 + 非 root 用户

# ---- Stage 1: Restore ----
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS restore
WORKDIR /src

# 先只拷贝 csproj,利用 Docker 缓存层
COPY ["Pms.Api/Pms.Api.csproj", "Pms.Api/"]
COPY ["Pms.Application/Pms.Application.csproj", "Pms.Application/"]
COPY ["Pms.Infrastructure/Pms.Infrastructure.csproj", "Pms.Infrastructure/"]
COPY ["Pms.Domain/Pms.Domain.csproj", "Pms.Domain/"]
RUN dotnet restore "Pms.Api/Pms.Api.csproj"

# ---- Stage 2: Build ----
FROM restore AS build
WORKDIR /src
COPY . .
RUN dotnet build "Pms.Api/Pms.Api.csproj" -c Release --no-restore

# ---- Stage 3: Publish ----
FROM build AS publish
RUN dotnet publish "Pms.Api/Pms.Api.csproj" \
    -c Release \
    --no-build \
    -o /app/publish \
    /p:UseAppHost=false

# ---- Stage 4: Runtime ----
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app

# 安装全球化依赖(根据需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libgssapi-krb5-2 \
    && rm -rf /var/lib/apt/lists/*

# 创建非 root 用户
RUN adduser --disabled-password --gecos "" --uid 5678 appuser

# 拷贝发布产物
COPY --from=publish /app/publish .

# 设置环境变量
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV DOTNET_EnableDiagnostics=0
ENV COMPlus_EnableDiagnostics=0

# 切换非 root 用户
USER 5678

EXPOSE 8080

# 健康检查(Docker 级别,K8s 中会被覆盖)
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD curl -f http://localhost:8080/health/live || exit 1

ENTRYPOINT ["dotnet", "Pms.Api.dll"]

2.3 镜像大小对比

版本基础镜像镜像大小构建时间
第一版sdk:8.0~800MB慢(每次全量还原)
优化版aspnet:8.0(多阶段)~210MB快(csproj 层缓存)
Alpine 版aspnet:8.0-alpine~80MB中(注意 native 依赖)

如果想用 Alpine:

FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS final
RUN apk add --no-cache icu-libs krb5-libs
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false

⚠️ Alpine 用 musl libc,某些 native 库(如 SQL Server 的 Microsoft.Data.SqlClient 在特定版本)可能有问题,务必测试。

2.4 .dockerignore

# .dockerignore
**/bin/
**/obj/
**/.vs/
**/.vscode/
**/*.user
**/*.suo
**/node_modules/
**/TestResults/
**/*.trx
Dockerfile
.dockerignore
.git/
.gitignore
README.md
*.md

💬 互动一下:你的 Docker 镜像有多大?用 docker images 看一下,如果超过 300MB,基本都是没做多阶段构建或者用了 SDK 镜像作为运行时。


3. Deployment:你的第一个 Pod

3.1 最小化 Deployment

# k8s/api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pms-api
  namespace: pms
  labels:
    app: pms-api
    tier: backend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pms-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # 滚动更新时最多多启动 1 个 Pod
      maxUnavailable: 0  # 滚动更新时不允许 Pod 不可用(零停机)
  template:
    metadata:
      labels:
        app: pms-api
        tier: backend
    spec:
      containers:
        - name: pms-api
          image: registry.example.com/pms/api:1.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Production
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: 1000m
              memory: 1Gi

3.2 关键字段解释

字段作用建议值
replicasPod 副本数生产至少 2
selector.matchLabels选择器,必须和 template.labels 匹配和 labels 一致
maxSurge滚动更新时超出期望副本数的数量1 或 25%
maxUnavailable滚动更新时不可用 Pod 数0(零停机)
imagePullPolicy镜像拉取策略IfNotPresent(生产)、Always(latest)
resources.requests资源预留(K8s 调度依据)根据压测设定
resources.limits资源上限(超过会被 OOMKill 或限流)requests 的 2-4 倍

3.3 部署命令

# 应用配置
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/api-deployment.yaml

# 查看部署状态
kubectl get deployments -n pms
kubectl get pods -n pms -o wide
kubectl describe pod pms-api-xxx -n pms

# 查看日志
kubectl logs -f pms-api-xxx -n pms
kubectl logs -f -l app=pms-api -n pms --tail=100  # 所有匹配 Pod

4. Service 与 Ingress:让外部能访问

4.1 Service

Pod 是临时的——重启、滚动更新、扩缩容都会换 IP。Service 提供稳定的访问入口:

# k8s/api-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: pms-api
  namespace: pms
  labels:
    app: pms-api
spec:
  type: ClusterIP  # 集群内部访问(默认值)
  selector:
    app: pms-api
  ports:
    - name: http
      port: 80         # Service 端口
      targetPort: 8080 # 容器端口
      protocol: TCP

Service 类型:

类型用途生产推荐
ClusterIP集群内部访问✅ 内部微服务
NodePort节点端口(30000-32767)❌ 开发调试用
LoadBalancer云厂商负载均衡器✅ 公有云直接暴露
ExternalNameDNS CNAME引用外部服务

4.2 Ingress

Service 是四层(TCP/UDP),Ingress 是七层(HTTP/HTTPS),支持域名路由、SSL 终止、路径转发:

# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: pms-ingress
  namespace: pms
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
    # 船岸同步大文件上传超时
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - pms.example.com
      secretName: pms-tls
  rules:
    - host: pms.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: pms-api
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: pms-web
                port:
                  number: 80

4.3 安装 Nginx Ingress Controller

# Helm 安装
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.replicaCount=2

5. ConfigMap 与 Secret:配置外置

5.1 ConfigMap

# k8s/api-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: pms-api-config
  namespace: pms
data:
  # 非敏感配置
  ConnectionStrings__PmsDb: "Server=pms-db;Database=PmsDb;TrustServerCertificate=True;"
  Redis__Configuration: "pms-redis:6379"
  Logging__LogLevel__Default: "Warning"
  Logging__LogLevel__Pms: "Information"
  Features__ShipSync: "true"
  Features__Workflow: "true"
  Sync__IntervalMinutes: "5"
  Sync__BatchSize: "100"

ASP.NET Core 的配置系统会自动把 __ 映射为配置层级:

  • ConnectionStrings__PmsDbConnectionStrings:PmsDb
  • Logging__LogLevel__DefaultLogging:LogLevel:Default

5.2 Secret

# k8s/api-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: pms-api-secret
  namespace: pms
type: Opaque
stringData:  # stringData 会自动 base64 编码
  ConnectionStrings__PmsDb: "Server=pms-db;Database=PmsDb;User Id=pms;Password=YourStr0ngP@ss;TrustServerCertificate=True;"
  Redis__Password: "your-redis-password"
  Jwt__SecretKey: "your-super-secret-jwt-key-at-least-32-chars"
  Jwt__Issuer: "pms-api"
  Satellite__ApiKey: "sat-api-key-xxx"

⚠️ 注意

  • Secret 只是 base64 编码,不是加密!
  • 生产环境要启用 etcd 加密(EncryptionConfiguration)
  • 更安全的方案:Vault、Sealed Secrets、外部 KMS

5.3 在 Deployment 中引用

spec:
  template:
    spec:
      containers:
        - name: pms-api
          image: registry.example.com/pms/api:1.0.0
          envFrom:
            - configMapRef:
                name: pms-api-config
            - secretRef:
                name: pms-api-secret
          # 或者引用单个 key
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Production
            - name: ConnectionStrings__PmsDb
              valueFrom:
                secretKeyRef:
                  name: pms-api-secret
                  key: ConnectionStrings__PmsDb
          # 也可以挂载为文件
          volumeMounts:
            - name: config-volume
              mountPath: /app/config
      volumes:
        - name: config-volume
          configMap:
            name: pms-api-config-files

5.4 配置热更新

ConfigMap 更新后,挂载为 Volume 的文件会自动更新(但环境变量不会):

// Program.cs:启用配置热更新
builder.Configuration.AddJsonFile("config/appsettings.json",
    optional: true,
    reloadOnChange: true);

但环境变量方式不会热更新——需要重启 Pod。可以用 Reloader 自动滚动:

# 安装 Reloader
helm repo add stakater https://stakater.github.io/stakater-charts
helm install reloader stakater/reloader -n reloader --create-namespace

在 Deployment 上加注解:

metadata:
  annotations:
    reloader.stakater.com/auto: "true"
    # 或指定 ConfigMap/Secret
    reloader.stakater.com/search: "true"

6. 健康检查:livenessProbe 与 readinessProbe

6.1 三种探针

探针作用失败后果ASP.NET Core 对应
livenessProbe容器是否活着重启 Pod/health/live
readinessProbe容器是否能接收流量从 Service Endpoints 摘除/health/ready
startupProbe容器是否启动完成杀死 Pod 重启(慢启动应用用)启动慢时用

6.2 ASP.NET Core 健康检查端点

// Program.cs
builder.Services.AddHealthChecks()
    .AddDbContextCheck<PmsDbContext>("database")
    .AddCheck<RedisHealthCheck>("redis")
    .AddCheck<WorkflowEngineHealthCheck>("workflow");

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false  // liveness 只检查进程是否活着
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

app.MapHealthChecks("/health/startup", new HealthCheckOptions
{
    Predicate = _ => true  // startup 检查所有依赖
});

6.3 Deployment 探针配置

spec:
  template:
    spec:
      containers:
        - name: pms-api
          # ...
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 30
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
            successThreshold: 1
          startupProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 30  # 最多等 10 + 30*5 = 160 秒

6.4 探针调优踩坑

坑 1:liveness 检查依赖数据库

// ❌ liveness 检查数据库连接,数据库抖动时所有 Pod 被重启
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => true  // 检查所有依赖
});

数据库短暂抖动 → 健康检查失败 → K8s 重启所有 Pod → 数据库雪上加霜。

正确做法:liveness 只检查进程存活,依赖检查放在 readiness。

坑 2:initialDelaySeconds 太短

应用启动需要 20 秒,但 initialDelaySeconds: 5,探针在应用还没启动时就开始检查,失败 3 次后重启 Pod,陷入无限重启循环。

坑 3:readiness 检查太严格

readiness 失败会从 Service 摘除 Pod。如果 readiness 检查了一个非核心依赖(如第三方 API),第三方服务抖动时 Pod 全部被摘,虽然 Pod 本身是好的。


7. 资源管理:requests 与 limits

7.1 为什么必须设置

不设置 resources:

  • Pod 被调度到资源不足的节点 → OOMKill
  • 一个 Pod 占满节点资源 → 影响其他 Pod
  • K8s 无法做容量规划和自动伸缩

7.2 推荐设置

resources:
  # requests:调度依据,保证至少有这么多资源
  requests:
    cpu: 250m      # 0.25 核
    memory: 512Mi  # 512 MB
  # limits:使用上限,超过会被限流(CPU)或杀死(内存)
  limits:
    cpu: 1000m     # 1 核
    memory: 1Gi    # 1 GB

PMS 项目推荐值(根据实际压测调整):

服务CPU RequestCPU LimitMemory RequestMemory Limit
pms-api250m1000m512Mi1Gi
pms-web (NGINX)50m200m64Mi128Mi
pms-sync100m500m256Mi512Mi
SQL Server1000m2000m2Gi4Gi
Redis100m300m128Mi256Mi

7.3 CPU vs Memory 的区别

维度CPUMemory
超限行为限流(throttled),变慢但不杀OOMKilled,直接杀死 Pod
可压缩性可压缩(可以少给点)不可压缩(给了就占了)
建议 ratiolimits/requests = 2~4limits/requests = 1~2

7.4 QoS 等级

K8s 根据 requests/limits 给 Pod 分 QoS 等级:

等级条件节点压力时
Guaranteedrequests = limits(CPU 和 Memory)最后被杀
Burstable设置了 requests 但不等于 limits中等优先级
BestEffort没有设置 requests/limits最先被杀

数据库等关键服务应设为 Guaranteed:

resources:
  requests:
    cpu: 1000m
    memory: 2Gi
  limits:
    cpu: 1000m
    memory: 2Gi

8. 滚动更新与回滚

8.1 滚动更新策略

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%         # 可以超出期望副本数的比例
      maxUnavailable: 0     # 更新时不可用的 Pod 数
  • maxSurge: 1, maxUnavailable: 0:先启动新 Pod,等新 Pod ready 后再杀旧的——零停机
  • maxSurge: 0, maxUnavailable: 1:先杀旧 Pod 再启动新的——有短暂停机,但省资源

8.2 更新镜像

# 方式1:set image
kubectl set image deployment/pms-api pms-api=registry.example.com/pms/api:1.1.0 -n pms

# 方式2:apply 修改后的 YAML
kubectl apply -f k8s/api-deployment.yaml

# 查看滚动状态
kubectl rollout status deployment/pms-api -n pms

# 暂停/恢复滚动
kubectl rollout pause deployment/pms-api -n pms
kubectl rollout resume deployment/pms-api -n pms

8.3 回滚

# 查看历史版本
kubectl rollout history deployment/pms-api -n pms

# 回滚到上一版本
kubectl rollout undo deployment/pms-api -n pms

# 回滚到指定版本
kubectl rollout undo deployment/pms-api -n pms --to-revision=3

# 查看回滚状态
kubectl rollout status deployment/pms-api -n pms

8.4 优雅关闭

滚动更新时旧 Pod 会收到 SIGTERM,默认有 30 秒宽限期。ASP.NET Core 需要处理:

// Program.cs:配置优雅关闭
builder.Services.Configure<HostOptions>(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});

// .NET 8 默认会响应 SIGTERM,但建议显式配置生命周期
app.Lifetime.ApplicationStopping.Register(() =>
{
    Log.Information("应用正在关闭,等待在途请求完成...");
});

同时配置 preStop 钩子,确保在 Pod 被从 Service Endpoints 摘除后再关闭:

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: pms-api
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]
          # preStop 执行后才发 SIGTERM
          # 10 秒足够让 Endpoint Controller 更新路由表

9. 持久化存储:数据库与文件

9.1 SQL Server StatefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pms-db
  namespace: pms
spec:
  serviceName: pms-db
  replicas: 1
  selector:
    matchLabels:
      app: pms-db
  template:
    metadata:
      labels:
        app: pms-db
    spec:
      securityContext:
        fsGroup: 10001  # SQL Server 容器的 mssql 用户 GID
      containers:
        - name: mssql
          image: mcr.microsoft.com/mssql/server:2022-latest
          ports:
            - containerPort: 1433
          env:
            - name: ACCEPT_EULA
              value: "Y"
            - name: MSSQL_SA_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: pms-db-secret
                  key: sa-password
            - name: MSSQL_PID
              value: "Standard"
          resources:
            requests:
              cpu: 1000m
              memory: 2Gi
            limits:
              cpu: 2000m
              memory: 4Gi
          volumeMounts:
            - name: mssql-data
              mountPath: /var/opt/mssql
  volumeClaimTemplates:
    - metadata:
        name: mssql-data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 100Gi
        storageClassName: managed-ssd

9.2 文件存储(船舶证书、附件)

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pms-files
  namespace: pms
spec:
  accessModes:
    - ReadWriteMany  # 多 Pod 共享读写
  resources:
    requests:
      storage: 50Gi
  storageClassName: nfs-client
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pms-api
spec:
  template:
    spec:
      containers:
        - name: pms-api
          volumeMounts:
            - name: files
              mountPath: /app/uploads
      volumes:
        - name: files
          persistentVolumeClaim:
            claimName: pms-files

⚠️ 注意

  • ReadWriteOnce:单节点读写(数据库用)
  • ReadWriteMany:多节点共享读写(文件上传用,需要 NFS/CephFS 等)
  • 生产环境数据库建议用云厂商托管服务(RDS),不要自己跑 StatefulSet

10. 多环境管理:Namespace 与 Kustomize

10.1 Namespace 隔离

# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: pms-dev
  labels:
    environment: development
---
apiVersion: v1
kind: Namespace
metadata:
  name: pms-staging
  labels:
    environment: staging
---
apiVersion: v1
kind: Namespace
metadata:
  name: pms-prod
  labels:
    environment: production

10.2 Kustomize

不用模板引擎,用原生 YAML 覆盖:

k8s/
├── base/
│   ├── kustomization.yaml
│   ├── api-deployment.yaml
│   ├── api-service.yaml
│   └── api-configmap.yaml
└── overlays/
    ├── dev/
    │   ├── kustomization.yaml
    │   └── patches.yaml
    ├── staging/
    │   ├── kustomization.yaml
    │   └── patches.yaml
    └── prod/
        ├── kustomization.yaml
        └── patches.yaml

base/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - api-deployment.yaml
  - api-service.yaml
  - api-configmap.yaml

overlays/prod/kustomization.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: pms-prod
resources:
  - ../../base
replicas:
  - name: pms-api
    count: 4  # 生产 4 副本
images:
  - name: registry.example.com/pms/api
    newTag: 1.0.0
patches:
  - path: patches.yaml
configMapGenerator:
  - name: pms-api-config
    behavior: merge
    literals:
      - Logging__LogLevel__Default=Warning
      - Sync__IntervalMinutes=5

overlays/prod/patches.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pms-api
spec:
  template:
    spec:
      containers:
        - name: pms-api
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              cpu: 2000m
              memory: 2Gi

部署:

kubectl apply -k k8s/overlays/prod
kubectl apply -k k8s/overlays/dev

11. Helm Chart:打包你的应用

11.1 Chart 结构

pms-chart/
├── Chart.yaml
├── values.yaml
├── values-dev.yaml
├── values-prod.yaml
├── templates/
│   ├── _helpers.tpl
│   ├── api-deployment.yaml
│   ├── api-service.yaml
│   ├── sync-deployment.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   └── secret.yaml
└── charts/

11.2 模板化 Deployment

# templates/api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "pms.fullname" . }}-api
  labels:
    {{- include "pms.labels" . | nindent 4 }}
    app.kubernetes.io/component: api
spec:
  replicas: {{ .Values.api.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: pms-api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: pms-api
    spec:
      containers:
        - name: pms-api
          image: "{{ .Values.image.registry }}/pms/api:{{ .Values.image.tag | default .Chart.AppVersion }}"
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: {{ include "pms.fullname" . }}-config
            - secretRef:
                name: {{ include "pms.fullname" . }}-secret
          resources:
            {{- toYaml .Values.api.resources | nindent 12 }}
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080

11.3 values.yaml

image:
  registry: registry.example.com
  tag: "1.0.0"

api:
  replicaCount: 2
  resources:
    requests:
      cpu: 250m
      memory: 512Mi
    limits:
      cpu: 1000m
      memory: 1Gi

sync:
  replicaCount: 1
  resources:
    requests:
      cpu: 100m
      memory: 256Mi
    limits:
      cpu: 500m
      memory: 512Mi

ingress:
  enabled: true
  host: pms.example.com
  tls:
    enabled: true
    secretName: pms-tls

11.4 部署

# 开发环境
helm install pms ./pms-chart -f pms-chart/values-dev.yaml -n pms-dev

# 生产环境
helm install pms ./pms-chart -f pms-chart/values-prod.yaml -n pms-prod

# 升级
helm upgrade pms ./pms-chart -f pms-chart/values-prod.yaml -n pms-prod

# 回滚
helm rollback pms 1 -n pms-prod
helm history pms -n pms-prod

12. PMS 项目完整部署清单

12.1 命名空间

apiVersion: v1
kind: Namespace
metadata:
  name: pms

12.2 一键部署脚本

#!/bin/bash
# deploy.sh

set -e

NAMESPACE=${1:-pms}
ENV=${2:-prod}
IMAGE_TAG=${3:-latest}

echo "=== 部署 PMS 到 K8s ==="
echo "命名空间: $NAMESPACE"
echo "环境: $ENV"
echo "镜像标签: $IMAGE_TAG"

# 创建命名空间
kubectl apply -f k8s/namespace.yaml

# 配置
kubectl apply -f k8s/configmap.yaml -n $NAMESPACE
kubectl apply -f k8s/secret.yaml -n $NAMESPACE

# 存储
kubectl apply -f k8s/pvc.yaml -n $NAMESPACE

# 数据库
kubectl apply -f k8s/db-statefulset.yaml -n $NAMESPACE
kubectl apply -f k8s/db-service.yaml -n $NAMESPACE
kubectl rollout status statefulset/pms-db -n $NAMESPACE --timeout=300s

# Redis
kubectl apply -f k8s/redis-deployment.yaml -n $NAMESPACE
kubectl apply -f k8s/redis-service.yaml -n $NAMESPACE

# 应用
kubectl apply -f k8s/api-deployment.yaml -n $NAMESPACE
kubectl apply -f k8s/sync-deployment.yaml -n $NAMESPACE
kubectl apply -f k8s/web-deployment.yaml -n $NAMESPACE

# 服务
kubectl apply -f k8s/api-service.yaml -n $NAMESPACE
kubectl apply -f k8s/web-service.yaml -n $NAMESPACE

# Ingress
kubectl apply -f k8s/ingress.yaml -n $NAMESPACE

# 等待就绪
kubectl rollout status deployment/pms-api -n $NAMESPACE --timeout=180s
kubectl rollout status deployment/pms-web -n $NAMESPACE --timeout=120s
kubectl rollout status deployment/pms-sync -n $NAMESPACE --timeout=120s

echo "=== 部署完成 ==="
kubectl get pods -n $NAMESPACE
kubectl get svc -n $NAMESPACE
kubectl get ingress -n $NAMESPACE

12.3 部署后验证

# 检查 Pod 状态
kubectl get pods -n pms

# 检查事件
kubectl get events -n pms --sort-by='.lastTimestamp'

# 检查日志
kubectl logs -l app=pms-api -n pms --tail=50

# 测试健康检查
kubectl port-forward svc/pms-api 8080:80 -n pms
curl http://localhost:8080/health/live
curl http://localhost:8080/health/ready

# 检查资源使用
kubectl top pods -n pms
kubectl top nodes

13. Checklist

Dockerfile

  • 使用多阶段构建(SDK 构建 + ASPNET 运行时)
  • 先拷贝 csproj 再 restore,利用分层缓存
  • 使用 .dockerignore 排除无关文件
  • 非 root 用户运行
  • 镜像大小控制在 200MB 以内(Alpine 版 80MB)
  • 固定基础镜像版本标签,不用 latest

Deployment

  • 生产至少 2 副本
  • 配置 requestslimits(CPU 和 Memory)
  • livenessProbe 只检查进程存活
  • readinessProbe 检查依赖就绪
  • 慢启动应用配 startupProbe
  • 滚动更新 maxUnavailable: 0 实现零停机
  • 配置 preStop 钩子和优雅关闭超时

配置

  • 非敏感配置用 ConfigMap,敏感配置用 Secret
  • 不用环境变量硬编码配置
  • 生产环境启用 etcd 加密
  • 配置 Reloader 实现配置变更自动滚动
  • 不同环境用不同 Namespace / Kustomize overlay

网络

  • Service 类型正确(内部用 ClusterIP)
  • Ingress 配置 TLS
  • 合理设置 proxy-timeout(船岸同步长连接)
  • 上传文件大小限制
  • 配置 NetworkPolicy 限制 Pod 间访问

存储

  • 数据库用 StatefulSet + PVC
  • 文件存储用 ReadWriteMany PVC
  • 配置存储备份策略
  • 关键数据考虑用云厂商托管服务
  • PVC 容量监控

安全

  • 非 root 容器运行(runAsNonRoot: true)
  • 只读根文件系统(readOnlyRootFilesystem: true,按需挂载卷)
  • 禁用特权容器
  • 镜像漏洞扫描(Trivy / Clair)
  • RBAC 最小权限
  • Secret 不用明文提交到 Git(Sealed Secrets / Vault)

运维

  • Helm Chart 版本化管理
  • 配置 PodDisruptionBudget 保证最小可用副本数
  • HPA 自动伸缩(CPU/内存/自定义指标)
  • 日志采集(Fluent Bit / Vector)
  • 监控告警(Prometheus + Grafana)
  • 链路追踪(OpenTelemetry)

总结

把 ASP.NET Core 应用部署到 K8s,核心就四件事:

  1. 构建小镜像:多阶段构建 + 非 root + 分层缓存
  2. 声明式配置:Deployment / Service / ConfigMap / Secret,一切皆 YAML
  3. 可靠性保障:健康检查 + 资源限制 + 优雅关闭 + 滚动更新
  4. 环境管理:Namespace 隔离 + Kustomize/Helm 模板化

K8s 的学习曲线确实陡,但一旦跑通第一套完整部署,后面就是复制粘贴改参数。关键是不要把所有概念一次性塞进脑子里——先跑起来一个 Pod,再加 Service,再加 Inress,一步步来。

💬 最后互动:你的应用跑在 K8s 上了吗?是从什么方案迁过来的(裸机 / Docker Compose / Service Fabric)?迁移过程中踩过最大的坑是什么?我先来——第一次部署时没配 readinessProbe,滚动更新时新 Pod 还在启动(EF Core 迁移要 15 秒),旧 Pod 已经被杀了,结果那 15 秒所有请求 503。加上 readinessProbe 和 maxUnavailable: 0 后问题解决。评论区聊聊你的故事。

更多推荐