March 30, 2023

Can I? IPv6 and Graviton Only

For any followers I’m an advocate for modern technologies. This is across the sustainability, performance and security. I’ve written a lot about Graviton in the past and I’ve also filmed videos with the AWS team about Graviton and I’ve also talked about security features and modern http protocols.

The question I’ve got, can I run a reasonable complex website using modern technologies that employ all the modern protocols and security features.

Goals

  • IPv6 Only
  • HTTP/3 (QUIC) Only
  • Graviton Only

AWS Services

What AWS services might you see in a semi-complex web application.

  • S3
  • CloudFront + WAFv2
  • ECS + ALB
  • ElastiCache
  • Aurora MySQL
  • Lambda

Deployment

  • CodePipeline
  • CodeBuild
  • CodeDeploy

Initial Analysis

Let’s have a look at each of the services and see what they support just by reading the documentation.

Service IPv6 HTTP/3 Graviton
S3 Yes No N/A
CloudFront Yes Yes N/A
WAFv2 Yes Unknown N/A
ECS Yes N/A Yes
ALB Yes No N/A
ElastiCache Yes N/A Yes
Aurora MySQL Yes N/A Yes
Lambda Yes N/A Yes
CodePipeline N/A N/A N/A
CodeBuild Unknown N/A Yes
CodeDeploy N/A N/A N/A

https://docs.aws.amazon.com/vpc/latest/userguide/aws-ipv6-support.html

Remember I said “only”!

It seems that while IPv6 is supported by a lot of services, many services have limitations and/or require dual stack to operate. Gah! That means I should basically stop now as it seems my hypthoses doesn’t hold true. You can’t run only IPv6 services, which is a real shame!

Let’s build something.

And of course we’ll do it using my favourite framework; CDK python.

Build

VPC

Interestingly enough, you can’t create an IPv6 only VPC, but you can subnets.

Out of the box CDK doesn’t support IPv6 and you must jump through a lot of L1 constructs and an escape hatch to get it to mostly work…

        cani_vpc = ec2.Vpc(self, "cani_vpc", nat_gateways=0)

        ip6_cidr = ec2.CfnVPCCidrBlock(
            self,
            "cidr_v6",
            vpc_id=cani_vpc.vpc_id,
            amazon_provided_ipv6_cidr_block=True,
        )

        all_subnets = []

        all_subnets = (
            cani_vpc.public_subnets
            + cani_vpc.private_subnets
            + cani_vpc.isolated_subnets
        )

        vpc_v6_cidr = Fn.select(0, cani_vpc.vpc_ipv6_cidr_blocks)
        subnet_v6_cidrs = Fn.cidr(vpc_v6_cidr, 256, str(128 - 64))

        for i, subnet in enumerate(all_subnets):
            cidr6 = Fn.select(i, subnet_v6_cidrs)
            sub_node = subnet.node.default_child
            sub_node.ipv6_cidr_block = cidr6
            subnet.node.add_dependency(ip6_cidr)

        if cani_vpc.public_subnets:
            igw_id = cani_vpc.internet_gateway_id

            for subnet in cani_vpc.public_subnets:
                subnet.add_route(
                    "DefaultRoute6",
                    router_type=ec2.RouterType.GATEWAY,
                    router_id=igw_id,
                    destination_ipv6_cidr_block="::/0",
                    enables_internet_connectivity=True,
                )
        if cani_vpc.private_subnets:
            eigw = ec2.CfnEgressOnlyInternetGateway(
                self, "eigw6", vpc_id=cani_vpc.vpc_id
            )

            for subnet in cani_vpc.private_subnets:
                subnet.add_route(
                    "DefaultRoute6",
                    router_type=ec2.RouterType.EGRESS_ONLY_INTERNET_GATEWAY,
                    router_id=eigw,
                    destination_ipv6_cidr_block="::/0",
                    enables_internet_connectivity=True,
                )

Pipeline

Now let’s get AWS CodeBuild, Pipeline and Commit to store and deploy our VPC before we move on.

It’s relatively easy to use Graviton containers in CodeBuild but they are really old and AWS should really update them.

