How to get all availability zones at runtime with cdktf?

809 views Asked by At

I have a subnet declaration like the following which creates one subnet for each AWS AZ in us-west-2:

    const dataAwsAvailabilityZonesAll =
      new aws.datasources.DataAwsAvailabilityZones(this, "all", {});


    const zoneNames = Fn.lookup(
      dataAwsAvailabilityZonesAll.fqn,
      "names",
      undefined
    );
    const availabilityZone = Fn.element(
      zoneNames,
      Token.asNumber("count.index")
    );

    const publicSubnet = new aws.vpc.Subnet(this, "pub_subnet", {
      availabilityZone,
      cidrBlock: "${cidrsubnet(aws_vpc.vpc.cidr_block, 8, count.index)}",
      mapPublicIpOnLaunch: true,
      tags: {
        name: environment + "-public-subnet-${(count.index + 1)}",
        ["kubernetes.io/cluster/" + eksClusterName]: "shared",
        "kubernetes.io/role/elb": "1",
        environment: environment,
        public: "true",
      },
      vpcId: "${aws_vpc.vpc.id}",
    });
    publicSubnet.addOverride("count", Fn.lengthOf(zoneNames));

I'd like to refactor this to get rid of the usages of Fn.count, Fn.lookup, count.index, etc. Instead, I'd like to have direct references to each subnet in typescript. I've tried doing the following:

    const testSubnets = []
    for (let i = 0; i++; i < Fn.lengthOf(zoneNames)) {
      const s = Fn.element(zoneNames, i)
      testSubnets.push(new aws.vpc.Subnet(this, `subnet-${s}`, {
        availabilityZone: s,
        cidrBlock:
          `\${cidrsubnet(aws_vpc.vpc.cidr_block, 4, ${i + 3*dataAwsAvailabilityZonesAll.names.length})}`,
        mapPublicIpOnLaunch: true,
        tags: {
          name: environment + `${environment}-large-public-subnet-${i}`,
          ["kubernetes.io/cluster/" + eksClusterName]: "shared",
          "kubernetes.io/role/elb": "1",
          environment: environment,
          public: "true",
        },
        vpcId: clusterVpc.id
      }))
    }

but this alternative doesn't create any subnets at all. I've also tried using dataAwsAvailabilityZonesAll.names.length and dataAwsAvailabilityZonesAll.names directly but couldn't figure those out either. It seems that dataAwsAvailabilityZonesAll.names is empty at runtime and that it doesn't resolve until much later in the CDKTF lifecycle.

How can I get the names of each AZ in a given region when using CDKTF? Instead of trying to automate each AZ, should I declare the AZs as a constant and use those when creating a subnet?

1

There are 1 answers

2
Martin Atkins On BEST ANSWER

When working with CDK for Terraform it's important to be mindful of which code is really running at synth time in your source language vs. what is just getting translated into equivalent code in the Terraform language for Terraform to deal with at runtime.

Data resources will only be read by Terraform itself when generating the plan or during the apply phase, depending on the dependencies of the data block in the configuration. Therefore you can't interact with the results of the data resource directly in your generation code. Instead, you need to ask CDK for Terraform to generate code that will make use of that result.

For this to work, you'll need to set the special forEach property when defining your aws.vpc.Subnet object, which in turn tells CDK for Terraform to generate a Terraform resource "aws_subnet" block with the for_each meta-argument set.

To populate that from a dynamically-loaded collection, you'll need to use CDK for Terraform Iterators, which are an abstraction over dynamic collections ready to be passed in to forEach.

For example:

const zoneIterator = TerraformIterator.fromList(zoneNames);
new aws.vpc.Subnet(this, `public`, {
  forEach: zoneIterator,

  availabilityZone: zoneIterator.value,
  // ...
}

Notice that this aws.vpc.Subnet now represents multiple subnets at once: one for each element of zoneNames. Therefore the properties of its definition object must represent rules for deriving the arguments from the current element, rather than hard-coded values. If you will use escape hatches as part of those rules, you can use each.value to refer to the availability zone of the current element; that Terraform expression is equivalent to zoneIterator.value in CDK for Terraform.

According to the latest discussions I could find on the subject there doesn't seem to be a first-class way to refer to individual instances of a resource declared with forEach in this way, so this is a situation where "escape hatches" are required. The complexity of that can be minimized by placing the escape hatch into a local value and then accessing that local value later in your code:

// Constructs a local value which is a map from
// availability zone name to subnet ID.
const subnetIds = new TerraformLocal(
  this, "subnet_ids_by_az",
  `\${ tomap({ for n, s in aws_subnet.public : n => s.id }) }`,
);

// Can refer to this subnetIds value elsewhere,
// using the normal methods of TerraformLocal
// to coerce it into different types as needed.
new TerraformOutput(this, "subnet_ids", {
  value: subnetIds,
});