Why my CDK VPC constructor does not respect maximum availability zones

In this article, we'll know the environment variable account and region are requirements for the production CDK stack. Also, StacksPros need to be inherited as well.

Why my CDK VPC constructor does not respect maximum availability zones
Photo by Pero Kalimero / Unsplash

Problem

In the following code snippet, we configure the maximum number of availability zones(AZs). However, CDK does not respect the value.

$ cdk --version
2.5.0 (build 0951122)
$ cat ./bin/vpc-additional-subnet-stack.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { VpcAdditionalSubnetStackStack } from '../lib/vpc-additional-subnet-stack-stack';

const app = new cdk.App();
new VpcAdditionalSubnetStackStack(app, 'VpcAdditionalSubnetStackStack', {

  env: { account: '111111111111', region: 'eu-west-1' },

});%

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from "aws-cdk-lib/aws-ec2";

export class VpcAdditionalSubnetStackStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id);

    const subnetConfig = [
      {
          cidrMask: 22,
          name: "outputSubnet",
          subnetType: ec2.SubnetType.PUBLIC,
      },
      {
          cidrMask: 22,
          name: "database",
          subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
      },
      {
          cidrMask: 22,
          name: "application",
          subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
      },
    ];

    // VPC
    const vpc = new ec2.Vpc(this, "Lab-VPC", {
      cidr: "10.0.0.0/16",
      maxAzs: 3,
      subnetConfiguration: subnetConfig,
    });

  }
}

Also, we can view there are 3 AZs in eu-west-1 region.

$ aws ec2 describe-availability-zones

{
    "AvailabilityZones": [
        {
            "State": "available",
            "OptInStatus": "opt-in-not-required",
            "Messages": [],
            "RegionName": "eu-west-1",
            "ZoneName": "eu-west-1a",
            "ZoneId": "euw1-az1",
            "GroupName": "eu-west-1",
            "NetworkBorderGroup": "eu-west-1",
            "ZoneType": "availability-zone"
        },
        {
            "State": "available",
            "OptInStatus": "opt-in-not-required",
            "Messages": [],
            "RegionName": "eu-west-1",
            "ZoneName": "eu-west-1b",
            "ZoneId": "euw1-az2",
            "GroupName": "eu-west-1",
            "NetworkBorderGroup": "eu-west-1",
            "ZoneType": "availability-zone"
        },
        {
            "State": "available",
            "OptInStatus": "opt-in-not-required",
            "Messages": [],
            "RegionName": "eu-west-1",
            "ZoneName": "eu-west-1c",
            "ZoneId": "euw1-az3",
            "GroupName": "eu-west-1",
            "NetworkBorderGroup": "eu-west-1",
            "ZoneType": "availability-zone"
        }
    ]
}

Dive into the problem

According to the Stack.availabilityZones:

If the stack is environment-agnostic (either account and/or region are tokens), this property will return an array with 2 tokens that will resolve at deploy-time to the first two availability zones returned from CloudFormation's Fn::GetAZs intrinsic function.

From the code snippet, we had already set up the account and region. It seems that the Stack does not inherit the environment. Thus, we can check the account and region of Stack again as the following snippet.

export class VpcAdditionalSubnetStackStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    console.log('account: ', Stack.of(this).account);
    console.log('region: ', Stack.of(this).region);
    console.log('availability zones', Stack.of(this).availabilityZones);
$ cdk synth
account:  ${Token[AWS.AccountId.6]}
region:  ${Token[AWS.Region.10]}
availability zones [ '${Token[TOKEN.200]}', '${Token[TOKEN.202]}' ]
...
... 

AWS CDK encodes a token whose value is not yet known at construction time[2]. We can know the environment variables are not been used in VpcAdditionalSubnetStackStack Stack.

    super(scope, id);

From the code snippet, which does not pass props in the call tosuper(), and the environment variable we pass when creating VpcAdditionalSubnetStackStack is ignored. Therefore, CDK considers this to be environment-agnostic and creates only 2 AZ.

$ cat ./lib/vpc-additional-subnet-stack-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';

export class VpcAdditionalSubnetStackStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here

    // example resource
    // const queue = new sqs.Queue(this, 'VpcAdditionalSubnetStackQueue', {
    //   visibilityTimeout: cdk.Duration.seconds(300)
    // });
  }
}

By default, the CDK helps us pull a template that defines the basic constructor. Here is the example template:

    super(scope, id, props);
$ cdk synth
account:  111111111111
region:  eu-west-1
availability zones [ 'eu-west-1a', 'eu-west-1b', 'eu-west-1c' ]
...

After updating the super() function, we can view the account and region output.

Summary

In order to create the availability zones of an AWS region, we must set up the following items:

  • Environment variable for account and region, it can be set up via AWS credential or env property.
  • Confirm the Stack inherits theStackProps, if not the stack will ignore the environment variable we set up in previous steps.

References

  1. https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2-readme.html#advanced-subnet-configuration
  2. Tokens - https://docs.aws.amazon.com/zh_cn/cdk/v2/guide/tokens.html
  3. https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Stack.html#availabilityzones
  4. https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.StackProps.html#env