How to pytest python AWS CDK VPC from_lookup

Viewed 220

When using a stack that gets a VPC using the from_lookup method, running unit tests will fail unless you setup credentials, which is not desirable and practical

How can a pytest be written to cope for this?

Example stack module ec2_stack.py

# Deps
from aws_cdk import (
    Stack,
    aws_ec2 as _ec2,
)
from constructs import Construct


class Ec2Test(Stack):
    """
    An EC2 test
    """

    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        **kwargs,
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)
        self.vpc_id = self.node.try_get_context("vpc_id")
        self.inst_type = self.node.try_get_context("inst_type")
        self.base_ami_id = self.node.try_get_context("base_ami_id")

        self._vpc = _ec2.Vpc.from_lookup(
            scope=self,
            id="my-vpc",
            vpc_id=self.vpc_id,
        )
        self._machine_image = _ec2.MachineImage.generic_linux(
            {"ap-southeast-2": self.base_ami_id}
        )
        self.__create_instance()

    def __create_instance(self):
        """
        Create EC2 instance resource
        """
        _ec2.Instance(
            self,
            "ec2",
            instance_type=_ec2.InstanceType(self.inst_type),
            vpc=self._vpc,
            machine_image=self._machine_image,
        )
2 Answers

One solution is to use a fixture and add an optional parameter to pass an IVpc object, until mocking libraries are written for CDK (moto exists to mock boto3, but couldn't find a CDK equivalent)

Stack constructor update

    def __init__(
        self,
        scope: Construct,
        construct_id: str,
        vpc: _ec2.IVpc = None,  # New parameter
        **kwargs,
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)
        self.vpc_id = self.node.try_get_context("vpc_id")
        self.inst_type = self.node.try_get_context("inst_type")
        self.base_ami_id = self.node.try_get_context("base_ami_id")
        if vpc is not None:
            # Now VPC can be provided optionally
            self._vpc = vpc
        else:
            self._vpc = _ec2.Vpc.from_lookup(
                scope=self,
                id="my-vpc",
                vpc_id=self.vpc_id,
            )
        self._machine_image = _ec2.MachineImage.generic_linux(
            {"ap-southeast-2": self.base_ami_id}
        )
        self.__create_instance()

Example pytest code: test_ec2_stack_vpc.py

# deps
import aws_cdk as _cdk
from aws_cdk import (
    assertions as assertions,
    aws_ec2 as _ec2,
)
from constructs import Construct
import pytest

from ec2_stack import Ec2Test

EXPECTED_VPC = "vpc-123"
EXPECTED_INST_TYPE = "t3.micro"
EXPECTED_AMI_ID = "ami-123"

TEST_CONTEXT_EC2 = {
      "inst_type": EXPECTED_INST_TYPE,
      "vpc_id": EXPECTED_VPC,
      "base_ami_id": EXPECTED_AMI_ID,
}


@pytest.fixture
def make_vpc():
    def _make_vpc(scope, id):
        return _ec2.Vpc.from_vpc_attributes(
            scope,
            id,
            availability_zones=["ap-southeast-2"],
            vpc_id=EXPECTED_VPC,
            private_subnet_ids=["subnet-9876"],
            private_subnet_names=["priv-8"],
        )

    return _make_vpc


class EmptyStack(_cdk.Stack):
    """
    Stack only to provide context
    """

    def __init__(
        self, scope: Construct, construct_id: str, vpc: _ec2.IVpc = None, **kwargs
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)


def test_ec2_created(make_vpc):
    app = _cdk.App(context=TEST_CONTEXT_EC2)
    e_stack = EmptyStack(app, "empty-stack")
    test_vpc = make_vpc(e_stack, "test-vpc")
    stack = Ec2Test(app, "ec2-stack", vpc=test_vpc)
    template = assertions.Template.from_stack(stack)

    template.has_resource_properties(
        "AWS::EC2::Instance", {"InstanceType": EXPECTED_INST_TYPE}
    )

I think that perhaps you are considering this from the wrong angle:

The from methods assume that the resource exists during synth, and generate a token that will correspond with the right values once CloudFormation is handed the synth'd template and resolves all the things it needs to to import the various resources imported in from methods.

In addition, when you are Unit testing CDK, what you are actually doing is just verifying that the template contains the resources you want it to contain, with certain configurations. Importing a resource such as VPC it will have whatever values it has when it was generated by whatever process generates it (another stack, an org level set up functionality, whatever). As such, mocking it and testing for it to have specific values in the stack that is importing it is folly - that could change at any time, and your tests would be reporting that it still passes because your mock value will not have changed even though the actual resource did.

So, you should:

  1. Test that your resources that need VPC access have VPC properties set on them. They may resolve to tokens or they may not be 100% clear, but you can have faith that the from method will generally work as described and will add the proper values at deployment - you do not have to test that it does, because the CDK teams already do. - You are not testing the values here, but just that the given properties exist on a resource and they are not None/empty/null.

  2. Then, depending on the nature of how your VPC is created:

    • If you control the VPC in another stack, then you unit test it there for its specific values. As long as you and your team understand that changing tests once established is bad (and if you find that you are doing it a lot, then your code needs to be changed to be more unit testable, not your tests) then you can be reasonably confident that everything will work as expected

    • If you do not control the vpc at all (say using the default or your org has created vpcs in each of its sub accounts for example) then you'll need higher level tests than Unit tests - Integration/Contract testing and health check tests (ie - tests where its OK to have credentials to check on things).

The From Methods, when cdk deploy is used or the CloudFormation deployment initializes from a CodeDeploy are "basically" (generalization incoming) just using the existing SDK methods (ie: like boto3) to get this information at deploy time. The SDKs are just built on top of aws cli sdk anyways, and that is pretty much what every internal resource uses to get information from others it needs at run time as well.

Because these methods are built ins from the CDK teams (and the SDK teams) you do not have to test that they import as expected from things outside the scope of the current test (the vpc is not created during the scope of this test, so it is outside its scope) because you can have faith that AWS is going to do the best they can to make sure it continues to operate as expected. (bugs obviously happen but generally, yes) - after all - you don't go testing that every import worked, right? or that every library that you install that its documented functionally works as expected in isolation (not as part of your behavior) right? from methods are the same thing.

Related