├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── example ├── ecs-pipeline │ ├── bin │ │ ├── envvars.ts │ │ └── pipeline.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lib │ │ ├── cluster.ts │ │ ├── deploy.ts │ │ └── environment.ts │ ├── package-lock.json │ ├── package.json │ ├── test.json │ └── tsconfig.json └── simple-pipeline │ ├── bin │ └── pipeline.ts │ ├── cdk.json │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── index.ts ├── jest.config.js ├── lib ├── build-action.ts ├── build-manifest.ts ├── codebuild.ts ├── pipeline.ts └── test-action.ts ├── package-lock.json ├── package.json ├── test └── multiarch-container-build-pipeline.test.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2020": true 6 | }, 7 | "extends": [ 8 | "standard", 9 | "eslint:recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 11 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "semi": ["error", "always"], 20 | "space-before-function-paren": ["error", "never"], 21 | "indent": ["error", 2], 22 | "no-new": "off", 23 | "sort-imports": ["error"] 24 | }, 25 | "ignorePatterns": ["**/*.d.ts", "**/*.js"] 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | # Parcel build directories 11 | .cache 12 | .build 13 | 14 | # Editor cruft 15 | *~ 16 | 17 | # CDK context 18 | cdk.context.json 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | 8 | **/node_modules/ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Amazon Web Services, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Multi-Architecture Container Build Pipeline Library 2 | 3 | ## Introduction 4 | 5 | This repository contains an [AWS Cloud Development Kit 6 | (CDK)](https://docs.aws.amazon.com/cdk/latest/guide/home.html) pattern library 7 | to help you create code pipelines that build multi-architecture container 8 | images. This can help you build container images that run on both x86 9 | (Intel/AMD) and arm64 architectures, allowing you to better utilize the growing 10 | portfolio of Amazon EC2 instance families. 11 | 12 | The [AWS Graviton processor family](https://aws.amazon.com/ec2/graviton/) uses 13 | the Arm 64-bit (arm64) architecture. It provides up to 40% better 14 | price/performance vs. comparable X86-based compute. Many applications are easily 15 | adaptable to the arm64 architecture by simply recompiling the code. Programs 16 | written in scripting languages such as JavaScript, Ruby, and Python, and 17 | applications based on compiled byte code, such as Java and .NET, can usually be 18 | run without any modification by using a native arm64 runtime such as [Amazon 19 | Corretto](https://aws.amazon.com/corretto/). 20 | 21 | The Docker Image Manifest V2 specification allows container image repositories, 22 | including [Amazon ECR](https://aws.amazon.com/ecr/), to host images for multiple 23 | architectures. This allows you to run `docker pull` on a host and automatically 24 | receive the correct image for the host's CPU architecture. This pipeline library 25 | takes advantage of this functionality by constructing the multi-architecture 26 | manifest for you. 27 | 28 | ## Theory of operation 29 | 30 | This library builds a pipeline using [AWS 31 | CodePipeline](https://aws.amazon.com/codepipeline/) to produce an 32 | easily-accessible multi-architecture Docker image in [Amazon 33 | ECR](https://aws.amazon.com/ecr/). 34 | 35 | The pipeline stages are as follows: 36 | 37 | 1. Source stage: obtain the source code for the Docker image. 38 | 2. Build stage: the architecture-specific container images are built in 39 | parallel. 40 | 3. Test stage: the architecture-specific container images are tested in 41 | parallel. 42 | 4. Manifest build stage: the multi-architecture image manifest is produced and 43 | pushed to Amazon ECR. 44 | 45 | ## Usage 46 | 47 | First, you'll need to build an application using AWS CDK. Download the library 48 | and import it into your CDK application: 49 | 50 | ```sh 51 | $ npm install aws-multiarch-container-build-pipeline 52 | ``` 53 | 54 | ```ts 55 | import { Pipeline, Architecture } from 'aws-multiarch-container-build-pipeline'; 56 | ``` 57 | 58 | ### Source action 59 | 60 | Your application will need to create a CodePipeline source action. Many of the 61 | source actions provided by the [aws-codepipeline-actions 62 | library](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-codepipeline-actions-readme.html) 63 | are supported, including AWS CodeCommit, BitBucket, and GitHub. BitBucket and 64 | GitHub are supported **only** via the 65 | [`CodeStarConnectionsSourceAction`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_codepipeline_actions.CodeStarConnectionsSourceAction.html) 66 | class. In the source action properties, ensure `codeBuildCloneOutput` is set to 67 | `true`. 68 | 69 | Here's a simple example: 70 | 71 | ```ts 72 | const sourceAction = new CodeStarConnectionsSourceAction({ 73 | connectionArn: process.env.CODESTAR_CONNECTION_ARN, 74 | actionName: 'Source', 75 | owner: 'mycompany', 76 | repo: 'myapp', 77 | branch: 'main', 78 | // ensure this is set to `true` or CodeBuild won't be able to run `git` commands 79 | codeBuildCloneOutput: true, 80 | output: new Artifact() 81 | }); 82 | ``` 83 | 84 | ### ECR repository 85 | 86 | Your application will need to create an ECR repository or reference an existing 87 | repository. 88 | 89 | To create a new one: 90 | 91 | ```ts 92 | const imageRepo = new ecr.Repository(this, 'MyAppImageRepo'); 93 | ``` 94 | 95 | To reference an existing repository, you can use one of the static 96 | `fromRepository*` methods available in the [Repository 97 | class](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ecr.Repository.html). Here's an example: 98 | 99 | ```ts 100 | const ecrRepo = ecr.Repository.fromRepositoryName(this, 'MyAppImageRepo', myapp); 101 | ``` 102 | 103 | ### Construct the pipeline 104 | 105 | Then, your application can construct the pipeline: 106 | 107 | ```ts 108 | new Pipeline(this, 'Pipeline', { 109 | sourceAction: s3Source, 110 | imageRepo: ecrRepo, 111 | architectures: [Architecture.Arm64, Architecture.X86_64] 112 | }); 113 | ``` 114 | 115 | The following attributes can be passed to the pipeline constructor: 116 | 117 | | Attribute | Description | Required? | 118 | |---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| 119 | | `sourceAction` | A CodePipeline source action. Tells the pipeline where to get the source code and is used as the source stage. | Yes | 120 | | `imageRepo` | An ECR image repository. Used for storing and fetching images and manifests. | Yes | 121 | | `architectures` | Array of CPU architectures used for building and testing images. Defaults to `amd64`. Supported values include `amd64` and `arm64`. | | 122 | | `buildPath` | Path inside repository in which `Dockerfile` is located. Defaults to `.`. | | 123 | | `dockerBuildArgs` | Optional map of Docker build args. Equivalent to passing `--build-arg` to `docker build`. | | 124 | | `imageTag` | Tag to apply to generated images. Defaults to output of `git describe --tags --always`. You can use CodePipeline variable substitutions here, such as `'#{Source.CommitId}'`. | | 125 | | `buildTimeout` | Build timeout | | 126 | | `testTimeout` | Test timeout | | 127 | | `testBuildSpecPath` | Location of CodeBuild buildspec path used for test stage inside repository. Defaults to `./buildspec-test.yml`. | | 128 | 129 | ## Example 130 | 131 | An example of a minimal CDK application that uses this library can be found in 132 | the [example](example/) folder of this repository. 133 | 134 | ## License 135 | 136 | MIT 137 | -------------------------------------------------------------------------------- /example/ecs-pipeline/bin/envvars.ts: -------------------------------------------------------------------------------- 1 | const DefaultGitBranch = 'main'; 2 | 3 | function throwExpression(errorMessage: string): never { 4 | throw new Error(errorMessage); 5 | } 6 | 7 | export function getDomainName() { 8 | return process.env.DOMAIN_NAME || throwExpression('Missing DOMAIN_NAME'); 9 | } 10 | 11 | export function getCodeStarConnectionArn() { 12 | return process.env.CODESTAR_CONNECTION_ARN || throwExpression('Missing CODESTAR_CONNECTION_ARN'); 13 | } 14 | 15 | export function getGitHubOwner() { 16 | return process.env.GITHUB_OWNER || throwExpression('Missing GITHUB_OWNER'); 17 | } 18 | 19 | export function getGitHubRepo() { 20 | return process.env.GITHUB_REPO || throwExpression('Missing GITHUB_REPO'); 21 | } 22 | 23 | export function getGitBranch() { 24 | return process.env.GIT_BRANCH || DefaultGitBranch; 25 | } 26 | -------------------------------------------------------------------------------- /example/ecs-pipeline/bin/pipeline.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | 4 | import { App, Stack, StackProps } from 'aws-cdk-lib'; 5 | import { Architecture, BuildReleasePipeline } from 'aws-multiarch-container-build-pipeline'; 6 | import { Artifact, StageOptions } from 'aws-cdk-lib/aws-codepipeline'; 7 | import { CodeStarConnectionsSourceAction, ManualApprovalAction } from 'aws-cdk-lib/aws-codepipeline-actions'; 8 | import { getCodeStarConnectionArn, getDomainName, getGitBranch, getGitHubOwner, getGitHubRepo } from './envvars'; 9 | 10 | import { ApplicationEnvironment } from '../lib/environment'; 11 | import { ClusterStack } from '../lib/cluster'; 12 | import { Construct } from 'constructs'; 13 | import { DeployAction } from '../lib/deploy'; 14 | import { HostedZone } from 'aws-cdk-lib/aws-route53'; 15 | import { Repository as ImageRepository } from 'aws-cdk-lib/aws-ecr'; 16 | import { capitalize } from 'lodash'; 17 | 18 | const app = new App(); 19 | 20 | class EcsPipelineStack extends Stack { 21 | constructor(scope: Construct, id: string, props?: StackProps) { 22 | super(scope, id, props); 23 | 24 | const imageRepo = new ImageRepository(this, 'Repository', { 25 | repositoryName: 'multiarch-container-build-pipeline-test-ecs' 26 | }); 27 | 28 | const testCluster = new ClusterStack(this, 'Test', { 29 | hostedZone: HostedZone.fromLookup(this, 'ProdZone', { 30 | domainName: getDomainName() 31 | }), 32 | env: ApplicationEnvironment.TEST 33 | }); 34 | 35 | const prodCluster = new ClusterStack(this, 'Prod', { 36 | hostedZone: HostedZone.fromLookup(this, 'TestZone', { 37 | domainName: getDomainName() 38 | }), 39 | env: ApplicationEnvironment.PROD 40 | }); 41 | 42 | const artifact = new Artifact(); 43 | 44 | const sourceAction = new CodeStarConnectionsSourceAction({ 45 | connectionArn: getCodeStarConnectionArn(), 46 | actionName: 'Source', 47 | owner: getGitHubOwner(), 48 | repo: getGitHubRepo(), 49 | branch: getGitBranch(), 50 | codeBuildCloneOutput: true, 51 | output: artifact 52 | }); 53 | 54 | const pipeline = new BuildReleasePipeline(this, 'Pipeline', { 55 | sourceAction, 56 | imageRepo, 57 | architectures: [Architecture.Arm64, Architecture.X86_64] 58 | }); 59 | 60 | pipeline.addStage(this.deployStage(ApplicationEnvironment.TEST, artifact, testCluster)); 61 | 62 | pipeline.addStage({ 63 | stageName: 'Approve', 64 | actions: [ 65 | new ManualApprovalAction({ 66 | actionName: 'Approve' 67 | }) 68 | ] 69 | }); 70 | 71 | pipeline.addStage(this.deployStage(ApplicationEnvironment.PROD, artifact, prodCluster)); 72 | } 73 | 74 | private deployStage(env: ApplicationEnvironment, artifact: Artifact, stack: ClusterStack): StageOptions { 75 | const deployAction = new DeployAction(this, `DeployTo${capitalize(env)}Env`, { 76 | stack, 77 | input: artifact 78 | }); 79 | 80 | return { 81 | stageName: `DeployTo${capitalize(env)}`, 82 | actions: [deployAction] 83 | }; 84 | } 85 | } 86 | 87 | new EcsPipelineStack(app, 'MultiArchECSPipelineDemo', { 88 | env: { 89 | region: process.env.CDK_DEFAULT_REGION, 90 | account: process.env.CDK_DEFAULT_ACCOUNT 91 | } 92 | }); 93 | -------------------------------------------------------------------------------- /example/ecs-pipeline/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/pipeline.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/ecs-pipeline/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /example/ecs-pipeline/lib/cluster.ts: -------------------------------------------------------------------------------- 1 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 2 | 3 | import { ARecord, CfnRecordSet, IHostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53'; 4 | import { Aspects, CfnParameter, IAspect, Stack, Tag } from 'aws-cdk-lib'; 5 | import { Construct, IConstruct } from 'constructs'; 6 | import { IVpc, InstanceClass, InstanceSize, InstanceType, Port, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2'; 7 | 8 | import { ApplicationEnvironment } from './environment'; 9 | import { ApplicationLoadBalancedEc2Service } from 'aws-cdk-lib/aws-ecs-patterns'; 10 | import { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 11 | import { AutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling'; 12 | import { LoadBalancerTarget } from 'aws-cdk-lib/aws-route53-targets'; 13 | import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; 14 | 15 | const DefaultImage = 'public.ecr.aws/nginx/nginx:1.24.0'; 16 | 17 | export interface ClusterProps { 18 | hostedZone: IHostedZone; 19 | env: ApplicationEnvironment 20 | } 21 | 22 | // https://github.com/aws/aws-cdk/issues/19275#issuecomment-1152860147 23 | /** 24 | * Add a dependency from capacity provider association to the cluster 25 | * and from each service to the capacity provider association. 26 | */ 27 | class CapacityProviderDependencyAspect implements IAspect { 28 | public visit(node: IConstruct): void { 29 | if (node instanceof ecs.Ec2Service) { 30 | const children = node.cluster.node.findAll(); 31 | for (const child of children) { 32 | if (child instanceof ecs.CfnClusterCapacityProviderAssociations) { 33 | child.node.addDependency(node.cluster); 34 | node.node.addDependency(child); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | export class ClusterStack extends Stack { 42 | public readonly cluster: ecs.Cluster; 43 | public readonly x86Service: ecs.IService; 44 | public readonly arm64Service: ecs.IService; 45 | public readonly x86TaskDefinition: ecs.TaskDefinition; 46 | public readonly arm64TaskDefinition: ecs.TaskDefinition; 47 | public readonly x86CapacityProvider: ecs.AsgCapacityProvider; 48 | public readonly arm64CapacityProvider: ecs.AsgCapacityProvider; 49 | 50 | constructor(scope: Construct, id: string, props: ClusterProps) { 51 | super(scope, id); 52 | 53 | const vpc = new Vpc(this, 'VPC', { 54 | maxAzs: 2, 55 | natGateways: 1 56 | }); 57 | 58 | this.cluster = new ecs.Cluster(this, 'Cluster', { 59 | vpc 60 | }); 61 | 62 | const image = new CfnParameter(this, 'Image', { 63 | default: DefaultImage 64 | }); 65 | 66 | const x86ServiceInstance = new ServiceInstance(this, 'x86Instances', { 67 | vpc, 68 | domainZone: props.hostedZone, 69 | env: props.env, 70 | cluster: this.cluster, 71 | architecture: 'x86', 72 | image: image.valueAsString 73 | }); 74 | this.x86CapacityProvider = x86ServiceInstance.capacityProvider; 75 | this.x86TaskDefinition = x86ServiceInstance.taskDefinition; 76 | this.x86Service = x86ServiceInstance.service; 77 | 78 | const x86WeightedRecord = new ARecord(this, 'x86WeightedRecord', { 79 | target: RecordTarget.fromAlias(new LoadBalancerTarget(x86ServiceInstance.loadBalancer)), 80 | zone: props.hostedZone, 81 | recordName: `svc.${props.env}.${props.hostedZone.zoneName}.` 82 | }); 83 | (x86WeightedRecord.node.defaultChild as CfnRecordSet).weight = 100; 84 | (x86WeightedRecord.node.defaultChild as CfnRecordSet).setIdentifier = 'x86'; 85 | 86 | const arm64ServiceInstance = new ServiceInstance(this, 'arm64Instances', { 87 | vpc, 88 | domainZone: props.hostedZone, 89 | env: props.env, 90 | cluster: this.cluster, 91 | architecture: 'arm64', 92 | image: image.valueAsString 93 | }); 94 | this.arm64CapacityProvider = arm64ServiceInstance.capacityProvider; 95 | this.arm64TaskDefinition = arm64ServiceInstance.taskDefinition; 96 | this.arm64Service = arm64ServiceInstance.service; 97 | 98 | const arm64WeightedRecord = new ARecord(this, 'Arm64WeightedRecord', { 99 | target: RecordTarget.fromAlias(new LoadBalancerTarget(arm64ServiceInstance.loadBalancer)), 100 | zone: props.hostedZone, 101 | recordName: `svc.${props.env}.${props.hostedZone.zoneName}.` 102 | }); 103 | (arm64WeightedRecord.node.defaultChild as CfnRecordSet).weight = 100; 104 | (arm64WeightedRecord.node.defaultChild as CfnRecordSet).setIdentifier = 'arm64'; 105 | 106 | Aspects.of(this).add(new Tag('env', props.env)); 107 | Aspects.of(this).add(new CapacityProviderDependencyAspect()); 108 | } 109 | } 110 | 111 | interface InstanceGroupProps { 112 | vpc: IVpc; 113 | cluster: ecs.Cluster; 114 | architecture: 'x86' | 'arm64'; 115 | domainZone: IHostedZone; 116 | env: ApplicationEnvironment; 117 | image: string 118 | } 119 | 120 | class ServiceInstance extends Construct { 121 | public autoScalingGroup: AutoScalingGroup; 122 | public capacityProvider: ecs.AsgCapacityProvider; 123 | public service: ecs.IService; 124 | public taskDefinition: ecs.TaskDefinition; 125 | public loadBalancer: ApplicationLoadBalancer; 126 | 127 | constructor(scope: Construct, id: string, props: InstanceGroupProps) { 128 | super(scope, id); 129 | 130 | const instanceType = props.architecture === 'x86' ? InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM) : InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM); 131 | const machineImage = props.architecture === 'x86' ? ecs.EcsOptimizedImage.amazonLinux2(ecs.AmiHardwareType.STANDARD) : ecs.EcsOptimizedImage.amazonLinux2(ecs.AmiHardwareType.ARM); 132 | 133 | this.autoScalingGroup = new AutoScalingGroup(this, 'Instances', { 134 | vpc: props.vpc, 135 | instanceType, 136 | machineImage, 137 | minCapacity: 2, 138 | maxCapacity: 10, 139 | vpcSubnets: { 140 | subnetType: SubnetType.PRIVATE_WITH_EGRESS 141 | } 142 | }); 143 | this.autoScalingGroup.role.addManagedPolicy({ 144 | managedPolicyArn: 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role' 145 | }); 146 | this.autoScalingGroup.role.addManagedPolicy({ 147 | managedPolicyArn: 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore' 148 | }); 149 | this.autoScalingGroup.addUserData( 150 | `echo ECS_CLUSTER="${props.cluster.clusterName}" >> /etc/ecs/ecs.config` 151 | ); 152 | 153 | this.capacityProvider = new ecs.AsgCapacityProvider(this, 'CapacityProvider', { 154 | autoScalingGroup: this.autoScalingGroup, 155 | enableManagedScaling: true 156 | }); 157 | props.cluster.addAsgCapacityProvider(this.capacityProvider); 158 | 159 | const service = new ApplicationLoadBalancedEc2Service(this, 'Service', { 160 | domainName: `${props.architecture}.svc.${props.env}.${props.domainZone.zoneName}.`, 161 | domainZone: props.domainZone, 162 | cluster: props.cluster, 163 | cpu: 1024, 164 | memoryReservationMiB: 1024, 165 | capacityProviderStrategies: [{ 166 | capacityProvider: this.capacityProvider.capacityProviderName, 167 | base: 0, 168 | weight: 1 169 | }], 170 | desiredCount: 2, 171 | publicLoadBalancer: true, 172 | taskImageOptions: { 173 | image: ecs.ContainerImage.fromRegistry(props.image), 174 | containerPort: 80 175 | } 176 | }); 177 | service.taskDefinition.addToExecutionRolePolicy(new PolicyStatement({ 178 | actions: [ 179 | 'ecr:GetAuthorizationToken', 180 | 'ecr:BatchCheckLayerAvailability', 181 | 'ecr:GetDownloadUrlForLayer', 182 | 'ecr:BatchGetImage' 183 | ], 184 | resources: ['*'] 185 | })); 186 | this.service = service.service; 187 | this.taskDefinition = service.taskDefinition; 188 | this.loadBalancer = service.loadBalancer; 189 | this.autoScalingGroup.connections.allowFrom(service.loadBalancer, Port.allTcp(), 'Allow HTTP traffic'); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /example/ecs-pipeline/lib/deploy.ts: -------------------------------------------------------------------------------- 1 | import { Namespace as BuildManifestNamespace, DockerImageEnvVar } from '../../../lib/build-manifest'; 2 | import { BuildSpec, ComputeType, LinuxArmBuildImage, PipelineProject } from 'aws-cdk-lib/aws-codebuild'; 3 | 4 | import { Artifact } from 'aws-cdk-lib/aws-codepipeline'; 5 | import { CodeBuildAction } from 'aws-cdk-lib/aws-codepipeline-actions'; 6 | import { Construct } from 'constructs'; 7 | import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; 8 | import { Stack } from 'aws-cdk-lib'; 9 | 10 | const ChangeSetName = 'DeployImageUpdate'; 11 | 12 | export interface DeployProps { 13 | input: Artifact; 14 | stack: Stack; 15 | } 16 | 17 | export class DeployAction extends CodeBuildAction { 18 | constructor(scope: Construct, id: string, props: DeployProps) { 19 | const project = new PipelineProject(scope, id, { 20 | environment: { 21 | buildImage: LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0, 22 | computeType: ComputeType.SMALL 23 | }, 24 | buildSpec: buildBuildSpec(props) 25 | }); 26 | project.addToRolePolicy(new PolicyStatement({ 27 | actions: ['cloudformation:UpdateStack', 'cloudformation:CreateChangeSet', 'cloudformation:DescribeChangeSet', 'cloudformation:ExecuteChangeSet', 'cloudformation:DeleteChangeSet', 'cloudformation:DescribeStacks'], 28 | resources: [props.stack.formatArn({ 29 | service: 'cloudformation', 30 | resource: 'stack', 31 | resourceName: props.stack.stackName + '/*' 32 | })] 33 | })); 34 | 35 | super({ 36 | actionName: 'Deploy', 37 | project, 38 | input: props.input, 39 | environmentVariables: { 40 | IMAGE: { 41 | value: `#{${BuildManifestNamespace}.${DockerImageEnvVar}}` 42 | } 43 | } 44 | }); 45 | } 46 | } 47 | 48 | function buildBuildSpec(props: DeployProps): BuildSpec { 49 | return BuildSpec.fromObject({ 50 | version: '0.2', 51 | phases: { 52 | build: { 53 | commands: [ 54 | 'exitcode=0', 55 | `aws cloudformation create-change-set --capabilities CAPABILITY_IAM --stack-name ${props.stack.stackName} --use-previous-template --parameters ParameterKey=Image,ParameterValue=$IMAGE --change-set-name ${ChangeSetName}`, 56 | `if aws cloudformation wait change-set-create-complete --stack-name ${props.stack.stackName} --change-set-name ${ChangeSetName}; then 57 | echo "Deploying changes" 58 | aws cloudformation execute-change-set --stack-name ${props.stack.stackName} --change-set-name ${ChangeSetName} 59 | aws cloudformation wait stack-update-complete --stack-name ${props.stack.stackName} 60 | else 61 | reason=$(aws cloudformation describe-change-set --stack-name ${props.stack.stackName} --change-set-name ${ChangeSetName} --query 'StatusReason' --output text) 62 | if echo $reason | grep -q "didn't contain changes"; then 63 | echo "No changes to deploy" 64 | else 65 | echo "Error creating change set" 66 | exitcode=1 67 | fi 68 | fi`, 69 | `aws cloudformation delete-change-set --stack-name ${props.stack.stackName} --change-set-name ${ChangeSetName} || :`, 70 | 'exit $exitcode' 71 | ] 72 | } 73 | } 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /example/ecs-pipeline/lib/environment.ts: -------------------------------------------------------------------------------- 1 | export enum ApplicationEnvironment { 2 | // eslint-disable-next-line no-unused-vars 3 | TEST = 'test', 4 | // eslint-disable-next-line no-unused-vars 5 | PROD = 'prod', 6 | } 7 | 8 | export interface EcsApplicationEnvironmentProps { 9 | env: ApplicationEnvironment 10 | } 11 | -------------------------------------------------------------------------------- /example/ecs-pipeline/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-pipeline", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "simple-pipeline", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "aws-cdk": "^2.87.0", 13 | "aws-multiarch-container-build-pipeline": "file:../..", 14 | "lodash": "^4.17.21", 15 | "typescript": "~5.1.3" 16 | }, 17 | "bin": { 18 | "pipeline": "bin/pipeline.js" 19 | }, 20 | "devDependencies": { 21 | "@types/lodash": "^4.14.195", 22 | "@types/node": "^20.4.1" 23 | } 24 | }, 25 | "../..": { 26 | "version": "0.2.0", 27 | "license": "MIT", 28 | "devDependencies": { 29 | "@types/jest": "^29.5.1", 30 | "@types/node": "20.1.7", 31 | "@typescript-eslint/eslint-plugin": "^6.0.0", 32 | "@typescript-eslint/parser": "^6.0.0", 33 | "aws-cdk-lib": "2.87.0", 34 | "constructs": "^10.0.0", 35 | "eslint": "^8.44.0", 36 | "jest": "^29.5.0", 37 | "standard": "^17.1.0", 38 | "ts-jest": "^29.1.0", 39 | "typescript": "~5.1.3" 40 | }, 41 | "peerDependencies": { 42 | "aws-cdk-lib": "2.87.0", 43 | "constructs": "^10.0.0" 44 | } 45 | }, 46 | "node_modules/@types/lodash": { 47 | "version": "4.14.195", 48 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", 49 | "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", 50 | "dev": true 51 | }, 52 | "node_modules/@types/node": { 53 | "version": "20.4.1", 54 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz", 55 | "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==", 56 | "dev": true 57 | }, 58 | "node_modules/aws-cdk": { 59 | "version": "2.87.0", 60 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.87.0.tgz", 61 | "integrity": "sha512-dBm74nl3dMUxoAzgjcfKnzJyoVNIV//B1sqDN11cC3LXEflYapcBxPxZHAyGcRXg5dW3m14dMdKVQfmt4N970g==", 62 | "bin": { 63 | "cdk": "bin/cdk" 64 | }, 65 | "engines": { 66 | "node": ">= 14.15.0" 67 | }, 68 | "optionalDependencies": { 69 | "fsevents": "2.3.2" 70 | } 71 | }, 72 | "node_modules/aws-multiarch-container-build-pipeline": { 73 | "resolved": "../..", 74 | "link": true 75 | }, 76 | "node_modules/fsevents": { 77 | "version": "2.3.2", 78 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 79 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 80 | "hasInstallScript": true, 81 | "optional": true, 82 | "os": [ 83 | "darwin" 84 | ], 85 | "engines": { 86 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 87 | } 88 | }, 89 | "node_modules/lodash": { 90 | "version": "4.17.21", 91 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 92 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 93 | }, 94 | "node_modules/typescript": { 95 | "version": "5.1.6", 96 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", 97 | "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", 98 | "bin": { 99 | "tsc": "bin/tsc", 100 | "tsserver": "bin/tsserver" 101 | }, 102 | "engines": { 103 | "node": ">=14.17" 104 | } 105 | } 106 | }, 107 | "dependencies": { 108 | "@types/lodash": { 109 | "version": "4.14.195", 110 | "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", 111 | "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", 112 | "dev": true 113 | }, 114 | "@types/node": { 115 | "version": "20.4.1", 116 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz", 117 | "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==", 118 | "dev": true 119 | }, 120 | "aws-cdk": { 121 | "version": "2.87.0", 122 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.87.0.tgz", 123 | "integrity": "sha512-dBm74nl3dMUxoAzgjcfKnzJyoVNIV//B1sqDN11cC3LXEflYapcBxPxZHAyGcRXg5dW3m14dMdKVQfmt4N970g==", 124 | "requires": { 125 | "fsevents": "2.3.2" 126 | } 127 | }, 128 | "aws-multiarch-container-build-pipeline": { 129 | "version": "file:../..", 130 | "requires": { 131 | "@types/jest": "^29.5.1", 132 | "@types/node": "20.1.7", 133 | "@typescript-eslint/eslint-plugin": "^6.0.0", 134 | "@typescript-eslint/parser": "^6.0.0", 135 | "aws-cdk-lib": "2.87.0", 136 | "constructs": "^10.0.0", 137 | "eslint": "^8.44.0", 138 | "jest": "^29.5.0", 139 | "standard": "^17.1.0", 140 | "ts-jest": "^29.1.0", 141 | "typescript": "~5.1.3" 142 | } 143 | }, 144 | "fsevents": { 145 | "version": "2.3.2", 146 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 147 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 148 | "optional": true 149 | }, 150 | "lodash": { 151 | "version": "4.17.21", 152 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 153 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 154 | }, 155 | "typescript": { 156 | "version": "5.1.6", 157 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", 158 | "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /example/ecs-pipeline/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-pipeline", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "Amazon Web Services, Inc.", 6 | "contributors": [ 7 | "Michael S. Fischer" 8 | ], 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "tsc", 12 | "watch": "tsc -w", 13 | "test": "jest", 14 | "cdk": "cdk", 15 | "lint": "eslint . --ext .ts" 16 | }, 17 | "bin": { 18 | "pipeline": "bin/pipeline.js" 19 | }, 20 | "dependencies": { 21 | "aws-cdk": "^2.87.0", 22 | "aws-multiarch-container-build-pipeline": "file:../..", 23 | "lodash": "^4.17.21", 24 | "typescript": "~5.1.3" 25 | }, 26 | "devDependencies": { 27 | "@types/lodash": "^4.14.195", 28 | "@types/node": "^20.4.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/ecs-pipeline/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "containerDefinitions": [ 3 | { 4 | "name": "app", 5 | "image": "public.ecr.aws/a1b8g0o0/ecs-anywhere-demo-app:latest", 6 | "cpu": 256, 7 | "memory": 256, 8 | "portMappings": [ 9 | { 10 | "containerPort": 80, 11 | "hostPort": 0, 12 | "protocol": "tcp" 13 | } 14 | ], 15 | "essential": true, 16 | "environment": [], 17 | "mountPoints": [], 18 | "volumesFrom": [] 19 | } 20 | ], 21 | "family": "ecs-anywhere-demo-app", 22 | "networkMode": "bridge", 23 | "volumes": [], 24 | "placementConstraints": [], 25 | "requiresCompatibilities": [ 26 | "EXTERNAL" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /example/ecs-pipeline/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /example/simple-pipeline/bin/pipeline.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | 4 | import { App, Stack, StackProps } from 'aws-cdk-lib'; 5 | import { Architecture, BuildReleasePipeline } from 'aws-multiarch-container-build-pipeline'; 6 | 7 | import { Artifact } from 'aws-cdk-lib/aws-codepipeline'; 8 | import { CodeStarConnectionsSourceAction } from 'aws-cdk-lib/aws-codepipeline-actions'; 9 | import { Construct } from 'constructs'; 10 | import { Repository as ImageRepository } from 'aws-cdk-lib/aws-ecr'; 11 | 12 | const app = new App(); 13 | 14 | class PipelineStack extends Stack { 15 | constructor(scope: Construct, id: string, props?: StackProps) { 16 | super(scope, id, props); 17 | 18 | const imageRepo = new ImageRepository(this, 'Repository', { 19 | repositoryName: 'multiarch-container-build-pipeline-test' 20 | }); 21 | 22 | if (!process.env.CODESTAR_CONNECTION_ARN) { 23 | throw new Error('CODESTAR_CONNECTION_ARN is not set'); 24 | } 25 | 26 | const sourceAction = new CodeStarConnectionsSourceAction({ 27 | connectionArn: process.env.CODESTAR_CONNECTION_ARN, 28 | actionName: 'Source', 29 | owner: 'otterley', 30 | repo: 'multiarch-container-build-pipeline-test', 31 | branch: 'main', 32 | codeBuildCloneOutput: true, 33 | output: new Artifact() 34 | }); 35 | 36 | new BuildReleasePipeline(this, 'Pipeline', { 37 | sourceAction, 38 | imageRepo, 39 | architectures: [Architecture.Arm64, Architecture.X86_64] 40 | }); 41 | } 42 | } 43 | 44 | new PipelineStack(app, 'SimpleMultiarchPipelineDemo'); 45 | -------------------------------------------------------------------------------- /example/simple-pipeline/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/pipeline.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/simple-pipeline/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /example/simple-pipeline/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-pipeline", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "simple-pipeline", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "aws-cdk": "^2.87.0", 13 | "aws-multiarch-container-build-pipeline": "file:../..", 14 | "typescript": "~5.1.3" 15 | }, 16 | "bin": { 17 | "pipeline": "bin/pipeline.js" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20.4.1" 21 | } 22 | }, 23 | "../..": { 24 | "version": "0.2.0", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@types/jest": "^29.5.1", 28 | "@types/node": "20.1.7", 29 | "@typescript-eslint/eslint-plugin": "^6.0.0", 30 | "@typescript-eslint/parser": "^6.0.0", 31 | "aws-cdk-lib": "2.87.0", 32 | "constructs": "^10.0.0", 33 | "eslint": "^8.44.0", 34 | "jest": "^29.5.0", 35 | "standard": "^17.1.0", 36 | "ts-jest": "^29.1.0", 37 | "typescript": "~5.1.3" 38 | }, 39 | "peerDependencies": { 40 | "aws-cdk-lib": "2.87.0", 41 | "constructs": "^10.0.0" 42 | } 43 | }, 44 | "node_modules/@types/node": { 45 | "version": "20.4.1", 46 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz", 47 | "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==", 48 | "dev": true 49 | }, 50 | "node_modules/aws-cdk": { 51 | "version": "2.87.0", 52 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.87.0.tgz", 53 | "integrity": "sha512-dBm74nl3dMUxoAzgjcfKnzJyoVNIV//B1sqDN11cC3LXEflYapcBxPxZHAyGcRXg5dW3m14dMdKVQfmt4N970g==", 54 | "bin": { 55 | "cdk": "bin/cdk" 56 | }, 57 | "engines": { 58 | "node": ">= 14.15.0" 59 | }, 60 | "optionalDependencies": { 61 | "fsevents": "2.3.2" 62 | } 63 | }, 64 | "node_modules/aws-multiarch-container-build-pipeline": { 65 | "resolved": "../..", 66 | "link": true 67 | }, 68 | "node_modules/fsevents": { 69 | "version": "2.3.2", 70 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 71 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 72 | "hasInstallScript": true, 73 | "optional": true, 74 | "os": [ 75 | "darwin" 76 | ], 77 | "engines": { 78 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 79 | } 80 | }, 81 | "node_modules/typescript": { 82 | "version": "5.1.6", 83 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", 84 | "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", 85 | "bin": { 86 | "tsc": "bin/tsc", 87 | "tsserver": "bin/tsserver" 88 | }, 89 | "engines": { 90 | "node": ">=14.17" 91 | } 92 | } 93 | }, 94 | "dependencies": { 95 | "@types/node": { 96 | "version": "20.4.1", 97 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz", 98 | "integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==", 99 | "dev": true 100 | }, 101 | "aws-cdk": { 102 | "version": "2.87.0", 103 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.87.0.tgz", 104 | "integrity": "sha512-dBm74nl3dMUxoAzgjcfKnzJyoVNIV//B1sqDN11cC3LXEflYapcBxPxZHAyGcRXg5dW3m14dMdKVQfmt4N970g==", 105 | "requires": { 106 | "fsevents": "2.3.2" 107 | } 108 | }, 109 | "aws-multiarch-container-build-pipeline": { 110 | "version": "file:../..", 111 | "requires": { 112 | "@types/jest": "^29.5.1", 113 | "@types/node": "20.1.7", 114 | "@typescript-eslint/eslint-plugin": "^6.0.0", 115 | "@typescript-eslint/parser": "^6.0.0", 116 | "aws-cdk-lib": "2.87.0", 117 | "constructs": "^10.0.0", 118 | "eslint": "^8.44.0", 119 | "jest": "^29.5.0", 120 | "standard": "^17.1.0", 121 | "ts-jest": "^29.1.0", 122 | "typescript": "~5.1.3" 123 | } 124 | }, 125 | "fsevents": { 126 | "version": "2.3.2", 127 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 128 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 129 | "optional": true 130 | }, 131 | "typescript": { 132 | "version": "5.1.6", 133 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", 134 | "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /example/simple-pipeline/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-pipeline", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "Amazon Web Services, Inc.", 6 | "contributors": [ 7 | "Michael S. Fischer" 8 | ], 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "tsc", 12 | "watch": "tsc -w", 13 | "test": "jest", 14 | "cdk": "cdk", 15 | "lint": "eslint . --ext .ts" 16 | }, 17 | "bin": { 18 | "pipeline": "bin/pipeline.js" 19 | }, 20 | "dependencies": { 21 | "aws-cdk": "^2.87.0", 22 | "aws-multiarch-container-build-pipeline": "file:../..", 23 | "typescript": "~5.1.3" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20.4.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/simple-pipeline/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/pipeline'; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/build-action.ts: -------------------------------------------------------------------------------- 1 | import { Aws, Duration } from 'aws-cdk-lib'; 2 | import { BuildEnvironmentVariable, BuildSpec, ComputeType, PipelineProject } from 'aws-cdk-lib/aws-codebuild'; 3 | import { CodeBuildAction, CodeBuildActionType } from 'aws-cdk-lib/aws-codepipeline-actions'; 4 | 5 | import { ArchitectureMap } from './codebuild'; 6 | import { Artifact } from 'aws-cdk-lib/aws-codepipeline'; 7 | import { Construct } from 'constructs'; 8 | import { Repository } from 'aws-cdk-lib/aws-ecr'; 9 | 10 | const DEFAULT_BUILD_PATH = '.'; 11 | const DEFAULT_COMPUTE_TYPE = ComputeType.SMALL; 12 | 13 | interface BuildActionProps { 14 | arch: string, 15 | 16 | // Build path, in which Dockerfile lives 17 | buildPath?: string 18 | 19 | // Compute type used for build process 20 | computeType?: ComputeType 21 | 22 | // Build timeout 23 | timeout?: Duration 24 | 25 | // Source artifact 26 | source: Artifact 27 | 28 | // ECR repository 29 | imageRepo: Repository 30 | 31 | // Docker Image Tag, defaults to output of `git describe --tags --always` 32 | imageTag?: string 33 | 34 | // Docker build arguments (see `--build-arg`) 35 | dockerBuildArgs?: {[key:string]:string} 36 | } 37 | 38 | export class BuildAction extends CodeBuildAction { 39 | constructor(scope: Construct, id: string, props: BuildActionProps) { 40 | const project = new PipelineProject(scope, `BuildProject-${props.arch}`, { 41 | buildSpec: BuildSpec.fromObject(createBuildSpec(props)), 42 | environment: { 43 | buildImage: ArchitectureMap[props.arch], 44 | computeType: props.computeType || DEFAULT_COMPUTE_TYPE, 45 | // Must run privileged in order to run Docker 46 | privileged: true 47 | }, 48 | timeout: props.timeout 49 | }); 50 | 51 | if (project.role) { 52 | props.imageRepo.grantPullPush(project.role); 53 | } 54 | 55 | const environmentVariables: { [name:string]: BuildEnvironmentVariable } = {}; 56 | if (props.imageTag) { 57 | environmentVariables.IMAGE_TAG = { 58 | value: props.imageTag 59 | }; 60 | } 61 | 62 | super({ 63 | actionName: props.arch, 64 | project, 65 | environmentVariables, 66 | input: props.source, 67 | type: CodeBuildActionType.BUILD 68 | }); 69 | } 70 | } 71 | 72 | const createBuildSpec = function(props: BuildActionProps): { [key:string]:any } { 73 | const buildSpec = { 74 | version: '0.2', 75 | env: { 76 | 'git-credential-helper': 'yes' 77 | }, 78 | phases: { 79 | pre_build: { 80 | commands: [ 81 | dockerLoginCommand() 82 | ] 83 | }, 84 | build: { 85 | commands: [ 86 | // eslint-disable-next-line no-template-curly-in-string 87 | ': ${IMAGE_TAG=$(git describe --tags --always)}', 88 | 'test -n "$IMAGE_TAG"', // fail if empty 89 | dockerBuildCommand(props), 90 | dockerPushCommand(props) 91 | ], 92 | 'on-failure': 'ABORT' 93 | } 94 | } 95 | }; 96 | return buildSpec; 97 | }; 98 | 99 | const imageTag = function(props: BuildActionProps): string { 100 | // eslint-disable-next-line no-template-curly-in-string 101 | return props.imageRepo.repositoryUri + ':${IMAGE_TAG}-' + props.arch; 102 | }; 103 | 104 | const dockerBuildCommand = function(props: BuildActionProps): string { 105 | const args = [ 106 | 'docker', 'build', 107 | '-t', imageTag(props) 108 | ]; 109 | for (const [key, value] of Object.entries(props.dockerBuildArgs || {})) { 110 | args.push('--build-arg', `${key}=${value}`); 111 | } 112 | args.push(props.buildPath || DEFAULT_BUILD_PATH); 113 | return args.join(' '); 114 | }; 115 | 116 | const dockerLoginCommand = function(): string { 117 | return `aws ecr get-login-password | docker login --username AWS --password-stdin ${Aws.ACCOUNT_ID}.dkr.ecr.${Aws.REGION}.amazonaws.com`; 118 | }; 119 | 120 | const dockerPushCommand = function(props: BuildActionProps): string { 121 | return `docker push ${imageTag(props)}`; 122 | }; 123 | -------------------------------------------------------------------------------- /lib/build-manifest.ts: -------------------------------------------------------------------------------- 1 | import { Aws, Duration } from 'aws-cdk-lib'; 2 | import { BuildEnvironmentVariable, BuildSpec, ComputeType, PipelineProject } from 'aws-cdk-lib/aws-codebuild'; 3 | import { CodeBuildAction, CodeBuildActionType } from 'aws-cdk-lib/aws-codepipeline-actions'; 4 | 5 | import { Architecture } from './pipeline'; 6 | import { ArchitectureMap } from './codebuild'; 7 | import { Artifact } from 'aws-cdk-lib/aws-codepipeline'; 8 | import { Construct } from 'constructs'; 9 | import { Repository } from 'aws-cdk-lib/aws-ecr'; 10 | 11 | const DEFAULT_COMPUTE_TYPE = ComputeType.SMALL; 12 | 13 | export const Namespace = 'Manifest'; 14 | export const DockerImageEnvVar = 'DockerImage'; 15 | 16 | interface BuildManifestActionProps { 17 | architectures: Architecture[] 18 | 19 | // Compute type used for build process 20 | computeType?: ComputeType 21 | 22 | // Build timeout 23 | timeout?: Duration 24 | 25 | // Source artifact 26 | source: Artifact 27 | 28 | // Docker Image Tag, defaults to output of `git describe --tags --always` 29 | imageTag?: string 30 | 31 | // ECR repository 32 | imageRepo: Repository 33 | } 34 | 35 | export class BuildManifestAction extends CodeBuildAction { 36 | constructor(scope: Construct, id: string, props: BuildManifestActionProps) { 37 | const project = new PipelineProject(scope, 'BuildManifest', { 38 | buildSpec: BuildSpec.fromObject(createBuildSpec(props)), 39 | environment: { 40 | buildImage: ArchitectureMap.amd64, 41 | computeType: props.computeType || DEFAULT_COMPUTE_TYPE, 42 | // Must run privileged in order to run Docker 43 | privileged: true 44 | }, 45 | timeout: props.timeout 46 | }); 47 | 48 | if (project.role) { 49 | props.imageRepo.grantPullPush(project.role); 50 | } 51 | 52 | const environmentVariables: { [name:string]: BuildEnvironmentVariable } = {}; 53 | if (props.imageTag) { 54 | environmentVariables.IMAGE_TAG = { 55 | value: props.imageTag 56 | }; 57 | } 58 | 59 | super({ 60 | actionName: 'ManifestBuilder', 61 | project, 62 | environmentVariables, 63 | input: props.source, 64 | type: CodeBuildActionType.BUILD, 65 | variablesNamespace: Namespace 66 | }); 67 | } 68 | } 69 | 70 | const createBuildSpec = function(props: BuildManifestActionProps): { [key:string]:any } { 71 | const buildSpec = { 72 | version: '0.2', 73 | env: { 74 | 'git-credential-helper': 'yes', 75 | variables: { 76 | DOCKER_CLI_EXPERIMENTAL: 'enabled' 77 | }, 78 | 'exported-variables': [ 79 | DockerImageEnvVar 80 | ] 81 | }, 82 | phases: { 83 | pre_build: { 84 | commands: [ 85 | dockerLoginCommand() 86 | ] 87 | }, 88 | build: { 89 | commands: [ 90 | // eslint-disable-next-line no-template-curly-in-string 91 | ': ${IMAGE_TAG=$(git describe --tags --always)}', 92 | 'test -n "$IMAGE_TAG"', // fail if empty 93 | `TAG=${props.imageRepo.repositoryUri}:$IMAGE_TAG`, 94 | 'echo TAG: $TAG', 95 | dockerManifestCreateCommand(props) 96 | ], 97 | 'on-failure': 'ABORT' 98 | }, 99 | post_build: { 100 | commands: [ 101 | 'docker manifest inspect $TAG', 102 | 'docker manifest push $TAG', 103 | `export ${DockerImageEnvVar}=$TAG`, 104 | 'echo Build completed on `date`' 105 | ] 106 | } 107 | } 108 | }; 109 | return buildSpec; 110 | }; 111 | 112 | const dockerManifestCreateCommand = function(props: BuildManifestActionProps): string { 113 | // eslint-disable-next-line no-template-curly-in-string 114 | return 'docker manifest create $TAG ' + props.architectures.map(arch => '${TAG}-' + arch).join(' '); 115 | }; 116 | 117 | const dockerLoginCommand = function(): string { 118 | return `aws ecr get-login-password | docker login --username AWS --password-stdin ${Aws.ACCOUNT_ID}.dkr.ecr.${Aws.REGION}.amazonaws.com`; 119 | }; 120 | -------------------------------------------------------------------------------- /lib/codebuild.ts: -------------------------------------------------------------------------------- 1 | import { IBuildImage, LinuxArmBuildImage, LinuxBuildImage } from 'aws-cdk-lib/aws-codebuild'; 2 | 3 | export const ArchitectureMap: { [architecture:string]: IBuildImage } = { 4 | amd64: LinuxBuildImage.AMAZON_LINUX_2_3, 5 | arm64: LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0 6 | }; 7 | -------------------------------------------------------------------------------- /lib/pipeline.ts: -------------------------------------------------------------------------------- 1 | import { Artifact, Pipeline as CodePipeline, IStage, StageOptions } from 'aws-cdk-lib/aws-codepipeline'; 2 | import { CodeBuildAction, CodeStarConnectionsSourceAction } from 'aws-cdk-lib/aws-codepipeline-actions'; 3 | 4 | import { BuildAction } from './build-action'; 5 | import { BuildManifestAction } from './build-manifest'; 6 | import { ComputeType } from 'aws-cdk-lib/aws-codebuild'; 7 | import { Construct } from 'constructs'; 8 | import { Duration } from 'aws-cdk-lib'; 9 | import { Repository } from 'aws-cdk-lib/aws-ecr'; 10 | import { TestAction } from './test-action'; 11 | 12 | export enum Architecture { 13 | // eslint-disable-next-line no-unused-vars 14 | X86_64 = 'amd64', 15 | 16 | // eslint-disable-next-line no-unused-vars 17 | Arm64 = 'arm64' 18 | } 19 | 20 | const DEFAULT_ARCHITECTURES = [Architecture.X86_64]; 21 | 22 | interface BuildReleasePipelineProps { 23 | // A source action 24 | sourceAction: CodeBuildAction | CodeStarConnectionsSourceAction 25 | 26 | // ECR repository name 27 | imageRepo: Repository 28 | 29 | // architectures to build on, defaults to ['amd64'] 30 | architectures?: Architecture[] 31 | 32 | // Build path, in which Dockerfile lives 33 | buildPath?: string 34 | 35 | // Docker build arguments (see `--build-arg`) 36 | dockerBuildArgs?: {[key:string]:string} 37 | 38 | // Build timeout 39 | buildTimeout?: Duration 40 | 41 | // Docker image tag, defaults to value of `git describe --tags --always` if 42 | // possible 43 | imageTag?: string 44 | 45 | // Test timeout 46 | testTimeout?: Duration 47 | 48 | // Location of CodeBuild build specification file for test stage, defaults to 49 | // 'buildspec-test.yml' 50 | testBuildspecPath?: string 51 | 52 | // Compute type used for build process 53 | computeType?: ComputeType 54 | } 55 | 56 | export class BuildReleasePipeline extends Construct { 57 | public pipeline: CodePipeline; 58 | 59 | constructor(scope: Construct, id: string, props: BuildReleasePipelineProps) { 60 | super(scope, id); 61 | 62 | let sourceArtifact: Artifact; 63 | const sourceArtifacts = props.sourceAction.actionProperties.outputs ?? []; 64 | if (sourceArtifacts.length === 1) { 65 | sourceArtifact = sourceArtifacts[0]; 66 | } else { 67 | throw new Error('Source action must have exactly 1 output defined'); 68 | } 69 | 70 | this.pipeline = new CodePipeline(this, 'pipeline', { 71 | restartExecutionOnUpdate: true 72 | }); 73 | 74 | this.pipeline.addStage({ 75 | stageName: 'Source', 76 | actions: [props.sourceAction] 77 | }); 78 | 79 | const buildActions: { [arch:string]: BuildAction } = {}; 80 | const testActions: { [arch:string]: TestAction } = {}; 81 | const testOutputs: { [arch:string]: Artifact } = {}; 82 | 83 | for (const arch of props.architectures || DEFAULT_ARCHITECTURES) { 84 | buildActions[arch] = new BuildAction(this, `BuildAction-${arch}`, { 85 | ...props, 86 | arch, 87 | timeout: props.buildTimeout, 88 | source: sourceArtifact 89 | }); 90 | } 91 | 92 | this.pipeline.addStage({ 93 | stageName: 'Build', 94 | actions: (props.architectures || DEFAULT_ARCHITECTURES).map(arch => buildActions[arch]) 95 | }); 96 | 97 | for (const arch of props.architectures || DEFAULT_ARCHITECTURES) { 98 | testOutputs[arch] = new Artifact(`test_${arch}` 99 | .replace(/[^A-Za-z0-9_]/g, '')); 100 | const action = new TestAction(this, `TestAction-${arch}`, { 101 | ...props, 102 | arch, 103 | timeout: props.testTimeout, 104 | source: sourceArtifact 105 | }); 106 | testActions[arch] = action; 107 | } 108 | 109 | this.pipeline.addStage({ 110 | stageName: 'Test', 111 | actions: (props.architectures || DEFAULT_ARCHITECTURES).map(arch => testActions[arch]) 112 | }); 113 | 114 | this.pipeline.addStage({ 115 | stageName: 'BuildManifest', 116 | actions: [ 117 | new BuildManifestAction(this, 'BuildManifest', { 118 | ...props, 119 | architectures: props.architectures || DEFAULT_ARCHITECTURES, 120 | source: sourceArtifact 121 | }) 122 | ] 123 | }); 124 | } 125 | 126 | public addStage(props: StageOptions): IStage { 127 | return this.pipeline.addStage(props); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/test-action.ts: -------------------------------------------------------------------------------- 1 | import { BuildSpec, ComputeType, PipelineProject } from 'aws-cdk-lib/aws-codebuild'; 2 | import { CodeBuildAction, CodeBuildActionType } from 'aws-cdk-lib/aws-codepipeline-actions'; 3 | 4 | import { ArchitectureMap } from './codebuild'; 5 | import { Artifact } from 'aws-cdk-lib/aws-codepipeline'; 6 | import { Construct } from 'constructs'; 7 | import { Duration } from 'aws-cdk-lib'; 8 | import { Repository } from 'aws-cdk-lib/aws-ecr'; 9 | 10 | const DEFAULT_BUILDSPEC_PATH = 'buildspec-test.yml'; 11 | const DEFAULT_COMPUTE_TYPE = ComputeType.LARGE; 12 | 13 | interface TestActionProps { 14 | arch: string, 15 | 16 | // Compute type used for test process 17 | computeType?: ComputeType 18 | 19 | // Location of buildspec file for test stage, defaults to 'buildspec-test.yml' 20 | buildspecPath?: string 21 | 22 | // Build timeout 23 | timeout?: Duration 24 | 25 | // Source artifact 26 | source: Artifact 27 | 28 | // ECR repository 29 | imageRepo: Repository 30 | } 31 | 32 | export class TestAction extends CodeBuildAction { 33 | constructor(scope: Construct, id: string, props: TestActionProps) { 34 | const project = new PipelineProject(scope, `TestProject-${props.arch}`, { 35 | buildSpec: BuildSpec.fromSourceFilename(props.buildspecPath || DEFAULT_BUILDSPEC_PATH), 36 | environment: { 37 | buildImage: ArchitectureMap[props.arch], 38 | computeType: props.computeType || DEFAULT_COMPUTE_TYPE, 39 | // Must run privileged in order to run Docker 40 | privileged: true 41 | }, 42 | timeout: props.timeout 43 | }); 44 | 45 | if (project.role) { 46 | props.imageRepo.grantPull(project.role); 47 | } 48 | 49 | super({ 50 | actionName: props.arch, 51 | project, 52 | input: props.source, 53 | type: CodeBuildActionType.BUILD 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-multiarch-container-build-pipeline", 3 | "author": "Amazon Web Services, Inc.", 4 | "contributors": [ 5 | "Michael S. Fischer" 6 | ], 7 | "version": "0.2.0", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "tsc", 11 | "watch": "tsc -w", 12 | "test": "jest", 13 | "lint": "eslint . --ext .ts" 14 | }, 15 | "devDependencies": { 16 | "@types/jest": "^29.5.1", 17 | "@types/node": "20.1.7", 18 | "@typescript-eslint/eslint-plugin": "^6.0.0", 19 | "@typescript-eslint/parser": "^6.0.0", 20 | "aws-cdk-lib": "2.87.0", 21 | "constructs": "^10.0.0", 22 | "eslint": "^8.44.0", 23 | "jest": "^29.5.0", 24 | "standard": "^17.1.0", 25 | "ts-jest": "^29.1.0", 26 | "typescript": "~5.1.3" 27 | }, 28 | "peerDependencies": { 29 | "aws-cdk-lib": "2.87.0", 30 | "constructs": "^10.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/multiarch-container-build-pipeline.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; 3 | import * as cdk from '@aws-cdk/core'; 4 | import * as MultiarchContainerBuildPipeline from '../lib/multiarch-container-build-pipeline-stack'; 5 | 6 | test('Empty Stack', () => { 7 | const app = new cdk.App(); 8 | // WHEN 9 | const stack = new MultiarchContainerBuildPipeline.MultiarchContainerBuildPipelineStack(app, 'MyTestStack'); 10 | // THEN 11 | expectCDK(stack).to(matchTemplate({ 12 | "Resources": {} 13 | }, MatchStyle.EXACT)) 14 | }); 15 | */ 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------