Skip to content

Commit 6272b8f

Browse files
piotrjclaude
andcommitted
feat(runners): support OnDemandOptions.AllocationStrategy in EC2 Fleet
Previously, createInstances always set SpotOptions.AllocationStrategy even for on-demand fleets, making instance_allocation_strategy and instance_types ordering meaningless for on-demand users wanting `prioritized`. Now the fleet request conditionally sets SpotOptions or OnDemandOptions based on targetCapacityType, and the spot-to- on-demand failover path maps spot-only strategies to `lowest-price`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6d3b7db commit 6272b8f

File tree

6 files changed

+74
-21
lines changed

6 files changed

+74
-21
lines changed

lambdas/functions/control-plane/src/aws/runners.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DefaultTargetCapacityType, SpotAllocationStrategy } from '@aws-sdk/client-ec2';
1+
import { DefaultTargetCapacityType, FleetOnDemandAllocationStrategy, SpotAllocationStrategy } from '@aws-sdk/client-ec2';
22

33
export type RunnerType = 'Org' | 'Repo';
44

@@ -39,7 +39,7 @@ export interface RunnerInputParameters {
3939
instanceTypes: string[];
4040
targetCapacityType: DefaultTargetCapacityType;
4141
maxSpotPrice?: string;
42-
instanceAllocationStrategy: SpotAllocationStrategy;
42+
instanceAllocationStrategy: SpotAllocationStrategy | FleetOnDemandAllocationStrategy;
4343
};
4444
numberOfRunners: number;
4545
amiIdSsmParameterName?: string;

lambdas/functions/control-plane/src/aws/runners.test.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
DescribeInstancesCommand,
1111
type DescribeInstancesResult,
1212
EC2Client,
13+
FleetOnDemandAllocationStrategy,
1314
SpotAllocationStrategy,
1415
TerminateInstancesCommand,
1516
} from '@aws-sdk/client-ec2';
@@ -366,11 +367,31 @@ describe('create runner', () => {
366367
});
367368

368369
it('calls create fleet of 1 instance with the on-demand capacity', async () => {
369-
await createRunner(createRunnerConfig({ ...defaultRunnerConfig, capacityType: 'on-demand' }));
370+
await createRunner(
371+
createRunnerConfig({ ...defaultRunnerConfig, capacityType: 'on-demand', allocationStrategy: 'lowest-price' }),
372+
);
370373
expect(mockEC2Client).toHaveReceivedCommandWith(CreateFleetCommand, {
371374
...expectedCreateFleetRequest({
372375
...defaultExpectedFleetRequestValues,
373376
capacityType: 'on-demand',
377+
allocationStrategy: 'lowest-price',
378+
}),
379+
});
380+
});
381+
382+
it('calls create fleet with on-demand capacity and prioritized allocation strategy', async () => {
383+
await createRunner(
384+
createRunnerConfig({
385+
...defaultRunnerConfig,
386+
capacityType: 'on-demand',
387+
allocationStrategy: FleetOnDemandAllocationStrategy.PRIORITIZED,
388+
}),
389+
);
390+
expect(mockEC2Client).toHaveReceivedCommandWith(CreateFleetCommand, {
391+
...expectedCreateFleetRequest({
392+
...defaultExpectedFleetRequestValues,
393+
capacityType: 'on-demand',
394+
allocationStrategy: FleetOnDemandAllocationStrategy.PRIORITIZED,
374395
}),
375396
});
376397
});
@@ -581,12 +602,13 @@ describe('create runner with errors fail over to OnDemand', () => {
581602
}),
582603
});
583604

584-
// second call with with OnDemand fallback
605+
// second call with with OnDemand fallback, allocation strategy defaults to lowest-price
585606
expect(mockEC2Client).toHaveReceivedNthCommandWith(2, CreateFleetCommand, {
586607
...expectedCreateFleetRequest({
587608
...defaultExpectedFleetRequestValues,
588609
totalTargetCapacity: 1,
589610
capacityType: 'on-demand',
611+
allocationStrategy: 'lowest-price',
590612
}),
591613
});
592614
});
@@ -623,12 +645,13 @@ describe('create runner with errors fail over to OnDemand', () => {
623645
}),
624646
});
625647

