In case anyone has the same problem, I was able to hack together a solution using the legacy CdkPipeline API following the archived version of the tutorial I mentioned in my question.
Here is a minimum viable pipeline stack that includes...
- a CDK pipeline source action (in "Source" stage)
- an application source action (in "Source" stage)
- a CDK build action (in "Build" stage) + self-mutating pipeline ("UpdatePipeline" stage)
- an application build action (in "Build" stage)
lib/cdkpipelines-demo-pipeline-stack.ts
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import * as core from '@aws-cdk/core';
import {Construct, SecretValue, Stack, StackProps} from '@aws-cdk/core';
import {CdkPipeline, SimpleSynthAction} from "@aws-cdk/pipelines";
import * as iam from "@aws-cdk/aws-iam";
import * as ecr from "@aws-cdk/aws-ecr";
import * as codebuild from "@aws-cdk/aws-codebuild";
/**
* The stack that defines the application pipeline
*/
export class CdkpipelinesDemoPipelineStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const sourceArtifact = new codepipeline.Artifact();
const cloudAssemblyArtifact = new codepipeline.Artifact();
const pipeline = new CdkPipeline(this, 'Pipeline', {
// The pipeline name
pipelineName: 'MyServicePipeline',
cloudAssemblyArtifact,
// Where the source can be found
sourceAction: new codepipeline_actions.GitHubSourceAction({
actionName: 'GitHub',
output: sourceArtifact,
oauthToken: SecretValue.secretsManager('github-token'),
owner: 'OWNER',
repo: 'REPO',
}),
// How it will be built and synthesized
synthAction: SimpleSynthAction.standardNpmSynth({
sourceArtifact,
cloudAssemblyArtifact,
// We need a build step to compile the TypeScript Lambda
buildCommand: 'npm run build'
}),
});
const pipelineRole = pipeline.codePipeline.role;
// Add application source action
const appSourceArtifact = new codepipeline.Artifact();
const appSourceAction = this.createAppSourceAction(appSourceArtifact);
const sourceStage = pipeline.stage("Source");
sourceStage.addAction(appSourceAction);
// Add application build action
const codeBuildServiceRole = this.createCodeBuildServiceRole(this, pipelineRole);
const repository = this.createApplicationRepository(this, codeBuildServiceRole);
const pipelineProject = this.createCodeBuildPipelineProject(
this, codeBuildServiceRole, repository, 'REGION', 'ACCOUNT_ID');
const appBuildOutput = new codepipeline.Artifact();
const appBuildAction = this.createAppCodeBuildAction(
this, appSourceArtifact, appBuildOutput, pipelineProject, codeBuildServiceRole);
const buildStage = pipeline.stage("Build");
buildStage.addAction(appBuildAction);
// This is where we add the application stages...
}
createAppSourceAction(appSourceArtifact: codepipeline.Artifact): codepipeline_actions.GitHubSourceAction {
return new codepipeline_actions.GitHubSourceAction({
actionName: 'GitHub-App-Source',
output: appSourceArtifact,
oauthToken: SecretValue.secretsManager('github-token'),
owner: 'SOURCE-OWNER',
repo: 'SOURCE-REPO',
});
}
createCodeBuildServiceRole(scope: core.Construct, pipelineRole: iam.IRole): iam.Role {
const role = new iam.Role(scope, 'CodeBuildServiceRole', {
assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
});
role.assumeRolePolicy?.addStatements(new iam.PolicyStatement({
sid: "PipelineAssumeCodeBuildServiceRole",
effect: iam.Effect.ALLOW,
actions: ["sts:AssumeRole"],
principals: [pipelineRole]
}));
// Required policies to create an AWS CodeBuild service role
role.addToPolicy(new iam.PolicyStatement({
sid: "CloudWatchLogsPolicy",
effect: iam.Effect.ALLOW,
actions: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
resources: ["*"]
}));
role.addToPolicy(new iam.PolicyStatement({
sid: "CodeCommitPolicy",
effect: iam.Effect.ALLOW,
actions: ["codecommit:GitPull"],
resources: ["*"]
}));
role.addToPolicy(new iam.PolicyStatement({
sid: "S3GetObjectPolicy",
effect: iam.Effect.ALLOW,
actions: [
"s3:GetObject",
"s3:GetObjectVersion"
],
resources: ["*"]
}));
role.addToPolicy(new iam.PolicyStatement({
sid: "S3PutObjectPolicy",
effect: iam.Effect.ALLOW,
actions: [
"s3:PutObject"
],
resources: ["*"]
}));
role.addToPolicy(new iam.PolicyStatement({
sid: "S3BucketIdentity",
effect: iam.Effect.ALLOW,
actions: [
"s3:GetBucketAcl",
"s3:GetBucketLocation"
],
resources: ["*"]
}));
// This statement allows CodeBuild to upload Docker images to Amazon ECR repositories.
// source: https://docs.aws.amazon.com/codebuild/latest/userguide/sample-docker.html#sample-docker-running
role.addToPolicy(new iam.PolicyStatement({
sid: "ECRUploadPolicy",
effect: iam.Effect.ALLOW,
actions: [
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:GetAuthorizationToken",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart"
],
resources: ["*"]
}));
return role;
}
createApplicationRepository(scope: core.Construct, codeBuildServiceRole: iam.Role): ecr.Repository {
const repository = new ecr.Repository(scope, 'Repository', {
repositoryName: 'cdkpipelines-demo-image-repository'
});
repository.grantPullPush(codeBuildServiceRole);
return repository;
}
createCodeBuildPipelineProject(scope: core.Construct,
codeBuildServiceRole: iam.Role,
repository: ecr.Repository,
region: string,
accountId: string): codebuild.PipelineProject {
return new codebuild.PipelineProject(scope, 'BuildProject', {
buildSpec: codebuild.BuildSpec.fromSourceFilename("buildspec.yml"),
environment: {
buildImage: codebuild.LinuxBuildImage.fromCodeBuildImageId("aws/codebuild/standard:4.0"),
privileged: true,
computeType: codebuild.ComputeType.SMALL,
environmentVariables: {
AWS_DEFAULT_REGION: {value: region},
AWS_ACCOUNT_ID: {value: accountId},
IMAGE_REPO_NAME: {value: repository.repositoryName},
IMAGE_TAG: {value: "latest"},
}
},
role: codeBuildServiceRole
});
}
createAppCodeBuildAction(scope: core.Construct,
input: codepipeline.Artifact,
output: codepipeline.Artifact,
pipelineProject: codebuild.PipelineProject,
serviceRole: iam.Role) {
return new codepipeline_actions.CodeBuildAction({
actionName: "App-Build",
checkSecretsInPlainTextEnvVariables: false,
input: input,
outputs: [output],
project: pipelineProject,
role: serviceRole,
type: codepipeline_actions.CodeBuildActionType.BUILD,
})
}
}