EC2 Spot
What does ZopNight detect here?
Spot migration candidates are running non-production EC2 instances with 30-day average CPU at or above 5%, after vetoes for production tags, DR/standby roles, bastions, stateful data services, and Windows licensing. ZopNight derives the savings fraction from real on-demand versus Spot SKU rates, abstaining whenever no live rate exists.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-097 |
| Category | discount |
| Severity | low |
| Metric | none — pure configuration read |
| Source | spot_adoption.go |
Where it applies
The vetoes that keep a box off Spot
- resource.Status: running
- idle-abstain (folded from RC-013 merge): 30-day avg CPUUtilization < 5% => defer to RC-003 (rightsizing) / RC-001 (idle stop). Only abstains when the CPU series is present (5% deliberately matches the RC-003 1-tier band so the rules partition cleanly).
- CPU confidence/evidence: CPU present and ≥ 5% => rec carries the windowed CPU series as MetricEvidence at Confidence=high; CPU absent (rare; CPUUtilization is native to the AWS/EC2 CloudWatch namespace, agent-free) => rec still fires at Confidence=medium with no CPU evidence.
- metadata: lifecycle=spot / purchase_type=Spot => skip, already Spot (source of truth)
- tag fallback: instance_lifecycle=spot (customer-applied)
- tag: any spotinst:* key => skip, managed by Spot.io Ocean / Elastigroup (already spot-optimized)
- tag: aws:autoscaling:groupName => skip (ASG-managed)
- env tag (env/environment/stage/tier = prod): hard veto ahead of pricing (item T0061): a production workload receives no Spot rec. Suitability is not inferred from name-absence; the dev/test-name veto was removed (RC-013 fold) so dev/test boxes now fire here.
- prod-NAME veto (#1906 B1): a prod-named box with no env tag (e.g. PRD-A-SHAN-38) is now vetoed via the shared isProdName helper, not only via isDevTestName.
- critical-role NAME veto (#1906 B2): a DR / backup / standby / failover / replica box is a resilience asset that must absorb production traffic on failover, so it must never run on interruptible Spot. Vetoed via matchesCriticalRole, where criticalRolePatterns = dr, drp, disaster, backup, bkp, standby, passive, failover, replica, matched at a segment boundary (a regex bounded by start/end or a non-alphanumeric separator) so dr trips DR-A-02 / x-standby-y but not drive.
- bastion / jump-host veto: a Spot reclaim (2-minute notice) tears down the SSH entry point and drops access to the whole environment behind it, so it is vetoed via isBastionOrJumpHost.
- stateful data-service veto (W4-F05): a self-managed database / Kafka / Elasticsearch / Redis box holds durable state and is typically single-replica, so a Spot reclaim risks data loss with no graceful drain, so it is vetoed via matchesStatefulWorkload (prefers the discoverer’s engine/data-tier classification, falls back to a boundary-matched name token).
- OS/license veto (#1906 B4): Windows and SQL-Server instances carry per-second licensing and slow boot/license-activation, so a Spot reclaim is materially more disruptive. Read via isSpotIncompatibleOS from resource.Metadata (os_family, falling back to the raw platform string); an absent OS signal abstains (the rec may still fire) rather than vetoing on missing evidence.
Real per-SKU spot delta, or nothing
real-rate-or-abstain (2026-06-27 directive: no advisory/$0 fallback for a cost rule). fraction = model.DeriveSpotFraction(tiers, <negative sentinel>) returns a genuine per-SKU delta against the Spot rate for that instance type in (0,1), or the negative sentinel on any guard failure (missing/zero/inverted/out-of-band OnDemand or Spot rate). A non-positive result is exactly “no live rate” and the rule abstains, returning nil. When a real rate is present, savings = cost × fraction, optimized = cost − savings. The cost basis arrives via EvaluationContext.Pricing; a flat fraction is never fabricated on it.
Moving to a mixed instances policy
- Assess workload interruption tolerance (stateless? containerised? batch?)
- If suitable, replace the standalone instance with an Auto Scaling Group using a mixed instances policy (or an EC2 Fleet, request type=instant, for non-stateful single-replacement cases)
- Use Spot interruption notices (2-minute warning) to drain gracefully
- Consider multiple instance types to improve availability
- Tag the new instances with instance_lifecycle=spot for tracking
CPU utilization as idle gate and evidence
CPUUtilization (declared via RequiredMetrics, 30d; idle-abstain gate + evidence; read from evalCtx.MetricsByName via MetricByNameForUID)