Skip to content

Add ordered instance_types with insufficient-capacity fall-through - #702

Draft
cchristous wants to merge 2 commits into
hashicorp:mainfrom
cchristous:instance-type-fallbacks
Draft

Add ordered instance_types with insufficient-capacity fall-through#702
cchristous wants to merge 2 commits into
hashicorp:mainfrom
cchristous:instance-type-fallbacks

Conversation

@cchristous

@cchristous cchristous commented Aug 24, 2026

Copy link
Copy Markdown

Closes #701

What

Adds an optional, ordered instance_types ([]string) to the shared run
config. When set, it replaces the single instance_type: Packer launches the
first type and, on an insufficient-capacity error, falls through to the next
type in the list, succeeding on the first that launches. If every type is
capacity-starved, the build fails with the last error.

source "amazon-ebs" "mac" {
  # cheapest-first; falls through on Dedicated Host capacity exhaustion
  instance_types = ["mac2.metal", "mac2-m2.metal", "mac-m4.metal"]
  tenancy        = "host"
  # ... all other placement config is preserved across attempts ...
}

Rationale — why this is the right change

A build pinned to one instance_type fails hard with
InsufficientInstanceCapacity / InsufficientHostCapacity when that type is
capacity-starved, even when an equivalent type would launch. The driving case:
macOS arm64_mac AMIs build on mac2.metal (M1) via tenancy=host into a
License Manager host resource group; M1 Dedicated Host capacity is
intermittently exhausted, but the same AMI is cross-compatible with
mac2-m2.metal (M2) and mac-m4.metal (M4). An ordered list lets the build
succeed on the first type with capacity.

Design choices:

  • instance_types []string, replacing instance_type — chosen over an
    instance_type_fallbacks supplement because it reads as one ordered priority
    list and matches the existing precedent of spot_instance_types on the Spot
    path. It's mutually exclusive with instance_type and spot_instance_types,
    reusing the existing exclusivity rule; instance_type alone is untouched and
    fully backward compatible.
  • Centralized, testable capacity classifier (awserrors.IsCapacityError) —
    fall-through happens only on InsufficientInstanceCapacity,
    InsufficientHostCapacity, InsufficientReservedInstanceCapacity. Every
    other error (config/permission/quota — AuthFailure, InstanceLimitExceeded,
    Unsupported, InvalidParameterValue, …) fails the build immediately, so
    real misconfigurations are never masked by silently trying another type.
  • Placement config reused across attempts — the RunInstancesInput is
    built once; only InstanceType changes per attempt, so tenancy,
    host_resource_group_arn, subnet/AZ, license specifications, and block device
    mappings are preserved.
  • Burstable (T-family) types rejected in instance_types — the credit
    specification (forced standard for burstable families to avoid AWS's
    unlimited default) is derived from instance_type, which is empty when
    instance_types is used. Rather than silently launch a t3/t4g entry with
    surprise unlimited credits, Prepare rejects burstable entries with a clear
    error; the feature targets non-burstable capacity-constrained families anyway.
    (If burstable support is ever wanted, the correct fix is per-candidate credit
    derivation inside the fallback loop — not relaxing this guard.)
  • Both common packages — the repo keeps builder/common (aws-sdk-go v1,
    amazon-instance) and common (aws-sdk-go v2, the EBS builders) in lockstep;
    the field, validation, and fallback loop are added to both so all four
    builders behave consistently and the generated docs (driven off the v1
    partial) stay correct.

Alternatives considered: instance_type_fallbacks as a supplement (rejected —
two knobs for one concept); Spot support (out of scope — spot_instance_types
already exists); a broader capacity-code set (rejected — kept to unambiguous
insufficient-capacity codes to avoid masking config errors).

Risk

  • Blast radius: Medium. Touches the shared StepRunSourceInstance.Run
    launch path used by all on-demand builders. Behavior is unchanged unless the
    new instance_types field is set.
  • Change criticality: Low–Medium. The launch call is core, but the change
    wraps the existing RunInstances retry in an ordered loop without altering
    the single-type path; the classifier is conservative (capacity codes only).
  • Overall: Low–Medium.
  • Mitigations & why it's safe to merge: With instance_types unset the
    candidate list is [instance_type], so the loop runs exactly one iteration
    issuing the same RunInstances call with the same per-attempt
    iamInstanceProfile retry as before — byte-for-byte identical behavior for
    existing templates. The only new behavior is gated entirely on the new field.
    The classifier is allow-list based, so only the three insufficient-capacity
    codes trigger fall-through; anything else returns immediately, preserving
    today's fail-fast semantics. Unit tests cover the classifier (capacity vs
    non-capacity codes, wrapped errors, nil) and the fallback loop (first success,
    fall-through, all-exhausted returns the last error, non-capacity aborts
    immediately, single-type path) with a mocked EC2 client; the loop is extracted
    behind the ec2iface.EC2API (v1) / clients.Ec2Client (v2) interface for
    isolated testing. HCL2 spec, struct-markdown partials, and compiled web-docs
    were regenerated via make generate.

