使用 GitHub Actions 部署 .NET Core 项目到 Windows Server IIS

每次都把打包好的文件手动复制粘贴到服务器然后再手动停止重启 IIS 未免也太复杂了对吧。能省一点力气就省一点么。虽然还有很多功能还没做到。

闲话

最近无聊在研究 CI/CD,本来是像无头苍蝇一样在看 Jenkins 来着,但是在某位不愿透露姓名的大佬(@陈宁)的点拨下开始看 GitHub Actions 了。

看到一半就想说,我要是能用这玩意儿把我自己的某个项目放到我的服务器上,岂不美哉?

刚好,我这儿有一个自己的基于 .NET Core 的一套 API,还有一个阿里云服务器,那就来试试!

当然了,这篇文章只说 .NET Core -> Windows Server IIS 这一种场景。其他的应该比这个要简单,就不介绍了。

毕竟也没亲自试过对吧。

所以涉及到这么几个东西:

  • GitHub Repository
  • GitHub Actions
  • GitHub Self-hosted Runner
  • 运行 Windows Server 且已安装 IIS 的服务器

当然了,IIS 的配置这儿就不说了,超 scope 了。就默认 IIS 已经配置好了,且 IIS 网站也建好了,也绑定到了某个端口和物理路径。

在 Windows Server 上安装 GitHub Self-hosted Runner

为什么要安装个这东西呢?

长话短说,就是,我们需要在服务器上安装这个东西,能让服务器根据我们的命令,从 github.com 上下载我们打包好的 Artifact。

这里的 Artifact,特指我们通过 dotnet publish 或其他等效操作打包好的可以直接部署到 IIS 的一堆文件。只要我们把这堆文件上传到 GitHub,再让服务器把文件从 GitHub 上下载到服务器的某个位置,再在 IIS 里进行绑定,那不就可以了!

要安装 GitHub Self-hosted Runner,需要:

  1. 访问 GitHub 上的 Repository
  2. 点击 Settings
  3. 点击左侧 Code and automation 下 Actions 的 Runners
  4. 点击 New self-hosted runner 按钮
  5. 在打开的页面中,选择你的服务器操作系统和架构。这里我选择的是 Windows x64
  6. 在服务器的 Powershell 中,运行 GitHub 提供的命令。

在运行 Invoke-WebRequest 命令时,可能会遇见报错。如果提示“未能创建 SSL/TLS 安全通道,可以运行

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12
[Net.ServicePointManager]::SecurityProtocol

来解决问题。

在下载这一步可能需要花费一点时间。整个压缩包大概 70M 左右,具体花费时间取决于服务器速度。

如果发现运行 configure 命令时报错,试着返回 GitHub 页面刷新一下获取一个新的 token 再重试。

到此,Runner 就安装好了。我们可以在 Actions Workflow 中的 yaml 文件中声明 runs-on: self-hosted 后,给这个 Runner 发送命令。

编写 yaml 文件

接下来我们开始编写给 workflow 用的 yaml 文件。

我的需求是这样的:

  • 在 main 分支有代码 push 或有 PR 被 merge 到 main 分支时,执行 workflow。
  • 在部署之前,需要运行 dotnet build 命令,以确保代码可以编译通过。
  • 在部署之前,需要运行 dotnet test 命令,以确保单元测试可以通过。
  • 运行 dotnet publish 命令,将代码打包成发布文件。
  • 将打包好的发布文件上传到 GitHub Artifact。
  • 中断 IIS 服务。
  • 服务器将上传上去的 GitHub Artifact 下载到服务器 IIS 指定的位置。
  • 重新启动 IIS 服务。

由于一个 workflow 对应一个 yaml 文件,所以,yaml 文件的内容应该像下面这样:

name: Deploy to Server # 随便起一个名字就可以了,显示在 GitHub Actions 中

on:
  push:
    branches: [ "main" ] # main 分支被 push 时触发
  pull_request:
    branches: [ "main" ] # main 分支有 PR 被 merge 时触发

jobs:
  build:                                        # 构建代码。可以任意起名
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3               # 将代码 pull 到容器内
      - name: Setup .NET                        # 安装 .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 6.0.x                 # 此处取决于项目的 .NET 版本
      - name: Restore dependencies              # 下载依赖文件
        run: dotnet restore
      - name: Build project                     # 构建项目
        run: dotnet build --no-restore
      - name: Run unit tests                    # 运行单元测试
        run: dotnet test --no-build --verbosity normal
      - name: Publish project demo              # 将 Demo 环境的代码打包
        run: dotnet publish MyAPI/MyAPI.csproj -c release -o release --nologo /p:EnvironmentName=Demo
      - name: Upload a build artifact demo      # 将打包好的 Demo 发布文件上传到 GitHub Artifact
        uses: actions/upload-artifact@v3
        with:
          name: myapi-demo
          path: release
      - name: Publish project staging           # 将 Staging 环境的代码打包
        run: dotnet publish MyAPI/MyAPI.csproj -c release -o release --nologo /p:EnvironmentName=Staging
      - name: Upload a build artifact staging   # 将打包好的 Staging 发布文件上传到 GitHub Artifact
        uses: actions/upload-artifact@v3
        with:
          name: myapi-staging
          path: release

  deploy-demo:                                              # 部署 Demo 环境代码。可以任意起名
    needs: build                                            # 需要在上面的 build 执行完成后再执行
    runs-on: self-hosted                                    # 运行在安装在服务器的 runner 上
    steps:
      - name: Take application offline                      # 停止 IIS 服务
        run: New-Item -Type File -Name app_offline.htm -Path C:/Users/Administrator/Desktop/GitHub-Actions/MyAPI.Demo -Force
      - name: Download new binaries over the top of the app # 将 Artifact 下载到指定文件夹
        uses: actions/download-artifact@v3
        with:
          name: myapi-demo
          path: C:/Users/Administrator/Desktop/GitHub-Actions/MyAPI.Demo
      - name: Bring the app back online                     # 重新启动 IIS 服务
        run: remove-item C:/Users/Administrator/Desktop/GitHub-Actions/MyAPI.Demo/app_offline.htm

  deploy-staging:                                           # 部署 Staging 环境代码。可以任意起名
    needs: deploy-demo                                      # 需要在上面的 deploy-demo 执行完成后再执行
    runs-on: self-hosted                                    # 运行在安装在服务器的 runner 上
    steps:
      - name: Take application offline                      # 停止 IIS 服务
        run: New-Item -Type File -Name app_offline.htm -Path C:/Users/Administrator/Desktop/GitHub-Actions/MyAPI.Staging -Force
      - name: Download new binaries over the top of the app # 将 Artifact 下载到指定文件夹
        uses: actions/download-artifact@v3
        with:
          name: myapi-staging
          path: C:/Users/Administrator/Desktop/GitHub-Actions/MyAPI.Staging
      - name: Bring the app back online                     # 重新启动 IIS 服务
        run: remove-item C:/Users/Administrator/Desktop/GitHub-Actions/MyAPI.Staging/app_offline.htm

