src/providers/codebuild.ts
Rama main · réplica de lectura del repositorio original.
import {
Annotations,
ArnFormat,
Aws,
aws_codebuild as codebuild,
aws_ec2 as ec2,
aws_iam as iam,
aws_logs as logs,
aws_stepfunctions as stepfunctions,
Duration,
RemovalPolicy,
Stack,
} from 'aws-cdk-lib';
import { ComputeType, LinuxGpuBuildImage } from 'aws-cdk-lib/aws-codebuild';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';
import {
Architecture,
BaseProvider,
IParameterizedRunnerProvider,
IRunnerProvider,
IRunnerProviderStatus,
Os,
providerParam,
RunnerConfig,
runnerDefaultLabels,
runnerEnvironment,
RunnerImage,
RunnerProviderProps,
RunnerVersion,
} from './common';
import { IRunnerImageBuilder, RunnerImageBuilder, RunnerImageBuilderProps, RunnerImageComponent } from '../image-builders';
export interface CodeBuildRunnerProviderProps extends RunnerProviderProps {
/**
* Runner image builder used to build Docker images containing GitHub Runner and all requirements.
*
* The image builder must contain the {@link RunnerImageComponent.docker} component unless `dockerInDocker` is set to false.
*
* The image builder determines the OS and architecture of the runner.
*
* @default CodeBuildRunnerProvider.imageBuilder()
*/
readonly imageBuilder?: IRunnerImageBuilder;
/**
* GitHub Actions labels used for this provider.
*
* These labels are used to identify which provider should spawn a new on-demand runner. Every job sends a webhook with the labels it's looking for
* based on runs-on. We match the labels from the webhook with the labels specified here. If all the labels specified here are present in the
* job's labels, this provider will be chosen and spawn a new runner.
*
* @default ['codebuild']
*/
readonly labels?: string[];
/**
* GitHub Actions runner group name.
*
* If specified, the runner will be registered with this group name. Setting a runner group can help managing access to self-hosted runners. It
* requires a paid GitHub account and organization level runner registration.
*
* The group must exist or the runner will not start.
*
* Users will still be able to trigger this runner with the correct labels. But the runner will only be able to run jobs from repos allowed to use the group.
*
* @default undefined
*/
readonly group?: string;
/**
* VPC to launch the runners in.
*
* @default no VPC
*/
readonly vpc?: ec2.IVpc;
/**
* Security groups to assign to this instance.
*
* @default a new security group, if {@link vpc} is used
*/
readonly securityGroups?: ec2.ISecurityGroup[];
/**
* Where to place the network interfaces within the VPC.
*
* @default no subnet
*/
readonly subnetSelection?: ec2.SubnetSelection;
/**
* The type of compute to use for this build.
* See the {@link ComputeType} enum for the possible values.
*
* The compute type determines CPU, memory, and disk space:
* - SMALL: 2 vCPU, 3 GB RAM, 64 GB disk
* - MEDIUM: 4 vCPU, 7 GB RAM, 128 GB disk
* - LARGE: 8 vCPU, 15 GB RAM, 128 GB disk
* - X2_LARGE: 72 vCPU, 145 GB RAM, 256 GB disk (Linux) or 824 GB disk (Windows)
*
* Use a larger compute type when you need more disk space for building larger Docker images.
*
* For more details, see https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-compute-types.html#environment.types
*
* @default {@link ComputeType#SMALL}
*/
readonly computeType?: codebuild.ComputeType;
/**
* The number of minutes after which AWS CodeBuild stops the build if it's
* not complete. For valid values, see the timeoutInMinutes field in the AWS
* CodeBuild User Guide.
*
* @default Duration.hours(1)
*/
readonly timeout?: Duration;
/**
* Support building and running Docker images by enabling Docker-in-Docker (dind) and the required CodeBuild privileged mode. Disabling this can
* speed up provisioning of CodeBuild runners. If you don't intend on running or building Docker images, disable this for faster start-up times.
*
* @default true
*/
readonly dockerInDocker?: boolean;
/**
* Use GPU compute for builds. When enabled, the default compute type is BUILD_GENERAL1_SMALL (4 vCPU, 16 GB RAM, 1 NVIDIA A10G GPU).
*
* You can override the compute type using the `computeType` property (for example, to use BUILD_GENERAL1_LARGE for more resources),
* subject to the supported GPU compute types.
*
* When using GPU compute, ensure your runner image includes any required GPU libraries (for example, CUDA)
* either by using a base image that has them preinstalled (such as an appropriate nvidia/cuda image) or by
* adding image components that install them. The default image builder does not automatically switch to a
* CUDA-enabled base image when GPU is enabled.
*
* GPU compute is only available for Linux x64 images. Not supported on Windows or ARM.
*
* @default false
*/
readonly gpu?: boolean;
}
/**
* Runner config for the CodeBuild family fragment.
*
* @internal
*/
interface CodeBuildRunnerConfig extends RunnerConfig {
readonly projectName: string;
}
/**
* GitHub Actions runner provider using CodeBuild to execute jobs.
*
* Creates a project that gets started for each job.
*
* This construct is not meant to be used by itself. It should be passed in the providers property for GitHubRunners.
*/
export class CodeBuildRunnerProvider extends BaseProvider implements IRunnerProvider, IParameterizedRunnerProvider {
/** @internal */
public static readonly _FAMILY = 'codebuild';
/**
* The fragment that runs any CodeBuild provider. Reads the config {@link _runnerConfig} generated for it from
* `$.providerParams`. Renders what CodeBuildStartBuild used to render per provider.
*
* @internal
*/
public static _stateMachineFragment(scope: Construct): stepfunctions.IChainable {
const p = providerParam<CodeBuildRunnerConfig>;
return new stepfunctions.CustomState(scope, 'CodeBuild Runner', {
stateJson: {
Type: 'Task',
Resource: `arn:${Aws.PARTITION}:states:::codebuild:startBuild.sync`,
Parameters: {
'ProjectName.$': p('projectName'),
'EnvironmentVariablesOverride': runnerEnvironment((name, value) => ({
'Name': name,
'Type': 'PLAINTEXT',
'Value.$': value,
})),
},
},
});
}
/**
* Create new image builder that builds CodeBuild specific runner images.
*
* You can customize the OS, architecture, VPC, subnet, security groups, etc. by passing in props.
*
* You can add components to the image builder by calling `imageBuilder.addComponent()`.
*
* The default OS is Ubuntu running on x64 architecture.
*
* Included components:
* * `RunnerImageComponent.requiredPackages()`
* * `RunnerImageComponent.runnerUser()`
* * `RunnerImageComponent.git()`
* * `RunnerImageComponent.githubCli()`
* * `RunnerImageComponent.awsCli()`
* * `RunnerImageComponent.docker()`
* * `RunnerImageComponent.githubRunner()`
*/
public static imageBuilder(scope: Construct, id: string, props?: RunnerImageBuilderProps) {
return RunnerImageBuilder.new(scope, id, {
os: Os.LINUX_UBUNTU,
architecture: Architecture.X86_64,
components: [
RunnerImageComponent.requiredPackages(),
RunnerImageComponent.runnerUser(),
RunnerImageComponent.git(),
RunnerImageComponent.githubCli(),
RunnerImageComponent.awsCli(),
RunnerImageComponent.docker(),
RunnerImageComponent.githubRunner(props?.runnerVersion ?? RunnerVersion.latest()),
],
...props,
});
}
/**
* CodeBuild project hosting the runner.
*/
readonly project: codebuild.Project;
/**
* Labels associated with this provider.
*/
readonly labels: string[];
/**
* Grant principal used to add permissions to the runner role.
*/
readonly grantPrincipal: iam.IPrincipal;
/**
* Docker image loaded with GitHub Actions Runner and its prerequisites. The image is built by an image builder and is specific to CodeBuild.
*/
private readonly image: RunnerImage;
/**
* Log group where provided runners will save their logs.
*
* Note that this is not the job log, but the runner itself. It will not contain output from the GitHub Action but only metadata on its execution.
*/
readonly logGroup: logs.ILogGroup;
private readonly group?: string;
private readonly vpc?: ec2.IVpc;
private readonly securityGroups?: ec2.ISecurityGroup[];
private readonly dind: boolean;
private readonly defaultLabels: boolean;
constructor(scope: Construct, id: string, props?: CodeBuildRunnerProviderProps) {
super(scope, id, props);
// warn against isolated networks
if (props?.subnetSelection?.subnetType == ec2.SubnetType.PRIVATE_ISOLATED) {
Annotations.of(this).addWarning('Private isolated subnets cannot pull from public ECR and VPC endpoint is not supported yet. ' +
'See https://github.com/aws/containers-roadmap/issues/1160');
}
// error out on no-nat networks because the build will hang
if (props?.subnetSelection?.subnetType == ec2.SubnetType.PUBLIC) {
Annotations.of(this).addError('Public subnets do not work with CodeBuild as it cannot be assigned an IP. ' +
'See https://docs.aws.amazon.com/codebuild/latest/userguide/vpc-support.html#best-practices-for-vpcs');
}
this.labels = props?.labels ?? ['codebuild'];
this.group = props?.group;
this.vpc = props?.vpc;
if (props?.securityGroups) {
this.securityGroups = props.securityGroups;
} else {
if (this.vpc) {
this.securityGroups = [new ec2.SecurityGroup(this, 'SG', { vpc: this.vpc })];
}
}
this.dind = props?.dockerInDocker ?? true;
this.defaultLabels = props?.defaultLabels ?? true;
let buildSpec = {
version: 0.2,
env: {
variables: {
RUNNER_JIT_CONFIG: '',
RUNNER_NAME: 'unspecified',
RUNNER_LABEL: 'unspecified',
OWNER: 'unspecified',
REPO: 'unspecified',
GITHUB_DOMAIN: 'github.com',
},
},
phases: {
install: {
commands: [
this.dind ? 'nohup dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2 &' : '',
this.dind ? 'timeout 15 sh -c "until docker info; do echo .; sleep 1; done"' : '',
'/home/runner/job-reporter.sh ${RUNNER_NAME}',
],
},
build: {
commands: [
'sudo --preserve-env=AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,AWS_DEFAULT_REGION,AWS_REGION -Hu runner /home/runner/run.sh --jitconfig "${RUNNER_JIT_CONFIG}"',
'STATUS=$(grep -Phors "finish job request for job [0-9a-f-]+ with result: .*" /home/runner/_diag/ | tail -n1 | awk \'{print $NF}\')',
'if [ -n "$STATUS" ]; then echo CDKGHA JOB DONE "$RUNNER_LABEL" "$STATUS"; fi',
],
},
},
};
const imageBuilder = props?.imageBuilder ?? CodeBuildRunnerProvider.imageBuilder(this, 'Image Builder');
const image = this.image = imageBuilder.bindDockerImage();
if (image.os.is(Os.WINDOWS)) {
buildSpec.phases.install.commands = [
'cd \\actions',
'& ./job-reporter.ps1 "${Env:RUNNER_NAME}"',
];
buildSpec.phases.build.commands = [
'cd \\actions',
'./run.cmd --jitconfig "${Env:RUNNER_JIT_CONFIG}"',
'$STATUS = Select-String -Path \'./_diag/*.log\' -Pattern \'finish job request for job [0-9a-f\\-]+ with result: (.*)\' | %{$_.Matches.Groups[1].Value} | Select-Object -Last 1',
'if ($STATUS) { echo "CDKGHA JOB DONE $\{Env:RUNNER_LABEL\} $STATUS" }',
];
}
if (props?.gpu) {
if (image.os.is(Os.WINDOWS) || image.architecture.is(Architecture.ARM64)) {
throw new Error('CodeBuild GPU is only supported for Linux x64 images. Set gpu: false or use a Linux x64 image.');
}
if (props?.computeType !== undefined && props.computeType !== ComputeType.SMALL && props.computeType !== ComputeType.LARGE) {
throw new Error(`CodeBuild GPU only supports SMALL (1 GPU) or LARGE (4 GPUs). Got ${props.computeType}.`);
}
}
// choose build image
let buildImage: codebuild.IBuildImage | undefined;
if (image.os.isIn(Os._ALL_LINUX_VERSIONS)) {
if (props?.gpu && image.architecture.is(Architecture.X86_64)) {
buildImage = LinuxGpuBuildImage.fromEcrRepository(image.imageRepository, image.imageTag);
} else if (image.architecture.is(Architecture.X86_64)) {
buildImage = codebuild.LinuxBuildImage.fromEcrRepository(image.imageRepository, image.imageTag);
} else if (image.architecture.is(Architecture.ARM64)) {
buildImage = codebuild.LinuxArmBuildImage.fromEcrRepository(image.imageRepository, image.imageTag);
}
}
if (image.os.is(Os.WINDOWS)) {
if (image.architecture.is(Architecture.X86_64)) {
buildImage = codebuild.WindowsBuildImage.fromEcrRepository(image.imageRepository, image.imageTag, codebuild.WindowsImageType.SERVER_2019);
}
}
if (buildImage === undefined) {
throw new Error(`Unable to find supported CodeBuild image for ${image.os.name}/${image.architecture.name}`);
}
// create project
this.logGroup = new logs.LogGroup(
this,
'Logs',
{
retention: props?.logRetention ?? RetentionDays.ONE_MONTH,
removalPolicy: RemovalPolicy.DESTROY,
},
);
this.project = new codebuild.Project(
this,
'CodeBuild',
{
description: `GitHub Actions self-hosted runner for labels ${this.labels}`,
buildSpec: codebuild.BuildSpec.fromObject(buildSpec),
vpc: this.vpc,
securityGroups: this.securityGroups,
subnetSelection: props?.subnetSelection,
timeout: props?.timeout ?? Duration.hours(1),
environment: {
buildImage,
computeType: props?.computeType ?? ComputeType.SMALL,
privileged: this.dind && !image.os.is(Os.WINDOWS),
},
logging: {
cloudWatch: {
logGroup: this.logGroup,
},
},
},
);
this.grantPrincipal = this.project.grantPrincipal;
// allow SSM Session Manager access
// this.project.role?.addToPrincipalPolicy(MINIMAL_SSM_SESSION_MANAGER_POLICY_STATEMENT);
// step function won't let us pass `debugSessionEnabled: true` unless we use batch, so we can't use this
}
/**
* Config for the shared CodeBuild fragment.
*
* @internal
*/
_runnerConfig(): CodeBuildRunnerConfig {
return {
family: CodeBuildRunnerProvider._FAMILY,
provider: this.node.path,
projectName: this.project.projectName,
runnerGroup: this.group ?? '',
defaultLabels: runnerDefaultLabels(this.image.os, this.image.architecture, this.defaultLabels),
};
}
/**
* @internal
*/
_grantStateMachine(stateMachineRole: iam.IGrantable) {
stateMachineRole.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['codebuild:StartBuild', 'codebuild:StopBuild', 'codebuild:BatchGetBuilds', 'codebuild:BatchGetReports'],
resources: [this.project.projectArn],
}));
// managed rule for the startBuild.sync integration
// every CodeBuild provider emits the same statement so policy minimization keeps just one
stateMachineRole.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'],
resources: [Stack.of(this).formatArn({
service: 'events',
resource: 'rule',
resourceName: 'StepFunctionsGetEventForCodeBuildStartBuildRule',
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
})],
}));
}
/**
* @internal
*/
_status(statusFunctionRole: iam.IGrantable): IRunnerProviderStatus {
this.image.imageRepository.grant(statusFunctionRole, 'ecr:DescribeImages');
return {
type: this.constructor.name,
labels: this.labels,
constructPath: this.node.path,
vpcArn: this.vpc?.vpcArn,
securityGroups: this.securityGroups?.map(sg => sg.securityGroupId),
roleArn: this.project.role?.roleArn,
logGroup: this.logGroup.logGroupName,
image: {
imageRepository: this.image.imageRepository.repositoryUri,
imageTag: this.image.imageTag,
imageBuilderLogGroup: this.image.logGroup?.logGroupName,
},
};
}
/**
* The network connections associated with this resource.
*/
public get connections(): ec2.Connections {
return this.project.connections;
}
}