Kubernetes 部署实战:把 ASP.NET Core 应用搬上 K8
你已经写好了一个 ASP.NET Core 船舶管理系统,在本地跑得好好的。现在老板说"上云吧,用 K8s"。于是你开始看教程——Pod、Service、Deployment、Ingress、ConfigMap……概念看了一堆,还是不知道怎么把自己的应用真正部署上去。本文不堆概念,直接从一个真实的 ASP.NET Core PMS 项目出发,从 Dockerfile 写到 Helm Chart,每一步都有完整 YAML 和踩过的坑。
目录
- 部署架构总览
- Dockerfile 优化:从 800MB 到 80MB
- Deployment:你的第一个 Pod
- Service 与 Ingress:让外部能访问
- ConfigMap 与 Secret:配置外置
- 健康检查:livenessProbe 与 readinessProbe
- 资源管理:requests 与 limits
- 滚动更新与回滚
- 持久化存储:数据库与文件
- 多环境管理:Namespace 与 Kustomize
- Helm Chart:打包你的应用
- PMS 项目完整部署清单
- 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 关键字段解释
| 字段 | 作用 | 建议值 |
|---|---|---|
replicas | Pod 副本数 | 生产至少 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 | 云厂商负载均衡器 | ✅ 公有云直接暴露 |
ExternalName | DNS 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__PmsDb→ConnectionStrings:PmsDbLogging__LogLevel__Default→Logging: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 Request | CPU Limit | Memory Request | Memory Limit |
|---|---|---|---|---|
| pms-api | 250m | 1000m | 512Mi | 1Gi |
| pms-web (NGINX) | 50m | 200m | 64Mi | 128Mi |
| pms-sync | 100m | 500m | 256Mi | 512Mi |
| SQL Server | 1000m | 2000m | 2Gi | 4Gi |
| Redis | 100m | 300m | 128Mi | 256Mi |
7.3 CPU vs Memory 的区别
| 维度 | CPU | Memory |
|---|---|---|
| 超限行为 | 限流(throttled),变慢但不杀 | OOMKilled,直接杀死 Pod |
| 可压缩性 | 可压缩(可以少给点) | 不可压缩(给了就占了) |
| 建议 ratio | limits/requests = 2~4 | limits/requests = 1~2 |
7.4 QoS 等级
K8s 根据 requests/limits 给 Pod 分 QoS 等级:
| 等级 | 条件 | 节点压力时 |
|---|---|---|
| Guaranteed | requests = 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 副本
- 配置
requests和limits(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,核心就四件事:
- 构建小镜像:多阶段构建 + 非 root + 分层缓存
- 声明式配置:Deployment / Service / ConfigMap / Secret,一切皆 YAML
- 可靠性保障:健康检查 + 资源限制 + 优雅关闭 + 滚动更新
- 环境管理:Namespace 隔离 + Kustomize/Helm 模板化
K8s 的学习曲线确实陡,但一旦跑通第一套完整部署,后面就是复制粘贴改参数。关键是不要把所有概念一次性塞进脑子里——先跑起来一个 Pod,再加 Service,再加 Inress,一步步来。
💬 最后互动:你的应用跑在 K8s 上了吗?是从什么方案迁过来的(裸机 / Docker Compose / Service Fabric)?迁移过程中踩过最大的坑是什么?我先来——第一次部署时没配
readinessProbe,滚动更新时新 Pod 还在启动(EF Core 迁移要 15 秒),旧 Pod 已经被杀了,结果那 15 秒所有请求 503。加上 readinessProbe 和maxUnavailable: 0后问题解决。评论区聊聊你的故事。
更多推荐
所有评论(0)