抢占式实例的自动监控与恢复

admin 2026-03-12 23:15:26 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文针对红队基础设施中抢占式实例易被回收的问题,提出一种通用自动监控与恢复方案。通过定期TCP探测SSH端口判断实例状态,避免依赖云厂商API,并利用Terraform声明式特性自动补齐实例。实践表明该方案能在5分钟内完成恢复,有效保障了业务连续性。 综合评分: 93 文章分类: 红队,实战经验,安全建设,云安全,安全开发


cover_image

抢占式实例的自动监控与恢复

原创

r0fus0d r0fus0d

ffffffff0x

2026年3月12日 00:46 北京

这个问题的背景

搞红队基础设施的都知道,云上资源开销是个绕不过去的问题。一个代理池场景随随便便就是好几台 ECS,按量计费一天下来费用不低。后来我们在 RedC 里把阿里云和 AWS 的代理场景全部切到了抢占式实例(Spot Instance),成本直接砍掉 60%~90%,效果立竿见影。

但抢占式实例有个众所周知的问题:它随时可能被云厂商回收


实际遇到

之前开了个某个云的代理池场景,几十台抢占式 ECS,跑着跑着其中部分被回收了。直到发现代理可用率掉了一半才上控制台看——原来是被释放了。

这就引出了两个很现实的问题:

  1. 怎么知道实例被回收了? 云厂商的中断通知机制各不相同,有的要接 API,有的要轮询元数据服务,而且只能在实例内部获取。对于一个管理多个场景、跨多个云厂商的工具来说,逐一对接太碎片化了。
  2. 知道了之后怎么办? 手动上去重新开?那还不如用按量的。能不能自动把被回收的实例补回来?


思路:不依赖云厂商通知,从外部探测

一开始想的是去对接各个云厂商的中断通知 API,但想了想放弃了。原因:

  • 阿里云的抢占式实例中断事件可以通过 CloudMonitor 事件订阅拿到,但需要额外配置事件规则、回调地址
  • AWS 可以用 EventBridge 监听 Spot 中断警告,也挺麻烦
  • 腾讯云、火山引擎各有各的接口

这些方案对一个本地运行的 GUI 工具来说,太重了。

换个角度想——抢占式实例被回收之后,最直观的表现是什么?就是机器没了,连不上了。

所以最终选了一个最简单粗暴的方案:定期 TCP 探测 SSH 端口。每隔一段时间,对所有运行中的 Spot 场景的公网 IP 做一次 22 端口探测。连不上,就说明实例大概率已经被回收了。

这个方案的好处是:

  • 不依赖任何云厂商的特定 API
  • 不需要在实例内部装 agent
  • 对 RedC 已有的场景管理结构完全兼容

坏处也有——不够实时,探测间隔决定了你最晚多久能发现。不过对我们的场景来说,3 分钟的延迟完全可以接受。


具体怎么做的

[整体架构]

监控模块作为一个后台常驻 goroutine 运行,启动后按固定间隔执行扫描。流程大概是这样:

启动监控 → 等待 60s(让应用初始化完)→ 每 3 分钟扫描一轮
    ↓
遍历所有运行中的场景
    ↓
检查 .tf 文件是否包含 spot 相关配置(spot_strategy / market_type / is_spot_instance)
    ↓
从 terraform output 中提取公网 IP
    ↓
对每个 IP 做 TCP :22 探测(3 次重试,每次超时 10s,间隔 15s)
    ↓
连不上 → 标记为已回收 → 发送通知 → 触发自动恢复

[怎么判断一个场景是 Spot 实例]

不是所有场景都用了抢占式实例,所以第一步是识别。做法很简单——扫描场景目录下的 .tf 文件,看有没有这几个关键字:

spotPatterns := []string{
    `spot_strategy`,    // 阿里云
    `market_type`,      // AWS
    `is_spot_instance`, // 腾讯云
}

阿里云的 Spot 实例在 Terraform 里长这样:

resource "alicloud_instance" "instance" {
  instance_type              = "ecs.n1.tiny"
  spot_strategy              = "SpotWithPriceLimit"
  // ...
}

AWS 的则是:

resource "aws_instance" "pte_node" {
  instance_type = "t4g.nano"
  instance_market_options {
    market_type = "spot"
  }
  // ...
}

通过静态文本匹配就能判断,不需要调用云 API。

[SSH 探测的细节]

探测逻辑做了一些容错处理:

