Skip to content

Commit 4473737

Browse files
committed
feat: Add feature to enable dynamic instance types via workflow labels
1 parent 989b524 commit 4473737

File tree

8 files changed

+68
-11
lines changed

8 files changed

+68
-11
lines changed

lambdas/functions/control-plane/src/scale-runners/scale-up.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface ActionRequestMessage {
3131
installationId: number;
3232
repoOwnerType: string;
3333
retryCounter?: number;
34+
labels?: string[];
3435
}
3536

3637
export interface ActionRequestMessageRetry extends ActionRequestMessage {
@@ -212,25 +213,59 @@ export async function createRunners(
212213
githubRunnerConfig: CreateGitHubRunnerConfig,
213214
ec2RunnerConfig: CreateEC2RunnerConfig,
214215
ghClient: Octokit,
216+
requestedInstanceType?: string,
215217
): Promise<void> {
216218
const instances = await createRunner({
219+
environment: ec2RunnerConfig.environment,
217220
runnerType: githubRunnerConfig.runnerType,
218221
runnerOwner: githubRunnerConfig.runnerOwner,
219-
numberOfRunners: 1,
220-
...ec2RunnerConfig,
222+
launchTemplateName: ec2RunnerConfig.launchTemplateName,
223+
ec2instanceCriteria: requestedInstanceType
224+
? {
225+
instanceTypes: [requestedInstanceType],
226+
maxSpotPrice: ec2RunnerConfig.ec2instanceCriteria.maxSpotPrice,
227+
instanceAllocationStrategy: ec2RunnerConfig.ec2instanceCriteria.instanceAllocationStrategy,
228+
targetCapacityType: ec2RunnerConfig.ec2instanceCriteria.targetCapacityType,
229+
}
230+
: ec2RunnerConfig.ec2instanceCriteria,
231+
subnets: ec2RunnerConfig.subnets,
232+
numberOfRunners: ec2RunnerConfig.numberOfRunners ?? 1,
233+
amiIdSsmParameterName: ec2RunnerConfig.amiIdSsmParameterName,
234+
tracingEnabled: ec2RunnerConfig.tracingEnabled,
235+
onDemandFailoverOnError: ec2RunnerConfig.onDemandFailoverOnError,
221236
});
222237
if (instances.length !== 0) {
223238
await createStartRunnerConfig(githubRunnerConfig, instances, ghClient);
224239
}
225240
}
226241

227242
export async function scaleUp(eventSource: string, payload: ActionRequestMessage): Promise<void> {
228-
logger.info(`Received ${payload.eventType} from ${payload.repositoryOwner}/${payload.repositoryName}`);
229-
243+
logger.debug(`Received event`, { payload });
230244
if (eventSource !== 'aws:sqs') throw Error('Cannot handle non-SQS events!');
245+
246+
const dynamicEc2TypesEnabled = yn(process.env.ENABLE_DYNAMIC_EC2_TYPES, { default: false });
247+
const requestedInstanceType = payload.labels?.find(label => label.startsWith('ghr-ec2-'))?.replace('ghr-ec2-', '');
248+
249+
if (dynamicEc2TypesEnabled && requestedInstanceType) {
250+
logger.info(`Dynamic EC2 instance type requested: ${requestedInstanceType}`);
251+
}
252+
253+
// Store the requested instance type for use in createRunners
254+
const ec2Config = {
255+
...payload,
256+
requestedInstanceType: dynamicEc2TypesEnabled ? requestedInstanceType : undefined,
257+
};
231258
const enableOrgLevel = yn(process.env.ENABLE_ORGANIZATION_RUNNERS, { default: true });
232259
const maximumRunners = parseInt(process.env.RUNNERS_MAXIMUM_COUNT || '3');
233-
const runnerLabels = process.env.RUNNER_LABELS || '';
260+
261+
// Combine configured runner labels with dynamic EC2 instance type label if present
262+
let runnerLabels = process.env.RUNNER_LABELS || '';
263+
if (dynamicEc2TypesEnabled && requestedInstanceType) {
264+
const ec2Label = `ghr-ec2-${requestedInstanceType}`;
265+
runnerLabels = runnerLabels ? `${runnerLabels},${ec2Label}` : ec2Label;
266+
logger.debug(`Added dynamic EC2 instance type label: ${ec2Label} to runner config.`);
267+
}
268+
234269
const runnerGroup = process.env.RUNNER_GROUP_NAME || 'Default';
235270
const environment = process.env.ENVIRONMENT;
236271
const ssmTokenPath = process.env.SSM_TOKEN_PATH;
@@ -337,6 +372,7 @@ export async function scaleUp(eventSource: string, payload: ActionRequestMessage
337372
onDemandFailoverOnError,
338373
},
339374
githubInstallationClient,
375+
ec2Config.requestedInstanceType,
340376
);
341377

342378
await publishRetryMessage(payload);

lambdas/functions/webhook/src/runners/dispatch.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ async function handleWorkflowJob(
4545
installationId: body.installation?.id ?? 0,
4646
queueId: queue.id,
4747
repoOwnerType: body.repository.owner.type,
48+
labels: body.workflow_job.labels,
4849
});
4950
logger.info(`Successfully dispatched job for ${body.repository.full_name} to the queue ${queue.id}`);
5051
return {
@@ -70,13 +71,16 @@ export function canRunJob(
7071
runnerLabelsMatchers: string[][],
7172
workflowLabelCheckAll: boolean,
7273
): boolean {
74+
// Filter out ghr-ec2- labels as they are handled by the dynamic EC2 instance type feature
75+
const filteredLabels = workflowJobLabels.filter(label => !label.startsWith('ghr-ec2-'));
76+
7377
runnerLabelsMatchers = runnerLabelsMatchers.map((runnerLabel) => {
7478
return runnerLabel.map((label) => label.toLowerCase());
7579
});
7680
const matchLabels = workflowLabelCheckAll
77-
? runnerLabelsMatchers.some((rl) => workflowJobLabels.every((wl) => rl.includes(wl.toLowerCase())))
78-
: runnerLabelsMatchers.some((rl) => workflowJobLabels.some((wl) => rl.includes(wl.toLowerCase())));
79-
const match = workflowJobLabels.length === 0 ? !matchLabels : matchLabels;
81+
? runnerLabelsMatchers.some((rl) => filteredLabels.every((wl) => rl.includes(wl.toLowerCase())))
82+
: runnerLabelsMatchers.some((rl) => filteredLabels.some((wl) => rl.includes(wl.toLowerCase())));
83+
const match = filteredLabels.length === 0 ? !matchLabels : matchLabels;
8084

8185
logger.debug(
8286
`Received workflow job event with labels: '${JSON.stringify(workflowJobLabels)}'. The event does ${

lambdas/functions/webhook/src/sqs/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ const logger = createChildLogger('sqs');
66

77
export interface ActionRequestMessage {
88
id: number;
9-
eventType: string;
109
repositoryName: string;
1110
repositoryOwner: string;
11+
eventType: string;
1212
installationId: number;
1313
queueId: string;
1414
repoOwnerType: string;
15+
labels?: string[];
1516
}
1617

1718
export interface MatcherConfig {

main.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,9 @@ module "runners" {
187187
github_app_parameters = local.github_app_parameters
188188
enable_organization_runners = var.enable_organization_runners
189189
enable_ephemeral_runners = var.enable_ephemeral_runners
190-
enable_jit_config = var.enable_jit_config
190+
enable_dynamic_ec2_types = var.enable_dynamic_ec2_types
191191
enable_job_queued_check = var.enable_job_queued_check
192+
enable_jit_config = var.enable_jit_config
192193
enable_on_demand_failover_for_errors = var.enable_runner_on_demand_failover_for_errors
193194
disable_runner_autoupdate = var.disable_runner_autoupdate
194195
enable_managed_runner_security_group = var.enable_managed_runner_security_group

modules/multi-runner/variables.tf

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ variable "multi_runner_config" {
7575
disable_runner_autoupdate = optional(bool, false)
7676
ebs_optimized = optional(bool, false)
7777
enable_ephemeral_runners = optional(bool, false)
78+
enable_dynamic_ec2_types = optional(bool, false)
7879
enable_job_queued_check = optional(bool, null)
7980
enable_on_demand_failover_for_errors = optional(list(string), [])
8081
enable_organization_runners = optional(bool, false)
@@ -179,7 +180,8 @@ variable "multi_runner_config" {
179180
disable_runner_autoupdate: "Disable the auto update of the github runner agent. Be aware there is a grace period of 30 days, see also the [GitHub article](https://github.blog/changelog/2022-02-01-github-actions-self-hosted-runners-can-now-disable-automatic-updates/)"
180181
ebs_optimized: "The EC2 EBS optimized configuration."
181182
enable_ephemeral_runners: "Enable ephemeral runners, runners will only be used once."
182-
enable_job_queued_check: "Enables JIT configuration for creating runners instead of registration token based registraton. JIT configuration will only be applied for ephemeral runners. By default JIT confiugration is enabled for ephemeral runners an can be disabled via this override. When running on GHES without support for JIT configuration this variable should be set to true for ephemeral runners."
183+
enable_dynamic_ec2_types: "Enable dynamic EC2 instance types based on workflow job labels. When enabled, jobs can request specific instance types via the 'gh-ec2-instance-type' label (e.g., 'gh-ec2-t3.large')."
184+
enable_job_queued_check: "(Optional) Only scale if the job event received by the scale up lambda is is in the state queued. By default enabled for non ephemeral runners and disabled for ephemeral. Set this variable to overwrite the default behavior."
183185
enable_on_demand_failover_for_errors: "Enable on-demand failover. For example to fall back to on demand when no spot capacity is available the variable can be set to `InsufficientInstanceCapacity`. When not defined the default behavior is to retry later."
184186
enable_organization_runners: "Register runners to organization, instead of repo level"
185187
enable_runner_binaries_syncer: "Option to disable the lambda to sync GitHub runner distribution, useful when using a pre-build AMI."

modules/runners/scale-up.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ resource "aws_lambda_function" "scale_up" {
2828
AMI_ID_SSM_PARAMETER_NAME = var.ami_id_ssm_parameter_name
2929
DISABLE_RUNNER_AUTOUPDATE = var.disable_runner_autoupdate
3030
ENABLE_EPHEMERAL_RUNNERS = var.enable_ephemeral_runners
31+
ENABLE_DYNAMIC_EC2_TYPES = var.enable_dynamic_ec2_types
3132
ENABLE_JIT_CONFIG = var.enable_jit_config
3233
ENABLE_JOB_QUEUED_CHECK = local.enable_job_queued_check
3334
ENABLE_METRIC_GITHUB_APP_RATE_LIMIT = var.metrics.enable && var.metrics.metric.enable_github_app_rate_limit

modules/runners/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,12 @@ variable "enable_ephemeral_runners" {
526526
default = false
527527
}
528528

529+
variable "enable_dynamic_ec2_types" {
530+
description = "Enable dynamic EC2 instance types based on workflow job labels. When enabled, jobs can request specific instance types via the 'gh:ec2:instance-type' label."
531+
type = bool
532+
default = false
533+
}
534+
529535
variable "enable_job_queued_check" {
530536
description = "Only scale if the job event received by the scale up lambda is is in the state queued. By default enabled for non ephemeral runners and disabled for ephemeral. Set this variable to overwrite the default behavior."
531537
type = bool

variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,12 @@ variable "enable_ephemeral_runners" {
647647
default = false
648648
}
649649

650+
variable "enable_dynamic_ec2_types" {
651+
description = "Enable dynamic EC2 instance types based on workflow job labels. When enabled, jobs can request specific instance types via the 'gh-ec2-instance-type' label (e.g., 'gh-ec2-t3.large')."
652+
type = bool
653+
default = false
654+
}
655+
650656
variable "enable_job_queued_check" {
651657
description = "Only scale if the job event received by the scale up lambda is in the queued state. By default enabled for non ephemeral runners and disabled for ephemeral. Set this variable to overwrite the default behavior."
652658
type = bool

0 commit comments

Comments
 (0)