class CaniPipelineStack(Stack):
    """The pipeline to deploy this thing"""

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

        # Defaults for all CodeBuild projects
        code_build_defaults = pipelines.CodeBuildOptions(  # pylint: disable=unused-variable
            # Control the build environment
            build_environment=codebuild.BuildEnvironment(
                build_image=codebuild.LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_2_0,
                compute_type=codebuild.ComputeType.LARGE,
                privileged=True,
            ),
            partial_build_spec=codebuild.BuildSpec.from_object(
                {"phases": {"install": {"commands": ["n 16.20.0"]}}}
            ),
        )

        repository = codecommit.Repository.from_repository_name(
            self, "cani_repo", repository_name="cani"
        )

        pipeline = pipelines.CodePipeline(
            self,
            "Pipeline",
            code_build_defaults=code_build_defaults,
            synth=pipelines.ShellStep(
                "Synth",
                input=pipelines.CodePipelineSource.code_commit(
                    repository=repository, branch="main"
                ),
                install_commands=["n 16.20.0"],
                commands=[
                    "pip install --upgrade pip",
                    "pip install -r requirements.txt",
                    "npm install -g aws-cdk",
                    "cdk synth",
                ],
            ),
        )

        pipeline.add_stage(
            CaniStage(
                self,
                "CaniStack",
                env=Environment(account="zzzzzzzzzzzzzzzzz", region="ap-southeast-2"),
            )
        )


class CaniStage(Stage):
    """Deploy the app"""

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

        CaniStack(self, "caniStack", stack_name="CaniStack")

ElastiCache

Interestingly ElastiCache is able to be deployed IPv4, IPv6 Only, or Dual Stack. The only way that IPv6 Only works is if you have Subnets that only have a v6 CIDR defined you can’t define a subnet group using VPC subnets that are dual stack, (which seems a bit strange to me…).

On the graviton side, there is the full gamut of Graviton processors available.

        redis_subnet_group = elasticache.CfnSubnetGroup(
            scope=self,
            id="redis_subnet_group",
            subnet_ids=private_subnets,
            description="subnet group for redis",
        )

        redis_sec_group = ec2.SecurityGroup(
            self,
            "redis_sec_group",
            vpc=cani_vpc,
            allow_all_outbound=False,
        )

        redis_cluster = elasticache.CfnCacheCluster(
            scope=self,
            id="redis_cluster",
            engine="redis",
            cache_node_type="cache.t4g.small",
            num_cache_nodes=1,
            network_type="dual_stack",
            cache_subnet_group_name=redis_subnet_group.ref,
            vpc_security_group_ids=[redis_sec_group.security_group_id],
        )

Aurora MySQL

RDS (and in our case Aurora) support for IPv6 is different again (sigh…). You have the choice of IPv4 or Dual Stack but no IPv6 only support. Graviton again is well supported.

        aurora_cluster = rds.DatabaseCluster(
            self,
            "aurora_cluster",
            engine=rds.DatabaseClusterEngine.aurora_mysql(
                version=rds.AuroraMysqlEngineVersion.VER_3_03_0
            ),
            instances=1,
            network_type=rds.NetworkType.DUAL,
            instance_props=rds.InstanceProps(
                instance_type=ec2.InstanceType.of(
                    ec2.InstanceClass.BURSTABLE4_GRAVITON, ec2.InstanceSize.MEDIUM
                ),
                vpc_subnets=ec2.SubnetSelection(
                    subnet_type=ec2.SubnetType.PRIVATE_ISOLATED
                ),
                vpc=cani_vpc,
            ),
        )

Lambda

Lambda does not really support IPv6 beyond public endpoints, which is pretty poor.

Lambda has a simple flag to support Graviton. It’s a really simple flag.

Lastly, Lambda does have a URL capability to call it directly via a HTTP/S interface. Looking at the documentation there is no indication it supports anything above http/1.1 which is pretty poor. While there may not be a huge number of benefits from 1.1 to 2, there is likely a setup improvement with HTTP/3 and given that Lambda functions are short lived, the TCP/TLS setup time could be a significant portion of the overall connection and execution time.

        lambda_.Function(self, "lambda_function",
            runtime=lambda_.Runtime.PYTHON_3_9,
            handler="lambda-handler.handler",
            code=lambda_.Code.from_asset("lambda"),
            architecture=lambda_.Architecture.ARM_64
        )

S3

S3 is a bit similar to Lambda. IPv6 support isn’t complete and mostly includes endpoint dual stack support. HTTP/2 and HTTP/3 appears absent from the documentation which I can only guess neither are supported.

bucket = s3.Bucket(self, "bucket")

ECS + ALB

