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

Published: (December 14, 2025 at 04:44 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

VPC Configuration with an Egress‑Only Internet Gateway (IPv6)

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'),
      subnetType: SubnetType.PUBLIC,
      routeTable: publicRouteTable
    });
    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'),
      subnetType: SubnetType.PUBLIC,
      routeTable: publicRouteTable
    });
    const cfnPublicSubnetAZ2 = publicSubnetAZ2.node.defaultChild as CfnSubnet;
    cdk.Tags.of(cfnPublicSubnetAZ2).add('Name', publicSubnetAZ2Name);

    // Private Subnets (Multi‑AZ) with IPv6 egress
    const privateRouteTable = new aws_ec2.RouteTable(this, 'PrivateRouteTable', {
      vpc: myVpc
    });
    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
    });
    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
    });
    const cfnPrivateSubnetAZ2 = privateSubnetAZ2.node.defaultChild as CfnSubnet;
    cdk.Tags.of(cfnPrivateSubnetAZ2).add('Name', privateSubnetAZ2Name);
  }
}

Verifying IPv6 Connectivity with Lambda

The Lambda function below retrieves the source IPv6 address by calling ifconfig.me. Ensure the subnet has “Allow IPv6 traffic for dual‑stack subnets” enabled.

import json
import urllib.request

def lambda_handler(event, context):
    """
    Calls http://ifconfig.me/ to obtain the caller's IPv6 address.
    """
    try:
        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:
        return {
            'statusCode': 500,
            'body': json.dumps({
                'message': 'Error retrieving IP address',
                'error': str(e)
            })
        }

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

# Local test
if __name__ == '__main__':
    result = lambda_handler({}, {})
    print(json.dumps(result, indent=2, ensure_ascii=False))

Sample response

{
  "statusCode": 200,
  "body": "{\"message\": \"Successfully retrieved IP address\", \"ip_address\": \"2600:1f13:94:1a02:ef65:ac4:50ab:5e1\"}"
}

AWS Service Configuration

When using AWS SDKs with IPv6, enable dual‑stack endpoints:

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

For more details, see the AWS documentation.

S3 Example (IPv6‑aware)

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 Exception as e:
    logger.error(f"Error accessing S3: {e}")
Back to Blog

Related posts

Read more »