👋

NAT Gateway極力使わず、IPv6でEgress only internet gatewayのみの構成で実現したい

に公開

Community Builder 12/13の記事になります

IPv4環境から外部に通信する場合、NAT Gatewayを介して通信するのが一般的かと思われる。NAT Gatewayはトラフィックに応じて課金が発生することから、IPv6も利用できるところは使いたい。
Interface型VPCEndpointも散り積もればバカにならないコストになります

IPv6で扱えるようにIPv4、IPv6 dualstack VPC環境を作成

  1. IPv6 Amazon Provied
  2. Egress only internet gateway
  3. VPCEndpoint (Gateway/Interface)未設定

VPCv2 Constructsで作成

vpc 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);

    }
}

LambdaでIPv6疎通確認

Allow IPv6 traffic for dual-stack subnetsを有効にした、VPC Lambdaで確認
ifconfig.meからLambdaからアクセスする送信元IPv6アドレスを取得する簡単なFunctionsになります。

import json
import urllib.request

import json
import urllib.request


def lambda_handler(event, context):
    """
    http://ifconfig.me/ にリクエストして、パブリックIPアドレスを取得するLambda関数
    """
    try:

        # ifconfig.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:
        # ネットワークエラーの処理
        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)
            })
        }


# ローカルテスト用
if __name__ == '__main__':
    # テストイベントとコンテキスト
    test_event = {}
    test_context = {}

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

AWSサービス

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

S3

        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.")

S3 Bucket取得成功

DynamoDB

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

S3 Vectors

        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("")

Bedrock Runtime

dualstack endpoint未対応。

ECS

ECS FargateはデフォルトでIPv4を優先するため、IPv6
dual-stack環境でもIPv4で接続を試みています。

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

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

EKS Kubernetes環境

ECR dualstack endpointは存在するので、Egress out internet Gateway経由でイメージがプルできる。

最初pullできない事象があった。

KubernetesはIPv4/IPv6 dualstackに対応しているが、EKSはIPv6 only clusterしか設定できない

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

まとめ

IPv6を使えるサービスが多くなってきる。ただ、まだ使えないサービスもあるので、VPC Endpointと併用していくのが現状と思います

Discussion