diff --git a/.github/workflows/self-hosted.yml b/.github/workflows/self-hosted.yml index bd1acfa2..0b13c268 100644 --- a/.github/workflows/self-hosted.yml +++ b/.github/workflows/self-hosted.yml @@ -33,11 +33,20 @@ jobs: arch: ARM64 label: fargate-arm64 docker: false + - os: linux + arch: X64 + label: fargate-x64-spot + docker: false + - os: linux + arch: ARM64 + label: fargate-arm64-spot + docker: false runs-on: [self-hosted, "${{ matrix.os }}", "${{ matrix.arch }}", "${{ matrix.label }}"] steps: - run: export - - run: | + - name: Check arch + run: | if [ "${{ matrix.arch }}" != "${RUNNER_ARCH}" ]; then echo "Expected RUNNER_ARCH to be ${{ matrix.arch }} but it's $RUNNER_ARCH" exit 1 diff --git a/API.md b/API.md index 7951d8f1..f9b8a9df 100644 --- a/API.md +++ b/API.md @@ -322,6 +322,7 @@ Any object. | container | aws-cdk-lib.aws_ecs.ContainerDefinition | Container definition hosting the runner. | | grantPrincipal | aws-cdk-lib.aws_iam.IPrincipal | Grant principal used to add permissions to the runner role. | | label | string | Label associated with this provider. | +| spot | boolean | Use spot pricing for Fargate tasks. | | task | aws-cdk-lib.aws_ecs.FargateTaskDefinition | Fargate task hosting the runner. | | securityGroup | aws-cdk-lib.aws_ec2.ISecurityGroup | Security group attached to the task. | | vpc | aws-cdk-lib.aws_ec2.IVpc | VPC used for hosting the task. | @@ -412,6 +413,18 @@ Label associated with this provider. --- +##### `spot`Required + +```typescript +public readonly spot: boolean; +``` + +- *Type:* boolean + +Use spot pricing for Fargate tasks. + +--- + ##### `task`Required ```typescript @@ -1150,6 +1163,7 @@ const fargateRunnerProps: FargateRunnerProps = { ... } | label | string | GitHub Actions label used for this provider. | | memoryLimitMiB | number | The amount (in MiB) of memory used by the task. | | securityGroup | aws-cdk-lib.aws_ec2.ISecurityGroup | Security Group to assign to the task. | +| spot | boolean | Use Fargate spot capacity provider to save money. | | vpc | aws-cdk-lib.aws_ec2.IVpc | VPC to launch the runners in. | --- @@ -1308,6 +1322,22 @@ Security Group to assign to the task. --- +##### `spot`Optional + +```typescript +public readonly spot: boolean; +``` + +- *Type:* boolean +- *Default:* false + +Use Fargate spot capacity provider to save money. + +* Runners may fail to start due to missing capacity. +* Runners might be stopped prematurely with spot pricing. + +--- + ##### `vpc`Optional ```typescript diff --git a/README.md b/README.md index 9df45acc..0badc589 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,15 @@ The best way to browse API documentation is on [Constructs Hub][13]. It is avail A runner provider creates compute resources on-demand and uses [actions/runner][5] to start a runner. -| Provider | Time limit | vCPUs | RAM | Storage | sudo | Docker | -|-----------|--------------------------|--------------------------|-----------------------------------|------------------------------|------|--------| -| CodeBuild | 8 hours (default 1 hour) | 2 (default), 4, 8, or 72 | 3gb (default), 7gb, 15gb or 145gb | 50gb to 824gb (default 64gb) | ✔ | ✔ | -| Fargate | Unlimited | 0.25 to 4 (default 1) | 512mb to 30gb (default 2gb) | 20gb to 200gb (default 25gb) | ✔ | ❌ | -| Lambda | 15 minutes | 1 to 6 (default 2) | 128mb to 10gb (default 2gb) | Up to 10gb (default 10gb) | ❌ | ❌ | +| | CodeBuild | Fargate | Lambda | +|----------------|--------------------------|---------------|---------------| +| **Time limit** | 8 hours | Unlimited | 15 minutes | +| **vCPUs** | 2, 4, 8, or 72 | 0.25 to 4 | 1 to 6 | +| **RAM** | 3gb, 7gb, 15gb, or 145gb | 512mb to 30gb | 128mb to 10gb | +| **Storage** | 50gb to 824gb | 20gb to 200gb | Up to 10gb | +| **sudo** | ✔ | ✔ | ❌ | +| **Docker** | ✔ | ❌ | ❌ | +| **Spot** | ❌ | ✔ | ❌ | The best provider to use mostly depends on your current infrastructure. When in doubt, CodeBuild is always a good choice. Execution history and logs are easy to view, and it has no restrictive limits unless you need to run for more than 8 hours. @@ -81,7 +85,7 @@ You can also create your own provider by implementing `IRunnerProvider`. ## Customizing -The default providers configured by [`GitHubRunners`](https://constructs.dev/packages/@cloudsnorkel/cdk-github-runners/v/0.0.11/api/GitHubRunners?lang=typescript) are useful for testing but probably not too much for actual production work. They run in the default VPC or no VPC and have no added IAM permissions. You would usually want to configure the providers yourself. +The default providers configured by `GitHubRunners` are useful for testing but probably not too much for actual production work. They run in the default VPC or no VPC and have no added IAM permissions. You would usually want to configure the providers yourself. For example: diff --git a/src/providers/fargate.ts b/src/providers/fargate.ts index 981049ab..f94919e6 100644 --- a/src/providers/fargate.ts +++ b/src/providers/fargate.ts @@ -98,6 +98,38 @@ export interface FargateRunnerProps extends RunnerProviderProps { * @default 20 */ readonly ephemeralStorageGiB?: number; + + /** + * Use Fargate spot capacity provider to save money. + * + * * Runners may fail to start due to missing capacity. + * * Runners might be stopped prematurely with spot pricing. + * + * @default false + */ + readonly spot?: boolean; +} + +class EcsFargateSpotLaunchTarget implements stepfunctions_tasks.IEcsLaunchTarget { + /** + * Called when the Fargate launch type configured on RunTask + */ + public bind(_task: stepfunctions_tasks.EcsRunTask, + launchTargetOptions: stepfunctions_tasks.LaunchTargetBindOptions): stepfunctions_tasks.EcsLaunchTargetConfig { + if (!launchTargetOptions.taskDefinition.isFargateCompatible) { + throw new Error('Supplied TaskDefinition is not compatible with Fargate'); + } + + return { + parameters: { + CapacityProviderStrategy: [ + { + CapacityProvider: 'FARGATE_SPOT', + }, + ], + }, + }; + } } /** @@ -153,6 +185,11 @@ export class FargateRunner extends Construct implements IRunnerProvider { */ readonly connections: ec2.Connections; + /** + * Use spot pricing for Fargate tasks. + */ + readonly spot: boolean; + constructor(scope: Construct, id: string, props: FargateRunnerProps) { super(scope, id); @@ -169,6 +206,7 @@ export class FargateRunner extends Construct implements IRunnerProvider { enableFargateCapacityProviders: true, }, ); + this.spot = props.spot ?? false; this.task = new ecs.FargateTaskDefinition( this, @@ -218,7 +256,7 @@ export class FargateRunner extends Construct implements IRunnerProvider { integrationPattern: IntegrationPattern.RUN_JOB, // sync taskDefinition: this.task, cluster: this.cluster, - launchTarget: new stepfunctions_tasks.EcsFargateLaunchTarget(), + launchTarget: this.spot ? new EcsFargateSpotLaunchTarget() : new stepfunctions_tasks.EcsFargateLaunchTarget(), assignPublicIp: this.assignPublicIp, securityGroups: this.securityGroup ? [this.securityGroup] : undefined, containerOverrides: [ @@ -255,4 +293,4 @@ export class FargateRunner extends Construct implements IRunnerProvider { }, ); } -} \ No newline at end of file +}