Raxx · internal docs

internal · gated

CI agent instance sizing (#4545)

Symptom

The Woodpecker CI heavy-build agent class (c7a.xlarge / c6a.xlarge / c5a.xlarge, all 4 vCPU / 8 GB) OOMs on memory-bound compile lanes even with WOODPECKER_MAX_WORKFLOWS=1 and a 6 GiB per-container Docker memory cap (WOODPECKER_BACKEND_DOCKER_LIMIT_MEM / ..._MEM_SWAP, applied live 2026-08-29 as a stopgap).

Evidence

Conclusion: this needs either swap or a larger instance class. A swapfile alone is a backstop, not a fix — it trades OOM-kills for a documented 3.5x-wall-time-class slowdown once the working set spills past physical RAM (the CARGO_BUILD_JOBS=1 data point above is the closest proxy for what swap thrashing would feel like on this same compile). An EBS volume by itself is not a fix either — disk isn't RAM.

Pricing (us-east-2, priced 2026-08-29/31 via aws pricing + `aws ec2

describe-spot-price-history`; spot fluctuates, treat as a live reference point not a locked-in number)

Instance class vCPU RAM On-demand $/hr Spot $/hr (us-east-2, low AZ) Monthly @ 2x on-demand floor Monthly @ 1x OD + 1x spot Headroom vs ~8 GiB working set (cap = RAM − 2 GiB host reserve)
c6a.xlarge (current fleet) 4 8 GB $0.1530 $0.0435 $223.55 $143.56 None — fails deterministically today
r6a.large 2 16 GB $0.1134 $0.0326 $165.69 $106.66 Fits (14 GiB cap), but halves vCPU vs current fleet — regression risk for the parallel C++ build
m6a.xlarge 4 16 GB $0.1728 $0.0622 $252.48 $171.68 Fits, ~6 GiB margin above the 8 GiB target, same vCPU count as today
r6a.xlarge 4 32 GB $0.2268 $0.0828 $331.38 $226.18 Fits, ~22 GiB margin, same vCPU, room to raise WOODPECKER_MAX_WORKFLOWS later
c6a.2xlarge 8 16 GB $0.3060 $0.1059 $447.10 $300.92 Fits, but pays for 8 vCPU the workload doesn't need (memory-bound, not CPU-bound) — rejected

"Monthly @ 2x on-demand floor" is a costing reference point per the task brief, not the actual fleet policy — the fleet keeps its documented mixed policy (on_demand_base_capacity = 1, spot burst above the floor, price-capacity-optimized allocation); the "1x OD + 1x spot" column is closer to today's real steady-state cost shape.

Recommendation

Leanest: m6a.xlarge (4 vCPU / 16 GB). Keeps the same vCPU count as the current fleet (no parallel-compile regression for the C++ build), and its 16 GB fixes the OOM with real margin: cap the container at 14 GiB (16 GB − 2 GiB host reserve for dockerd/agent/SSM), which sits ~6 GiB above the observed ~8 GiB avenue-api working set.

One step up: r6a.xlarge (4 vCPU / 32 GB). Same vCPU as "lean," double the RAM. Cap the container at 30 GiB (32 GB − 2 GiB host reserve), ~22 GiB above the working-set target — headroom for the working set to grow, or for eventually raising WOODPECKER_MAX_WORKFLOWS above 1 once co-scheduling is otherwise addressed.

Both options are wired as a paired, documented two-line change in infra/ci/terraform.tfvars.example (commented "option A: lean" / "option B: headroom" blocks) and in infra/ci/variables.tf (agent_instance_types and agent_docker_limit_mem_bytes descriptions). Picking one is a two-line tfvars edit; no other files need to change.

r6a.large (2 vCPU / 16 GB) was considered — it's the cheapest option that clears the memory bar — but it halves vCPU count vs. the current 4-vCPU fleet, which risks slowing the TradeMasterAPI C++ build's parallel compilation. Rejected in favor of keeping vCPU count constant. c6a.2xlarge (8 vCPU / 16 GB) was considered and rejected: it pays for double the vCPU count for a workload that's memory-bound, not CPU-bound, without adding meaningfully more RAM headroom than m6a.xlarge.

Swapfile backstop (either option)

Not required for either recommended option — both fit the ~8 GiB working set with real headroom. If added anyway as a defense-in-depth backstop against future working-set growth, a 4-8 GB swapfile would need to be added in infra/ci/templates/user_data_agent.sh.tpl (this instance class has none today) — e.g., a fallocate + mkswap + swapon step alongside the existing Docker install step, before the agent's .env/compose files are written. Be honest with the operator about the tradeoff: swap is a backstop, not a substitute for RAM sizing — Project Avenue's own CARGO_BUILD_JOBS=1 measurement (3.5x wall time to shave ~1 GB off peak RSS) is the closest proxy for how much a swap-driven slowdown would cost here if the working set spills past physical RAM under either recommended class.

Rollout (deferred — NOT part of this PR)

The actual cutover is a separate, later, operator-scheduled action:

  1. Announce a ci-maintenance-silence window.
  2. Land one of the two tfvars option blocks above (uncomment, comment out the "current" block).
  3. terraform plan — expect a launch template replacement and an ASG instance refresh (instance_refresh block already handles rolling replacement, min_healthy_percentage = 0).
  4. terraform apply during the window.
  5. Confirm agents register, run a canary build of each heavy lane (queue C++ build, avenue-api cargo build), watch CloudWatch for OOM events.
  6. Close the silence window.

This PR does not perform any of the above — it only lands the priced options and the tfvars/variable plumbing to make picking one a two-line change when the operator schedules the cutover.

Follow-ups