How would I pass a Stack down to a pipeline stage when using AWS-CDK?
I'm currently trying to create a pipeline which could take a stack as an input.
I've followed the aws-cdk workshop and have a pipeline which self-updates and can deploy a pre-packaged lambda but I'm trying to create a pipeline construct library so that my team can just create a new instance of a pipeline and pass in a later created stack which has relevant roles and event rules added.
My current code is below:
pipelines-stack.ts
import {
Stack,
StackProps,
pipelines,
DefaultStackSynthesizer,
SecretValue,
aws_codebuild as codebuild,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { PipelineStage } from './pipeline-stage';
export type PipelineStackProps = StackProps;
export interface Environment {
readonly name: string;
readonly accountNumber: string;
}
export class PipelineStack extends Stack {
constructor(scope: Construct, id: string, props: PipelineStackProps,
environments: Environment[], repoName: string, serviceName: string, lambdaPath: string, policies: Array<string>) {
super(scope, id, props);
new DefaultStackSynthesizer({
deployRoleArn: 'arn:aws:iam::{ACCOUNTID}:role/service-role/ops-codepipeline-role',
});
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
pipelineName: serviceName,
synth: new pipelines.CodeBuildStep('Synth', {
input: pipelines.CodePipelineSource.gitHub(`company_repo/${repoName}`, 'main', {
authentication: SecretValue.secretsManager('github-oauth-token', { jsonField: 'OAUTH_TOKEN' }),
}),
buildEnvironment: {
environmentVariables: {
NPM_TOKEN: {
value: '/codepipeline/npm_token',
type: codebuild.BuildEnvironmentVariableType.PARAMETER_STORE,
},
},
},
commands: [
'echo "@company:registry=https://npm.pkg.github.com/" > .npmrc',
'echo "//npm.pkg.github.com/:_authToken=\\${NPM_TOKEN}" >> .npmrc',
'npm ci',
'npm run build',
'npm test',
'npx cdk synth',
'pip install -r lambda/requirements.txt -t lambda',
],
}),
crossAccountKeys: true,
});
for (const environment of environments) {
const environmentName = environment.name;
const envAccountNumber = environment.accountNumber;
pipeline.addStage(
new PipelineStage(this, environmentName, {
env: {
account: envAccountNumber,
region: 'eu-west-2',
},
}, lambdaPath, policies),
);
}
}
}
pipeline-stage.ts
import { Stage, StageProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { LambdaStack } from './lambda-stack';
export class PipelineStage extends Stage {
constructor(scope: Construct, id: string, props: StageProps, lambdaPath: string, policies: Array<string>) {
super(scope, id, props);
new LambdaStack(this, 'LambdaStack', lambdaPath, policies);
}
}
lambda-stack.ts
import {
Stack,
StackProps,
aws_lambda as lambda,
aws_events as events,
aws_events_targets as targets,
aws_iam as iam,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class LambdaStack extends Stack {
constructor(scope: Construct, id: string, lambdaPath: string, policies: Array<string>, props?: StackProps) {
super(scope, id, props);
const role = new iam.Role(this, 'LambdaCleanupRole', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
});
for (const policy of policies) {
role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName(policy));
}
// The code that defines your stack goes here
const fn = new lambda.Function(this, 'lambda-cleanup', {
runtime: lambda.Runtime.PYTHON_3_8,
handler: 'app.handler',
code: lambda.Code.fromAsset(lambdaPath),
role,
});
const rule = new events.Rule(this, 'Schedule Rule', {
schedule: events.Schedule.expression('rate(1 day)'),
});
rule.addTarget(new targets.LambdaFunction(fn));
}
}
Proposed solution is below:
pipelines-stack.ts
export class PipelineStack extends Stack {
constructor(scope: Construct, id: string, props: PipelineStackProps,
environments: Environment[], repoName: string, serviceName: string, lambdaPath: string, policies: Array<string>, lambdaStack: Stack) {
super(scope, id, props);
// pipeline implementation here
const stage = pipeline.addStage(
new PipelineStage(this, environmentName, {
env: {
account: envAccountNumber,
region: 'eu-west-2',
},
}, lambdaPath, policies, lambdaStack),
);
pipeline-stage.ts
export class PipelineStage extends Stage {
constructor(scope: Construct, id: string, props: StageProps, lambdaPath: string, policies: Array<string>, lambdaStack: Stack) {
super(scope, id, props);
lambdaStack
}
}
But doing this results in a The given Stage construct ('Default/BymilesLambdaPipelineStack/dev') should contain at least one Stack
error
The final goal would be for the team to import an npm package from our registry and do something similar to below:
lambda-deployment-stack.ts
import { LambdaStack } from './lambda-stack.ts'
import { PipelineStack } from '@company-register/cdk-pipeline-python'
const app = new App();
const policies: string[] = [
'service-role/AWSLambdaBasicExecutionRole',
'AWSLambda_FullAccess',
];
new PipelineStack(app, 'LambdaCleanupPipeline', {
env: {
region: 'eu-west-2',
},
}, [
{ name: 'Test', accountNumber: '11111111' },
{ name: 'Ops', accountNumber: '22222222' }],
'bymiles-lambda-cleanup-cdk',
'bymiles-lambda-cleanup-cdk',
new LambdaStack(stack, 'BymilesLambdaStack', 'test/test_lambda', policies)
);
app.synth();
CodePudding user response:
My understnding of the proposed solution: implementers should pass a LambdaStack
to a shared PipelineStack
. PipelineStage
is an implementation detail of
the pipeline.
In lambda-deployment-stack.ts
, implementers pass a function with the signature (scope: cdk.Stage) => void
to the common PipelineStack
.
This dependency injection pattern has two purposes: (1) it defers stack construction until the stage scope is available and (2) it encapsulates the policy and other details of no concern to the pipeline.
// lambda-deployment-stack.ts
// PipelineStack accepts this function signature as a prop
// defers the lambda stack creation until the stage scope is available
const makeLambdaStack = (scope: cdk.Stage): void => {
new LambdaStack(scope, 'BymilesLambdaStack', 'test/test_lambda', policies);
};
Instead of accepting the LambdaStack, policies and paths to the PipelineStack
, have PipelineStack
take the wrapper function as a stackMaker
prop.
// pipeline-stack.ts
export class PipelineStack extends Stack {
constructor(scope: Construct, id: string, props: PipelineStackProps,
environments: Environment[], repoName: string, serviceName: string,
stackMaker: (scope: cdk.Stage) => void) {
super(scope, id, props);
// etc...
Pipeline
in turn passes makeLambdaStack
down to the PipelineStage
, where the function is called and the Lambda actually constructed.
// pipeline-stage.ts
export class PipelineStage extends Stage {
constructor(
scope: Construct,
id: string,
props: StageProps,
stackMaker: (scope: cdk.Stage) => void) {
super(scope, id, props);
props.stackMaker(this) // <- actually call the maker function, in the sceope of the Pipeline Stage
}
}