Subclass Stack
and accept the source configuration input as a custom prop type.1
// SourceConfigPipelineStack.ts
interface SourceConfigPipelineStackProps extends cdk.StackProps {
source: pipelines.CodePipelineSource;
}
export class SourceConfigPipelineStack extends cdk.Stack {
constructor(
scope: Construct,
id: string,
props: SourceConfigPipelineStackProps
) {
super(scope, id);
const pipeline = new pipelines.CodePipeline(this, id, {
pipelineName: id,
synth: new pipelines.CodeBuildStep('Synth', {
input: props.source,
installCommands: [],
commands: [],
}),
});
}
}
Pipeline consumers then pass their own source as configuration:
// app.ts
new SourceConfigPipelineStack(app, 'MyPipelineStack', {
env,
source: pipelines.CodePipelineSource.connection('user/example4-be', 'main', {
connectionArn:
'arn:aws:codestar-connections:us-east-1:111...1111:connection/1111-1111.....1111',
}),
});
Edit: Is it "bad" to put ARN configuration in code?
Not according to AWS. The CDK "best practices" doc says it's reasonable to hardcode cross-stack ARNs:
When the two stacks are in different AWS CDK apps, use a static from
method to import an externally-defined resource based on its ARN ... (for example, Table.fromArn()
for a DynamoDB table). Use the CfnOutput
construct to print the ARN or other required value in the output of cdk deploy
, or look in the AWS console. Or the second app can parse the CloudFormation template generated by the first app and retrieve that value from the Outputs
section.
Hardcoding ARNs in code is sometimes worse, sometimes better than the alternatives like Parameter
, Secret
or CfnOutput
.
Edit: Handle multi-environment config with a Configuration Factory
All Apps have app-level config items (e.g. defaultInstanceSize
), which often differ by environment. Prod accounts need full-powered resources, dev accounts don't. Consider encapsulating (non-secret) config in a Configuration Factory. The constructor receives an account and region and returns plaintext configuration object. Stacks receive the config as props.
// app.ts
const { env, isProd, retainOnDelete, enableDynamoCache, defaultInstanceSize, repoName, branchName, githubConnectionArn } =
// the config factory is using the account and region from the --profile flag
new EnvConfigurator('SuperApp', process.env.CDK_DEFAULT_ACCOUNT, process.env.CDK_DEFAULT_REGION).config;
new SourceConfigPipelineStack(app, 'MyPipelineStack', {
env,
source: pipelines.CodePipelineSource.connection(repoName, branchName, {
connectionArn: githubConnectionArn
}),
stackTerminationProtection: isProd,
});
The local config pattern has several advantages:
- Config values are easily discoverable and centralised in a single place
- Callers can be allowed to provide type-constrained overrides
- Easily assert against configuration values
- Config values are under version control
- Pipeline-friendly: avoid cross-account permission headaches
Local config can be used alongside Parameter
and CfnOutput
and Secret
, which have complimentary advantages. Apps typically use each one. Reasonable people can disagree about where exactly to draw the boundaries.
(1) The fundamental CDK pattern is Construct composition: "Composition is the key pattern for defining higher-level abstractions through constructs... In general, composition is preferred over inheritance when developing AWS CDK constructs." In this case, it makes sense to subclass Stack
rather than the Construct
base class, because the OP use case is a cloned repo with, presumably, the deploy stages non-optionally encapsulated in the stack.