GCP MIG 重建后 Ansible 初始化实战:PowerShell + NSSM 挂载 Windows 服务

GCP MIG 重建后 Ansible 初始化实战:PowerShell + NSSM 挂载 Windows 服务

在 GCP 托管实例组(MIG)自动修复或重建 Windows 实例后,如何确保新实例自动完成系统初始化,并用 NSSM 把 PowerShell 脚本挂载为常驻服务?本文给出从 MIG 延迟窗口配置到 Ansible Playbook 调用、再到幂等 PowerShell 脚本的完整落地方案。

背景与问题

GCP 的 MIG 在实例不健康时会自动重建 VM,但重建出来的新实例并不会自动执行你自定义的 Ansible 初始化流程。如果服务依赖 NSSM 挂载的 PowerShell 进程,而该进程尚未启动,健康检查就会失败,进而触发又一次重建,形成循环。

解决思路分两层:MIG 侧利用 initial_delay_sec 给初始化留出窗口期 ;Ansible 侧通过动态清单发现新实例,执行 PowerShell 脚本完成 NSSM 服务挂载。


一、MIG 侧:配置初始延迟窗口

MIG 的自动修复策略中有一个关键参数 initial_delay_sec。官方文档说明:这是新 VM 初始化并运行启动脚本所需的秒数,在此期间 MIG 会忽略不成功的健康检查,防止过早重建实例。取值范围 0 到 3600 秒,控制台默认值通常为 300 秒。

实操建议 :先手动计时一次完整的 Ansible 初始化流程(上传脚本 → 安装 NSSM → 创建服务 → 启动),然后在此基础上加 30%~50% 的缓冲。例如实测耗时 4 分钟,可以设 initial_delay_sec 为 360。设置过短会导致 MIG 在服务就绪前就判定实例不健康,引发反复重建。


二、Ansible 侧:发现新实例并执行初始化

MIG 重建的实例 IP 会变化,静态 inventory 不可靠。推荐使用 GCP 动态清单插件(google.cloud.gcp_compute),基于实例标签(Labels)自动发现当前所有匹配的 Windows 主机。在实例模板中为 VM 打上统一标签(如 role=app-worker),Ansible 即可自动抓取新重建的实例。

Playbook 的核心任务分为三步:上传 PowerShell 脚本、执行脚本、验证服务状态。其中执行脚本使用 ansible.windows.win_shell,该模块默认通过 PowerShell 运行命令,适合调用 .ps1 文件。

