CI/CD 持续集成与部署
大约 9 分钟约 2793 字
CI/CD 持续集成与部署
简介
CI/CD(Continuous Integration / Continuous Delivery / Deployment)不是简单地“自动跑脚本”,而是把代码检查、构建、测试、制品管理、环境部署、回滚和审计串成一条稳定交付链路。对于 ASP.NET Core 项目,真正有价值的 CI/CD 通常同时包含:代码质量门禁、Docker 镜像构建、分环境配置、灰度或预发布验证,以及故障时可快速回退的发布策略。
特点
实现
GitHub Actions 基础 CI:构建、测试、产物上传
# .github/workflows/dotnet-ci.yml
name: dotnet-ci
on:
pull_request:
branches: [main]
push:
branches: [main, develop]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --configuration Release --no-build --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage"
- name: Publish artifacts
run: dotnet publish src/MyApp/MyApp.csproj -c Release -o ./publish --no-build
- name: Upload publish output
uses: actions/upload-artifact@v4
with:
name: publish-output
path: ./publish# 本地验证流水线关键命令
dotnet restore
dotnet build -c Release
dotnet test -c Release --no-build
dotnet publish src/MyApp/MyApp.csproj -c Release -o ./publishCI 最小闭环通常至少包括:
1. 还原依赖
2. 构建
3. 单元测试
4. 制品输出
5. 日志/报告上传Docker 制品构建与镜像发布
# Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish src/MyApp/MyApp.csproj -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]# .github/workflows/docker-publish.yml
name: docker-publish
on:
push:
branches: [main]
tags:
- 'v*'
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}/myapp
tags: |
type=ref,event=branch
type=sha
type=semver,pattern={{version}}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}# 本地镜像验证
docker build -t myapp:local .
docker run --rm -p 8080:8080 myapp:local
curl http://localhost:8080/health分环境部署:Staging / Production
# .github/workflows/deploy.yml
name: deploy
on:
workflow_dispatch:
inputs:
environment:
description: Deploy environment
required: true
type: choice
options:
- staging
- production
image_tag:
description: Docker image tag
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
docker pull ghcr.io/${{ github.repository }}/myapp:${{ inputs.image_tag }}
docker stop myapp || true
docker rm myapp || true
docker run -d \
--name myapp \
--restart unless-stopped \
-p 5000:8080 \
-e ASPNETCORE_ENVIRONMENT=${{ inputs.environment == 'production' && 'Production' || 'Staging' }} \
-e ConnectionStrings__DefaultConnection='${{ secrets.DB_CONNECTION }}' \
ghcr.io/${{ github.repository }}/myapp:${{ inputs.image_tag }}// appsettings.Staging.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
}
}// appsettings.Production.json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Error"
}
}
}生产环境不要把敏感配置写进仓库:
- 数据库连接串
- JWT 密钥
- 第三方 API Key
- SMTP 账号密码
应统一放入 GitHub Secrets、环境变量或专门的密钥管理系统中。回滚、健康检查与发布门禁
# 生产部署前增加健康检查
- name: Verify health
run: |
curl --fail --retry 10 --retry-delay 3 https://staging.example.com/health// Program.cs
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.Run();# 容器回滚示意
docker pull ghcr.io/org/myapp:previous-stable
docker stop myapp
docker rm myapp
docker run -d --name myapp -p 5000:8080 ghcr.io/org/myapp:previous-stable推荐的发布门禁:
1. PR 必须通过 CI
2. main 分支必须受保护
3. 生产环境使用 GitHub Environment 审批
4. 发布后自动执行健康检查
5. 失败后自动/手动回滚质量门禁与代码检查
# .github/workflows/quality-gate.yml
name: quality-gate
on:
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # SonarQube 需要
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
# 格式化检查
- name: Format check
run: dotnet format --verify-no-changes --verbosity diagnostic
# 代码分析(SonarQube)
- name: SonarQube scan
uses: SonarSource/sonarqube-scan-action@v2
with:
args: >
-D sonar.qualitygate.wait=true
-D sonar.cs.opencover.reportsPaths=**/coverage.opencover.xml
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
# 依赖漏洞扫描
- name: Dependency check
run: dotnet list package --vulnerable --include-transitive
# 安全扫描
- name: Security audit
run: |
dotnet tool install --global security-scan
security-scan scan ./src
# 测试覆盖率检查
- name: Test coverage
run: |
dotnet test --configuration Release \
--collect:"XPlat Code Coverage" \
--results-directory ./coverage
# 检查覆盖率是否达标(80%)
COVERAGE=$(cat ./coverage/*/coverage.cobertura.xml | grep -oP 'line-rate="\K[^"]+' | awk '{sum+=$1; n++} END {print sum/n * 100}')
echo "Coverage: ${COVERAGE}%"蓝绿部署与金丝雀发布
# 蓝绿部署策略
# blue 和 green 两个环境轮流使用
name: blue-green-deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Determine active environment
id: active
run: |
CURRENT=$(curl -s http://lb.example.com/active-slot)
if [ "$CURRENT" = "blue" ]; then
echo "slot=green" >> $GITHUB_OUTPUT
else
echo "slot=blue" >> $GITHUB_OUTPUT
fi
- name: Deploy to ${{ steps.active.outputs.slot }}
run: |
SLOT=${{ steps.active.outputs.slot }}
docker pull ghcr.io/org/myapp:${{ github.sha }}
docker stop myapp-$SLOT || true
docker rm myapp-$SLOT || true
docker run -d --name myapp-$SLOT -p 50${{ steps.active.outputs.slot == 'blue' && '10' || '20' }}:8080 \
ghcr.io/org/myapp:${{ github.sha }}
- name: Health check on new slot
run: |
SLOT=${{ steps.active.outputs.slot }}
PORT=${{ steps.active.outputs.slot == 'blue' && '5010' || '5020' }}
curl --fail --retry 10 --retry-delay 3 http://localhost:$PORT/health
- name: Switch traffic
run: |
SLOT=${{ steps.active.outputs.slot }}
curl -X POST http://lb.example.com/switch?slot=$SLOT
- name: Cleanup old slot
run: |
OTHER=${{ steps.active.outputs.slot == 'blue' && 'green' || 'blue' }}
docker stop myapp-$OTHER || true# 金丝雀发布 — 逐步增加新版本流量
# 通常需要配合 Service Mesh(如 Istio)或网关实现
name: canary-deploy
on:
workflow_dispatch:
inputs:
canary_weight:
description: 'Canary traffic percentage'
required: true
default: '10'
jobs:
canary:
runs-on: ubuntu-latest
steps:
- name: Deploy canary
run: |
# 更新网关路由规则,将指定比例流量导向新版本
curl -X PUT http://gateway/api/routes \
-d "{\"canary_weight\": ${{ inputs.canary_weight }}, \"version\": \"${{ github.sha }}\"}"
- name: Monitor metrics
run: |
# 监控 5 分钟内的错误率和延迟
for i in $(seq 1 30); do
ERROR_RATE=$(curl -s http://monitor/metrics | jq '.error_rate')
LATENCY_P99=$(curl -s http://monitor/metrics | jq '.latency_p99')
echo "Error rate: $ERROR_RATE, P99 latency: ${LATENCY_P99}ms"
if (( $(echo "$ERROR_RATE > 1.0" | bc -l) )); then
echo "Error rate too high, rolling back"
curl -X POST http://gateway/api/rollback
exit 1
fi
sleep 10
done制品管理与版本策略
# 语义化版本管理
name: version
on:
push:
branches: [main]
jobs:
version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.semver.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine version
id: semver
uses: paulhatch/semantic-version@v5
with:
tag_prefix: "v"
major_pattern: "BREAKING CHANGE:"
minor_pattern: "feat:"
format: "${major}.${minor}.${patch}-build.${increment}"
- name: Create git tag
run: |
git tag v${{ steps.semver.outputs.version }}
git push origin v${{ steps.semver.outputs.version }}
- name: Generate changelog
run: |
# 基于 conventional commits 生成变更日志
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$PREV_TAG" ]; then
echo "## Changes since ${PREV_TAG}" > CHANGELOG.md
git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" >> CHANGELOG.md
fi// 在 ASP.NET Core 中注入构建信息
// csproj 中启用
// <PropertyGroup>
// <GenerateAssemblyInfo>true</GenerateAssemblyInfo>
// </PropertyGroup>
builder.Services.AddHttpContextAccessor();
// 自定义端点返回版本信息
app.MapGet("/version", () =>
{
var assembly = Assembly.GetExecutingAssembly();
var version = assembly.GetName().Version?.ToString() ?? "unknown";
var buildDate = File.GetLastWriteTimeUtc(assembly.Location);
return Results.Ok(new
{
version,
buildDate,
environment = builder.Environment.EnvironmentName,
aspnetCoreVersion = typeof(Host).Assembly.GetName().Version?.ToString()
});
});多环境配置管理
# GitHub Environment 配置
# GitHub → Settings → Environments
# 创建: Development, Staging, Production
# 每个环境可以设置不同的 Secrets 和 Protection Rules
# Production 环境的保护规则:
# - 需要至少 1 人审批
# - 只有指定分支可以部署
# - 部署超时 15 分钟
# - 部署前必须通过 CI// appsettings.json — 基础配置
// appsettings.Development.json — 开发环境覆盖
// appsettings.Staging.json — 预发布环境覆盖
// appsettings.Production.json — 生产环境覆盖
// Program.cs — 验证配置完整性
var builder = WebApplication.CreateBuilder(args);
// 生产环境必须的配置检查
if (builder.Environment.IsProduction())
{
var config = builder.Configuration;
var requiredKeys = new[]
{
"ConnectionStrings:DefaultConnection",
"Jwt:Secret",
"Redis:Configuration",
"Smtp:Password"
};
var missing = requiredKeys.Where(k => string.IsNullOrEmpty(config[k]));
if (missing.Any())
{
throw new InvalidOperationException(
$"生产环境缺少必要配置: {string.Join(", ", missing)}");
}
}CI/CD 监控与告警
# 部署后自动验证
- name: Post-deploy smoke tests
run: |
# 基本健康检查
curl --fail https://staging.example.com/health
# 关键接口冒烟测试
curl --fail -X POST https://staging.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"test"}'
curl --fail https://staging.example.com/api/products?page=1
- name: Notify deployment result
if: always()
uses: slackapi/slack-github-action@v1
with:
channel-id: 'deployments'
slack-message: |
${{ job.status == 'success' && '✅' || '❌' }} 部署 ${{ job.status }}
环境: ${{ inputs.environment }}
版本: ${{ inputs.image_tag }}
触发: ${{ github.actor }}
链接: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}优点
缺点
总结
CI/CD 真正解决的是“如何稳定、可追踪地把代码变成上线系统”,而不是单纯把部署自动化。对 ASP.NET Core 项目来说,最重要的是先形成稳定的 CI 门禁,再逐步把制品构建、环境部署、回滚和健康检查补齐,形成完整交付闭环。
关键知识点
- CI 保证代码质量,CD 保证交付效率,二者不能只做其中一半。
- 制品必须标准化,否则很难做到稳定回滚。
- 生产配置与密钥必须和代码分离。
- 部署完成后必须有健康检查和可观测性验证。
项目落地视角
- 中小项目可以先从 GitHub Actions + Docker + SSH 部署做起。
- 中大型项目通常会引入镜像仓库、预发布环境和审批机制。
- 单体应用重点在制品和配置治理,微服务则更关注环境和服务编排。
- 发布成功只是第一步,真正上线还要看健康、日志、错误率和核心业务指标。
常见误区
- 只做了自动部署,没有自动测试和质量门禁。
- 所有环境直接复用同一套硬编码配置。
- 流水线能跑通,但没有版本号、没有回滚策略。
- 生产出问题时只能重新发版,不能快速切回上一稳定版本。
进阶路线
- 学习蓝绿发布、金丝雀发布和滚动发布策略。
- 将 GitHub Actions 与 Kubernetes / Argo CD / Helm 结合起来。
- 补充质量门禁:SAST、依赖漏洞扫描、镜像扫描、契约测试。
- 打通 OpenTelemetry、日志、告警和发布审计链路。
适用场景
- ASP.NET Core Web API / MVC / Worker Service 项目。
- 需要频繁迭代和多人协作的业务系统。
- 使用 Docker 镜像交付的服务端项目。
- 希望减少人工部署差异和上线风险的团队。
落地建议
- 先建立最小可用流水线:restore → build → test → publish。
- 再逐步引入 Docker 制品、环境审批、健康检查和回滚机制。
- 所有生产发布都绑定 commit/tag/image tag,便于追溯。
- 将部署脚本和基础设施配置纳入版本管理,不依赖手工记忆。
排错清单
- 流水线失败时,先区分是依赖还原、构建、测试还是部署阶段失败。
- 发布成功但服务不可用时,先看容器日志、配置注入和健康检查。
- 生产和预发行为不一致时,先核对环境变量和外部依赖。
- 回滚失败时,检查上一稳定制品是否仍可用、配置是否兼容。
复盘问题
- 当前流水线最大瓶颈是在构建速度、测试质量,还是部署安全性?
- 你的发布流程里,哪些步骤仍然依赖人工操作?
- 如果生产环境现在出问题,能否在几分钟内切回上一稳定版本?
- 现有 CI/CD 体系是否已经覆盖你最真实的上线风险?
