DEV Community

Soushi Hiruta
Soushi Hiruta

Posted on

We want to achieve this configuration using only an Egress-only internet gateway with IPv6

When communicating externally from an IPv4 environment, it is generally considered standard to do so via a NAT Gateway. Since NAT Gateways incur charges based on traffic volume, we want to utilize IPv6 wherever possible.

Even Interface-type VPCEndpoints can add up to significant costs over time.

Created with VPCv2 constructs

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { SubnetType, CfnEIP, CfnSubnet } from 'aws-cdk-lib/aws-ec2';
import  * as aws_ec2 from '@aws-cdk/aws-ec2-alpha';
import { IpAddressType } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { AvailabilityZoneRebalancing } from 'aws-cdk-lib/aws-ecs';


export class VPCSubnet extends Construct {
    constructor(scope: Construct, id: string) {
        super(scope, id);

        const myVpc = new aws_ec2.VpcV2(this, 'Vpc', {
          primaryAddressBlock: aws_ec2.IpAddresses.ipv4('10.1.0.0/16'),
          vpcName: 'vpc-Develop-Network',
         secondaryAddressBlocks: [aws_ec2.IpAddresses.amazonProvidedIpv6({
            cidrBlockName: 'AmazonProvided'
          })]
        });

        // Internet Gateway for Public Subnet
        const igw = new aws_ec2.InternetGateway(this, 'IGW', {
          vpc: myVpc
        });

        // Egress-Only Internet Gateway for IPv6
       const eigw = new aws_ec2.EgressOnlyInternetGateway(this, 'EIGW', {
          vpc: myVpc
        });

        // Public Subnets (Multi-AZ)
        const publicRouteTable = new aws_ec2.RouteTable(this, 'PublicRouteTable', {
          vpc: myVpc
        });

        publicRouteTable.addRoute('PublicIGWRoute', '0.0.0.0/0', { gateway: igw });

        const publicSubnetAZ1Name = cdk.Fn.join('', ['sub-public-', cdk.Fn.select(0, cdk.Fn.getAzs())]);
        const publicSubnetAZ1 = new aws_ec2.SubnetV2(this, 'PublicSubnetAZ1', {
          vpc: myVpc,
          subnetName: publicSubnetAZ1Name,
          availabilityZone: cdk.Fn.select(0, cdk.Fn.getAzs()),
          ipv4CidrBlock: new aws_ec2.IpCidr('10.1.0.0/24'),
          //ipv6CidrBlock: new aws_ec2.IpCidr(cdk.Fn.select(0, cdk.Fn.cidr(cdk.Fn.select(0, myVpc.ipv6CidrBlocks), 4, '64'))),
          subnetType: SubnetType.PUBLIC,
          routeTable: publicRouteTable
        });

        // Explicitly set Name tag
        const cfnPublicSubnetAZ1 = publicSubnetAZ1.node.defaultChild as CfnSubnet;
        cdk.Tags.of(cfnPublicSubnetAZ1).add('Name', publicSubnetAZ1Name);

        const publicSubnetAZ2Name = cdk.Fn.join('', ['sub-public-', cdk.Fn.select(1, cdk.Fn.getAzs())]);
        const publicSubnetAZ2 = new aws_ec2.SubnetV2(this, 'PublicSubnetAZ2', {
          vpc: myVpc,
          subnetName: publicSubnetAZ2Name,
          availabilityZone: cdk.Fn.select(1, cdk.Fn.getAzs()),
          ipv4CidrBlock: new aws_ec2.IpCidr('10.1.1.0/24'),
          //ipv6CidrBlock: new aws_ec2.IpCidr(cdk.Fn.select(1, cdk.Fn.cidr(cdk.Fn.select(0, myVpc.ipv6CidrBlocks), 4, '64'))),
          subnetType: SubnetType.PUBLIC,
          routeTable: publicRouteTable
        });

        // Explicitly set Name tag
        const cfnPublicSubnetAZ2 = publicSubnetAZ2.node.defaultChild as CfnSubnet;
        cdk.Tags.of(cfnPublicSubnetAZ2).add('Name', publicSubnetAZ2Name);

        // Elastic IP for NAT Gateway
       // const eip = new CfnEIP(this, 'NatEIP', {
       //   domain: 'vpc'
       // });

        // NAT Gateway in Public Subnet
      //  const natGateway = new aws_ec2.NatGateway(this, 'NatGateway', {
      //    subnet: publicSubnetAZ1,
      //    allocationId: eip.attrAllocationId
      //  });

        // Private Subnets (Multi-AZ) with NAT Gateway
        const privateRouteTable = new aws_ec2.RouteTable(this, 'PrivateRouteTable', {
          vpc: myVpc
        });

     //  privateRouteTable.addRoute('PrivateNatRoute', '0.0.0.0/0', { gateway: natGateway });
       privateRouteTable.addRoute('PrivateEIGWRoute', '::/0', { gateway: eigw });

        const privateSubnetAZ1Name = cdk.Fn.join('', ['sub-private-', cdk.Fn.select(0, cdk.Fn.getAzs())]);
        const privateSubnetAZ1 = new aws_ec2.SubnetV2(this, 'PrivateSubnetAZ1', {
          vpc: myVpc,
          subnetName: privateSubnetAZ1Name,
          availabilityZone: cdk.Fn.select(0, cdk.Fn.getAzs()),
          ipv4CidrBlock: new aws_ec2.IpCidr('10.1.2.0/24'),
          ipv6CidrBlock: new aws_ec2.IpCidr(cdk.Fn.select(2, cdk.Fn.cidr(cdk.Fn.select(0, myVpc.ipv6CidrBlocks), 4, '64'))),
          subnetType: SubnetType.PRIVATE_WITH_EGRESS,
          routeTable: privateRouteTable
        });

        // Explicitly set Name tag
        const cfnPrivateSubnetAZ1 = privateSubnetAZ1.node.defaultChild as CfnSubnet;
        cdk.Tags.of(cfnPrivateSubnetAZ1).add('Name', privateSubnetAZ1Name);

        const privateSubnetAZ2Name = cdk.Fn.join('', ['sub-private-', cdk.Fn.select(1, cdk.Fn.getAzs())]);
        const privateSubnetAZ2 = new aws_ec2.SubnetV2(this, 'PrivateSubnetAZ2', {
          vpc: myVpc,
          subnetName: privateSubnetAZ2Name,
          availabilityZone: cdk.Fn.select(1, cdk.Fn.getAzs()),
          ipv4CidrBlock: new aws_ec2.IpCidr('10.1.3.0/24'),
          ipv6CidrBlock: new aws_ec2.IpCidr(cdk.Fn.select(3, cdk.Fn.cidr(cdk.Fn.select(0, myVpc.ipv6CidrBlocks), 4, '64'))),
          subnetType: SubnetType.PRIVATE_WITH_EGRESS,
          routeTable: privateRouteTable
        });

        // Explicitly set Name tag
        const cfnPrivateSubnetAZ2 = privateSubnetAZ2.node.defaultChild as CfnSubnet;
        cdk.Tags.of(cfnPrivateSubnetAZ2).add('Name', privateSubnetAZ2Name);

    }
}
Enter fullscreen mode Exit fullscreen mode

