Skip to content

Modularizing Stacks with BaseStackProvider and DefaultStackProvider

In complex CDK projects, managing inlined stacks within PipelineBlueprint.builder() can become cumbersome. To enhance organization and reusability, the BaseStackProvider and DefaultStackProvider abstraction offers a powerful solution.

The DefaultStackProvider serves as an abstract base class that you can extend to define your stack provisioning logic. The core implementation lies within the mandatory stacks() function.

Creating a Custom Stack Provider with DefaultStackProvider

import { Stage, DefaultStackProvider } from '@cdklabs/cdk-cicd-wrapper';
import * as cdk from 'aws-cdk-lib';

export class ExampleProvider extends DefaultStackProvider {

  stacks(): void {
    new cdk.Stack(this.scope, 'ExampleStack', {
      env: this.env,
      // ... other stack properties
    });
  }
}

Creating a Custom Stack Provider with BaseStackProvider

The BaseStackProvider serves as an abstract base class that you can extend to define your stack provisioning logic. The core implementation lies within the mandatory stacks() function.

import { Stage, BaseStackProvider } from '@cdklabs/cdk-cicd-wrapper';
import * as cdk from 'aws-cdk-lib';

export class ExampleProvider extends BaseStackProvider {

  stacks(): void {
    // Define your stack configuration here
    new cdk.Stack(this.scope, 'ExampleStack', {
      env: this.env,
      // ... other stack properties
    });
  }
}

Leveraging the Custom Provider

Once you've created your custom provider, integrate it seamlessly within your pipeline blueprint:

import { PipelineBlueprint } from '@cdklabs/cdk-cicd-wrapper';
import { ExampleProvider } from './example-provider'; // Assuming your provider is in a separate file

const pipeline = PipelineBlueprint.builder()
  .addStack(new ExampleProvider())
  .synth(app);

Sharing resources across DefaultStackProviders

It is common to share resources across multiple stack provider. To achieve this, you can create the resource in one of the DefaultStackProvider and register it with the register(key: string, value: any) method. You can then retrieve the resource in another DefaultStackProvider using the get(key: string) method. The following example demonstrates how to share a DynamoDB table across two DefaultStackProviders.

import { DefaultStackProvider } from '@cdklabs/cdk-cicd-wrapper';
import * as cdk from 'aws-cdk-lib';

export class DynamoDBProvider extends DefaultStackProvider {
  stacks(): void {
    const table = new cdk.Table(this.scope, 'ExampleTable', {
      partitionKey: { name: 'id', type: cdk.AttributeType.STRING },
      billingMode: cdk.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    this.register('exampleTable', table);
  }
}
import { DefaultStackProvider } from '@cdklabs/cdk-cicd-wrapper';
import * as cdk from 'aws-cdk-lib';

export class LambdaProvider extends DefaultStackProvider {
  stacks(): void {
    const table = this.get('exampleTable') as cdk.Table;

    // Use the DynamoDB table in your stack
    // ...
  }
}

Best Practices

  • Modular Organization: For optimal maintainability, create separate providers for distinct logical units within your application. This promotes code clarity and simplifies future modifications.
  • Extensibility with Hooks: The DefaultStackProvider provides optional preHooks and postHooks methods that you can override to execute custom logic before and after stack creation, respectively. This empowers you to inject additional processing steps into your pipeline as needed.
  • Secure Key Management: Utilize a dedicated AWS Key Management Service (KMS) key for encryption purposes. This key can be retrieved using the this.encryptionKey property within your custom provider class.
  • Centralized Configuration Management: Access and leverage SSM Parameters to store and retrieve configuration values securely. You can utilize the resolve(ssmParameterName: string) function provided by the DefaultStackProvider to retrieve these parameters within your stacks
  • Resource Sharing: To share resources across multiple DefaultStackProviders, create the resource in one provider and register it using the register(key: string, value: any) method. You can then retrieve the resource in another provider using the get(key: string) method.
  • SSM Parameter Caching: To minimize the number of SSM Parameter retrievals, the DefaultStackProvider caches parameter values for the duration of the pipeline execution. This ensures optimal performance and reduces unnecessary API calls.
  • Namespace Isolation: To prevent naming conflicts, each DefaultStackProvider operates within its own namespace. This ensures that stack names, resource identifiers, and other naming conventions remain unique across the pipeline.