Skip to content

CDK Overrides

Attention

⚠ Overriding CloudFormation templates is an advanced feature which can cause your stacks to not deploy successfully. Please use with caution!

Copilot generates CloudFormation templates using configuration specified in the manifest. However, not all CloudFormation properties are configurable in the manifest. For example, you might want to configure the Ulimits for your workload container, but the property is not exposed in manifests.

Overrides with yamlpatch or cdk allow you to add, delete, or replace any property or resource in a CloudFormation template.

When should I use CDK overrides over YAML patch?

Both options are a "break the glass" mechanism to access and configure functionality that is not surfaced by Copilot manifests.

We recommend using the AWS Cloud Development Kit (CDK) overrides over YAML patches if you'd like to leverage the expressive power of a programming language. The CDK allows you to make safe and powerful modifications to your CloudFormation template.

How to get started

You can extend your CloudFormation template with the CDK by running the copilot [noun] override command. For example, you can run copilot svc override to update the template of a Load Balanced Web Service.

The command will generate a new CDK application under the copilot/[name]/override directory with the following structure:

.
├── bin/
│   └── override.ts
├── .gitignore
├── cdk.json
├── package.json
├── README.md
├── stack.ts
└── tsconfig.json

You can get started by editing the stack.ts file. For example, if you decided to override the ECS service properties with copilot svc override, the following stack.ts file will be generated for you to modify:

import * as cdk from 'aws-cdk-lib';
import { aws_ecs as ecs } from 'aws-cdk-lib';

export class TransformedStack extends cdk.Stack {
    constructor (scope: cdk.App, id: string, props?: cdk.StackProps) {
        super(scope, id, props);
         this.template = new cdk.cloudformation_include.CfnInclude(this, 'Template', {
            templateFile: path.join('.build', 'in.yaml'),
        });
        this.appName = template.getParameter('AppName').valueAsString;
        this.envName = template.getParameter('EnvName').valueAsString;

        this.transformService();
    }

    // TODO: implement me.
    transformService() {
      const service = this.template.getResource("Service") as ecs.CfnService;
      throw new error("not implemented");
    }
}

How does it work?

As can be seen in the above stack.ts file, Copilot will use the cloudformation_include module provided by the CDK to help author transformations. This library is the CDK’s recommendation from their "Import or migrate an existing AWS CloudFormation template" guide. It enables accessing the resources not surfaced by the Copilot manifest as L1 constructs.
The CfnInclude object is initialized from the hidden .build/in.yaml CloudFormation template. This is how Copilot and the CDK communicate. Copilot writes the manifest-generated CloudFormation template under the .build/ directory, which then gets parsed by the cloudformation_include library into a CDK construct.

Every time you run copilot [noun] package or copilot [noun] deploy, Copilot will first generate the CloudFormation template from the manifest file, and then pass it down to your CDK application to override properties.

We highly recommend using the --diff flag with the package or deploy command to first visualize your CDK changes before a deployment.

Examples

The following example modifies the nlb resource of a Load Balanced Web Service to assign Elastic IP addresses to the Network Load Balancer.

In this example, you can view how to:

  • Delete a resource property.
  • Create new resources.
  • Modify a property of an existing resource.
View sample stack.ts
import * as cdk from 'aws-cdk-lib';
import * as path from 'path';
import { aws_elasticloadbalancingv2 as elbv2 } from 'aws-cdk-lib';
import { aws_ec2 as ec2 } from 'aws-cdk-lib';

interface TransformedStackProps extends cdk.StackProps {
    readonly appName: string;
    readonly envName: string;
}

export class TransformedStack extends cdk.Stack {
    public readonly template: cdk.cloudformation_include.CfnInclude;
    public readonly appName: string;
    public readonly envName: string;

    constructor (scope: cdk.App, id: string, props: TransformedStackProps) {
        super(scope, id, props);
        this.template = new cdk.cloudformation_include.CfnInclude(this, 'Template', {
            templateFile: path.join('.build', 'in.yml'),
        });
        this.appName = props.appName;
        this.envName = props.envName;
        this.transformPublicNetworkLoadBalancer();
    }

    /**
     * transformPublicNetworkLoadBalancer removes the "Subnets" properties from the NLB,
     * and adds a SubnetMappings with predefined elastic IP addresses.
     */
    transformPublicNetworkLoadBalancer() {
        const elasticIPs = [new ec2.CfnEIP(this, 'ElasticIP1'), new ec2.CfnEIP(this, 'ElasticIP2')];
        const publicSubnets = cdk.Fn.importValue(`${this.appName}-${this.envName}-PublicSubnets`);

        // Apply the override.
        const nlb = this.template.getResource("PublicNetworkLoadBalancer") as elbv2.CfnLoadBalancer;
        nlb.addDeletionOverride('Properties.Subnets');
        nlb.subnetMappings = [{
            allocationId: elasticIPs[0].attrAllocationId,
            subnetId: cdk.Fn.select(0, cdk.Fn.split(",", publicSubnets)),
        }, {
            allocationId: elasticIPs[1].attrAllocationId,
            subnetId: cdk.Fn.select(1, cdk.Fn.split(",", publicSubnets)),
        }]
    }
}

The following example showcases how you can add a property for only a particular environment, such as a production environment:

View sample stack.ts
import * as cdk from 'aws-cdk-lib';
import * as path from 'path';
import { aws_iam as iam } from 'aws-cdk-lib';

interface TransformedStackProps extends cdk.StackProps {
    readonly appName: string;
    readonly envName: string;
}

export class TransformedStack extends cdk.Stack {
    public readonly template: cdk.cloudformation_include.CfnInclude;
    public readonly appName: string;
    public readonly envName: string;

    constructor (scope: cdk.App, id: string, props: TransformedStackProps) {
        super(scope, id, props);
        this.template = new cdk.cloudformation_include.CfnInclude(this, 'Template', {
            templateFile: path.join('.build', 'in.yml'),
        });
        this.appName = props.appName;
        this.envName = props.envName;
        this.transformEnvironmentManagerRole();
    }

    transformEnvironmentManagerRole() {
        const environmentManagerRole = this.template.getResource("EnvironmentManagerRole") as iam.CfnRole;
        if (this.envName === "prod") {
            let assumeRolePolicy = environmentManagerRole.assumeRolePolicyDocument
            let statements = assumeRolePolicy.Statement
            statements.push({
                 "Effect": "Allow",
                 "Principal": { "Service": "ec2.amazonaws.com" },
                 "Action": "sts:AssumeRole"
            })
        }
    }
}

The following example showcases how you can delete a resource, the Copilot-created default log group that holds service logs.

View sample stack.ts
import * as cdk from 'aws-cdk-lib';
import * as path from 'path';

interface TransformedStackProps extends cdk.StackProps {
    readonly appName: string;
    readonly envName: string;
}

export class TransformedStack extends cdk.Stack {
    public readonly template: cdk.cloudformation_include.CfnInclude;
    public readonly appName: string;
    public readonly envName: string;

    constructor(scope: cdk.App, id: string, props: TransformedStackProps) {
        super(scope, id, props);
        this.template = new cdk.cloudformation_include.CfnInclude(this, 'Template', {
        templateFile: path.join('.build', 'in.yml'),
        });
        this.appName = props.appName;
        this.envName = props.envName;
        // Deletes the default log group resource.
        this.template.node.tryRemoveChild("LogGroup")
    }
}