CDK has some high-level constructs called ecs_patterns to get you up and running really quick. The big issue comes in with our image architectures are handled and can be a bit of struggle. Docker Hub seems to mostly just work (mostly…), but ECR seems to be a minefield. This is something that I think needs a rethink and some work to get right, not just for ECR, but for registries in general.

It’s relatively easy to set the architecture in ECS and you don’t need to jump through hoops to get there.

ALBs support IPv6. Again, this appears to be Dual stack and more the public facing side. They do work in subnets that are only IPv6, but again there does seem to be an underlying ‘second class citizen’ feel to IPv6 support. When using the ecs_patterns there are way too many hoops to jump through to enable dualstack. It should be default

ECS does support containers getting an IPv6 address automatically.

ALBs are yet to support HTTP/3. They only recently got TLS1.3.

        asset = DockerImageAsset(
            self,
            "image",
            directory="docker"
        )

        cluster = ecs.Cluster(self, "fargate_cluster", vpc=cani_vpc)

        lb_fs = load_balanced_fargate_service = (
            ecs_patterns.ApplicationLoadBalancedFargateService(
                self,
                "Service",
                cluster=cluster,
                memory_limit_mib=1024,
                desired_count=1,
                assign_public_ip=True,
                cpu=512,
                runtime_platform=ecs.RuntimePlatform(
                    cpu_architecture=ecs.CpuArchitecture.ARM64,
                    operating_system_family=ecs.OperatingSystemFamily.LINUX,
                ),
                task_image_options=ecs_patterns.ApplicationLoadBalancedTaskImageOptions(
                    image=ecs.ContainerImage.from_docker_image_asset(asset),
                    container_port=8000
                ),
                task_subnets=ec2.SubnetSelection(
                    subnets=cani_vpc.public_subnets
                ),
            )
        )

        for node in lb_fs.node.children:
            if isinstance(node, alb.ApplicationLoadBalancer):
                for subnode in node.node.children:
                    if isinstance(subnode, alb.CfnLoadBalancer):
                        subnode.add_override("Properties.IpAddressType", "dualstack")

CloudFront + WAFv2

Finally let’s take a look at CloudFront and WAFv2.

CloudFront supports IPv6 and HTTP/3. IPv6 is on by default via CDK which is awesome and enabling HTTP/3 is very straight forward.

        web_acl_arn = ssmr.SSMParameterReader(
            self, "web_acl_arn", parameter_name="waf_acl_arn", region="us-east-1"
        )

        cloudfront.Distribution(
            self,
            "distribution",
            web_acl_id=web_acl_arn.parametervalue,
            http_version=cloudfront.HttpVersion.HTTP2_AND_3,
            default_behavior=cloudfront.BehaviorOptions(
                origin=origins.LoadBalancerV2Origin(
                    lb_fs.load_balancer,
                    protocol_policy=cloudfront.OriginProtocolPolicy.HTTP_ONLY,
                ),
            ),
        )

Conclusion

If I’m totally honest it’s a really poor experience for IPv6 only. Support is messy and inconsistent across services. Requiring a VPC to have an IPv4 CIDR defined but never using it seems like some huge piece of left over technical debt that is really hard to get rid of somewhere.

I haven’t done a comparison to any other cloud provider out there but I’d guess the situation is just as dire. This is really disappointing as many governments are mandating support for IPv6 and many telcos around the world are deploying IPv6 only networks (though usually with DNS64 and NAT64/CGNAT).

With respect to IPv6 I give AWS a 5/10. It’s a good effort, but it’s got a long way to go before it’s ubiquitous and even further before it can be deployed as a single stack.

On the Graviton front, it’s well supported across many services which is awesome. If you aren’t running Windows or commercial software/databases, or you need some specific x86_64 (or Intel/AMD) specific instructions then you really have no excuse to not be using Graviton. I personally still find this a challenge to convince people, which I’m really surprised at. You get more performance, you save money and you save the planet. It’s a no brainer.

As Graviton is really an AWS only processor and ARM/AARCH64 is widely deployed in other scenarios yet, I give AWS 10/10.

Finally, HTTP/3. Honestly I think AWS could do better here. TLS 1.3 has just been announced as supported on ALBs 5 years after it was ratified. A lot of this stuff is a chicken-egg problem. Why put effort into building something that some bloke in his little Sydney home is complaining about, but if you don’t make it work, then people can’t use it. I guess AWS do realise that and are doing the right thing by adding support, but I’d love to see it go a bit faster.

You can find all the code on Github.

© Greg Cockburn

Powered by Hugo & Kiss.