Dual-stack IPv6 networking for Amazon ECS and AWS Fargate

Nathan Peck profile picture
Nathan Peck
Senior Developer Advocate at AWS

Terminology

Amazon Elastic Container Service (Amazon ECS) is a serverless orchestrator that manages container deployments on your behalf. As an orchestrator it not only launches application containers for you, but also configures various connectivity aspects, including networking, load balancer attachments, and other AWS integrations.

IPv4 is the most widely adopted Internet Protocol. It provides address space for up to 4,294,967,296 devices on the internet, however large portions of the address space are reserved and therefore not usable. As a result, there are not enough IP addresses available to give every device in the world it's own unique IPv4 address. This necessitates the use of more complex networking setups such as Network Address Translation (NAT) gateways that allow multiple devices to share a single public IPv4 address that is used for internet communications.

IPv6 is the most recent Internet Protocol, with approximately 3.4×1038 available addresses. This protocol will enable greatly simplified internet networking. Unfortunately IPv6 rollout is still only partially completed. This means that not every internet user can actually use IPv6 yet.

A dual-stack deployment is a deployment in which your networked cloud resources have both IPv4 addresses and IPv6 addresses. This transitional networking approach allows you to make use of both IPv4 and IPv6 networking.

This pattern will demonstrate how to setup a dual-stack deployment using Amazon ECS, and deploy a sample application that verifies that you can use an IPv6 egress only gateway to make requests to AWS services that have dual-stack endpoints that support IPv6.

Why?

IPv6 rollout is an ongoing project. At this time many internet service providers do not yet support IPv6. Therefore your architecture must still support IPv4 for many of your own users. Additionally, at this time in order to use Amazon ECS and many other AWS services you will still need to make use of IPv4 for some resources. You can find a list of AWS services that support IPv6, in the official AWS documentation.

Despite limited support for IPv6 you will likely want to begin testing IPv6 support for the AWS services that do have IPv6 support. A dual-stack deployment allows you to have the best of both worlds: IPv4 and IPv6.

WARNING

If you are seeking an IPv6 only deployment for Amazon ECS, this is not possible at this time. At this time Amazon ECS still has dependencies on IPv4 only resources, and therefore has only partial support for IPv6. It is not yet possible to completely avoid IPv4 usage. Further updates will be made to this pattern as additional IPv6 support is released.

Architecture

The following diagram depicts what will be created when you deploy this pattern:

Public subnetVPCAWS FargateContainerAWS PrivateLinkInternet gatewayEgress only gatewayDual-stack task:IPv4: 10.0.249.99IPv6: 2600:1f16:545:2755:c818:4f5c:f8d3:d3afApplication Load BalancerTarget Group: 10.0.249.99IPv6 out from taskIPv4 and IPv6 in from internetIPv6 out to internetIPv4 into taskIPv6 to AWSdual-stack endpointsIPv4 to AWS non dual-stackendpointsAWS ServicesAmazon Elastic Container Service (Amazon ECS)Amazon Simple Storage Service (Amazon S3)Amazon Elastic Container Registry (Amazon ECR)Amazon Elastic Compute Cloud (Amazon EC2)Amazon Simple Storage Service (Amazon S3)IPv4 and IPv6 in to load balancerIPv4 out from task

  • An ECS task is deployed into AWS Fargate capacity, in a VPC subnet that is dual-stack enabled. As a result the task and it's container are reachable via an IPv4 address as well as an IPv6 address.
  • Ingress from the internet is via an Application Load Balancer that is configured for dual-stack mode. As a result both IPv4 and IPv6 clients can talk to the dual stack endpoint for the load balancer.
  • Due to current Amazon ECS limitations, only the task's IPv4 address is registered into the ALB target group. Therefore, traffic from the ALB to the task is always over IPv4.
  • Due to current Amazon ECS and AWS Fargate limitations, several supporting dependencies such as Amazon Elastic Container Registry, Amazon S3 (for container image layers), and Amazon ECS, are accessed over IPv4, via AWS PrivateLink endpoints.
  • The application is able to use it's IPv6 support, to make request to the public internet and to dual-stack AWS services via an egress only gateway. As a verification, the application uses the Amazon EC2 dual-stack API endpoint, and the Amazon S3 dual-stack API endpoint.

