Skip to content

Commit

Permalink
add dockerSetupCommands
Browse files Browse the repository at this point in the history
  • Loading branch information
kichik committed May 12, 2024
1 parent 3de4f79 commit f815b14
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 11 deletions.
23 changes: 23 additions & 0 deletions src/image-builders/aws-image-builder/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ImageBuilderObjectBase } from './common';
import { ContainerRecipe, defaultBaseDockerImage } from './container';
import { DeleteAmiFunction } from './delete-ami-function';
import { FilterFailedBuildsFunction } from './filter-failed-builds-function';
import { generateBuildWorkflowWithDockerSetupCommands } from './workflow';
import { Architecture, Os, RunnerAmi, RunnerImage, RunnerVersion } from '../../providers';
import { singletonLogGroup, singletonLambda, SingletonLogType } from '../../utils';
import { BuildImageFunction } from '../build-image-function';
Expand Down Expand Up @@ -311,6 +312,7 @@ export class AwsImageBuilderRunnerImageBuilder extends RunnerImageBuilderBase {
private readonly role: iam.Role;
private readonly fastLaunchOptions?: FastLaunchOptions;
private readonly waitOnDeploy: boolean;
private readonly dockerSetupCommands: string[];

constructor(scope: Construct, id: string, props?: RunnerImageBuilderProps) {
super(scope, id, props);
Expand All @@ -332,6 +334,7 @@ export class AwsImageBuilderRunnerImageBuilder extends RunnerImageBuilderBase {
this.instanceType = props?.awsImageBuilderOptions?.instanceType ?? ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE);
this.fastLaunchOptions = props?.awsImageBuilderOptions?.fastLaunchOptions;
this.waitOnDeploy = props?.waitOnDeploy ?? true;
this.dockerSetupCommands = props?.dockerSetupCommands??[];

// confirm instance type
if (!this.architecture.instanceTypeMatch(this.instanceType)) {
Expand Down Expand Up @@ -597,13 +600,31 @@ export class AwsImageBuilderRunnerImageBuilder extends RunnerImageBuilderBase {

protected createPipeline(infra: imagebuilder.CfnInfrastructureConfiguration, dist: imagebuilder.CfnDistributionConfiguration, log: logs.LogGroup,
imageRecipeArn?: string, containerRecipeArn?: string): imagebuilder.CfnImagePipeline {
// set schedule
let scheduleOptions: imagebuilder.CfnImagePipeline.ScheduleProperty | undefined;
if (this.rebuildInterval.toDays() > 0) {
scheduleOptions = {
scheduleExpression: events.Schedule.rate(this.rebuildInterval).expressionString,
pipelineExecutionStartCondition: 'EXPRESSION_MATCH_ONLY',
};
}

// generate workflows, if needed
let workflows: imagebuilder.CfnImagePipeline.WorkflowConfigurationProperty[] | undefined;
let executionRole: iam.IRole | undefined;
if (this.dockerSetupCommands.length > 0) {
workflows = [{
workflowArn: generateBuildWorkflowWithDockerSetupCommands(this, 'Build', this.dockerSetupCommands).arn,
}];
executionRole = 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',
}));
}

// generate pipeline
const pipeline = new imagebuilder.CfnImagePipeline(this, this.amiOrContainerId('Pipeline', imageRecipeArn, containerRecipeArn), {
name: uniqueImageBuilderName(this),
// description: this.description,
Expand All @@ -615,6 +636,8 @@ export class AwsImageBuilderRunnerImageBuilder extends RunnerImageBuilderBase {
imageTestsConfiguration: {
imageTestsEnabled: false,
},
workflows: workflows,
executionRole: executionRole?.roleArn,
});
pipeline.node.addDependency(infra);
pipeline.node.addDependency(log);
Expand Down
3 changes: 2 additions & 1 deletion src/image-builders/aws-image-builder/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export abstract class ImageBuilderObjectBase extends cdk.Resource {
super(scope, id);
}

protected generateVersion(type: 'Component' | 'ImageRecipe' | 'ContainerRecipe', name: string, data: any): string {
protected generateVersion(type: 'Component' | 'ImageRecipe' | 'ContainerRecipe' | 'Workflow', name: string, data: any): string {
return new CustomResource(this, 'Version', {
serviceToken: this.versionFunction().functionArn,
resourceType: `Custom::ImageBuilder-${type}-Version`,
Expand All @@ -34,6 +34,7 @@ export abstract class ImageBuilderObjectBase extends cdk.Resource {
'imagebuilder:ListComponents',
'imagebuilder:ListContainerRecipes',
'imagebuilder:ListImageRecipes',
'imagebuilder:ListWorkflows',
],
resources: ['*'],
}),
Expand Down
22 changes: 20 additions & 2 deletions src/image-builders/aws-image-builder/versioner.lambda.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {
ImagebuilderClient,
ListComponentsCommand, ListComponentsResponse,
ListContainerRecipesCommand, ListContainerRecipesResponse,
ListComponentsCommand,
ListComponentsResponse,
ListContainerRecipesCommand,
ListContainerRecipesResponse,
ListImageRecipesCommand,
ListImageRecipesResponse,
ListWorkflowsCommand,
ListWorkflowsResponse,
} from '@aws-sdk/client-imagebuilder';
import * as AWSLambda from 'aws-lambda';
import { inc, maxSatisfying } from 'semver';
Expand Down Expand Up @@ -85,6 +89,20 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
} while (result.nextToken);
break;
}
case 'Workflow': {
let result: ListWorkflowsResponse = {};
do {
result = await ib.send(new ListWorkflowsCommand({
filters: [{
name: 'name',
values: [objectName],
}],
nextToken: result.nextToken,
}));
allVersions = allVersions.concat(result.workflowVersionList!.map(i => i.arn?.split('/').pop() || '1.0.0'));
} while (result.nextToken);
break;
}
}
} catch (e) {
if ((e as any).code !== 'ResourceNotFoundException') {
Expand Down
121 changes: 121 additions & 0 deletions src/image-builders/aws-image-builder/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { aws_imagebuilder as imagebuilder } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { ImageBuilderObjectBase } from './common';
import { uniqueImageBuilderName } from '../common';

/**
* Properties for Workflow construct.
*
* @internal
*/
export interface WorkflowProperties {
/**
* Workflow type.
*/
readonly type: 'BUILD' | 'TEST' | 'DISTRIBUTION';

/**
* YAML or JSON data for the workflow.
*/
readonly data: any;
}

/**
* Image builder workflow.
*
* @internal
*/
export class Workflow extends ImageBuilderObjectBase {
public readonly arn: string;
public readonly name: string;
public readonly version: string;

constructor(scope: Construct, id: string, props: WorkflowProperties) {
super(scope, id);

this.name = uniqueImageBuilderName(this);
this.version = this.generateVersion('Workflow', this.name, {
type: props.type,
data: props.data,
});

const workflow = new imagebuilder.CfnWorkflow(this, 'Workflow', {
name: uniqueImageBuilderName(this),
version: this.version,
type: props.type,
data: JSON.stringify(props.data),
});

this.arn = workflow.attrArn;
}
}

/**
* Returns a new build workflow based on arn:aws:imagebuilder:us-east-1:aws:workflow/build/build-container/1.0.1/1.
*
* It adds a DockerSetup step after bootstrapping but before the Docker image is built.
*
* @internal
*/
export function generateBuildWorkflowWithDockerSetupCommands(scope: Construct, id: string, dockerSetupCommands: string[]) {
return new Workflow(scope, id, {
type: 'BUILD',
data: {
name: 'build-container',
description: 'Workflow to build a container image',
schemaVersion: 1,
steps: [
{
name: 'LaunchBuildInstance',
action: 'LaunchInstance',
onFailure: 'Abort',
inputs: {
waitFor: 'ssmAgent',
},
},
{
name: 'BootstrapBuildInstance',
action: 'BootstrapInstanceForContainer',
onFailure: 'Abort',
if: {
stringEquals: 'DOCKER',
value: '$.imagebuilder.imageType',
},
inputs: {
'instanceId.$': '$.stepOutputs.LaunchBuildInstance.instanceId',
},
},
{
name: 'DockerSetup',
action: 'RunCommand',
onFailure: 'Abort',
if: {
stringEquals: 'DOCKER',
value: '$.imagebuilder.imageType',
},
inputs: {
'documentName': 'AWS-RunShellScript',
'parameters': {
commands: dockerSetupCommands,
},
'instanceId.$': '$.stepOutputs.LaunchBuildInstance.instanceId',
},
},
{
name: 'ApplyBuildComponents',
action: 'ExecuteComponents',
onFailure: 'Abort',
inputs: {
'instanceId.$': '$.stepOutputs.LaunchBuildInstance.instanceId',
},
},
],
outputs: [
{
name: 'InstanceId',
value: '$.stepOutputs.LaunchBuildInstance.instanceId',
},
],
},
});
}
4 changes: 3 additions & 1 deletion src/image-builders/codebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export class CodeBuildRunnerImageBuilder extends RunnerImageBuilderBase {
private readonly rebuildInterval: cdk.Duration;
private readonly role: iam.Role;
private readonly waitOnDeploy: boolean;
private readonly dockerSetupCommands: string[];

constructor(scope: Construct, id: string, props?: RunnerImageBuilderProps) {
super(scope, id, props);
Expand All @@ -98,6 +99,7 @@ export class CodeBuildRunnerImageBuilder extends RunnerImageBuilderBase {
this.baseImage = props?.baseDockerImage ?? defaultBaseDockerImage(this.os);
this.buildImage = props?.codeBuildOptions?.buildImage ?? this.getDefaultBuildImage();
this.waitOnDeploy = props?.waitOnDeploy ?? true;
this.dockerSetupCommands = props?.dockerSetupCommands ?? [];

// warn against isolated networks
if (props?.subnetSelection?.subnetType == ec2.SubnetType.PRIVATE_ISOLATED) {
Expand Down Expand Up @@ -309,7 +311,7 @@ export class CodeBuildRunnerImageBuilder extends RunnerImageBuilderBase {
commands: [
'echo "exec > >(tee -a /tmp/codebuild.log) 2>&1" > codebuild-log.sh',
`aws ecr get-login-password --region "$AWS_DEFAULT_REGION" | docker login --username AWS --password-stdin ${thisStack.account}.dkr.ecr.${thisStack.region}.amazonaws.com`,
],
].concat(this.dockerSetupCommands),
},
build: {
commands: commands.concat(
Expand Down
11 changes: 11 additions & 0 deletions src/image-builders/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,21 @@ export interface RunnerImageBuilderProps {
/**
* Base image from which Docker runner images will be built.
*
* When using private images from a different account or not on ECR, you may need to include additional setup commands with {@link dockerSetupCommands}.
*
* @default public.ecr.aws/lts/ubuntu:22.04 for Os.LINUX_UBUNTU, public.ecr.aws/amazonlinux/amazonlinux:2 for Os.LINUX_AMAZON_2, mcr.microsoft.com/windows/servercore:ltsc2019-amd64 for Os.WINDOWS
*/
readonly baseDockerImage?: string;

/**
* Additional commands to run on the build host before starting the Docker runner image build.
*
* Use this to execute commands such as `docker login` or `aws ecr get-login-password` to pull private base images.
*
* @default []
*/
readonly dockerSetupCommands?: string[];

/**
* Base AMI from which runner AMIs will be built.
*
Expand Down
10 changes: 5 additions & 5 deletions test/default.integ.snapshot/github-runners-test.assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
}
}
},
"deaec75154c038cd6fec17fd19b905a08bb9a78b97bc2e0fd132505eda22c650": {
"e11957e91069e38a8e846330a30f318c4bd52e5184eb9e1989f2fb1ee51f792d": {
"source": {
"path": "asset.deaec75154c038cd6fec17fd19b905a08bb9a78b97bc2e0fd132505eda22c650.lambda",
"path": "asset.e11957e91069e38a8e846330a30f318c4bd52e5184eb9e1989f2fb1ee51f792d.lambda",
"packaging": "zip"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "deaec75154c038cd6fec17fd19b905a08bb9a78b97bc2e0fd132505eda22c650.zip",
"objectKey": "e11957e91069e38a8e846330a30f318c4bd52e5184eb9e1989f2fb1ee51f792d.zip",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down Expand Up @@ -209,15 +209,15 @@
}
}
},
"3d73c0dc4026320310e3840669f8caa904429c36241fe5e00d41a67c7d9b0ef8": {
"923def6fb4b4595a3773d4d5d17bbe03ad63ff0a7dd8087e6cc14aed71e5b643": {
"source": {
"path": "github-runners-test.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "3d73c0dc4026320310e3840669f8caa904429c36241fe5e00d41a67c7d9b0ef8.json",
"objectKey": "923def6fb4b4595a3773d4d5d17bbe03ad63ff0a7dd8087e6cc14aed71e5b643.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down
5 changes: 3 additions & 2 deletions test/default.integ.snapshot/github-runners-test.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -9125,7 +9125,8 @@
"Action": [
"imagebuilder:ListComponents",
"imagebuilder:ListContainerRecipes",
"imagebuilder:ListImageRecipes"
"imagebuilder:ListImageRecipes",
"imagebuilder:ListWorkflows"
],
"Effect": "Allow",
"Resource": "*"
Expand All @@ -9148,7 +9149,7 @@
"S3Bucket": {
"Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}"
},
"S3Key": "deaec75154c038cd6fec17fd19b905a08bb9a78b97bc2e0fd132505eda22c650.zip"
"S3Key": "e11957e91069e38a8e846330a30f318c4bd52e5184eb9e1989f2fb1ee51f792d.zip"
},
"Description": "Custom resource handler that bumps up Image Builder versions",
"Environment": {
Expand Down

0 comments on commit f815b14

Please sign in to comment.