Skip to content

fix(aws): create subnets in an availability zone that offers the instance types - #878

Open
abrarshivani wants to merge 4 commits into
NVIDIA:mainfrom
abrarshivani:fix/aws-availability-zone-selection
Open

abrarshivani wants to merge 4 commits into
NVIDIA:mainfrom
abrarshivani:fix/aws-availability-zone-selection

Conversation

@abrarshivani

@abrarshivani abrarshivani commented Oct 5, 2026 •

Copy link
Copy Markdown

Description

Create subnets in an Availability Zone that offers the requested instance types, and fail the pre-flight when no zone offers them or the pinned zone doesn't.

Motivation

  • The gpu-driver-container precompiled CI runs g5g.xlarge in us-west-2, which us-west-2d doesn't offer.
  • When AWS puts the subnet in us-west-2d, RunInstances fails with Unsupported.
  • The rollback often fails with DependencyViolation and leaks the VPC.
  • Not a one-off: each of the ~10 arm64 jobs per nightly can hit it, and retries don't help.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to change)
  • 📝 Documentation update
  • 🔧 Refactoring (no functional changes)
  • 🧪 Test improvements
  • 🔨 Build/CI changes

Changes Made

  • Find the available standard zones that offer every needed instance type.
  • Pick the first in sorted order, skipping Local and Wavelength Zones.
  • Create every subnet in that zone, and fail subnet creation if no zone was selected.
  • Fail Create, CreateCluster and DryRun before creating anything when no zone qualifies.
  • Without the zone discovery permissions, log a warning and let AWS choose the zone as before; reject a pinned zone.
  • Ignore the worker type when a cluster has zero workers.
  • Add an optional availabilityZone to instance and cluster.
  • Record the chosen zone in status and show it in holodeck describe.
  • Keep older cache files without the zone loading.
  • Add the two EC2 calls to the client interface, fake and mocks.
  • Make the fake reject invalid NextToken values.
  • Document the field, use placeholder zones, and list the recommended IAM permissions.

Testing

  • Unit tests added/updated

  • E2E tests added/updated

  • Manual testing performed

  • Cover partial, missing and pinned zone offerings.

  • Cover Local, unavailable and constrained zones.

  • Cover cluster type intersection and zero-worker clusters.

  • Cover the cache round trip and describe.

  • Cover zone lookup failures, the missing-permission fallback, and subnet creation without a selected zone.

  • Use us-west-2, the fake's zone region, in every zone-selection test.

  • Confirm each new test fails without its fix.

Test Commands Run

go build ./...
go vet ./...
make lint
make test
go test ./tests/ -args -ginkgo.label-filter=mock

Checklist

  • My code follows the project's coding conventions
  • I have performed a self-review of my code
  • I have commented my code where necessary
  • I have updated the documentation (if applicable)
  • My changes generate no new warnings
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally
  • I have signed off my commits (git commit -s)

Additional Notes

  • ec2:DescribeAvailabilityZones and ec2:DescribeInstanceTypeOfferings are recommended. Without them Holodeck falls back to AWS choosing the zone, so the real-AWS smoke job exercises zone selection only once cnt-ci has them.
  • Offerings don't guarantee capacity, so InsufficientInstanceCapacity can still occur. Retrying another zone could be a follow-up.
  • describe shows the spec region, which can differ from the zone's region under AWS_REGION.
  • make generate doesn't run on Go 1.26. The new string fields need no deepcopy changes.

Add DescribeInstanceTypeOfferings and DescribeAvailabilityZones to the
EC2Client interface so the AWS provider can ask which zones of a region
offer an instance type, and implement them in the fake and in the mock
client.

The fake seeds us-west-2a through us-west-2d as standard zones and, by
default, offers every known instance type in all of them. Tests can
restrict a type to some zones, add Local Zones, or change a zone's state.
DescribeAvailabilityZones honours the zone-type and state filters, and
DescribeInstanceTypeOfferings honours the instance-type filter and pages
its results two at a time, so callers have to follow NextToken as they
must against the real API. An invalid NextToken, including a negative
one, returns InvalidNextToken instead of panicking. CreateSubnet now
keeps the requested AvailabilityZone on the stored subnet.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
…ance types

Holodeck created its subnets without an Availability Zone, so AWS picked
one. Not every zone in a region offers every instance type, and the
pre-flight check only validated instance types at the region level. The
gpu-driver-container precompiled CI hits this with g5g.xlarge in
us-west-2: when the subnet lands in us-west-2d, RunInstances fails with

  Unsupported: Your requested instance type (g5g.xlarge) is not
  supported in your requested Availability Zone (us-west-2d).

after the VPC and its networking already exist, and the rollback that
follows can fail with DependencyViolation and leak the VPC.

