“你的团队是不是还在本地 build 完 .Net 项目然后 RDP 进服务器、复制 DLL、祈祷 IIS 不要抽风”这期 DevOps 实战系列我写的是用 GitLab 管代码和流水线用 Arbess 管构建产物分发和主机部署打通从 commit 到 Windows 服务器上运行的整条自动化链路。很多小团队不是不想做 CI/CD而是被 Jenkins 插件、Kubernetes、容器化这些词吓住了。实际上如果你的目标只是“把 .Net 项目自动化构建完、扔到一台主机上跑起来”这套方案够轻、够直接也足够应付 90% 的日常发布场景。Arbess 是我们内部对“自动化部署执行外壳”的称呼本质是一组脚本加配置挂在 GitLab Runner 上把编译、发布、停止站点、备份、拷贝、启动的流程串起来。整套东西不追求大而全但每一步都是真实能落地的我会把脚本、参数、坑全写在下面你照着搭就行。1. 方案设计与流水线拓扑1.1 为什么是“Arbess GitLab”而不是 Jenkins先说明一下Arbess 不是什么商业平台它是我这边给部署自动化工具集起的内部代号。GitLab 负责源代码管理和 CI 调度Arbess 更像一个“只干部署活儿”的执行器。为什么不做 Jenkins我团队之前用过 Jenkins任务配置和权限管理都挺重每次加一个项目都要去 Web 界面里点半天。GitLab CI 的优势在于流水线定义直接放在仓库的.gitlab-ci.yml文件里跟代码一起走版本干净又容易回滚。Arbess 的存在解决另一个问题很多 .Net 项目尤其是旧 .NET Framework 项目依赖 Windows 环境没法简单扔进 Docker 容器里构建。所以 Runner 使用 Shell Executor直接在构建机上跑dotnet命令构建完成后再通过脚本推送并执行部署。这套路在 Windows 主机部署场景下非常稳定你不需要提前定制一个“带 SDK 的镜像”也不用操心容器内网络和共享文件夹权限。适合谁我强烈推荐给还在用 GitLab 管代码、但发布靠人肉点鼠标的团队项目以 .NET Framework / .NET Core 3.1 / .NET 6/8 为主、目标服务器是 Windows Server 的团队以及不想一次性引入 K8s 和容器编排、只想要“流水线 主机”简洁方案的团队。1.2 一条流水线到底拆成几个环节我习惯把整套流程拆成四个阶段build、test、publish、deploy。别嫌阶段多拆开以后每个阶段的失败点非常清楚谁挂了一看日志就知道不会出现“编译通过了但启动不了也不知道是发布目录没拷全还是依赖缺失”的模糊状态。build还原 NuGet 包执行编译。这个阶段主要产出 DLL、EXE 和中间产物。test如果有单元测试跑一遍dotnet test。小团队没有测试项目可以省略但至少保留一个阶段占位。publish用dotnet publish生成可发布的最终目录一般包含wwwroot、配置文件、依赖 DLL。注意这一步要指定 Runtime发布出来的结果才能直接扔到服务器上。deploy把 publish 目录打包传到目标主机调用 Arbess 脚本停止服务/站点备份旧版本拷贝新版本然后启动。从开发者的视角看整个流水线的交通规则是提交代码到main分支或打release-v*标签GitLab 自动触发流水线。构建和发布由 Runner 完成部署脚本由 Arbess 调度。如果你希望“点一下按钮再部署”也可以把deploy阶段设置成when: manual那就能在 GitLab 流水线界面上手动确认执行。1.3 主机、密钥与变量怎么管理这块很容易被忽略。很多初配流水线的人喜欢把服务器 IP、用户名、密码直接写在.gitlab-ci.yml里这是个非常危险的坏习惯因为代码仓库里任何人都能看到一旦仓库泄露服务器等于裸奔。正确的做法是把敏感信息全部放到 GitLab 的 CI/CD Variables 中DEPLOY_HOST目标服务器 IP 或域名。DEPLOY_ACCOUNT执行部署的用户名。DEPLOY_SECRET登录密码或客户端凭证。ARBESS_SCRIPT_ROOTArbess 脚本在 Runner 上的目录。WEBSITE_NAMEIIS 网站名或 Windows 服务名。在.gitlab-ci.yml里用$变量名引用这样日志输出不会直接暴露密钥项目成员离职后也能单独控制变量权限不至于顺手改掉服务器密码。另外Runner 执行部署时用到的用户我建议单独建一个“部署专用账号”权限只给到站点目录和 Windows 服务的启动/停止权限不要一上来就给管理员出事的时候追责和收敛都麻烦。2. 环境准备GitLab、Runner 与 .NET 构建环境2.1 通过 Docker 部署 GitLab顺手处理高危点GitLab 本身可以装在裸机也可以装在 Docker 里。我推荐用 Docker因为升级和备份都简单。下面是我常用的docker-compose.yml精简过只保留核心配置version: 3.6 services: gitlab: image: gitlab/gitlab-ce:16.11.3-ce.0 container_name: gitlab restart: unless-stopped hostname: gitlab.example.local environment: GITLAB_OMNIBUS_CONFIG: | external_url http://gitlab.example.local gitlab_rails[gitlab_shell_ssh_port] 2224 ports: - 8920:80 - 2224:22 volumes: - gitlab_config:/etc/gitlab - gitlab_logs:/var/log/gitlab - gitlab_data:/var/opt/gitlab volumes: gitlab_config: gitlab_logs: gitlab_data:用卷而不是容器内目录主要为了方便备份和迁移。第一次启动以后GitLab 会自动生成一个initial_root_password文件在容器里的/etc/gitlab/initial_root_password路径这个文件会在 24 小时后删除所以首次部署时先把 root 密码记下来。关于 GitLab 安全也就是很多团队担心的“高危漏洞”问题我的经验很简单不要长期停在旧版本新版本发布后两周内尽快升级。你可以在 CI 里做一个定时任务每天几行字用容器镜像的 digest 检查当前版本是不是落后太多。同时关掉公开注册功能在/etc/gitlab/gitlab.rb里把gitlab_rails[signup_enabled] false打开如果团队用 LDAP 或 OAuth也要及时关掉密码登录减少被爆破的可能。这个环境和代码一样只有保持新鲜才安全。2.2 注册 Shell Executor 类型的 RunnerRunner 是 GitLab 和 Arbess 之间的“手和脚”。我强烈建议在 Windows 构建机上直接用 Shell Executor而不是 Docker Executor。Shell Executor 下脚本直接在本机执行可以调用完整 .NET Framework、IIS 管理命令和 Windows 服务控制命令省掉大量容器内封装工作。在 GitLab 管理后台拿到注册 Token 后执行类似命令sudo gitlab-runner register \ --url http://gitlab.example.local \ --registration-token $REGISTRATION_TOKEN \ --executor shell \ --tag-list arbess,dotnet,win \ --description Arbess host agent \ --run-untagged false注册时我特意加了arbess、dotnet、win三个标签这样后续项目里可以只让指定 Runner 接活避免普通项目把部署机资源耗光。run-untaggedfalse意味着 Runner 不会去跑那些没有打标签的作业这在多人共用一个 GitLab 实例时尤其重要避免不小心把构建任务调度到生产服务器上。这个 Runner 就是 Arbess 的执行底座。你在.gitlab-ci.yml里只要写上tags: [arbess, dotnet]GitLab 就会把对应作业派给这个 RunnerArbess 脚本随后在目标主机上开始干活。对新手来说这步是整个链路里最陌生的地方但只要记住“tag 是 Runner 的门牌号”就不会搞混。2.3 .NET SDK 与 .NET Framework 补齐构建 .Net 项目前构建机上必须装齐两套东西新式 .NET SDK比如 .NET 6/7/8以及旧项目需要的 .NET Framework 3.5 / 4.8。装新 SDK 很简单用 winget 就行winget install Microsoft.DotNet.SDK.8但是如果你的项目还依赖 .NET Framework 3.5Windows Server 上默认是关闭的安装时可能会有“找不到源文件”的错误。如果你手头有系统安装 ISO可以这样用 DISM 启用dism /online /enable-feature /featurename:NetFx3 /all /source:D:\sources\sxs没有 ISO 就在“服务器管理器”里添加功能不过在线源经常因为网络问题失败所以准备一份 ISO 很省事。还有一件事执行.NET Framework相关部署时如果服务器报错类似 “Execution of user code in the .NET Framework is disabled. Enable clr enabled”不要慌这通常出现在 SQL Server 的 CLR 集成场景或某些被安全策略锁定的进程里说明启动宿主把 CLR 关闭了。非数据库场景你检查一下应用程序池的“启用 32 位应用程序”、托管管道模式“CLR 启用”改回 true 就行数据库场景用管理员账号跑sp_configure clr enabled, 1再RECONFIGURE。构建机环境是“地基”但也是最容易被追坑的地方。我见过不少流水线跑了一周突然挂掉原因就是运维重装系统后忘了装 SDK然后构建日志出现大量 “dotnet : 无法将“dotnet”项识别为 cmdlet” 报错。所以环境准备完成后建议先手动跑一遍dotnet --list-sdks和where msbuild确认命令可用再接 CI。3. 编写 .gitlab-ci.yml 实现自动构建与主机部署3.1 一个能直接上手的流水线骨架下面这个.gitlab-ci.yml是从我项目里扒出来的简化版你可以直接改成自己的解决方案路径。它的核心思路是把“构建”和“部署”彻底分开所以你看我把publish和deploy分得很清楚stages: - build - test - publish - deploy variables: SOLUTION_FILE: src/MyProject.sln CONFIG: Release PUBLISH_ROOT: artifacts/release cache: key: $CI_COMMIT_REF_SLUG paths: - src/**/bin/ - src/**/obj/ build: stage: build tags: [arbess, dotnet] script: - dotnet restore $SOLUTION_FILE - dotnet build $SOLUTION_FILE -c $CONFIG --no-restore artifacts: paths: - src/**/bin/$CONFIG/ expire_in: 1 day test: stage: test tags: [arbess, dotnet] script: - dotnet test $SOLUTION_FILE -c $CONFIG --no-build only: - main - merge_requests publish: stage: publish tags: [arbess, dotnet] script: - dotnet publish $SOLUTION_FILE -c $CONFIG --no-build -o $PUBLISH_ROOT -r win-x64 --self-contained false artifacts: paths: - $PUBLISH_ROOT/ expire_in: 1 week deploy: stage: deploy tags: [arbess, dotnet] variables: GIT_DEPTH: 1 script: - pwsh -File C:\Arbess\deploy-net.ps1 -PackagePath $PUBLISH_ROOT -TargetServer $DEPLOY_HOST environment: name: production url: http://app.example.com when: manual only: - main - /^release-.*/如果你看不懂--no-build我解释一下它告诉 dotnet 不要再次编译直接用前面 build 阶段生成的二进制。这样每个阶段职责分明也避免发布和编译版本不一致。特别是当你打 tag 发布正式包时--no-build能确保部署的产物就是刚才 build 阶段验证过的那一版。3.2 版本号、制品与缓存怎么定很多团队构建出来的包没有版本号回滚的时候在服务器目录里根本分不清哪个是哪个。我建议在 build 阶段把版本号打进程序集dotnet build $SOLUTION_FILE -c $CONFIG --no-restore /p:Version$CI_COMMIT_TAG如果当前 pipeline 是 tag 触发$CI_COMMIT_TAG就会带上v1.2.3这种值普通分支构建就忽略版本参数。这样发布目录里的 exe/dll 文件属性里能看到明确版本号GitLab 页面也能看到对应 tag。GitLab 的artifacts最好设置expire_in不要无限期保留。构建机磁盘被撑爆是小事关键是大家会无脑去下载过期制品给部署带来混乱。我一般 build 阶段留 1 天publish 阶段留 1 周正式部署其实当天就会拉取留太久没意义。缓存也不要盲目全目录缓存bin/obj就足够了。GitLab Runner 默认的缓存是本地的一旦缓存损坏反而会编译出陈旧代码所以遇到莫名其妙的“本地能编译CI 编译不过”问题先清理 Runner 缓存再说。3.3 主机上真正执行的部署脚本Arbess 的核心其实就在这个 PowerShell 脚本里。它要完成四件事备份旧文件、停掉正在运行的站点或服务、拷贝新文件、重新启动。下面是我 Windows Server 上 IIS 网站部署脚本的关键片段我用的是WebAdministration模块.NET 项目结合 IIS 很常见param( [string]$PackagePath, [string]$SiteName MyApp ) Import-Module WebAdministration $sitePath (Get-ItemProperty IIS:\Sites\$SiteName -Name physicalPath).Value $backupDir D:\Deploys\Backup\${SiteName}_$(Get-Date -Format yyyyMMdd_HHmmss) Write-Host Backup: $sitePath - $backupDir Copy-Item -Path $sitePath -Destination $backupDir -Recurse -Force Stop-WebSite -Name $SiteName Start-Sleep -Seconds 2 Remove-Item -Path $sitePath\* -Recurse -Force Copy-Item -Path $PackagePath\* -Destination $sitePath -Recurse -Force Start-WebSite -Name $SiteName Write-Host Deploy done.如果你的项目是 Windows 服务部署脚本就换成服务停止/启动Stop-Service -Name MyAppService -Force Start-Sleep -Seconds 2 Copy-Item -Path $PackagePath\* -Destination $serviceRoot -Recurse -Force Start-Service -Name MyAppService很多人部署失败是因为Stop-WebSite之后 IIS 还有残留进程占用 DLL此时立刻Remove-Item会报文件被占用。所以脚本里那个Start-Sleep 2不是摆设启动新的应用池前最好再等一下。经验之谈如果你的站点比较大两三秒不够建议改成轮询检查w3wp.exe进程退出再继续。4. 实战中我踩过和排查过的典型问题4.1 Docker 镜像拉取隐现故障net/http、ERR_CONNECTION_RESET用 Docker 部署 GitLab 时最常见的错误是拉取镜像失败报错类似error response from daemon: get https://registry-1.docker.io/v2/: net/http或者浏览器里访问 GitLab 页面直接net::ERR_CONNECTION_RESET。前者通常是从海外 Docker Hub 拉取镜像时网络不稳定或 DNS 解析失败。解决办法不是一遍遍重试而是配置镜像加速器或走内网私有 Registry。你可以在/etc/docker/daemon.json里加 registry-mirrors配置完systemctl restart docker再重拉。但注意镜像加速器也会抽风生产环境最好在能访问外网的机器上用docker pull先把镜像推送到私有仓库再让服务器从私有仓库拉取。这能避免大量“远程端重置”问题也更安全可控。ERR_CONNECTION_RESET则多见于 GitLab 容器映射端口没开或者 external_url 配置的端口跟宿主机映射端口不一致。我排查时一般ss -tlnp看一眼端口监听再用curl -v访问 GitLab 地址能很快确认是容器挂没挂还是网络层被防火墙拦。4.2 IIS / Windows 服务部署时文件锁定和启动失败这套流水线上线后最让我头疼的部署问题是流水线显示 deploy 成功但网站 502。原因是 IIS 应用程序池启动时新代码配置有问题比如连接字符串指向错误数据库或者缺少某个 DLL。脚本虽然拷贝成功但没有做“健康检查”。所以后来我在 Arbess 脚本里加了健康检查步骤部署结束后请求网站首页如果返回非 2xx 就自动回滚到刚才的备份目录$response Invoke-WebRequest -Uri http://localhost:8080/ -UseBasicParsing if ($response.StatusCode -ne 200) { Write-Warning Health check failed, rollback... Stop-WebSite -Name $SiteName Remove-Item -Path $sitePath\* -Recurse -Force Copy-Item -Path $backupDir\* -Destination $sitePath -Recurse -Force Start-WebSite -Name $SiteName }这个回滚机制救了两次大版本上线。主机部署不是“文件拷完就完事”必须把服务探活和回滚纳入脚本否则自动化反而会成为事故放大器。部署第二个坑是 Windows 服务启动失败但日志只显示“服务返回的特定服务错误”。面对这种问题不要迷恋第三方工具Windows 自带命令就有帮助net helpmsg 3521net helpmsg可以把错误码翻译成可读描述很多服务起不来的问题查完错误码就知道是依赖服务没启动、还是账号权限不足。服务启动失败时我会先看 Windows 事件查看器里对应的服务日志再配合net helpmsg比对基本能定位。另外用sc.exe stop和sc.exe start管理服务时最好用完整服务名别用显示名。4.3 运行库与功能服务报错速查部署 .Net 项目还会遇到一堆和运行库相关的错误这里我用一张速查表总结方便你直接翻报错现象原因处理办法dotnet : 无法将“dotnet”项识别为 cmdlet没安装或没加 PATH重装 SDK检查 PATHExecution of user code in the .NET Framework is disabledCLR 被安全策略关闭按场景启用 clr enabled 或应用池托管模式服务启动报“指定的服务未安装”服务名打错或未注册用sc.exe qc 服务名查看服务二进制路径站点文件 Delete 报“文件正由另一进程使用”w3wp 进程未退出停止站点后等待或taskkill /f /pid残留 w3wp容器端口正常但页面ERR_CONNECTION_RESETexternal_url 端口和宿主机映射不一致检查 GitLab VirtualHost 配置与端口net start mysql服务启动失败服务实例不存在或参数错误用mysqld --defaults-file... --initialize重建数据目录表格里那条 SQL Server 的clr enabled错误其实是很多 .Net 应用集成了数据库 CLR 存储过程才会遇到的。遇到时在目标数据库执行EXEC sp_configure clr enabled, 1; RECONFIGURE;然后新建数据库项目引用重新部署程序集就行。不要直接把整个 SQL Server 服务重启否则影响其他在线业务。5. 让流水线更皮实的经验沉淀5.1 CI/CD 的版本策略与回滚到现在这套流水线已经在我这边跑了接近一年最深的体会是自动化构建部署的核心不是“让所有步骤自动跑”而是“每次变更可追踪、可回滚”。GitLab CI 的 tag 天然给了你版本锚点。我习惯约定release-v1.2.3这种格式GitLab 会自动触发 deploy 阶段制品上的版本号、GitLab commit、服务器备份目录三者能一一对应。一旦出问题哪里出问题都能顺着找到。发布策略上小团队没必要搞蓝绿切换和灰度发布但要保证主机上永远保留最近三个备份目录超过三个的自动删除。占不了多少磁盘但回滚时能多一个选择。备份目录命名一定要带时间戳和版本号光有版本号没有时间戳排障时你还是不知道这是什么时候的产物。5.2 简单才是长久的如果你刚开始搭这套不要第一版就把所有高级功能全上。我见过一个同学上来就配了多环境、多 Runner 并发、自动伸缩结果流水线一直红最后发现是 Runner 注册的 tag 和项目没有对齐。稳定的流水线一定是从最简路径走通的一个项目、一台构建机、一个部署脚本跑绿以后再往里面加测试、加多环境、加通知。部署环节里的通知也别搞太复杂先把 GitLab 邮件通知打开再在流水线里加一个失败停止策略。我后来还加了一个微信机器人通知只发“失败”和“部署成功”没人愿意看一堆无关的刷屏消息。5.3 一个小技巧让主机部署可控最后分享一个实用习惯不要把deploy阶段设成自动。你在流水线里看到when: manual了吗我故意这么写的。因为主机部署不同于容器环境服务器资源、IIS 状态、数据库迁移这些都是变量自动跑虽然省事但在没有足够灰度测试的前提下手动点一下部署按钮往往能让团队对每次发布更有仪式感。等你们发布频率高到一周两三次再改成自动也不迟。回头看我搭这套的初衷其实就是不想每天下班前人都耗在“等等服务器上部署的是哪一版”这种问题上。现在提交代码后看一眼流水线绿了就是部署好红了回滚备份心态完全不同。这套方案没有花哨的云原生部署但确实解决了小团队最头疼的主机部署问题希望你也少踩几个我踩过的坑。