yaml 复制代码
- name: GCP MIG Windows 实例初始化
  hosts: windows_mig_instances
  tasks:
    - name: 确保 NSSM 已安装
      chocolatey.chocolatey.win_chocolatey:
        name: nssm
        state: present

    - name: 创建应用目录
      ansible.windows.win_file:
        path: C:\App\logs
        state: directory

    - name: 上传 NSSM 初始化脚本
      ansible.windows.win_copy:
        src: files/setup-nssm-service.ps1
        dest: C:\App\setup-nssm-service.ps1

    - name: 执行服务安装
      ansible.windows.win_shell: |
        powershell.exe -ExecutionPolicy Bypass -File C:\App\setup-nssm-service.ps1 `
          -Action install `
          -ServiceName "MyAppService" `
          -ScriptPath "C:\App\worker.ps1"
      register: install_result

    - name: 验证服务运行状态
      ansible.windows.win_service:
        name: MyAppService
        state: started
      register: svc_check

    - name: 输出初始化结果
      ansible.builtin.debug:
        msg: "服务状态: {{ svc_check.state }}"

为什么不用 community.windows.win_nssm 模块? 该模块确实可以直接创建 NSSM 服务,但它有一个关键限制:state=present 时服务创建后不会自动启动 ,需要额外配合 ansible.windows.win_service 来启动。如果你的场景只需要简单挂载且不涉及复杂日志轮转、环境变量等配置,用模块更简洁。但本文的场景需要控制日志重定向、自动重启策略等细节,用自定义 PowerShell 脚本封装更灵活。


三、核心脚本:setup-nssm-service.ps1

该脚本设计为幂等 ,支持 install / uninstall / status 三种操作,适合在 MIG 重建场景下反复执行。

powershell 复制代码
<#
.SYNOPSIS
    使用 NSSM 将 PowerShell 脚本挂载为 Windows 服务(幂等)
.DESCRIPTION
    供 Ansible 在 GCP MIG 重建后调用。
.PARAMETER Action
    install / uninstall / status
#>
param(
    [Parameter(Mandatory=$true)]
    [ValidateSet("install","uninstall","status")]
    [string]$Action,
    [string]$ServiceName = "MyAppService",
    [string]$ScriptPath  = "C:\App\worker.ps1",
    [string]$LogDir      = "C:\App\logs",
    [string]$DisplayName = "My Application Worker",
    [string]$Description = "由 Ansible 通过 NSSM 部署的后台任务"
)

$ErrorActionPreference = "Stop"

function Assert-Admin {
    $id = [Security.Principal.WindowsIdentity]::GetCurrent()
    $p = New-Object Security.Principal.WindowsPrincipal($id)
    if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw "此脚本必须以管理员身份运行"
    }
}

function Get-NssmPath {
    $cmd = Get-Command nssm.exe -ErrorAction SilentlyContinue
    if (-not $cmd) { throw "nssm.exe 未找到,请先 choco install nssm" }
    return $cmd.Source
}

function Test-ServiceExists($Name) {
    return ($null -ne (Get-Service -Name $Name -ErrorAction SilentlyContinue))
}

Assert-Admin
$nssm = Get-NssmPath

switch ($Action) {

    "install" {
        if (-not (Test-Path $ScriptPath)) { throw "脚本不存在: $ScriptPath" }
        if (-not (Test-Path $LogDir)) {
            New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
        }

        $psExe = (Get-Command powershell.exe).Source
        $args = "-ExecutionPolicy Bypass -NoProfile -File `"$ScriptPath`""

        if (-not (Test-ServiceExists $ServiceName)) {
            & $nssm install $ServiceName $psExe $args
        } else {
            & $nssm set $ServiceName Application $psExe
            & $nssm set $ServiceName AppParameters $args
        }

        & $nssm set $ServiceName Start SERVICE_AUTO_START
        & $nssm set $ServiceName DisplayName $DisplayName
        & $nssm set $ServiceName Description $Description
        & $nssm set $ServiceName AppStdout (Join-Path $LogDir "$ServiceName-stdout.log")
        & $nssm set $ServiceName AppStderr (Join-Path $LogDir "$ServiceName-stderr.log")
        & $nssm set $ServiceName AppRotateFiles 1
        & $nssm set $ServiceName AppRotateSeconds 86400
        & $nssm set $ServiceName AppRestartDelay 5000

        $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
        if ($svc.Status -ne "Running") {
            & $nssm start $ServiceName
        }
        Write-Host "完成。服务状态: $((Get-Service $ServiceName).Status)"
    }

    "uninstall" {
        if (-not (Test-ServiceExists $ServiceName)) {
            Write-Host "服务不存在: $ServiceName"; return
        }
        & $nssm stop $ServiceName 2>$null
        Start-Sleep -Seconds 2
        & $nssm remove $ServiceName confirm
        Write-Host "已卸载: $ServiceName"
    }

    "status" {
        if (-not (Test-ServiceExists $ServiceName)) {
            Write-Host "服务不存在: $ServiceName"; exit 1
        }
        Get-Service -Name $ServiceName | Format-List Name, Status, StartType, DisplayName
    }
}

脚本设计要点

幂等性:安装前检测服务是否存在,存在则只更新配置,不重复创建。MIG 重建或 Ansible 重跑都不会报错。

日志轮转 :AppRotateFiles 1 配合 AppRotateSeconds 86400 实现每日日志切分,避免日志文件无限膨胀。AppRestartDelay 5000 让服务崩溃 5 秒后自动重启,这对无人值守的 MIG 实例尤为关键。