The pre-flight now asks DescribeInstanceTypeOfferings which zones offer
the instance types the environment needs. For clusters that is the
control-plane type plus the worker type, but only when there are workers
to launch, so a control-plane-only cluster is not rejected over a worker
type it never uses. Only the region's standard zones in the available
state are considered, since Local and Wavelength Zones sort first and
support only a subset of services, and the first matching zone in sorted
order is chosen. If no zone offers every type, Create(), CreateCluster()
and DryRun() fail before anything is created. Every subnet Holodeck
creates, including both subnets of a cluster, is placed in the chosen
zone, and createSubnet and createPublicSubnet return an error rather
than let AWS choose when no zone was selected.

A new optional availabilityZone field on the instance and cluster specs
pins the zone. The pre-flight rejects it if that zone does not offer the
requested types and lists the zones that do.

The pre-flight now also needs the ec2:DescribeAvailabilityZones and
ec2:DescribeInstanceTypeOfferings IAM permissions, which the
prerequisites page now lists. Provider tests that run the pre-flight
against the fake use us-west-2, the only region whose zones it knows.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
The zone chosen by the pre-flight only appeared in the create log. Store
it as an availability-zone property in .status.properties, next to the
VPC and subnet IDs, for both single-node environments and clusters, and
read it back when the cache is loaded. holodeck describe shows it in the
provider section and in its JSON and YAML output as
provider.availabilityZone.

The change is additive: existing property names are unchanged, and a
cache file written before this change simply has no zone, which describe
omits. Delete does not use the property.

The mock e2e test now checks that the recorded zone is the one every
subnet was created in. Its configs are shared with the real-AWS runs, so
rather than edit them the mock suite sets the region to us-west-2, the
only region whose zones the fake knows, after loading each config.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 23:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The status update loop mutates copied properties and cannot reliably persist or add the selected zone.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds AWS Availability Zone selection based on requested instance-type availability, preventing unsupported instance launches and leaked networking resources.

Changes:

  • Selects or validates an Availability Zone during pre-flight checks.
  • Pins subnets to the selected zone and persists it in status.
  • Extends AWS fakes, tests, CLI output, API types, and documentation.
File Description
tests/​e2e_mock_test.go Verifies subnet and cached zone consistency.
pkg/​testutil/​mocks/​aws.go Mocks new EC2 discovery operations.
pkg/​provider/​aws/​status.go Persists the selected zone in status.
pkg/​provider/​aws/​image.go Implements zone discovery and selection.
pkg/​provider/​aws/​image_test.go Aligns tests with fake region data.
pkg/​provider/​aws/​create.go Pins created subnets to the selected zone.
pkg/​provider/​aws/​create_test.go Tests single-node zone behavior.
pkg/​provider/​aws/​cluster.go Carries the selected zone into cluster creation.
pkg/​provider/​aws/​cluster_test.go Tests cluster zone intersections and constraints.
pkg/​provider/​aws/​cache_test.go Tests zone cache compatibility.
pkg/​provider/​aws/​aws.go Adds zone state and cache handling.
pkg/​provider/​aws/​aws_test.go Aligns provider tests with seeded zones.
pkg/​provider/​aws/​aws_ginkgo_test.go Extends dry-run and cache assertions.
internal/​aws/​ec2_client.go Adds EC2 zone-discovery methods.
internal/​aws/​awsfake/​store.go Adds fake zone and offering data.
internal/​aws/​awsfake/​ec2.go Implements fake discovery and subnet recording.
internal/​aws/​awsfake/​awsfake_test.go Tests fake offerings, filtering, and pagination.
docs/​prerequisites.md Documents required IAM permissions.
docs/​guides/​multinode-clusters.md Documents cluster zone configuration.
docs/​commands/​create.md Documents automatic and pinned selection.
cmd/​cli/​describe/​describe.go Displays the selected zone.
cmd/​cli/​describe/​describe_test.go Tests zone display data.
api/​holodeck/​v1alpha1/​types.go Adds optional zone fields.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +149 to +153
case AvailabilityZone:
if properties.Value != cache.AvailabilityZone {
properties.Value = cache.AvailabilityZone
modified = true
}
@abrarshivani abrarshivani self-assigned this Oct 5, 2026
ArangoGutierrez
ArangoGutierrez previously approved these changes Oct 6, 2026

@ArangoGutierrez ArangoGutierrez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. Choosing the subnet zone from instance type offerings fixes a real failure mode where RunInstances would return "Unsupported" after all the VPC networking already existed.

The checkInstanceTypes pre-flight now intersects DescribeInstanceTypeOfferings with the region's available standard zones (Local and Wavelength Zones are skipped) and puts both subnets in that one zone. If no zone offers every requested type, or a pinned availabilityZone doesn't offer them, holodeck create now fails before the VPC is created and lists the zones that would work. The zone is also recorded in status, so holodeck describe shows it.