Testing

  • make test (unit, -race) — green.
  • New unit tests: awserrors capacity classifier; runInstanceWithFallback
    ordering/selection; instance_types validation — in both builder/common
    (v1) and common (v2).
  • Acceptance tests: not added. Deterministically provoking
    InsufficientInstanceCapacity against real AWS isn't practical; the
    fall-through logic is fully covered by unit tests with a mocked client.

Pre-Submission Review

Two automated review passes were run.

First pass (PR-review + simplify, before opening):

  • Reject burstable (T-family) types in instance_types to prevent a surprise
    unlimited-credits launch (credit spec is instance_type-derived).
  • runInstanceWithFallback returns a clear error instead of (nil, nil) on an
    empty candidate list (defensive).
  • v1 IsCapacityError switched to errors.As for parity with v2 and to
    recognize wrapped capacity errors.
  • Added tests: single-type success / capacity-error propagation; distinguishable
    "returns the last error" assertion; wrapped-capacity-error classifier cases;
    error-message content assertions on the validation tests; burstable rejection.
  • Simplify: hoisted the burstable regexp to a package-level compiled var
    (matching the existing reShutdownBehavior precedent); aligned the duplicated
    v1/v2 IsCapacityError doc comment.

Second pass (full multi-aspect review — correctness, tests, silent-failure,
comments, simplification). No critical/important correctness issues were found
(v1/v2 confirmed in lockstep, backward-compat byte-for-byte). Findings addressed:

  • Fixed the burstable regexp (:?(?: (non-capturing group); it now serves
    as a proper validation gate rather than matching an optional literal colon.
  • Renamed the regexp var to reBurstableInstanceType to match the adjacent
    reShutdownBehavior naming precedent.
  • On multi-type exhaustion, runInstanceWithFallback now wraps the last error
    with the list of types tried (via %w) so the persisted failure shows the
    fallback ran and was exhausted; the single-type path returns the raw error
    unchanged (preserving byte-for-byte backward compatibility).
  • Doc cleanup: removed hardcoded code-owned enumerations (capacity codes,
    placement fields) and the hardware narrative from the InstanceTypes field
    doc; regenerated partials and web-docs.
  • Added tests: empty-list guard; instance_type+spot_instance_types and
    all-three-selector exclusivity; zero-selector error-message assertion; and an
    assertion that the exhaustion error names the types tried.
  • Consciously skipped (noted, not defects): a non-empty-Instances guard on the
    success path (pre-existing, no real AWS path), per-entry nitro/credit
    validation for list types (outside the metal/GPU use case), and a full
    Run()-level wiring test (heavy state bag; the selection helper is covered).

Scoping note: instance_type still carries required:"true" in its struct tag.
It is already not strictly required (the pre-existing spot_instance_types is an
alternative), and Prepare enforces the real "exactly one of" rule; flipping
the tag would move instance_type between the required/not-required generated
doc partials for every builder, so it's left out of this PR's scope — happy to
change it if preferred.

Add an optional ordered `instance_types` list to the shared run config.
When set (in place of `instance_type`), Packer launches the first type and
falls through to the next on an insufficient-capacity error
(InsufficientInstanceCapacity / InsufficientHostCapacity /
InsufficientReservedInstanceCapacity), succeeding on the first that launches
and failing with the last error if all are exhausted. Any non-capacity error
aborts immediately so misconfiguration is not masked. All other placement
config (tenancy, host resource group, subnet/AZ, license specs, block device
mappings) is type-independent and preserved across attempts.

`instance_types` is mutually exclusive with `instance_type` and
`spot_instance_types`, and rejects empty and burstable (T-family) entries.
The capacity classifier is centralized in the awserrors package. Implemented
in both the v1 (builder/common) and v2 (common) packages, covering the
amazon-ebs, ebsvolume, ebssurrogate, and instance builders. HCL2 spec and
docs regenerated.
@hashicorp-cla-app

hashicorp-cla-app Bot commented Aug 24, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@hashicorp-cla-app

Copy link
Copy Markdown

CLA assistant check

Thank you for your submission! We require that all contributors sign our Contributor License Agreement ("CLA") before we can accept the contribution. Read and sign the agreement

Learn more about why HashiCorp requires a CLA and what the CLA includes

Have you signed the CLA already but the status is still pending? Recheck it.

…ests

Applies findings from the multi-agent review of hashicorp#702 (both v1 builder/common
and v2 common packages, kept in lockstep):

- Fix the burstable-type regexp: (:? -> (?: (non-capturing group). As written
  it matched an optional literal colon; now a proper validation gate.
- Rename burstableInstanceTypeRe -> reBurstableInstanceType to match the
  adjacent reShutdownBehavior naming precedent.
- Generalize the InstanceTypes doc: drop the hardcoded capacity-code list and
  placement-field enumeration (code-owned, rot-prone) and the hardware
  narrative; regenerate docs-partials and .web-docs.
- runInstanceWithFallback: on multi-type exhaustion, wrap the last error with
  the list of types tried so the failure record shows the fallback ran. The
  single-type path returns the raw error unchanged (backward compatible).
- Remove a redundant 'what' comment above the instance_type fallback default.
- Tests: assert the exhaustion error names the types tried; add empty-list
  guard test; add instance_type+spot_instance_types and all-three-selector
  exclusivity cases; assert the zero-selector error message.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: ordered instance-type capacity fallback for on-demand builds

1 participant