Dependencies

This pattern requires the following local dependencies:

  • Docker or similar OCI compatible image builder.
  • Amazon ECR Credential Helper - This credential helper makes it easier to automatically authenticate when building and pushing container images to Amazon ECR.
  • AWS SAM CLI for deploying CloudFormation stacks on your AWS account. You should follow the appropriate steps for installing SAM CLI.

Define the Amazon VPC

Download the following ipv6-vpc.yml file which defines a dual-stack VPC with both IPv4 and IPv6 support:

File: ipv6-vpc.ymlLanguage: yml
AWSTemplateFormatVersion: '2010-09-09'
Description: This stack deploys an dual stack VPC designed for IPv6 usage.
Parameters:
  DeployingToEC2:
    Type: String
    Default: false
    AllowedValues:
      - true
      - false
    Description: Set value to "true" in order to create additional ECS endpoints
                 to enable ECS on EC2 usage.

Conditions:
  CreateEcsOnEc2Resources: !Equals [ !Ref "DeployingToEC2", true ]

Mappings:
  # Hard values for the subnet masks. These masks define
  # the range of internal IP addresses that can be assigned.
  # The VPC can have all IP's from 10.0.0.0 to 10.0.255.255
  # There are four subnets which cover the ranges:
  #
  # 10.0.128.0 - 10.0.191.255 (16384 IP addresses)
  # 10.0.192.0 - 10.0.255.0 (16384 IP addresses)
  #
  # This template leaves some unutilized IP address space in the following
  # ranges in case you need to add more subnets in the future:
  #
  # 10.0.0.0 - 10.0.63.255 (16384 IP addresses)
  # 10.0.64.0 - 10.0.127.255 (16384 IP addresses)
  SubnetConfig:
    VPC:
      CIDR: '10.0.0.0/16'
    PublicOneIpv4:
      CIDR: '10.0.128.0/18'
    PublicTwoIpv4:
      CIDR: '10.0.192.0/18'
