Power Up Your AWS CDK Stack Props
Tags: aws, cdk, cloudformation, awscdk
Published: 2022-11-14 15:30:05

_Break out of traditional static stack configuration patterns by leveraging dynamic stack parameters with the AWS CDK_

## Overview

Traditional infrastructure as code frameworks such as CloudFormation and Terraform allow for basic parameterization by defining static values as parameters or environment variables which allow some flexibility in what infrastructure is deployed based on the template or module that defines the stack. Typically the amount of flexibility is hard coded in the modules and it is difficult to fully customize behavior without breaking into the underlying templates/modules. For example, a stack may allow for some input parameters that define an auto scaling strategy, but the implementation of that strategy needs to be predefined in the templates and only basic static parameters are allowed to be modified during deployment.

The beauty of an infrastructure framework like the AWS CDK which allows one to define infrastructure with an actual programming language means we no longer need to hard code or strictly define various aspects of our infrastructure as static parameters, but instead are allowed to dynamically configure stacks using all the language tools available to us as programmers.

## Example

Imagine we deployed an ECS service using AWS Fargate and wanted to set different auto scaling strategies for our various deployments. In a traditional CloudFormation template, you might decide to allow for a configurable scheduled scaling strategy and add these parameters to your template:

```yaml
  Parameters:
    ScaleOutNum: 30
    ScaleInNum: 10
    ScaleOutExpression: 'cron(00 10 * * ? *)'
    ScaleInExpression: 'cron(00 14 * * ? *)'
```

You would then configure some scheduled scaling resources inside the template which would take these parameters as inputs.  But what if you wanted to have multiple scaling times depending on the day of the week?  Or if you wanted to use target scaling instead of scheduled scaling?  Or if your development environment didn't need a complex scaling schedule?  You would need to refactor your CloudFormation template each time, adding some conditionals, and adjusting the parameter inputs to try to accommodate the different variations of how your infrastructure could be defined.

## Enter: the AWS CDK

Instead of defining your scaling strategy inside your infrastructure stack, that custom scaling strategy itself could be a parameter/method that is passed into the stack.

```typescript
// Define the scaling function:
// It takes in a FargateService as a parameter, and doesn't
// return anything. It only applies scaling methods on the
// service.
export interface FargateServiceStackProps extends StackProps {
  scalingStrategy?: (service: FargateService) => void;
}
```

Inside our stack, we can use the scaling method provided to apply custom scaling strategies to the `FargateService`.

```typescript
export class FargateServiceStack extends Stack {
  constructor(
    scope: Construct,
    id: string,
    { scalingStrategy, ...props }: FargateServiceStackProps
  ) {
    super(scope, id, props);

    let fargateService = {} as FargateService;

    // If there's a scaling strategy passed in to the stack,
    // apply it to this service
    if (scalingStrategy) {
      scalingStrategy(fargateService);
    }
  }
}
```

The `ScalingStrategy` can be as simple or as complicated as you'd like.  For example, setting a simple fixed capacity strategy might look like this:

```typescript
(service): ScalingStrategy => {
  service.autoScaleTaskCount({
    minCapacity: 10,
    maxCapacity: 10,
  });
};
```

One with scheduled scaling and CPU/Memory based scaling:

```typescript
(service): ScalingStrategy => {
  const scalableTarget = service.autoScaleTaskCount({
    minCapacity: 10,
    maxCapacity: 200,
  });

  scalableTarget.scaleOnSchedule('scale-out', {
    schedule: Schedule.cron({ hour: '10', minute: '00' }),
    minCapacity: 50,
    maxCapacity: 200,
  });

  scalableTarget.scaleOnSchedule('scale-in', {
    schedule: Schedule.cron({ hour: '14', minute: '00' }),
    minCapacity: 10,
    maxCapacity: 200,
  });

  scalableTarget.scaleOnCpuUtilization('CpuScaling', {
    targetUtilizationPercent: 50,
    scaleInCooldown: Duration.minutes(2),
    scaleOutCooldown: Duration.seconds(30),
  });
  scalableTarget.scaleOnMemoryUtilization('MemoryScaling', {
    targetUtilizationPercent: 50,
    scaleInCooldown: Duration.minutes(2),
    scaleOutCooldown: Duration.seconds(30),
  });
};
```

These scaling strategies can also be built into convenience functions, which leads to your stacks being defined like so:

```typescript
const app = new App();

new FargateServiceStack(app, 'FargateService-dev', {
  scalingStrategy: fixedCapacity({ fixedCapacity: 10 }),
});

new FargateServiceStack(app, 'FargateService-prod', {
  scalingStrategy: scheduledScalingFn({
    scaleOutMinCapacity: 50,
    scaleOutMaxCapacity: 200,
    scaleOutSchedule: Schedule.cron({ hour: '10', minute: '00' }),
    scaleInMinCapacity: 10,
    scaleInMaxCapacity: 200,
    scaleInSchedule: Schedule.cron({ hour: '14', minute: '00' }),
    cpuUtilizationPct: 50,
    memoryUtilizationPct: 50,
    scaleInCooldown: Duration.minutes(2),
    scaleOutCooldown: Duration.seconds(30),
  }),
});
```

## Example code
All example code can be found in this repo: https://github.com/rogerchi/cdk-dynamic-stack-props

## Conclusion

Using the AWS CDK allows us to use all of the possibilities of a full programming language when defining our Cloud applications, including passing in dynamic methods into our stacks, allowing us more control over our infrastructure.

If you liked this article, consider following me on Twitter: [@scythide](https://twitter.com/scythide)

## Further reading

Big thanks to [Yehuda Cohen](https://twitter.com/FunWithTheCloud) of [Fun With The Cloud](https://yehudacohen.substack.com/) for reviewing some drafts of this article.  He brought up the similarity between this pattern and the [Strategy Pattern](https://refactoring.guru/design-patterns/strategy), but for IaC.

Another alternative way that one might implement something like this would be with [CDK Aspects](https://aws.hashnode.com/the-power-of-aws-cdk-aspects) which I considered, but felt it added too much indirection to the solution.

## About me
**I am a Staff Engineer @ [Veho](https://shipveho.com).  We are still actively hiring for some key roles!  If you are passionate about serverless, or even looking to grow your skills, have a look at our [open positions](https://jobs.lever.co/veho?lever-via=00db_GfGxs)!**