david alfonso

Default resource values in the Python CDK

Imagine that we want find out what is the default subnet configuration created when instantiating a VPC like this:

from aws_lib import aws_ec2 as ec2, Stack

class MyStack(Stack):
    # ...
    def create_vpc(self):
        # there is an optional `subnet_configuration` parameter
        self.vpc = ec2.Vpc(self, "MyVpc", max_azs=2)

Method 1: Synthetizing

There is one straightforward way to know what resources will be created with a stack: synthetizing it. This means creating the CloudFormation template that would be provisioned if this stack was ever deployed.

You can do this by executing cdk synth and greping for MyVpc.

$ cdk synth --json | jq '.Resources | map_values(.Type)' | grep MyVpc | sort
  "MyVpcF9F0CA6F": "AWS::EC2::VPC",
  "MyVpcIGW5C4A4F63": "AWS::EC2::InternetGateway",
  "MyVpcPrivateSubnet1DefaultRouteA8CDE2FA": "AWS::EC2::Route",
  "MyVpcPrivateSubnet1RouteTable8819E6E2": "AWS::EC2::RouteTable",
  "MyVpcPrivateSubnet1RouteTableAssociation56D38C7E": "AWS::EC2::SubnetRouteTableAssociation",
  "MyVpcPrivateSubnet1Subnet5057CF7E": "AWS::EC2::Subnet",
  "MyVpcPrivateSubnet2DefaultRoute9CE96294": "AWS::EC2::Route",
  "MyVpcPrivateSubnet2RouteTableAssociation86A610DA": "AWS::EC2::SubnetRouteTableAssociation",
  "MyVpcPrivateSubnet2RouteTableCEDCEECE": "AWS::EC2::RouteTable",
  "MyVpcPrivateSubnet2Subnet0040C983": "AWS::EC2::Subnet",
  "MyVpcPublicSubnet1DefaultRoute95FDF9EB": "AWS::EC2::Route",
  "MyVpcPublicSubnet1EIP096967CB": "AWS::EC2::EIP",
  "MyVpcPublicSubnet1NATGatewayAD3400C1": "AWS::EC2::NatGateway",
  "MyVpcPublicSubnet1RouteTableAssociation2ECEE1CB": "AWS::EC2::SubnetRouteTableAssociation",
  "MyVpcPublicSubnet1RouteTableC46AB2F4": "AWS::EC2::RouteTable",
  "MyVpcPublicSubnet1SubnetF6608456": "AWS::EC2::Subnet",
  "MyVpcPublicSubnet2DefaultRoute052936F6": "AWS::EC2::Route",
  "MyVpcPublicSubnet2EIP8CCBA239": "AWS::EC2::EIP",
  "MyVpcPublicSubnet2NATGateway91BFBEC9": "AWS::EC2::NatGateway",
  "MyVpcPublicSubnet2RouteTable1DF17386": "AWS::EC2::RouteTable",
  "MyVpcPublicSubnet2RouteTableAssociation227DE78D": "AWS::EC2::SubnetRouteTableAssociation",
  "MyVpcPublicSubnet2Subnet492B6BFB": "AWS::EC2::Subnet",
  "MyVpcVPCGW488ACE0D": "AWS::EC2::VPCGatewayAttachment",

In this case, all associated resources begin with the same name, but in more complex cases it might be better to grep in the raw synthethized output and find out dependent resources.

Method 2: Looking at the CDK library resource

By synthetizing our stack, we've learnt that two private and two public subnets, along a couple of NAT gateways, would be created. But where is this defined?

The key is that all supported CDK libraries end up calling the TypeScript CDK library. This is done through the jsii library:

jsii allows code in any language to naturally interact with JavaScript classes. It is the technology that enables the AWS Cloud Development Kit to deliver polyglot libraries from a single codebase!

Looking at the Python definition of aws_ec2.Vpc we can see the actual name to look for in the CDK library:

class Vpc(
    jsii_type="aws-cdk-lib.aws_ec2.Vpc",  # This is the class in the aws-cdk-lib

We just have to open the appropriate file. In this case, packages/aws-cdk/aws-ec2/lib/vpc.ts (link) and search for class Vpc. These are the relevant contents:

export class Vpc extends VpcBase {
  // ...

   * The default subnet configuration
   * 1 Public and 1 Private subnet per AZ evenly split
  public static readonly DEFAULT_SUBNETS: SubnetConfiguration[] = [
      subnetType: SubnetType.PUBLIC,
      name: defaultSubnetName(SubnetType.PUBLIC),
      subnetType: SubnetType.PRIVATE_WITH_NAT,
      name: defaultSubnetName(SubnetType.PRIVATE_WITH_NAT),

  // ...

   * Vpc creates a VPC that spans a whole region.
   * It will automatically divide the provided VPC CIDR range, and create public and private subnets per Availability Zone.
   * Network routing for the public subnets will be configured to allow outbound access directly via an Internet Gateway.
   * Network routing for the private subnets will be configured to allow outbound access via a set of resilient NAT Gateways (one per AZ).
  constructor(scope: Construct, id: string, props: VpcProps = {}) {
    // ...
    const defaultSubnet = props.natGateways === 0 ? Vpc.DEFAULT_SUBNETS_NO_NAT : Vpc.DEFAULT_SUBNETS;
    this.subnetConfiguration = ifUndefined(props.subnetConfiguration, defaultSubnet);
    // ...

And there it is, the explanation of why two public and two private subnets along two NAT gateways were being defined.