账户说明 :脚本默认以 LocalSystem 运行服务。如果 worker.ps1 需要访问网络资源(如域 API、UNC 路径),需要在脚本中追加 nssm set <服务名> ObjectName "DOMAIN\User" "password" 来切换运行身份。敏感密码建议通过 Ansible Vault 传入,而不是硬编码在脚本里。


四、避坑清单

坑 1:initial_delay_sec 设得太短。这是 MIG + Ansible 组合最容易翻车的地方。如果延迟只有 60 秒,而 Ansible 安装 NSSM + 创建服务 + 启动需要 3 分钟,VM 会在服务就绪前被判定不健康而重建,形成死循环。务必先实测再设定。

坑 2:NSSM 不在 PATH 中 。脚本通过 Get-Command nssm.exe 定位二进制文件。虽然 Playbook 中先用 Chocolatey 安装了 NSSM,但 Chocolatey 安装后可能需要刷新环境变量。稳妥做法是在 Playbook 的 win_chocolatey 任务后加一个 win_shell 执行 refreshenv(如果目标机有 Chocolatey 的 refreshenv 函数),或者让 Playbook 使用绝对路径调用 NSSM。

坑 3:win_shell 无法启动后台进程 。Ansible 官方文档特别指出,win_shell 会等待所有子进程退出后才返回,因此不适合直接用来 spawn 长时间运行的进程。这正是我们需要 NSSM 的原因:把长时任务注册为 Windows 服务,由服务管理器托管,Ansible 只负责"安装服务"这个短时操作。

坑 4:服务创建后忘了启动 。如果你选择用 community.windows.win_nssm 模块而不是自定义脚本,务必记得模块的 state=present 不会自动启动服务 ,需要紧跟一个 ansible.windows.win_service 任务来 state: started。


总结

整个方案的执行链路是:MIG 重建实例 → initial_delay_sec 窗口期内健康检查被忽略 → Ansible 动态清单发现新 VM → 上传并执行 setup-nssm-service.ps1 → NSSM 注册服务并启动 → 服务开始响应健康检查 → MIG 确认实例健康。

这套方案的关键在于把"服务托管"交给 NSSM、把"配置管理"交给 Ansible、把"重建容错"交给 MIG 的初始延迟,三者各司其职,避免在脚本里硬编码复杂的重试逻辑。

作者简介

Moshow,一名热爱技术与分享的 Technical Lead & 数据科学家 & SpringBoot 专家。CSDN 博客之星 2025 年度 TOP100,Github 4K stars。

持有多项权威认证,包括:GCA 谷歌云架构师认证、Neo4j 数据科学家认证、阿里云 ACP 认证、金蝶高级 CRM 供应链工程师认证、PMP 项目管理认证、CSPM 项目管理专业人员能力评价认证、广东省人工智能训练师认证。

相关推荐
染指11101 小时前
160.Agent-LangGraph-自定义主管架构智能体
人工智能·windows·microsoft·langchain·1024程序员节·agents
꧁坚持很酷꧂2 小时前
Windows10电脑通过Xftp传输文件给Windows平板
windows·电脑
说实话起个名字真难啊2 小时前
windows server 2022安装winget方法
windows
阿尼卡~12 小时前
VS Code 配置 C 语言开发环境(Windows 10/11):从 GCC 安装到运行、断点调试,一篇搞定
c语言·开发语言·windows
liyi_hz200817 小时前
O2OA(翱途) V10.1 即将发布|功能再升级,安全再加固,协同更高效!
java·javascript·windows·开源软件
七牛云行业应用19 小时前
DeepSeek Harness(dsh)安装完整指南:Windows、macOS、Linux 三平台 + 桌面端与 Web 端全覆盖
linux·windows·macos·ai·ai编程
起个名字都难啊1 天前
visual studio C/C++空项目修改为CUDA c++
c++·windows·visual studio
xixiaoyunya1 天前
轻量级 Windows 文件共享方案实测:零配置部署私有网盘,兼容全平台浏览器访问
windows
垆边人似月.1 天前
第二篇:列表、字典与流程控制 / Lists, Dictionaries & Control Flow
windows