Search code examples
amazon-web-servicesamazon-ec2aws-cdkamazon-vpc

AWS CDK: Create a Subnet and Launch Instance in it With Existing VPC


I have a fairly simple task that I am unable to complete with AWS CDK: Create a subnet and launch an instance in it using an existing VPC.

The existing VPC is a CfnVpc that I obtained using the cdk migrate command. Here it is:

class Lab(Stack):

    @property
    def vpc(self) -> ec2.CfnVPC:
        return self._vpc

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(
            scope, construct_id, description="Nat instance with two subnets", **kwargs
        )
        self._vpc = ec2.CfnVPC(
            self,
            "VPC",
            enable_dns_support=True,
            enable_dns_hostnames=True,
            cidr_block=subnetConfig["VPC"]["CIDR"],
            tags=[
                {
                    "key": "Application",
                    "value": self.stack_name,
                },
                {
                    "key": "Network",
                    "value": "PublicA",
                },
            ],
        )

Here are my new resources:

class MyResources(Lab):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        vpc = ec2.Vpc.from_lookup(
            self, "L2VPC", tags={"aws:cloudformation:logical-id": "VPC"}
        )

        publicSubnetB = ec2.Subnet(
            self,
            "PublicSubnetB",
            availability_zone="us-east-1b",
            cidr_block="10.0.2.0/24",
            vpc_id=vpc.vpc_id,
        )
        cdk.Tags.of(publicSubnetB).add("Application", self.stack_name)
        cdk.Tags.of(publicSubnetB).add("Network", "PublicB")

        # These are also exposed from the cdk migrate command via properties. 
        ec2.CfnSubnetRouteTableAssociation(
            self,
            "PublicSubnetBRouteTableAssociation",
            subnet_id=publicSubnetB.subnet_id,
            route_table_id=self.publicRouteTable.ref,
        )
        ec2.CfnSubnetNetworkAclAssociation(
            self,
            "PublicSubnetBNetworkAclAssociation",
            subnet_id=publicSubnetB.subnet_id,
            network_acl_id=self.publicNetworkAcl.ref,
        )

And finally, here's the failing instance:

        instanceB = ec2.Instance(
            self,
            "InstanceB",
            instance_type=ec2.InstanceType.of(
                instance_class=ec2.InstanceClass.BURSTABLE3,
                instance_size=ec2.InstanceSize.MICRO,
            ),
            machine_image=ec2.AmazonLinuxImage(
                generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2
            ),
            vpc=vpc,
            vpc_subnets=ec2.SubnetSelection(
                subnet_type=ec2.SubnetType.PUBLIC, availability_zones=["us-east-1b"]
            ),
            user_data=ec2.UserData.custom(userDataB),
            private_ip_address="10.0.2.119",
            associate_public_ip_address=True,
            security_group=sg,
            user_data_causes_replacement=True,
        )

AWS CDK says that there is no public subnet in az us-east-1b - even though I created it just prior. The only way this works is if I comment out the instance code, run it, clear the context, run it again, and comment back in the instance code. There has got to be a better way!


Solution

  • The solution was very tricky and not well documented in my opinion.

    Since I was creating the subnet in the same stack as I used to look up the VPC, I had to use the ec2.Vpc.from_vpc_attributes() class method instead of the ec2.Vpc.from_lookup() class method.

    Does not work:

    # app.py
    
    vpc = ec2.Vpc.from_lookup(self, "IVpc", region="us-east-1")
    
    # ...[create public subnet ref CfnVpc]
    
    # ...[create instance in public subnet] 
    

    Error Logs

    jsii.errors.JavaScriptError: 
      Error: To set 'associatePublicIpAddress: true' you must select Public subnets (vpcSubnets: { subnetType: SubnetType.PUBLIC })
          at new Instance (/tmp/jsii-kernel-wHeWcm/node_modules/aws-cdk-lib/aws-ec2/lib/instance.js:1:5169)
          at Kernel._Kernel_create (/tmp/tmpxiyze867/lib/program.js:10108:25)
          at Kernel.create (/tmp/tmpxiyze867/lib/program.js:9779:93)
          at KernelHost.processRequest (/tmp/tmpxiyze867/lib/program.js:11696:36)
          at KernelHost.run (/tmp/tmpxiyze867/lib/program.js:11656:22)
          at Immediate._onImmediate (/tmp/tmpxiyze867/lib/program.js:11657:46)
          at process.processImmediate (node:internal/timers:478:21)
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/home/ubuntu/acg_nlb_lab/infra/app.py", line 80, in <module>
        MyInfra(app, STACK, env=env)
      File "/home/ubuntu/.pyenv/versions/cdk-3.10/lib/python3.10/site-packages/jsii/_runtime.py", line 118, in __call__
        inst = super(JSIIMeta, cast(JSIIMeta, cls)).__call__(*args, **kwargs)
      File "/home/ubuntu/acg_nlb_lab/infra/app.py", line 55, in __init__
        instanceB = ec2.Instance(
      File "/home/ubuntu/.pyenv/versions/cdk-3.10/lib/python3.10/site-packages/jsii/_runtime.py", line 118, in __call__
        inst = super(JSIIMeta, cast(JSIIMeta, cls)).__call__(*args, **kwargs)
      File "/home/ubuntu/.pyenv/versions/cdk-3.10/lib/python3.10/site-packages/aws_cdk/aws_ec2/__init__.py", line 71951, in __init__
        jsii.create(self.__class__, self, [scope, id, props])
      File "/home/ubuntu/.pyenv/versions/cdk-3.10/lib/python3.10/site-packages/jsii/_kernel/__init__.py", line 334, in create
        response = self.provider.create(
      File "/home/ubuntu/.pyenv/versions/cdk-3.10/lib/python3.10/site-packages/jsii/_kernel/providers/process.py", line 365, in create
        return self._process.send(request, CreateResponse)
      File "/home/ubuntu/.pyenv/versions/cdk-3.10/lib/python3.10/site-packages/jsii/_kernel/providers/process.py", line 342, in send
        raise RuntimeError(resp.error) from JavaScriptError(resp.stack)
    RuntimeError: To set 'associatePublicIpAddress: true' you must select Public subnets (vpcSubnets: { subnetType: SubnetType.PUBLIC })

    Works:

    # app.py
    
    # ...[create public subnet ref CfnVpc]
    
    # ...[create instance in public subnet] 
    
    vpc = ec2.Vpc.from_vpc_attributes(
        self,
        "IVpc",
        vpc_id=self.vpc.ref,
        availability_zones=["us-east-1a", "us-east-1b"],
        public_subnet_ids=[self.publicSubnet.ref, publicSubnetB.subnet_id],
        public_subnet_route_table_ids=[
            self.publicRouteTable.ref,
            self.publicRouteTable.ref,
        ],
    )
    
    # ...[able to use L2 IVpc and L2 instance]