Resources:
  # VPC in which containers will be networked.
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      EnableDnsSupport: true
      EnableDnsHostnames: true
      CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR']

  # This allocates two blocks of IPv6 capacity from the Amazon owned IP space
  CidrBlockOne:
    Type: AWS::EC2::VPCCidrBlock
    Properties:
      AmazonProvidedIpv6CidrBlock: true
      VpcId: !Ref VPC
  CidrBlockTwo:
    Type: AWS::EC2::VPCCidrBlock
    Properties:
      AmazonProvidedIpv6CidrBlock: true
      VpcId: !Ref VPC

  # Two dual stack subnets where containers can have both IPv4 and IPv6 addresses
  SubnetOne:
    Type: AWS::EC2::Subnet
    DependsOn: CidrBlockOne
    Properties:
      AvailabilityZone:
         Fn::Select:
         - 0
         - Fn::GetAZs: {Ref: 'AWS::Region'}
      VpcId: !Ref VPC
      CidrBlock: !FindInMap ['SubnetConfig', 'PublicOneIpv4', 'CIDR']
      Ipv6CidrBlock: !Select [0, !GetAtt VPC.Ipv6CidrBlocks]
      AssignIpv6AddressOnCreation: true
  SubnetTwo:
    Type: AWS::EC2::Subnet
    DependsOn: CidrBlockTwo
    Properties:
      AvailabilityZone:
         Fn::Select:
         - 1
         - Fn::GetAZs: {Ref: 'AWS::Region'}
      VpcId: !Ref VPC
      CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwoIpv4', 'CIDR']
      Ipv6CidrBlock: !Select [1, !GetAtt VPC.Ipv6CidrBlocks]
      AssignIpv6AddressOnCreation: true

  # Egress only gateway for IPv6
  EgressOnlyGateway:
    Type: AWS::EC2::EgressOnlyInternetGateway
    Properties:
      VpcId: !Ref VPC

  # Internet gateway, allows inbound and outbound for IPv4
  InternetGateway:
    Type: AWS::EC2::InternetGateway
  GatewayAttachement:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref 'VPC'
      InternetGatewayId: !Ref 'InternetGateway'

  # The route table describes how resources in the VPC will be able to reach
  # various internet endpoints or address ranges.
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  IPv6PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationIpv6CidrBlock: '::/0'
      EgressOnlyInternetGatewayId: !Ref EgressOnlyGateway
  IPv4PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref InternetGateway
  SubnetOneRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetOne
      RouteTableId: !Ref RouteTable
  SubnetTwoRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetTwo
      RouteTableId: !Ref RouteTable

  # PrivateLink endpoints that enable access over IPv4, for the dualstack deployment
  # Note that we share one security group for all of the PrivateLink endpoints.
  # This is in order to more easily grant ECS managed infrastructure permissions
  # to utilize all of the endpoints.
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Shared security group.
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-shared

  SecurityGroupAccessRule:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: -1
      SourceSecurityGroupId: !Ref SecurityGroup
      GroupId: !Ref SecurityGroup

  # The PrivateLink endpoints that provide access to required AWS services
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Gateway
      RouteTableIds:
        - !Ref RouteTable
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcId: !Ref VPC

  CloudWatchLogsEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref SubnetOne
        - !Ref SubnetTwo
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.logs
      VpcId: !Ref VPC

  SsmEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref SubnetOne
        - !Ref SubnetTwo
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      VpcId: !Ref VPC

  SsmMessagesEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref SubnetOne
        - !Ref SubnetTwo
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      VpcId: !Ref VPC

  EcrApiEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref SubnetOne
        - !Ref SubnetTwo
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.api
      VpcId: !Ref VPC

  EcrDkrEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref SubnetOne
        - !Ref SubnetTwo
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.dkr
      VpcId: !Ref VPC

  SecretsManagerEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref SubnetOne
        - !Ref SubnetTwo
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.secretsmanager
      VpcId: !Ref VPC

  # The following endpoints with the Condition: CreateEcsOnEc2Resources
  # are not necessary for ECS on AWS Fargate, but are needed for
  # ECS on EC2
  EcsAgentEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Condition: CreateEcsOnEc2Resources
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref SubnetOne
        - !Ref SubnetTwo
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecs-agent
      VpcId: !Ref VPC

  EcsTelemetryEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Condition: CreateEcsOnEc2Resources
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref SubnetOne
        - !Ref SubnetTwo
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecs-telemetry
      VpcId: !Ref VPC

  EcsEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Condition: CreateEcsOnEc2Resources
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref SubnetOne
        - !Ref SubnetTwo
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecs
      VpcId: !Ref VPC

Outputs:
  VpcId:
    Description: The ID of the VPC that this stack is deployed in
    Value: !Ref VPC
  SubnetIds:
    Description: Comma seperated list of subnets with IPv6 egress gateway based internet access
    Value: !Sub '${SubnetOne},${SubnetTwo}'
  PrivateLinkEndpointSecurityGroup:
    Description: The shared security group for all of the PrivateLink
                 endpoints. The ECS services and/or EC2 instances that host
                 those services must have permission to talk to this security group
    Value: !Ref SecurityGroup

Things to note in this template:

  • AWS::EC2::VPCCidrBlock - We request two blocks of Amazon provided IPv6 address space
  • AWS::EC2::Subnet - VPC subnets are configured to use the IPv6 blocks, and to assign IPv6 addresses to network interfaces in the subnet.
  • AWS::EC2::InternetGateway - The internet gateway provides both inbound and outbound access for IPv4 based internet communications
  • AWS::EC2::EgressOnlyInternetGateway - The egress only gateway serves a similar role compared to a NAT Gateway in a traditional IPv4 deployment. It allows resources in a private VPC subnet to communicate to the internet over IPv6.
  • IPv6PublicRoute - This route sends IPv6 traffic out through the egress only internet gateway.
  • IPv4PublicRoute - This route allows resources that have a public IPv4 address to communicate with the internet via the internet gateway.
  • AWS::EC2::VPCEndpoint - Because we intend to avoid using public IPv4 addresses as much as possible, these VPC endpoints grant workloads hosted in the VPC the ability to communicate with AWS services privately.

Define the ECS Cluster

Download the following cluster.yml file which defines the Amazon ECS cluster that will run container tasks.

File: cluster.ymlLanguage: yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Empty ECS cluster that has no EC2 instances. It is designed
             to be used with AWS Fargate serverless capacity

