Any way to define a 'nil Construct'?

Hello,
I have started developing a library of Constructs and some of them (such the the specialised ones with only AWS IAM resources) are non-regional.

As we iterate the accounts and regions we are instantiating Stacks which have these constructs in them; and some of them will be non-regional and hence should not be deployed into anywhere other than the default region.

I came up with the below code in trying to ‘gate’ regional instances of the constructs out of deployment but haven’t succeeded.

I’d appreciate any suggestions!

class CustomConstruct(Construct):
    """
    For now, just prevents non-regional constructs from being deployed
    into anywhere other than the default region.
    """
    def __init__(self, scope: Construct, name: str, regional: bool = True):
        if not regional and scope.region != DEFAULTS["aws-region"]:
            print(f"Notice: Construct '{name}' won't be deployed into region", scope.region)
        else:
            super().__init__(scope, name)

To see if I understand you correctly, let me rephrase this in my own words: You have stacks that contain custom constructs and you want to make sure a set of custom constructs (that are containing global resources) should be created in a stack that is in a different region than your “main” region.

I think what you are doing there is ok, but it has a few smaller flaws:

  1. If you nest the construct (so the parent is not a stack but another custom construct) your scope.region call will not work (except if you forward this). To solve this you could use TerraformStack.of(self) to get the TerraformStack your construct is in to call region on.
  2. prints can be easily overlooked. We have the concept Annotations in the construct ecosystem, so you could do Annotations.of(self).add_error("won't be deployed into region"). There is also add_warning, add_message and add_info, (or addWarning, addMessage, addInfo, addError if you are in a language that prefers camelCase). If an error is found the synth fails, so it’s a hard rule, warnings / infos are simply displayed after synth. You get the construct path in the beginning of the message so it’s easy to identify where the problem lies.
  3. What you are doing is a validation, you can create one like this (only the validate method is required) and invoke it like this. It might be a nice abstraction, but there is no greater benefit. I guess it’s a bit more idiomatic to the construct ecosystem.

Is the library you are building open source by any chance? I’d love to take a look :slight_smile:

Hi Daniel,
Thanks for the response. I appreciate you mentioning these points; they will be very useful.

The way I inserted a conditional in the constructor of the Construct was just incorrect… So now I have this working code that does the skipping at the top level (main.py) . Here ECRRepository is regional, IAMUser* aren’t:

class AppStack(AWSStack):
    def __init__(self, scope: Construct, name: str, environment: AWSEnvironment, spec: dict):
        super().__init__(scope, name, environment, spec)

        ECRRepository(self, item_name + "-ecr-repository")

        if self.region == DEFAULTS["aws-region"]:
            IAMUserApp(self, item_name + "-app-user")
            IAMUserDeploy(self, item_name + "-deployer-user")

The ‘library’ is very simple; and I no longer have that CustomConstruct base class. At this point It is not open source, however I can share these two classes whereby I attempted to capture the boilerplate. An AWSStack is basically a Stack that is coupled with an AWSEnvironment.

class AWSEnvironment:
    """
    Data class representing an AWS environment. This is passed into an
    AWSStack to associate it with an AWS region & account. Parameters:
        account: str : The AWS Account name such as 'development'
        region: str :  The AWS Region name such as 'us-east-1'
    """

    def __init__(self, account: str, region: str):
        self.account = account.strip()
        if self.account not in ALL_AWS_ACCOUNTS:
            print("Invalid account name", self.account, ". Expected one of:", ALL_AWS_ACCOUNTS)

        self.suffix = AWS_ACCOUNT_SUFFIXES[self.account]

        self.region = region.strip()
        if not re.match(r'^[a-z]{2}\-[a-z]+\-[0-9]$', self.region):
            print("Invalid region name", self.region, ". Example: us-east-1")


class AWSStack(TerraformStack):
    """
    Wraps TerraformStack, implementing most of the boilerplate,
    essential configuration and validations for a CDK stack on AWS
    """

    def __init__(self, scope: Construct, name: str,
                 environment: AWSEnvironment, spec: dict):
        super().__init__(scope, id=kebab2CamelCase(name))

        self.name = name

        self.account = environment.account
        self.region = environment.region
        self.env_suffix = environment.suffix
        self.spec = spec

        try:
            self.account_id = AWS_ACCOUNT_IDS[self.account]
        except KeyError:
            print(self.account, "is not a valid AWS account. Expected one of:", ALL_AWS_ACCOUNTS)
            sys.exit(1)

        self.role_arn = f"arn:aws:iam::{self.account_id}:role/OrgAccountAccessRole"
        self.state_bucket_name = STATE_BUCKETS[self.account]
        self.state_file_name = f"cdktf/{scope.name}/{self.name}.state"

        self.backend = S3Backend(
            self,
            bucket=self.state_bucket_name,
            region=DEFAULTS["aws-region"],
            key=self.state_file_name,
            role_arn=self.role_arn,
            encrypt=True,
            dynamodb_table="terraform-state-lock"
        )

        self.provider = AwsProvider(
            self,
            "AWS",
            region=self.region,
            assume_role={"role_arn": self.role_arn, "duration": "1h"}
        )

I will be applying your points #2 and #3 to refactor these prints at least. Wouldn’t be suprised to find more existing classes in the cdktf library that I could have used.

In its current state the CDKTF API docs for Python are all in one big page; I didn’t see a neat list of classes for example. TBH this has made using documentation a bit of a challenge as well.

Cheers