至此,一旦这个文件出现在 GitHub 的 main 分支上,这条 workflow 就被自动添加到 Actions 中了,同时会开始第一次运行。

遗留问题

目前这套流程还有一些遗留问题:

  • 没有办法给某个步骤添加审批,比如部署到 staging 需要 block 住等待某人 approve。所以现在会一口气跑完整个 workflow。这个可能是 GitHub Orginization 的功能,需要付费。
  • runner 这个东西在每次运行我们的 deployment 之前,一旦 runner 有新版本,它会自动升级自己,但是它的升级速度实在太慢(至少在中国大陆是这样的)。目前还不知道怎么屏蔽自动升级,或提高升级速度。
  • 安装好的可执行文件是一个 *.cmd 文件,无法直接安装为系统服务,编写成 *.bat 文件的话安装会报错。所以目前还得手动运行,并保持一个 cmd 窗口。勘误请见 2023年5月21日更新
  • 好像没法指定跑哪个 branch,只能跑 main branch。

结语

大概就是这个样子啦。如果有什么缺少我会回来更新文档。如果有什么更好的办法欢迎补充~!


2023年1月18日更新

突然想起来一个点,为了部署不同的分支到环境,可以曲线救国:新建一个分支,专门把这个分支上的代码发布到服务器上。所以在 yaml 文件中,on 下面的 branches 就是新建的这个分支。当然了,只要 push event 就可以了。

同时,也可以将发布到不同的环境拆分成不同的 actions。

所以此时,我建了两个 yaml 文件,分别叫 deploy-to-demo.ymldeploy-to-staging.yml。代码分别如下:

# file name: deploy-to-demo.yml

name: Deploy to Demo

on:
  push:
    branches: [ "deploy-to-demo" ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 6.0.x
      - name: Restore dependencies
        run: dotnet restore
      - name: Build project
        run: dotnet build --no-restore
      - name: Run unit tests
        run: dotnet test --no-build --verbosity normal
      - name: Publish project
        run: dotnet publish My.API/My.API.csproj -c release -o release --nologo /p:EnvironmentName=Demo
      - name: Upload a build artifact
        uses: actions/upload-artifact@v3
        with:
          name: my.api-demo
          path: release
          
  deploy:
    needs: build
    runs-on: self-hosted
    steps:
      - name: Take application offline
        run: New-Item -Type File -Name app_offline.htm -Path C:/Users/Administrator/Desktop/GitHub-Actions/My.API.Demo -Force
      - name: Download new binaries over the top of the app
        uses: actions/download-artifact@v3
        with:
          name: my.api-demo
          path: C:/Users/Administrator/Desktop/GitHub-Actions/My.API.Demo
      - name: Bring the app back online
        run: remove-item C:/Users/Administrator/Desktop/GitHub-Actions/My.API.Demo/app_offline.htm

# file name: deploy-to-staging.yml

name: Deploy to Staging

on:
  push:
    branches: [ "deploy-to-staging" ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: 6.0.x
      - name: Restore dependencies
        run: dotnet restore
      - name: Build project
        run: dotnet build --no-restore
      - name: Run unit tests
        run: dotnet test --no-build --verbosity normal
      - name: Publish project
        run: dotnet publish My.API/My.API.csproj -c release -o release --nologo /p:EnvironmentName=Staging
      - name: Upload a build artifact
        uses: actions/upload-artifact@v3
        with:
          name: my.api-staging
          path: release
          
  deploy:
    needs: build
    runs-on: self-hosted
    steps:
      - name: Take application offline
        run: New-Item -Type File -Name app_offline.htm -Path C:/Users/Administrator/Desktop/GitHub-Actions/My.API.Staging -Force
      - name: Download new binaries over the top of the app
        uses: actions/download-artifact@v3
        with:
          name: my.api-staging
          path: C:/Users/Administrator/Desktop/GitHub-Actions/My.API.Staging
      - name: Bring the app back online
        run: remove-item C:/Users/Administrator/Desktop/GitHub-Actions/My.API.Staging/app_offline.htm


2023年5月21日更新

对于前面遗留问题里提到的必须运行一个 console 这个问题,是我当时搞错了。

Runner 在 console 里安装的时候有个步骤,会问你是否要将这个 Runner 安装为服务。输入 y 就是了。