如何通过GitHub Actions实现Azure虚拟机自动启停适配流水线场景
GitHub Actions 3台Windows自托管运行器自动启停落地方案
核心约束梳理
先对齐要满足的规则和已知坑点,避免走回头路:
- 流水线触发时自动唤醒对应虚拟机,无需手动提前开机
- 流水线执行完成后自动关闭虚拟机,但关机前必须确认机器上没有其他正在运行的任务,不能打断其他作业
- 云平台计费规则坑:Azure虚拟机仅在操作系统内执行关机不会停止计费,必须通过平台API/门户触发解除分配式关机才会终止计费;AWS同理,必须走API触发实例停止,OS内关机无效
- 关机操作绝对不能在待关闭的自托管运行器上执行:运行器被关机时会强制中断Runner进程,导致流水线作业状态上报失败、标记为异常,必须把关机步骤放到其他运行器上(GitHub官方托管的
ubuntu-latest就够用,免费额度完全覆盖这类轻量操作)
具体实现步骤
1. 前期准备
- 给3台Windows自托管Runner分别打唯一标识标签,比如
win-runner-01/win-runner-02/win-runner-03,把每个标签对应云平台的虚拟机实例ID、云API访问凭证全部存到GitHub仓库的Secret里,不要硬编码在流水线中 - 给仓库默认的
GITHUB_TOKEN开通actions:read权限,方便后续查询Runner的运行状态 - 所有启停操作的控制面步骤,全部固定跑在GitHub官方提供的
ubuntu-latest托管运行器上,不占用自托管Runner资源,也不会出现关机把自身进程中断的问题
2. 流水线触发自动开机
在所有需要用到Windows自托管Runner的流水线最前端,加一个前置启动Job,逻辑如下:
- 先根据调度规则(比如轮询、按标签匹配)选定本次要跑任务的目标Runner,拿到对应的标签和云虚拟机ID
- 调用对应云平台的开机API启动VM,轮询等待VM进入运行状态、Runner服务成功连接GitHub后,再放行后续的实际业务Job
参考代码片段:
jobs: pre-wake-runner: runs-on: ubuntu-latest outputs: target_runner_label: ${{ steps.select-runner.outputs.runner_label }} target_vm_id: ${{ steps.select-runner.outputs.vm_id }} steps: - name: 选定本次任务的目标Runner id: select-runner run: | # 这里可替换成自定义调度逻辑,比如优先选当前空闲的Runner,以下为固定选01号机的示例 echo "runner_label=win-runner-01" >> $GITHUB_OUTPUT echo "vm_id=${{ secrets.AZURE_VM_01_RESOURCE_ID }}" >> $GITHUB_OUTPUT - name: 登录云平台启动VM uses: azure/login@v1 # AWS环境替换为aws-actions/configure-aws-credentials即可 with: creds: ${{ secrets.AZURE_SERVICE_PRINCIPAL }} - name: 等待VM和Runner就绪 run: | az vm start --ids ${{ steps.select-runner.outputs.vm_id }} --no-wait # 轮询最多等10分钟,直到VM正常启动 az vm wait --ids ${{ steps.select-runner.outputs.vm_id }} --created --timeout 600 # 实际业务构建任务,跑在刚唤醒的自托管Runner上 actual-build: needs: pre-wake-runner runs-on: ${{ needs.pre-wake-runner.outputs.target_runner_label }} steps: - name: 常规构建、测试、部署步骤 run: Write-Host "执行业务逻辑"
3. 任务结束后安全关机
在流水线最后加一个后置关机Job,必须配置if: always()规则,保证不管前面的构建是成功、失败、还是被手动取消,这个Job都会执行。核心要加一层校验:
- 调用GitHub API查询目标Runner的
busy状态,确认当前Runner上没有任何正在运行、排队的作业 - 仅当确认无活跃任务时,才调用云平台的停止API关机,否则直接跳过,等后续其他任务跑完再触发校验
参考代码片段:
post-shutdown-runner: needs: [pre-wake-runner, actual-build] runs-on: ubuntu-latest if: always() # 无论构建成功失败都执行,避免VM空跑计费 steps: - name: 检查目标Runner是否还有活跃任务 id: check-busy-status env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # 查询对应标签的Runner是否处于忙线状态 IS_BUSY=$(gh api repos/${{ github.repository }}/actions/runners --jq ".runners[] | select(.labels[].name == \"${{ needs.pre-wake-runner.outputs.target_runner_label }}\") | .busy") if [ "$IS_BUSY" = "true" ]; then echo "skip_shutdown=true" >> $GITHUB_OUTPUT echo "检测到Runner上还有其他运行中任务,跳过本次关机" else echo "skip_shutdown=false" >> $GITHUB_OUTPUT fi - name: 登录云平台 if: steps.check-busy-status.outputs.skip_shutdown == 'false' uses: azure/login@v1 with: creds: ${{ secrets.AZURE_SERVICE_PRINCIPAL }} - name: 触发VM解除分配(停止计费) if: steps.check-busy-status.outputs.skip_shutdown == 'false' run: | # 注意Azure这里用deallocate才是停止计费的操作,不要用restart或者OS内shutdown命令 az vm deallocate --ids ${{ needs.pre-wake-runner.outputs.target_vm_id }} --no-wait
避坑提醒
- AWS环境替换对应CLI命令即可:开机用
aws ec2 start-instances,关机用aws ec2 stop-instances,绝对不要在Windows系统内部执行shutdown /s命令,不会停止计费 - 不要给自托管Runner本身配置云平台关机权限,避免Runner被入侵后出现误操作
- 如果3台Runner做负载均衡,可以在选目标Runner的步骤里加逻辑,优先选已经开机且处于空闲状态的Runner,减少开关机等待时间
- 可以额外加个定时巡检Workflow,每天固定跑几次,把所有处于空闲但开机状态的VM关掉,避免异常情况下VM空跑产生费用
内容的提问来源于stack exchange,提问作者Sara June
相关产品推荐
相关产品推荐