Resources:

  # Cluster that keeps track of container deployments
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterSettings:
        - Name: containerInsights
          Value: enabled

  # This is a role which is used within Fargate to allow the Fargate agent
  # to download images, and upload logs.
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ecs-tasks.amazonaws.com]
            Action: ['sts:AssumeRole']
            Condition:
              ArnLike:
                aws:SourceArn: !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
      Path: /

      # This role enables basic features of ECS. See reference:
      # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/security-iam-awsmanpol.html#security-iam-awsmanpol-AmazonECSTaskExecutionRolePolicy
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

Outputs:
  ClusterName:
    Description: The ECS cluster into which to launch resources
    Value: !Ref ECSCluster
  ECSTaskExecutionRole:
    Description: The role used to start up a task
    Value: !Ref ECSTaskExecutionRole

Define the ECS Service

Download the following service.yml file which defines a container deployment in AWS Fargate, orchestrated by Amazon ECS:

File: service.ymlLanguage: yml
AWSTemplateFormatVersion: '2010-09-09'
Description: An example service that deploys in AWS VPC networking mode
             on AWS Fargate. Service runs with networking in public
             subnets and public IP addresses

Parameters:
  VpcId:
    Type: String
    Description: The VPC that the service is running inside of
  SubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: List of public subnet ID's to put the load balancer and tasks in
  ClusterName:
    Type: String
    Description: The name of the ECS cluster into which to launch capacity.
  ECSTaskExecutionRole:
    Type: String
    Description: The role used to start up an ECS task
  ServiceName:
    Type: String
    Default: web
    Description: A name for the service
  ImageUri:
    Type: String
    Description: The url of a container image that contains the application process
  ContainerCpu:
    Type: Number
    Default: 256
    Description: How much CPU to give the container. 1024 is 1 CPU
  ContainerMemory:
    Type: Number
    Default: 512
    Description: How much memory in megabytes to give the container
  ContainerPort:
    Type: Number
    Default: 3000
    Description: What port that the application expects traffic on
  DesiredCount:
    Type: Number
    Default: 1
    Description: How many copies of the service task to run
  PrivateLinkEndpointSecurityGroup:
    Type: String
    Description: The security group on the PrivateLink endpoints. It must accept traffic from the service's SG.