func (m *SpotMonitor) probeSSH(ip string) bool {
    addr := net.JoinHostPort(ip, "22")
&nbsp; &nbsp;&nbsp;for&nbsp;attempt :=&nbsp;0; attempt <&nbsp;3; attempt++ {
&nbsp; &nbsp; &nbsp; &nbsp; conn, err := net.DialTimeout("tcp", addr,&nbsp;10*time.Second)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;err ==&nbsp;nil&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; conn.Close()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;true
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;attempt <&nbsp;2&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;select&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;case&nbsp;<-time.After(15&nbsp;* time.Second):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;case&nbsp;<-m.stopCh:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;true&nbsp;// 正在停止,避免误报
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;false
}

一次连不上不算数,连续 3 次都连不上才判定为回收。每次超时 10 秒,重试间隔 15 秒。这样单个 IP 的判定周期大约是 1 分钟,能有效过滤掉网络抖动导致的误报。

另外一个细节是 alerted 机制——已经报过警的 IP 不会重复报。用 caseID:ip 作为 key 记录,避免每一轮扫描都重复告警同一个已挂的实例。

[自动恢复]

检测到实例被回收后,如果用户开启了自动恢复选项,会触发 Terraform 的 plan + apply 操作。Terraform 的声明式特性在这里很好用——你声明了要 2 台实例,现在只剩 1 台,apply 一下自然会把缺的那台补回来。

但这里踩了一个坑。

RedC 里,场景启动后状态是 running,而 TfApply() 方法内部有一个状态检查:如果当前状态已经是 running,会直接拒绝执行并返回错误”场景正在运行中”。这个检查本来是防止用户误操作的,但在自动恢复场景下就有点碍事。

解法是绕过上层封装,直接调用底层的 TfPlan() 和 TfApply() 函数:

func&nbsp;(m *SpotMonitor)&nbsp;attemptRecover(c *redc.Case, downIPs []string) {
&nbsp; &nbsp;&nbsp;// 直接调用底层函数,绕过 Case.TfApply() 的状态检查
&nbsp; &nbsp;&nbsp;if&nbsp;err := redc.TfPlan(c.Path, c.Parameter...); err !=&nbsp;nil&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 恢复失败,发通知
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;if&nbsp;err := redc.TfApply(c.Path, c.Parameter...); err !=&nbsp;nil&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 恢复失败,发通知
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// 如果之前标记了 terminated,改回 running
&nbsp; &nbsp;&nbsp;if&nbsp;c.State == redc.StateTerminated {
&nbsp; &nbsp; &nbsp; &nbsp; c.StatusChange(redc.StateRunning)
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// 清除该场景的告警记录,让新 IP 可以被监控
&nbsp; &nbsp; m.ResetAlert(c.Id)
}

恢复成功后,还要做一件事:清除该场景的告警记录(ResetAlert)。因为恢复后实例的 IP 会变,旧 IP 的告警记录已经没意义了,而新 IP 需要能被正常监控。

实现效果如下

开启 spot 实例监控

开个场景,验证一下,这里 一台 214,一台 222

手动把 214 释放掉

过一会,会自动把 214 那台重新开启,222 未受到影响,场景恢复

[通知集成]

  1. 系统通知:macOS / Windows / Linux 原生桌面通知,适合人在电脑前的场景
  2. Webhook 推送:支持 Slack、钉钉、飞书、Discord、企业微信,适合团队协作或不在电脑前的场景

通知内容包括场景名、被回收的 IP、是否全部回收、自动恢复结果等。

[做成可选项]

因为 Spot 监控需要持续做 TCP 探测,对于不用抢占式实例的用户来说是没必要的开销。所以把它做成了设置页面里的一个开关,默认关闭,用的时候再打开。自动恢复也是单独的开关,可以只开监控不开恢复。

配置存在本地的 gui_settings.json 里,重启不丢失。


实际效果

目前这套方案,有几个实际的体验:

  • 阿里云的抢占式 ECS 被回收后,大约 3 分钟内能检测到并触发恢复,新实例通常在 2 分钟内就起来了。整个中断窗口大约 5 分钟。
  • AWS 的 Spot Instance 同理,探测 + 恢复的总时间差不多。
  • 代理池场景里如果 2 台实例只回收了 1 台,Terraform apply 只会补那 1 台,不会影响另一台正在运行的实例,代理可用率不会归零。

对于红队基础设施的场景来说,这个恢复速度基本够用。

当然如果场景可用性要求特别高(比如不能断超过 1 分钟),那还是老老实用用按量实例吧。


小结

  • 不去对接各云厂商的中断通知,用 TCP 端口探测作为统一的健康检查手段
  • 利用 Terraform 的声明式特性,apply 一下就能把被关闭的实例补回来
  • 通知做多层覆盖,确保不会错过

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:ffffffff0x r0fus0d r0fus0d《抢占式实例的自动监控与恢复》

    评论:0   参与:  0