Verifying IPv6 Connectivity with Lambda

Confirmed in VPC Lambda with "Allow IPv6 traffic for dual-stack subnets" enabled

This is a simple function that retrieves the source IPv6 address accessed from Lambda via ifconfig.me.

import json
import urllib.request

import json
import urllib.request


def lambda_handler(event, context):
    """
    http://ifconfig.me/ 
    """
    try:

        # requestifconfig.me 
        with urllib.request.urlopen('http://ifconfig.me/', timeout=10) as response:
            ip_address = response.read().decode('utf-8').strip()

        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': 'Successfully retrieved IP address',
                'ip_address': ip_address
            })
        }

    except urllib.error.URLError as e:
        # network error
        return {
            'statusCode': 500,
            'body': json.dumps({
                'message': 'Error retrieving IP address',
                'error': str(e)
            })
        }

    except Exception as e:
        # other error
        return {
            'statusCode': 500,
            'body': json.dumps({
                'message': 'Unexpected error occurred',
                'error': str(e)
            })
        }


# for local test
if __name__ == '__main__':
    # test event
    test_event = {}
    test_context = {}

    result = lambda_handler(test_event, test_context)
    print(json.dumps(result, indent=2, ensure_ascii=False))```
{% endraw %}
bash
Response:
{
  "statusCode": 200,
  "body": "{\"message\": \"Successfully retrieved IP address\", \"ip_address\": \"2600:1f13:94:1a02:ef65:ac4:50ab:5e1\"}"
}
{% raw %}

Enter fullscreen mode Exit fullscreen mode

AWS Service

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

S3


python
        my_config = Config(
            use_dualstack_endpoint=True,
            retries={'max_attempts': 3, 'mode': 'standard'}
        )

        bucket_name = ''
        try:
            s3_resource = boto3.resource("s3",config=my_config)
            buckets = list(s3_resource.buckets.all())
            for bucket in buckets:
                bucket_name = bucket.name
                logger.info(bucket_name)

        except ClientError:
            logger.exception("Couldn't get buckets.")


Enter fullscreen mode Exit fullscreen mode

S3 Bucket Get Success

DynamoDB



try:
            client = boto3.client('dynamodb',config=my_config)
            response = client.get_item(
                TableName='testTable',
                Key={
                    'id': {
                        'S': '1'
                    },
                }
            )
            print(response['Item'])


Enter fullscreen mode Exit fullscreen mode

S3 Vectors


python
        try:

            bedrock = boto3.client("bedrock-runtime");
            client = boto3.client('s3vectors',config=my_config)
            response = bedrock.invoke_model(
                modelId="amazon.titan-embed-text-v2:0",
                body=json.dumps({"inputText": 'サンプル'})
            )

            # Extract embedding from response.
            model_response = json.loads(response["body"].read())
            embedding = model_response["embedding"]

            response = client.query_vectors(
                vectorBucketName='my-s3-vector-bucket',
                indexName='my-s3-vector-index',
                topK=3,
                queryVector={
                    'float32': embedding
                },
                returnMetadata=True,
                returnDistance=True
            )
            print(json.dumps(response["vectors"], indent=2))
        except ClientError:
            logger.exception("")


Enter fullscreen mode Exit fullscreen mode

Bedrock Runtime

dualstack endpoint no support

ECS

ECS Fargate prioritizes IPv4 by default, so it attempts to connect via IPv4 even in IPv6 dual-stack environments.


bash
ResourceInitializationError: unable to pull secrets or registry auth: The task 
cannot pull registry auth from Amazon ECR: There is a connection issue between the 
task and Amazon ECR. Check your task network configuration. operation error ECR: 
GetAuthorizationToken, exceeded maximum number of attempts, 3, https response error 
StatusCode: 0, RequestID: , request send failed, Post 
"https://api.ecr.us-west-2.amazonaws.com/": dial tcp 34.223.26.183:443: i/o timeout


Enter fullscreen mode Exit fullscreen mode

https://github.com/aws/containers-roadmap/issues/2641

EKS Kubernetes

The ECR dualstack endpoint exists, so images can be pulled via the Egress out internet Gateway.

Initially, there was an issue where pulling was not possible.

Kubernetes supports IPv4/IPv6 dualstack, but EKS can only be configured as an IPv6-only cluster.

https://docs.aws.amazon.com/eks/latest/best-practices/ipv6.html

Summary

More services are becoming IPv6-capable. However, since some services still cannot use it, the current approach is to use VPC Endpoints alongside them.

Top comments (0)