Resources:

  # This a role used by the application itself
  ApplicationRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: [ecs-tasks.amazonaws.com]
            Action: ['sts:AssumeRole']
            Condition:
              ArnLike:
                aws:SourceArn: !Sub arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:*
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
      Path: /

      ManagedPolicyArns:
        # This managed role allows the application to read Amazon S3
        # See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/security-iam-awsmanpol.html#security-iam-awsmanpol-amazons3readonlyaccess
        - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

        # This managed role allows the application to read Amazon EC2
        # See: https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonEC2ReadOnlyAccess.html
        - arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess

  # The task definition. This is a simple metadata description of what
  # container to run, and what resource requirements it has.
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Ref ServiceName
      Cpu: !Ref ContainerCpu
      Memory: !Ref ContainerMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      TaskRoleArn: !Ref ApplicationRole
      ExecutionRoleArn: !Ref ECSTaskExecutionRole
      ContainerDefinitions:
        - Name: !Ref ServiceName
          Cpu: !Ref ContainerCpu
          Memory: !Ref ContainerMemory
          Image: !Ref ImageUri
          PortMappings:
            - ContainerPort: !Ref ContainerPort
              HostPort: !Ref ContainerPort
          Environment:
            - Name: PORT
              Value: !Ref ContainerPort
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              mode: non-blocking
              max-buffer-size: 25m
              awslogs-group: !Ref LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Ref ServiceName

  # The service. The service is a resource which allows you to run multiple
  # copies of a type of task, and gather up their logs and metrics, as well
  # as monitor the number of running tasks and replace any that have crashed
  Service:
    Type: AWS::ECS::Service
    # Avoid race condition between ECS service creation and associating
    # the target group with the LB
    DependsOn: PublicLoadBalancerListener
    Properties:
      ServiceName: !Ref ServiceName
      Cluster: !Ref ClusterName
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups:
            - !Ref ServiceSecurityGroup
          Subnets: !Ref SubnetIds
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 75
      DesiredCount: !Ref DesiredCount
      TaskDefinition: !Ref TaskDefinition
      LoadBalancers:
        - ContainerName: !Ref ServiceName
          ContainerPort: !Ref ContainerPort
          TargetGroupArn: !Ref ServiceTargetGroup

  # Security group that limits network access
  # to the task
  ServiceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for service
      VpcId: !Ref VpcId

  # Keeps track of the list of tasks for the service
  ServiceTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 6
      HealthCheckPath: /
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      TargetType: ip
      Port: !Ref ContainerPort
      Protocol: HTTP
      UnhealthyThresholdCount: 10
      VpcId: !Ref VpcId
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 0

  # A public facing load balancer, this is used as ingress for
  # public facing internet traffic.
  PublicLoadBalancerSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Access to the public facing load balancer
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        # Allow access to public facing ALB from any IP address
        - CidrIp: 0.0.0.0/0
          IpProtocol: -1
  PublicLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      IpAddressType: dualstack
      LoadBalancerAttributes:
      - Key: idle_timeout.timeout_seconds
        Value: '30'
      Subnets: !Ref SubnetIds
      SecurityGroups:
        - !Ref PublicLoadBalancerSG
  PublicLoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: 'forward'
          ForwardConfig:
            TargetGroups:
              - TargetGroupArn: !Ref ServiceTargetGroup
                Weight: 100
      LoadBalancerArn: !Ref 'PublicLoadBalancer'
      Port: 80
      Protocol: HTTP

  # Open up the service's security group to traffic originating
  # from the security group of the load balancer.
  ServiceIngressfromLoadBalancer:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: Ingress from the public ALB
      GroupId: !Ref ServiceSecurityGroup
      IpProtocol: -1
      SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG'

  # Open up the PrivateLink endpoints to accepting inbound traffic
  # from the service deploying in AWS Fargate.
  PrivateLinkIngressFromService:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: Ingress from the services deployed in AWS Fargate
      GroupId: !Ref PrivateLinkEndpointSecurityGroup
      IpProtocol: -1
      SourceSecurityGroupId: !Ref ServiceSecurityGroup

  # This log group stores the stdout logs from this service's containers
  LogGroup:
    Type: AWS::Logs::LogGroup

Outputs:
  PublicURI:
    Description: The public URI of the service on the internet
    Value: !GetAtt PublicLoadBalancer.DNSName

A few things to note in this template:

  • The AWS::ECS::Service has the setting AssignPublicIp: DISABLED. This disables the ability for the task to communicate directly via the internet gateway. All communications from this service will be over IPv6, or via an AWS PrivateLink endpoint.
  • PrivateLinkIngressFromService - This security group ingress rule is what allows the deployed container service to make use of the PrivateLink endpoints.

Build and push the test image

In order to test dual-stack, this pattern provides a small test application that uses the AWS SDK to make API calls to two different dual-stack service endpoints: S3 and EC2.

  • app/index.js
  • app/package.json
  • app/Dockerfile
File: index.jsLanguage: js
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";
import { EC2Client, DescribeInstancesCommand } from "@aws-sdk/client-ec2";

import express from 'express';
const app = express()
const PORT = process.env.PORT || 3000;

// Use the dual stack endpoint for S3 to verify we can make API calls over IPv6
const s3 = new S3Client({
  useDualstackEndpoint: true
});

// Use the dual stack endpoint for EC2 to verify IPv6 egress gateway is working
const ec2 = new EC2Client({
  useDualstackEndpoint: true
});

app.get('/list-buckets', async function (req, res) {
  const command = new ListBucketsCommand({});
  const response = await s3.send(command);
  res.send(response.Buckets)
})

app.get('/list-ec2', async function (req, res) {
  const command = new DescribeInstancesCommand({});
  const response = await ec2.send(command);
  res.send(response)
})

app.get('/', async function (req, res) {
  res.send('OK')
})

app.listen(PORT)

console.log(`Listening on http://localhost:${PORT}`);

TIP

The sample application toggles useDualstackEndpoint: true when defining the AWS service client. This is what enables you to use IPv6 AWS endpoints from inside of a dual-stack VPC.

Download all three files and place them into a folder named app. The folder structure should look like this:

  • app - Folder containing the sample application
    • app/index.js - The sample application code
    • app/package.json - Defines dependencies for the sample application
    • app/Dockerfile - Defines how to build the sample application

