@cloudsnorkel/cdk-github-runners
Version:
CDK construct to create GitHub Actions self-hosted runners. Creates ephemeral runners on demand. Easy to deploy and highly customizable.
776 lines • 116 kB
JavaScript
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AwsImageBuilderFailedBuildNotifier = exports.AwsImageBuilderRunnerImageBuilder = exports.ImageBuilderComponent = void 0;
const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti");
const cdk = require("aws-cdk-lib");
const aws_cdk_lib_1 = require("aws-cdk-lib");
const aws_ecr_1 = require("aws-cdk-lib/aws-ecr");
const aws_logs_1 = require("aws-cdk-lib/aws-logs");
const ami_1 = require("./ami");
const base_image_1 = require("./base-image");
const container_1 = require("./container");
const delete_resources_function_1 = require("./delete-resources-function");
const filter_failed_builds_function_1 = require("./filter-failed-builds-function");
const workflow_1 = require("./workflow");
const providers_1 = require("../../providers");
const utils_1 = require("../../utils");
const build_image_function_1 = require("../build-image-function");
const common_1 = require("../common");
/**
* Components are a set of commands to run and optional files to add to an image. Components are the building blocks of images built by Image Builder.
*
* Example:
*
* ```
* new ImageBuilderComponent(this, 'AWS CLI', {
* platform: 'Windows',
* displayName: 'AWS CLI',
* description: 'Install latest version of AWS CLI',
* commands: [
* '$p = Start-Process msiexec.exe -PassThru -Wait -ArgumentList \'/i https://awscli.amazonaws.com/AWSCLIV2.msi /qn\'',
* 'if ($p.ExitCode -ne 0) { throw "Exit code is $p.ExitCode" }',
* ],
* }
* ```
*
* @deprecated Use `RunnerImageComponent` instead as this be internal soon.
*/
class ImageBuilderComponent extends cdk.Resource {
constructor(scope, id, props) {
super(scope, id);
this.assets = [];
this.platform = props.platform;
let steps = [];
if (props.assets) {
let inputs = [];
let extractCommands = [];
for (const asset of props.assets) {
this.assets.push(asset.asset);
if (asset.asset.isFile) {
inputs.push({
source: asset.asset.s3ObjectUrl,
destination: asset.path,
});
}
else if (asset.asset.isZipArchive) {
inputs.push({
source: asset.asset.s3ObjectUrl,
destination: `${asset.path}.zip`,
});
if (props.platform === 'Windows') {
extractCommands.push(`Expand-Archive "${asset.path}.zip" -DestinationPath "${asset.path}"`);
extractCommands.push(`del "${asset.path}.zip"`);
}
else {
extractCommands.push(`unzip "${asset.path}.zip" -d "${asset.path}"`);
extractCommands.push(`rm "${asset.path}.zip"`);
}
}
else {
throw new Error(`Unknown asset type: ${asset.asset}`);
}
}
steps.push({
name: 'Download',
action: 'S3Download',
inputs,
});
if (extractCommands.length > 0) {
steps.push({
name: 'Extract',
action: props.platform === 'Linux' ? 'ExecuteBash' : 'ExecutePowerShell',
inputs: {
commands: this.prefixCommandsWithErrorHandling(props.platform, extractCommands),
},
});
}
}
if (props.commands.length > 0) {
steps.push({
name: 'Run',
action: props.platform === 'Linux' ? 'ExecuteBash' : 'ExecutePowerShell',
inputs: {
commands: this.prefixCommandsWithErrorHandling(props.platform, props.commands),
},
});
}
if (props.reboot ?? false) {
steps.push({
name: 'Reboot',
action: 'Reboot',
inputs: {},
});
}
const data = {
name: props.displayName,
schemaVersion: '1.0',
phases: [
{
name: 'build',
steps,
},
],
};
const name = (0, common_1.uniqueImageBuilderName)(this);
const component = new aws_cdk_lib_1.aws_imagebuilder.CfnComponent(this, 'Component', {
name: name,
description: props.description,
platform: props.platform,
version: '1.0.0',
data: JSON.stringify(data),
});
this.arn = component.attrArn;
}
/**
* Grants read permissions to the principal on the assets buckets.
*
* @param grantee
*/
grantAssetsRead(grantee) {
for (const asset of this.assets) {
asset.grantRead(grantee);
}
}
prefixCommandsWithErrorHandling(platform, commands) {
if (platform == 'Windows') {
return [
'$ErrorActionPreference = \'Stop\'',
'$ProgressPreference = \'SilentlyContinue\'',
'Set-PSDebug -Trace 1',
].concat(commands);
}
else {
return [
'set -ex',
].concat(commands);
}
}
}
exports.ImageBuilderComponent = ImageBuilderComponent;
_a = JSII_RTTI_SYMBOL_1;
ImageBuilderComponent[_a] = { fqn: "@cloudsnorkel/cdk-github-runners.ImageBuilderComponent", version: "0.14.21" };
/**
* @internal
*/
class AwsImageBuilderRunnerImageBuilder extends common_1.RunnerImageBuilderBase {
constructor(scope, id, props) {
super(scope, id, props);
this.boundComponents = [];
if (props?.codeBuildOptions) {
aws_cdk_lib_1.Annotations.of(this).addWarning('codeBuildOptions are ignored when using AWS Image Builder to build runner images.');
}
this.os = props?.os ?? providers_1.Os.LINUX_UBUNTU;
this.architecture = props?.architecture ?? providers_1.Architecture.X86_64;
this.rebuildInterval = props?.rebuildInterval ?? aws_cdk_lib_1.Duration.days(7);
this.logRetention = props?.logRetention ?? aws_logs_1.RetentionDays.ONE_MONTH;
this.logRemovalPolicy = props?.logRemovalPolicy ?? aws_cdk_lib_1.RemovalPolicy.DESTROY;
this.vpc = props?.vpc ?? aws_cdk_lib_1.aws_ec2.Vpc.fromLookup(this, 'VPC', { isDefault: true });
this.securityGroups = props?.securityGroups ?? [new aws_cdk_lib_1.aws_ec2.SecurityGroup(this, 'SG', { vpc: this.vpc })];
this.subnetSelection = props?.subnetSelection;
this.instanceType = props?.awsImageBuilderOptions?.instanceType ?? aws_cdk_lib_1.aws_ec2.InstanceType.of(aws_cdk_lib_1.aws_ec2.InstanceClass.M6I, aws_cdk_lib_1.aws_ec2.InstanceSize.LARGE);
this.fastLaunchOptions = props?.awsImageBuilderOptions?.fastLaunchOptions;
this.storageSize = props?.awsImageBuilderOptions?.storageSize;
this.waitOnDeploy = props?.waitOnDeploy ?? true;
this.dockerSetupCommands = props?.dockerSetupCommands ?? [];
// normalize BaseContainerImageInput to BaseContainerImage (string support is deprecated, only at public API level)
const baseDockerImageInput = props?.baseDockerImage ?? (0, container_1.defaultBaseDockerImage)(this.os);
this.baseImage = typeof baseDockerImageInput === 'string' ? base_image_1.BaseContainerImage.fromString(baseDockerImageInput) : baseDockerImageInput;
// normalize BaseImageInput to BaseImage (string support is deprecated, only at public API level)
const baseAmiInput = props?.baseAmi ?? (0, ami_1.defaultBaseAmi)(this, this.os, this.architecture);
this.baseAmi = typeof baseAmiInput === 'string' ? base_image_1.BaseImage.fromString(baseAmiInput) : baseAmiInput;
// warn if using deprecated string format
if (props?.baseDockerImage && typeof props.baseDockerImage === 'string') {
aws_cdk_lib_1.Annotations.of(this).addWarning('Passing baseDockerImage as a string is deprecated. Please use BaseContainerImage static factory methods instead, e.g., BaseContainerImage.fromDockerHub("ubuntu", "22.04") or BaseContainerImage.fromString("public.ecr.aws/lts/ubuntu:22.04")');
}
if (props?.baseAmi && typeof props.baseAmi === 'string') {
aws_cdk_lib_1.Annotations.of(this).addWarning('Passing baseAmi as a string is deprecated. Please use BaseImage static factory methods instead, e.g., BaseImage.fromAmiId("ami-12345") or BaseImage.fromString("arn:aws:...")');
}
// tags for finding resources
this.tags = {
'GitHubRunners:Stack': cdk.Stack.of(this).stackName,
'GitHubRunners:Builder': this.node.path,
};
// confirm instance type
if (!this.architecture.instanceTypeMatch(this.instanceType)) {
throw new Error(`Builder architecture (${this.architecture.name}) doesn't match selected instance type (${this.instanceType} / ${this.instanceType.architecture})`);
}
// warn against isolated networks
if (props?.subnetSelection?.subnetType == aws_cdk_lib_1.aws_ec2.SubnetType.PRIVATE_ISOLATED) {
aws_cdk_lib_1.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');
}
// role to be used by AWS Image Builder
this.role = new aws_cdk_lib_1.aws_iam.Role(this, 'Role', {
assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal('ec2.amazonaws.com'),
});
// create container workflow if docker setup commands are provided
if (this.dockerSetupCommands.length > 0) {
this.containerWorkflow = (0, workflow_1.generateBuildWorkflowWithDockerSetupCommands)(this, 'Build', this.os, this.dockerSetupCommands);
this.containerWorkflowExecutionRole = aws_cdk_lib_1.aws_iam.Role.fromRoleArn(this, 'Image Builder Role', cdk.Stack.of(this).formatArn({
service: 'iam',
region: '',
resource: 'role',
resourceName: 'aws-service-role/imagebuilder.amazonaws.com/AWSServiceRoleForImageBuilder',
}));
}
}
platform() {
if (this.os.is(providers_1.Os.WINDOWS)) {
return 'Windows';
}
if (this.os.isIn(providers_1.Os._ALL_LINUX_VERSIONS)) {
return 'Linux';
}
throw new Error(`OS ${this.os.name} is not supported by AWS Image Builder`);
}
/**
* Called by IRunnerProvider to finalize settings and create the image builder.
*/
bindDockerImage() {
if (this.boundDockerImage) {
return this.boundDockerImage;
}
// create repository that only keeps one tag
const repository = new aws_cdk_lib_1.aws_ecr.Repository(this, 'Repository', {
imageScanOnPush: true,
imageTagMutability: aws_ecr_1.TagMutability.MUTABLE,
removalPolicy: aws_cdk_lib_1.RemovalPolicy.DESTROY,
emptyOnDelete: true,
});
const dist = new aws_cdk_lib_1.aws_imagebuilder.CfnDistributionConfiguration(this, 'Docker Distribution', {
name: (0, common_1.uniqueImageBuilderName)(this),
// description: this.description,
distributions: [
{
region: aws_cdk_lib_1.Stack.of(this).region,
containerDistributionConfiguration: {
ContainerTags: ['latest'],
TargetRepository: {
Service: 'ECR',
RepositoryName: repository.repositoryName,
},
},
},
],
tags: this.tags,
});
let dockerfileTemplate = `FROM {{{ imagebuilder:parentImage }}}
{{{ imagebuilder:environments }}}
{{{ imagebuilder:components }}}`;
for (const c of this.components) {
const commands = c.getDockerCommands(this.os, this.architecture);
if (commands.length > 0) {
dockerfileTemplate += '\n' + commands.join('\n') + '\n';
}
}
const recipe = new container_1.ContainerRecipe(this, 'Container Recipe', {
platform: this.platform(),
components: this.bindComponents(),
targetRepository: repository,
dockerfileTemplate: dockerfileTemplate,
parentImage: this.baseImage.image,
tags: this.tags,
});
const log = this.createLog('Docker Log', recipe.name);
const infra = this.createInfrastructure([
aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('EC2InstanceProfileForImageBuilderECRContainerBuilds'),
]);
if (this.waitOnDeploy) {
this.createImage(infra, dist, log, undefined, recipe.arn);
}
this.dockerImageCleaner(recipe, repository);
this.createPipeline(infra, dist, log, undefined, recipe.arn);
this.boundDockerImage = {
imageRepository: repository,
imageTag: 'latest',
os: this.os,
architecture: this.architecture,
logGroup: log,
runnerVersion: providers_1.RunnerVersion.specific('unknown'),
// no dependable as CloudFormation will fail to get image ARN once the image is deleted (we delete old images daily)
};
return this.boundDockerImage;
}
dockerImageCleaner(recipe, repository) {
// this is here to provide safe upgrade from old cdk-github-runners versions
// this lambda was used by a custom resource to delete all images builds on cleanup
// if we remove the custom resource and the lambda, the old images will be deleted on update
// keeping the lambda but removing the permissions will make sure that deletion will fail
const oldDeleter = (0, utils_1.singletonLambda)(build_image_function_1.BuildImageFunction, this, 'build-image', {
description: 'Custom resource handler that triggers CodeBuild to build runner images',
timeout: cdk.Duration.minutes(3),
logGroup: (0, utils_1.singletonLogGroup)(this, utils_1.SingletonLogType.RUNNER_IMAGE_BUILD),
loggingFormat: aws_cdk_lib_1.aws_lambda.LoggingFormat.JSON,
});
oldDeleter.addToRolePolicy(new aws_cdk_lib_1.aws_iam.PolicyStatement({
effect: aws_cdk_lib_1.aws_iam.Effect.DENY,
actions: ['imagebuilder:DeleteImage'],
resources: ['*'],
}));
// delete old version on update and on stack deletion
this.imageCleaner('Container', recipe.name.toLowerCase(), recipe.version);
// delete old docker images + IB resources daily
new aws_cdk_lib_1.aws_imagebuilder.CfnLifecyclePolicy(this, 'Lifecycle Policy Docker', {
name: (0, common_1.uniqueImageBuilderName)(this),
description: `Delete old GitHub Runner Docker images for ${this.node.path}`,
executionRole: new aws_cdk_lib_1.aws_iam.Role(this, 'Lifecycle Policy Docker Role', {
assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal('imagebuilder.amazonaws.com'),
inlinePolicies: {
ib: new aws_cdk_lib_1.aws_iam.PolicyDocument({
statements: [
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: ['tag:GetResources', 'imagebuilder:DeleteImage'],
resources: ['*'], // Image Builder doesn't support scoping this :(
}),
],
}),
ecr: new aws_cdk_lib_1.aws_iam.PolicyDocument({
statements: [
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: ['ecr:BatchGetImage', 'ecr:BatchDeleteImage'],
resources: [repository.repositoryArn],
}),
],
}),
},
}).roleArn,
policyDetails: [{
action: {
type: 'DELETE',
includeResources: {
containers: true,
},
},
filter: {
type: 'COUNT',
value: 2,
},
}],
resourceType: 'CONTAINER_IMAGE',
resourceSelection: {
recipes: [
{
name: recipe.name,
semanticVersion: recipe.version,
},
],
},
});
}
createLog(id, recipeName) {
return new aws_cdk_lib_1.aws_logs.LogGroup(this, id, {
logGroupName: `/aws/imagebuilder/${recipeName}`,
retention: this.logRetention,
removalPolicy: this.logRemovalPolicy,
});
}
createInfrastructure(managedPolicies) {
if (this.infrastructure) {
return this.infrastructure;
}
for (const managedPolicy of managedPolicies) {
this.role.addManagedPolicy(managedPolicy);
}
for (const component of this.boundComponents) {
component.grantAssetsRead(this.role);
}
this.infrastructure = new aws_cdk_lib_1.aws_imagebuilder.CfnInfrastructureConfiguration(this, 'Infrastructure', {
name: (0, common_1.uniqueImageBuilderName)(this),
// description: this.description,
subnetId: this.vpc?.selectSubnets(this.subnetSelection).subnetIds[0],
securityGroupIds: this.securityGroups?.map(sg => sg.securityGroupId),
instanceTypes: [this.instanceType.toString()],
instanceMetadataOptions: {
httpTokens: 'required',
// Container builds require a minimum of two hops.
httpPutResponseHopLimit: 2,
},
instanceProfileName: new aws_cdk_lib_1.aws_iam.CfnInstanceProfile(this, 'Instance Profile', {
roles: [
this.role.roleName,
],
}).ref,
});
return this.infrastructure;
}
workflowConfig(containerRecipeArn) {
if (this.containerWorkflow && this.containerWorkflowExecutionRole && containerRecipeArn) {
return {
workflows: [{
workflowArn: this.containerWorkflow.arn,
}],
executionRole: this.containerWorkflowExecutionRole.roleArn,
};
}
return undefined;
}
createImage(infra, dist, log, imageRecipeArn, containerRecipeArn) {
const image = new aws_cdk_lib_1.aws_imagebuilder.CfnImage(this, this.amiOrContainerId('Image', imageRecipeArn, containerRecipeArn), {
infrastructureConfigurationArn: infra.attrArn,
distributionConfigurationArn: dist.attrArn,
imageRecipeArn,
containerRecipeArn,
imageTestsConfiguration: {
imageTestsEnabled: false,
},
tags: this.tags,
...this.workflowConfig(containerRecipeArn),
});
image.node.addDependency(infra);
image.node.addDependency(log);
// do not delete the image as it will be deleted by imageCleaner().
// if we delete it here, imageCleaner() won't be able to find the image.
// if imageCleaner() can't find the image, it won't be able to delete the linked AMI/Docker image.
// use RETAIN_ON_UPDATE_OR_DELETE, so everything is cleaned only on rollback.
image.applyRemovalPolicy(aws_cdk_lib_1.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE);
return image;
}
amiOrContainerId(baseId, imageRecipeArn, containerRecipeArn) {
if (imageRecipeArn) {
return `AMI ${baseId}`;
}
if (containerRecipeArn) {
return `Docker ${baseId}`;
}
throw new Error('Either imageRecipeArn or containerRecipeArn must be defined');
}
createPipeline(infra, dist, log, imageRecipeArn, containerRecipeArn) {
// set schedule
let scheduleOptions;
if (this.rebuildInterval.toDays() > 0) {
scheduleOptions = {
scheduleExpression: aws_cdk_lib_1.aws_events.Schedule.rate(this.rebuildInterval).expressionString,
pipelineExecutionStartCondition: 'EXPRESSION_MATCH_ONLY',
};
}
// generate pipeline
const pipeline = new aws_cdk_lib_1.aws_imagebuilder.CfnImagePipeline(this, this.amiOrContainerId('Pipeline', imageRecipeArn, containerRecipeArn), {
name: (0, common_1.uniqueImageBuilderName)(this),
// description: this.description,
infrastructureConfigurationArn: infra.attrArn,
distributionConfigurationArn: dist.attrArn,
imageRecipeArn,
containerRecipeArn,
schedule: scheduleOptions,
imageTestsConfiguration: {
imageTestsEnabled: false,
},
tags: this.tags,
...this.workflowConfig(containerRecipeArn),
});
pipeline.node.addDependency(infra);
pipeline.node.addDependency(log);
return pipeline;
}
/**
* The network connections associated with this resource.
*/
get connections() {
return new aws_cdk_lib_1.aws_ec2.Connections({ securityGroups: this.securityGroups });
}
get grantPrincipal() {
return this.role;
}
bindAmi() {
if (this.boundAmi) {
return this.boundAmi;
}
const launchTemplate = new aws_cdk_lib_1.aws_ec2.LaunchTemplate(this, 'Launch template', {
requireImdsv2: true,
});
const launchTemplateConfigs = [{
launchTemplateId: launchTemplate.launchTemplateId,
setDefaultVersion: true,
}];
const fastLaunchConfigs = [];
if (this.fastLaunchOptions?.enabled ?? false) {
if (!this.os.is(providers_1.Os.WINDOWS)) {
throw new Error('Fast launch is only supported for Windows');
}
// create a separate launch template for fast launch so:
// - settings don't affect the runners
// - enabling fast launch on an existing builder works (without a new launch template, EC2 Image Builder will use the first version of the launch template, which doesn't have instance or VPC config)
// - setting vpc + subnet on the main launch template will cause RunInstances to fail
// - EC2 Image Builder seems to get confused with which launch template version to base any new version on, so a new template is always best
const fastLaunchTemplate = new aws_cdk_lib_1.aws_ec2.CfnLaunchTemplate(this, 'Fast Launch Template', {
launchTemplateData: {
metadataOptions: {
httpTokens: 'required',
},
instanceType: this.instanceType.toString(),
networkInterfaces: [{
subnetId: this.vpc?.selectSubnets(this.subnetSelection).subnetIds[0],
deviceIndex: 0,
groups: this.securityGroups.map(sg => sg.securityGroupId),
}],
tagSpecifications: [
{
resourceType: 'instance',
tags: [{
key: 'Name',
value: `${this.node.path}/Fast Launch Instance`,
}],
},
{
resourceType: 'volume',
tags: [{
key: 'Name',
value: `${this.node.path}/Fast Launch Instance`,
}],
},
],
},
tagSpecifications: [{
resourceType: 'launch-template',
tags: [{
key: 'Name',
value: `${this.node.path}/Fast Launch Template`,
}],
}],
});
launchTemplateConfigs.push({
launchTemplateId: fastLaunchTemplate.attrLaunchTemplateId,
setDefaultVersion: true,
});
fastLaunchConfigs.push({
enabled: true,
launchTemplate: {
launchTemplateId: fastLaunchTemplate.attrLaunchTemplateId,
},
maxParallelLaunches: this.fastLaunchOptions?.maxParallelLaunches ?? 6,
snapshotConfiguration: {
targetResourceCount: this.fastLaunchOptions?.targetResourceCount ?? 1,
},
});
}
const stackName = cdk.Stack.of(this).stackName;
const builderName = this.node.path;
const dist = new aws_cdk_lib_1.aws_imagebuilder.CfnDistributionConfiguration(this, 'AMI Distribution', {
name: (0, common_1.uniqueImageBuilderName)(this),
// description: this.description,
distributions: [
{
region: aws_cdk_lib_1.Stack.of(this).region,
amiDistributionConfiguration: {
Name: `${cdk.Names.uniqueResourceName(this, {
maxLength: 100,
separator: '-',
allowedSpecialCharacters: '_-',
})}-{{ imagebuilder:buildDate }}`,
AmiTags: {
'Name': this.node.id,
'GitHubRunners:Stack': stackName,
'GitHubRunners:Builder': builderName,
},
},
launchTemplateConfigurations: launchTemplateConfigs,
fastLaunchConfigurations: fastLaunchConfigs.length > 0 ? fastLaunchConfigs : undefined,
},
],
tags: this.tags,
});
const recipe = new ami_1.AmiRecipe(this, 'Ami Recipe', {
platform: this.platform(),
components: this.bindComponents(),
architecture: this.architecture,
baseAmi: this.baseAmi,
storageSize: this.storageSize,
tags: this.tags,
});
const log = this.createLog('Ami Log', recipe.name);
const infra = this.createInfrastructure([
aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName('EC2InstanceProfileForImageBuilder'),
]);
if (this.waitOnDeploy) {
this.createImage(infra, dist, log, recipe.arn, undefined);
}
this.createPipeline(infra, dist, log, recipe.arn, undefined);
this.boundAmi = {
launchTemplate: launchTemplate,
architecture: this.architecture,
os: this.os,
logGroup: log,
runnerVersion: providers_1.RunnerVersion.specific('unknown'),
};
this.amiCleaner(recipe, stackName, builderName);
return this.boundAmi;
}
amiCleaner(recipe, stackName, builderName) {
// this is here to provide safe upgrade from old cdk-github-runners versions
// this lambda was used by a custom resource to delete all amis when the builder was removed
// if we remove the custom resource, role and lambda, all amis will be deleted on update
// keeping the just role but removing the permissions along with the custom resource will make sure that deletion will fail
const stack = cdk.Stack.of(this);
if (stack.node.tryFindChild('delete-ami-dcc036c8-876b-451e-a2c1-552f9e06e9e1') == undefined) {
const role = new aws_cdk_lib_1.aws_iam.Role(stack, 'delete-ami-dcc036c8-876b-451e-a2c1-552f9e06e9e1', {
description: 'Empty role to prevent deletion of AMIs on cdk-github-runners upgrade',
assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal('lambda.amazonaws.com'),
inlinePolicies: {
deny: new aws_cdk_lib_1.aws_iam.PolicyDocument({
statements: [
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: ['ec2:DeregisterImage', 'ec2:DeleteSnapshot'],
resources: ['*'],
effect: aws_cdk_lib_1.aws_iam.Effect.DENY,
}),
],
}),
},
});
const l1role = role.node.defaultChild;
l1role.overrideLogicalId('deleteamidcc036c8876b451ea2c1552f9e06e9e1ServiceRole1CC58A6F');
}
// delete old version on update and on stack deletion
this.imageCleaner('Image', recipe.name.toLowerCase(), recipe.version);
// delete old AMIs + IB resources daily
new aws_cdk_lib_1.aws_imagebuilder.CfnLifecyclePolicy(this, 'Lifecycle Policy AMI', {
name: (0, common_1.uniqueImageBuilderName)(this),
description: `Delete old GitHub Runner AMIs for ${this.node.path}`,
executionRole: new aws_cdk_lib_1.aws_iam.Role(this, 'Lifecycle Policy AMI Role', {
assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal('imagebuilder.amazonaws.com'),
inlinePolicies: {
ib: new aws_cdk_lib_1.aws_iam.PolicyDocument({
statements: [
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: ['tag:GetResources', 'imagebuilder:DeleteImage'],
resources: ['*'], // Image Builder doesn't support scoping this :(
}),
],
}),
ami: new aws_cdk_lib_1.aws_iam.PolicyDocument({
statements: [
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: ['ec2:DescribeImages', 'ec2:DescribeImageAttribute'],
resources: ['*'],
}),
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: ['ec2:DeregisterImage', 'ec2:DeleteSnapshot'],
resources: ['*'],
conditions: {
StringEquals: {
'aws:ResourceTag/GitHubRunners:Stack': stackName,
'aws:ResourceTag/GitHubRunners:Builder': builderName,
},
},
}),
],
}),
},
}).roleArn,
policyDetails: [{
action: {
type: 'DELETE',
includeResources: {
amis: true,
snapshots: true,
},
},
filter: {
type: 'COUNT',
value: 2,
},
}],
resourceType: 'AMI_IMAGE',
resourceSelection: {
recipes: [
{
name: recipe.name,
semanticVersion: recipe.version, // docs say it's optional, but it's not
},
],
},
});
}
bindComponents() {
if (this.boundComponents.length == 0) {
this.boundComponents.push(...this.components.map(c => c._asAwsImageBuilderComponent(this, this.os, this.architecture)));
}
return this.boundComponents;
}
imageCleaner(type, recipeName, version) {
const cleanerFunction = (0, utils_1.singletonLambda)(delete_resources_function_1.DeleteResourcesFunction, this, 'aws-image-builder-delete-resources', {
description: 'Custom resource handler that deletes resources of old versions of EC2 Image Builder images',
initialPolicy: [
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: [
'imagebuilder:ListImageBuildVersions',
'imagebuilder:DeleteImage',
],
resources: ['*'],
}),
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: ['ec2:DescribeImages'],
resources: ['*'],
}),
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: ['ec2:DeregisterImage', 'ec2:DeleteSnapshot'],
resources: ['*'],
conditions: {
StringEquals: {
'aws:ResourceTag/GitHubRunners:Stack': cdk.Stack.of(this).stackName,
},
},
}),
new aws_cdk_lib_1.aws_iam.PolicyStatement({
actions: ['ecr:BatchDeleteImage'],
resources: ['*'],
}),
],
logGroup: (0, utils_1.singletonLogGroup)(this, utils_1.SingletonLogType.RUNNER_IMAGE_BUILD),
loggingFormat: aws_cdk_lib_1.aws_lambda.LoggingFormat.JSON,
timeout: cdk.Duration.minutes(10),
});
new aws_cdk_lib_1.CustomResource(this, `${type} Cleaner`, {
serviceToken: cleanerFunction.functionArn,
resourceType: 'Custom::ImageBuilder-Delete-Resources',
properties: {
ImageVersionArn: cdk.Stack.of(this).formatArn({
service: 'imagebuilder',
resource: 'image',
resourceName: `${recipeName}/${version}`,
arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME,
}),
},
});
}
}
exports.AwsImageBuilderRunnerImageBuilder = AwsImageBuilderRunnerImageBuilder;
/**
* @internal
*/
class AwsImageBuilderFailedBuildNotifier {
static createFilteringTopic(scope, targetTopic) {
const topic = new aws_cdk_lib_1.aws_sns.Topic(scope, 'Image Builder Builds');
const filter = new filter_failed_builds_function_1.FilterFailedBuildsFunction(scope, 'Image Builder Builds Filter', {
logGroup: (0, utils_1.singletonLogGroup)(scope, utils_1.SingletonLogType.RUNNER_IMAGE_BUILD),
loggingFormat: aws_cdk_lib_1.aws_lambda.LoggingFormat.JSON,
environment: {
TARGET_TOPIC_ARN: targetTopic.topicArn,
},
});
topic.addSubscription(new aws_cdk_lib_1.aws_sns_subscriptions.LambdaSubscription(filter));
targetTopic.grantPublish(filter);
return topic;
}
constructor(topic) {
this.topic = topic;
}
visit(node) {
if (node instanceof AwsImageBuilderRunnerImageBuilder) {
const builder = node;
const infraNode = builder.node.tryFindChild('Infrastructure');
if (infraNode) {
const infra = infraNode;
infra.snsTopicArn = this.topic.topicArn;
}
else {
cdk.Annotations.of(builder).addWarning('Unused builder cannot get notifications of failed builds');
}
}
}
}
exports.AwsImageBuilderFailedBuildNotifier = AwsImageBuilderFailedBuildNotifier;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVpbGRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9pbWFnZS1idWlsZGVycy9hd3MtaW1hZ2UtYnVpbGRlci9idWlsZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7O0FBQUEsbUNBQW1DO0FBQ25DLDZDQWdCcUI7QUFDckIsaURBQW9EO0FBQ3BELG1EQUFxRDtBQUVyRCwrQkFBa0Q7QUFDbEQsNkNBQTZEO0FBQzdELDJDQUFzRTtBQUN0RSwyRUFBc0U7QUFFdEUsbUZBQTZFO0FBQzdFLHlDQUFvRjtBQUNwRiwrQ0FBMEY7QUFDMUYsdUNBQW1GO0FBQ25GLGtFQUE2RDtBQUM3RCxzQ0FBb0c7QUFvSHBHOzs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FrQkc7QUFDSCxNQUFhLHFCQUFzQixTQUFRLEdBQUcsQ0FBQyxRQUFRO0lBYXJELFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBc0M7UUFDOUUsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztRQUhGLFdBQU0sR0FBc0IsRUFBRSxDQUFDO1FBSzlDLElBQUksQ0FBQyxRQUFRLEdBQUcsS0FBSyxDQUFDLFFBQVEsQ0FBQztRQUUvQixJQUFJLEtBQUssR0FBVSxFQUFFLENBQUM7UUFFdEIsSUFBSSxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDakIsSUFBSSxNQUFNLEdBQVUsRUFBRSxDQUFDO1lBQ3ZCLElBQUksZUFBZSxHQUFhLEVBQUUsQ0FBQztZQUNuQyxLQUFLLE1BQU0sS0FBSyxJQUFJLEtBQUssQ0FBQyxNQUFNLEVBQUUsQ0FBQztnQkFDakMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDO2dCQUU5QixJQUFJLEtBQUssQ0FBQyxLQUFLLENBQUMsTUFBTSxFQUFFLENBQUM7b0JBQ3ZCLE1BQU0sQ0FBQyxJQUFJLENBQUM7d0JBQ1YsTUFBTSxFQUFFLEtBQUssQ0FBQyxLQUFLLENBQUMs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