@ArangoGutierrez
ArangoGutierrez dismissed their stale review October 6, 2026 09:34

Approved before CI was checked. E2E Real Smoke fails at the new zone pre-flight with a 403 on ec2:DescribeAvailabilityZones for the CI IAM user, so this is not ready to merge yet. Follow-up review to come.

@ArangoGutierrez ArangoGutierrez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zone selection itself looks right, but it adds two IAM requirements to create and dryrun, and the real-AWS smoke job shows what that does to an identity without them: E2E Real Smoke fails in under a second with a 403 on DescribeAvailabilityZones for the cnt-ci user. The gpu-operator and device-plugin e2e jobs run under their own IAM identities, so they would hit the same wall the day this merges.

Comment thread pkg/provider/aws/image.go
// Wavelength Zones are excluded: they sort before the region's own zones and
// support only a subset of AWS services.
func (p *Provider) zonesOfferingAllInstanceTypes(instanceTypes []string) ([]string, error) {
zonesOutput, err := p.ec2.DescribeAvailabilityZones(context.TODO(), &ec2.DescribeAvailabilityZonesInput{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An UnauthorizedOperation here (or from DescribeInstanceTypeOfferings below) aborts the whole pre-flight, so every caller lacking the two new permissions loses create entirely. Please fall back to the old behaviour on that error: log a warning, leave selectedAvailabilityZone empty, let createSubnet omit AvailabilityZone, and reject a pinned availabilityZone with a message naming the missing permissions. If you would rather keep it strict, mark the PR as breaking and add an upgrade note, and we will grant the permissions to cnt-ci so the smoke job can go green before merge.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I will keep this non-breaking and fall back to the previous behavior only when the AZ discovery calls fail due to missing permissions. If availabilityZone is explicitly set, we should fail since we can't validate the requested zone. Other discovery errors should still fail the pre-flight. I would still recommend granting the new permissions to cnt-ci so the smoke job exercises the AZ aware path.

…issions

The availability zone pre-flight calls DescribeAvailabilityZones and
DescribeInstanceTypeOfferings. An identity without the
ec2:DescribeAvailabilityZones or ec2:DescribeInstanceTypeOfferings
permission gets UnauthorizedOperation from those calls, which aborted the
pre-flight, so callers that could create environments before the zone
selection change could no longer create anything. The real-AWS CI user is
one of them.

When either call fails with UnauthorizedOperation and no availabilityZone
is set, the pre-flight now logs a warning naming both permissions and
leaves the zone unselected, and createSubnet and createPublicSubnet omit
AvailabilityZone so that AWS chooses it as before. The region-level
instance type check still runs, and dryrun behaves the same way. If
availabilityZone is set, the pre-flight fails instead, because the
requested zone cannot be validated without those permissions. Any other
error from the zone discovery calls still fails the pre-flight.

The subnet functions still refuse an empty zone unless the pre-flight
recorded this fallback, so a path that skips the pre-flight cannot
silently let AWS pick the zone. The error code is read through the
ErrorCode method that smithy.APIError defines, which keeps smithy-go an
indirect dependency.

The prerequisites page lists the two permissions as recommended and
describes what happens without them.

Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 37519463798

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage decreased (-0.03%) to 51.849%

Details

  • Coverage decreased (-0.03%) from the base build.
  • Patch coverage: 26 uncovered changes across 5 files (183 of 209 lines covered, 87.56%).
  • 9 coverage regressions across 7 files.

Uncovered Changes

File Changed Covered %
pkg/testutil/mocks/aws.go 8 0 0.0%
internal/aws/awsfake/store.go 35 29 82.86%
internal/aws/awsfake/ec2.go 58 53 91.38%
pkg/provider/aws/status.go 5 1 20.0%
cmd/cli/describe/describe.go 7 4 57.14%
Total (9 files) 209 183 87.56%

Coverage Regressions

9 previously-covered lines in 7 files lost coverage.

File Lines Losing Coverage Coverage
cmd/cli/cleanup/cleanup.go 2 86.96%
pkg/provider/aws/image.go 2 90.13%
cmd/cli/create/create.go 1 43.49%
pkg/provider/aws/aws.go 1 85.32%
pkg/provider/aws/cluster.go 1 63.37%
pkg/provider/aws/delete.go 1 55.41%
pkg/provisioner/cluster.go 1 12.93%

Coverage Stats

Coverage Status
Relevant Lines: 11952
Covered Lines: 6197
Line Coverage: 51.85%
Coverage Strength: 0.52 hits per line

💛 - Coveralls

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.

4 participants