626-
// second call with with OnDemand failback, capacity is reduced by 1
648+
// second call with with OnDemand failback, capacity is reduced by 1, allocation strategy defaults to lowest-price
627649
expect(mockEC2Client).toHaveReceivedNthCommandWith(2, CreateFleetCommand, {
628650
...expectedCreateFleetRequest({
629651
...defaultExpectedFleetRequestValues,
630652
totalTargetCapacity: 1,
631653
capacityType: 'on-demand',
654+
allocationStrategy: 'lowest-price',
632655
}),
633656
});
634657
});
@@ -698,7 +721,7 @@ function createFleetMockWithWithOnDemandFallback(errors: string[], instances?: s
698721
interface RunnerConfig {
699722
type: RunnerType;
700723
capacityType: DefaultTargetCapacityType;
701-
allocationStrategy: SpotAllocationStrategy;
724+
allocationStrategy: SpotAllocationStrategy | FleetOnDemandAllocationStrategy;
702725
maxSpotPrice?: string;
703726
amiIdSsmParameterName?: string;
704727
tracingEnabled?: boolean;
@@ -730,7 +753,7 @@ function createRunnerConfig(runnerConfig: RunnerConfig): RunnerInputParameters {
730753
interface ExpectedFleetRequestValues {
731754
type: 'Repo' | 'Org';
732755
capacityType: DefaultTargetCapacityType;
733-
allocationStrategy: SpotAllocationStrategy;
756+
allocationStrategy: SpotAllocationStrategy | FleetOnDemandAllocationStrategy;
734757
maxSpotPrice?: string;
735758
totalTargetCapacity: number;
736759
imageId?: string;
@@ -778,10 +801,18 @@ function expectedCreateFleetRequest(expectedValues: ExpectedFleetRequestValues):
778801
],
779802
},
780803
],
781-
SpotOptions: {
782-
AllocationStrategy: expectedValues.allocationStrategy,
783-
MaxTotalPrice: expectedValues.maxSpotPrice,
784-
},
804+
...(expectedValues.capacityType === 'spot'
805+
? {
806+
SpotOptions: {
807+
AllocationStrategy: expectedValues.allocationStrategy,
808+
MaxTotalPrice: expectedValues.maxSpotPrice,
809+
},
810+
}
811+
: {
812+
OnDemandOptions: {
813+
AllocationStrategy: expectedValues.allocationStrategy,
814+
},
815+
}),
785816
TagSpecifications: [
786817
{
787818
ResourceType: 'instance',

lambdas/functions/control-plane/src/aws/runners.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
DescribeInstancesResult,
88
EC2Client,
99
FleetLaunchTemplateOverridesRequest,
10+
FleetOnDemandAllocationStrategy,
11+
SpotAllocationStrategy,
1012
Tag,
1113
TerminateInstancesCommand,
1214
_InstanceType,
@@ -187,11 +189,21 @@ async function processFleetResult(
187189
logger.warn(`Create fleet failed, initatiing fall back to on demand instances.`);
188190
logger.debug('Create fleet failed.', { data: fleet.Errors });
189191
const numberOfInstances = runnerParameters.numberOfRunners - instances.length;
192+
const onDemandValidStrategies = ['lowest-price', 'prioritized'];
193+
const failoverAllocationStrategy = onDemandValidStrategies.includes(
194+
runnerParameters.ec2instanceCriteria.instanceAllocationStrategy,
195+
)
196+
? runnerParameters.ec2instanceCriteria.instanceAllocationStrategy
197+
: 'lowest-price';
190198
const instancesOnDemand = await createRunner({
191199
...runnerParameters,
192200
numberOfRunners: numberOfInstances,
193201
onDemandFailoverOnError: ['InsufficientInstanceCapacity'],
194-
ec2instanceCriteria: { ...runnerParameters.ec2instanceCriteria, targetCapacityType: 'on-demand' },
202+
ec2instanceCriteria: {
203+
...runnerParameters.ec2instanceCriteria,
204+
targetCapacityType: 'on-demand',
205+
instanceAllocationStrategy: failoverAllocationStrategy,
206+
},
195207
});
196208
instances.push(...instancesOnDemand);
197209
return instances;
@@ -268,10 +280,20 @@ async function createInstances(
268280
),
269281
},
270282
],
271-
SpotOptions: {
272-
MaxTotalPrice: runnerParameters.ec2instanceCriteria.maxSpotPrice,
273-
AllocationStrategy: runnerParameters.ec2instanceCriteria.instanceAllocationStrategy,
274-
},
283+
...(runnerParameters.ec2instanceCriteria.targetCapacityType === 'spot'
284+
? {
285+
SpotOptions: {
286+
MaxTotalPrice: runnerParameters.ec2instanceCriteria.maxSpotPrice,
287+
AllocationStrategy:
288+
runnerParameters.ec2instanceCriteria.instanceAllocationStrategy as SpotAllocationStrategy,
289+
},
290+
}
291+
: {
292+
OnDemandOptions: {
293+
AllocationStrategy:
294+
runnerParameters.ec2instanceCriteria.instanceAllocationStrategy as FleetOnDemandAllocationStrategy,
295+
},
296+
}),
275297
TargetCapacitySpecification: {
276298
TotalTargetCapacity: runnerParameters.numberOfRunners,
277299
DefaultTargetCapacityType: runnerParameters.ec2instanceCriteria.targetCapacityType,

modules/multi-runner/variables.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ variable "multi_runner_config" {
214214
enable_runner_binaries_syncer: "Option to disable the lambda to sync GitHub runner distribution, useful when using a pre-build AMI."
215215
enable_ssm_on_runners: "Enable to allow access the runner instances for debugging purposes via SSM. Note that this adds additional permissions to the runner instances."
216216
enable_userdata: "Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI."
217-
instance_allocation_strategy: "The allocation strategy for spot instances. AWS recommends to use `capacity-optimized` however the AWS default is `lowest-price`."
217+
instance_allocation_strategy: "The allocation strategy for creating instances. For spot, AWS recommends `capacity-optimized`; for on-demand, use `lowest-price` or `prioritized`. The AWS default is `lowest-price`."
218218
instance_max_spot_price: "Max price price for spot instances per hour. This variable will be passed to the create fleet as max spot price for the fleet."
219219
instance_target_capacity_type: "Default lifecycle used for runner instances, can be either `spot` or `on-demand`."
220220
instance_types: "List of instance types for the action runner. Defaults are based on runner_os (al2023 for linux and Windows Server Core for win)."

modules/runners/variables.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ variable "instance_target_capacity_type" {
102102
}
103103

104104
variable "instance_allocation_strategy" {
105-
description = "The allocation strategy for spot instances. AWS recommends to use `capacity-optimized` however the AWS default is `lowest-price`."
105+
description = "The allocation strategy for creating instances. For spot, AWS recommends `capacity-optimized`; for on-demand, use `lowest-price` or `prioritized`. The AWS default is `lowest-price`."
106106
type = string
107107
default = "lowest-price"
108108

109109
validation {
110-
condition = contains(["lowest-price", "diversified", "capacity-optimized", "capacity-optimized-prioritized", "price-capacity-optimized"], var.instance_allocation_strategy)
110+
condition = contains(["lowest-price", "diversified", "capacity-optimized", "capacity-optimized-prioritized", "price-capacity-optimized", "prioritized"], var.instance_allocation_strategy)
111111
error_message = "The instance allocation strategy does not match the allowed values."
112112
}
113113
}

variables.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -554,11 +554,11 @@ variable "instance_target_capacity_type" {
554554
}
555555

556556
variable "instance_allocation_strategy" {
557-
description = "The allocation strategy for spot instances. AWS recommends using `price-capacity-optimized` however the AWS default is `lowest-price`."
557+
description = "The allocation strategy for creating instances. For spot, AWS recommends `price-capacity-optimized`; for on-demand, use `lowest-price` or `prioritized`. The AWS default is `lowest-price`."
558558
type = string
559559
default = "lowest-price"
560560
validation {
561-
condition = contains(["lowest-price", "diversified", "capacity-optimized", "capacity-optimized-prioritized", "price-capacity-optimized"], var.instance_allocation_strategy)
561+
condition = contains(["lowest-price", "diversified", "capacity-optimized", "capacity-optimized-prioritized", "price-capacity-optimized", "prioritized"], var.instance_allocation_strategy)
562562
error_message = "The instance allocation strategy does not match the allowed values."
563563
}
564564
}

0 commit comments

Comments
 (0)