Now build and push the sample application to a private Amazon ECR container registry using the following command:

Language: sh
REPO_URI=$(aws ecr create-repository --repository-name sample-app-repo --query 'repository.repositoryUri' --output text)
if [ -z "${REPO_URI}" ]; then
  REPO_URI=$(aws ecr describe-repositories --repository-names sample-app-repo --query 'repositories[0].repositoryUri' --output text)
fi
docker build -t ${REPO_URI}:ipv6-app ./app
docker push ${REPO_URI}:ipv6-app

Deploy everything

Now download the following parent.yml file that deploy the previous three templates:

File: parent.ymlLanguage: yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Parent stack that deploys VPC, Amazon ECS cluster for AWS Fargate,
             and a serverless Amazon ECS service deployment that hosts
             the task containers on AWS Fargate
Parameters:
  ImageUri:
    Type: String
    Description: The URI of the private container image to deploy

Resources:

  # The networking configuration. This creates an isolated
  # network specific to this particular environment
  VpcStack:
    Type: AWS::Serverless::Application
    Properties:
      Location: ipv6-vpc.yml

  # This stack contains the Amazon ECS cluster itself
  ClusterStack:
    Type: AWS::Serverless::Application
    Properties:
      Location: cluster.yml

  # This stack contains the container deployment
  ServiceStack:
    Type: AWS::Serverless::Application
    Properties:
      Location: service.yml
      Parameters:
        ImageUri: !Ref ImageUri
        VpcId: !GetAtt VpcStack.Outputs.VpcId
        SubnetIds: !GetAtt VpcStack.Outputs.SubnetIds
        ClusterName: !GetAtt ClusterStack.Outputs.ClusterName
        ECSTaskExecutionRole: !GetAtt ClusterStack.Outputs.ECSTaskExecutionRole
        PrivateLinkEndpointSecurityGroup: !GetAtt VpcStack.Outputs.PrivateLinkEndpointSecurityGroup

Outputs:
  PublicURI:
    Description: Public URI of the service on the internet
    Value: !GetAtt ServiceStack.Outputs.PublicURI

Your overall folder structure should look like:

  • parent.yml - Top level index file that defines the overall application deployment
  • ipv6-vpc.yml - Defines the dual-stack networking configuration
  • cluster.yml - Standard boilerplate for an Amazon ECS cluster
  • service.yml - The container deployment itself
  • app - Folder that holds the sample application code
    • app/index.js - The sample application code
    • app/package.json - Defines dependencies for the sample application
    • app/Dockerfile - Defines how to build the sample application

Now you can use the following command to deploy the application:

Language: sh
sam deploy \
  --template-file parent.yml \
  --stack-name ipv6-environment \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides ImageUri=${REPO_URI}:ipv6-app \
  --resolve-s3

Test it Out

First let's make sure that the load balancer has a dual-stack endpoint that supports both IPv4 and IPv6:

Language: sh
PUBLIC_URI=$(aws cloudformation describe-stacks --stack-name ipv6-environment --query "Stacks[0].Outputs[?OutputKey=='PublicURI'].OutputValue" --output text)

# Lookup the IPv4 address of the service
dig A $PUBLIC_URI

# Lookup the IPv6 address of the service
dig AAAA $PUBLIC_URI

Now test two different endpoints in the service. Both endpoints internally make use of IPv6 via the egress only gateway:

Language: sh
# Verify that the application is able to communiate to the S3 dual-stack endpoint
curl $PUBLIC_URI/list-buckets

# Verify that the application is able to communciate to the EC2 dual-stack endpoint
curl $PUBLIC_URI/list-ec2

Tear it Down

You can destroy your test deployment using the following commands:

Language: sh
# Delete the Amazon ECS deployment
sam delete --stack-name ipv6-environment --no-prompts

# Empty and delete the Amazon ECR container registry we created
aws ecr delete-repository --repository-name sample-app-repo --force

Alternative Patterns

Not quite right for you? Try another way to do this:

Infrastructure Pattern  Amazon ECS cluster with isolated VPC and no NAT Gateway

A completely isolated VPC network, with no access to the internet.

Infrastructure Pattern  Large sized AWS VPC for an Amazon ECS cluster

A VPC that provides access to the internet via AWS managed NAT Gateway.