AWSTemplateFormatVersion: '2010-09-09' Description: A stack for deploying containerized applications onto a cluster of EC2 hosts using Elastic Container Service. This stack runs containers on hosts that are in a private VPC subnet. Outbound network traffic from the hosts must go out through a NAT gateway. There are two load balancers, one inside the public subnet, which can be used to send traffic to the containers in the private subnet, and one in the private subnet, which can be used for private internal traffic between internal services. Parameters: DesiredCapacity: Type: Number Default: '3' Description: Number of EC2 instances to launch in your ECS cluster. MaxSize: Type: Number Default: '6' Description: Maximum number of EC2 instances that can be launched in your ECS cluster. ECSAMI: Description: AMI ID Type: AWS::SSM::Parameter::Value Default: /aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id InstanceType: Description: EC2 instance type Type: String Default: c4.xlarge AllowedValues: [t2.micro, t2.small, t2.medium, t2.large, m3.medium, m3.large, m3.xlarge, m3.2xlarge, m4.large, m4.xlarge, m4.2xlarge, m4.4xlarge, m4.10xlarge, c4.large, c4.xlarge, c4.2xlarge, c4.4xlarge, c4.8xlarge, c3.large, c3.xlarge, c3.2xlarge, c3.4xlarge, c3.8xlarge, r3.large, r3.xlarge, r3.2xlarge, r3.4xlarge, r3.8xlarge, i2.xlarge, i2.2xlarge, i2.4xlarge, i2.8xlarge] ConstraintDescription: Please choose a valid instance type. 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.0.0 - 10.0.0.255 # 10.0.1.0 - 10.0.1.255 # 10.0.2.0 - 10.0.2.255 # 10.0.3.0 - 10.0.3.255 # # If you need more IP addresses (perhaps you have so many # instances that you run out) then you can customize these # ranges to add more SubnetConfig: VPC: CIDR: '10.0.0.0/16' PublicOne: CIDR: '10.0.0.0/24' PublicTwo: CIDR: '10.0.1.0/24' PrivateOne: CIDR: '10.0.2.0/24' PrivateTwo: CIDR: '10.0.3.0/24' Resources: # VPC in which containers will be networked. # It has two public subnets, and two private subnets. # We distribute the subnets across the first two available subnets # for the region, for high availability. VPC: Type: AWS::EC2::VPC Properties: EnableDnsSupport: true EnableDnsHostnames: true CidrBlock: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] # Two public subnets, where containers can have public IP addresses PublicSubnetOne: Type: AWS::EC2::Subnet Properties: AvailabilityZone: Fn::Select: - 0 - Fn::GetAZs: {Ref: 'AWS::Region'} VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] MapPublicIpOnLaunch: true PublicSubnetTwo: Type: AWS::EC2::Subnet Properties: AvailabilityZone: Fn::Select: - 1 - Fn::GetAZs: {Ref: 'AWS::Region'} VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PublicTwo', 'CIDR'] MapPublicIpOnLaunch: true # Two private subnets where containers will only have private # IP addresses, and will only be reachable by other members of the # VPC PrivateSubnetOne: Type: AWS::EC2::Subnet Properties: AvailabilityZone: Fn::Select: - 0 - Fn::GetAZs: {Ref: 'AWS::Region'} VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PrivateOne', 'CIDR'] PrivateSubnetTwo: Type: AWS::EC2::Subnet Properties: AvailabilityZone: Fn::Select: - 1 - Fn::GetAZs: {Ref: 'AWS::Region'} VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PrivateTwo', 'CIDR'] # Setup networking resources for the public subnets. Containers # in the public subnets have public IP addresses and the routing table # sends network traffic via the internet gateway. InternetGateway: Type: AWS::EC2::InternetGateway GatewayAttachement: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref 'VPC' InternetGatewayId: !Ref 'InternetGateway' PublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref 'VPC' PublicRoute: Type: AWS::EC2::Route DependsOn: GatewayAttachement Properties: RouteTableId: !Ref 'PublicRouteTable' DestinationCidrBlock: '0.0.0.0/0' GatewayId: !Ref 'InternetGateway' PublicSubnetOneRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnetOne RouteTableId: !Ref PublicRouteTable PublicSubnetTwoRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnetTwo RouteTableId: !Ref PublicRouteTable # Setup networking resources for the private subnets. Containers # in these subnets have only private IP addresses, and must use a NAT # gateway to talk to the internet. We launch two NAT gateways, one for # each private subnet. NatGatewayOneAttachment: Type: AWS::EC2::EIP DependsOn: GatewayAttachement Properties: Domain: vpc NatGatewayTwoAttachment: Type: AWS::EC2::EIP DependsOn: GatewayAttachement Properties: Domain: vpc NatGatewayOne: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayOneAttachment.AllocationId SubnetId: !Ref PublicSubnetOne NatGatewayTwo: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayTwoAttachment.AllocationId SubnetId: !Ref PublicSubnetTwo PrivateRouteTableOne: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref 'VPC' PrivateRouteOne: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTableOne DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGatewayOne PrivateRouteTableOneAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTableOne SubnetId: !Ref PrivateSubnetOne PrivateRouteTableTwo: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref 'VPC' PrivateRouteTwo: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTableTwo DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGatewayTwo PrivateRouteTableTwoAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PrivateRouteTableTwo SubnetId: !Ref PrivateSubnetTwo # OPTIONAL: VPC Endpoint for DynamoDB # If a container needs to access DynamoDB this allows a container in the private subnet # to talk to DynamoDB directly without needing to go via the NAT gateway. This reduces # the amount of bandwidth through the gateway, meaning that the gateway is free to serve # your other traffic. DynamoDBEndpoint: Type: AWS::EC2::VPCEndpoint Properties: PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: "*" Principal: "*" Resource: "*" RouteTableIds: - !Ref 'PrivateRouteTableOne' - !Ref 'PrivateRouteTableTwo' ServiceName: !Join [ "", [ "com.amazonaws.", { "Ref": "AWS::Region" }, ".dynamodb" ] ] VpcId: !Ref 'VPC' # ECS Resources ECSCluster: Type: AWS::ECS::Cluster # A security group for the EC2 hosts that will run the containers. # Two rules, allowing network traffic from a public facing load # balancer and from other hosts in the security group. # # Remove any of the following ingress rules that are not needed. # If you want to make direct requests to a container using its # public IP address you'll need to add a security group rule # to allow traffic from all IP addresses. EcsHostSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Access to the ECS hosts that run containers VpcId: !Ref 'VPC' EcsSecurityGroupIngressFromPublicALB: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Ingress from the public ALB GroupId: !Ref 'EcsHostSecurityGroup' IpProtocol: -1 SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG' EcsSecurityGroupIngressFromPrivateALB: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Ingress from the private ALB GroupId: !Ref 'EcsHostSecurityGroup' IpProtocol: -1 SourceSecurityGroupId: !Ref 'PrivateLoadBalancerSG' EcsSecurityGroupIngressFromSelf: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Ingress from other containers in the same security group GroupId: !Ref 'EcsHostSecurityGroup' IpProtocol: -1 SourceSecurityGroupId: !Ref 'EcsHostSecurityGroup' # Autoscaling group. This launches the actual EC2 instances that will register # themselves as members of the cluster, and run the docker containers. ECSAutoScalingGroup: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: - !Ref PrivateSubnetOne - !Ref PrivateSubnetTwo LaunchConfigurationName: !Ref 'ContainerInstances' MinSize: '1' MaxSize: !Ref 'MaxSize' DesiredCapacity: !Ref 'DesiredCapacity' CreationPolicy: ResourceSignal: Timeout: PT15M UpdatePolicy: AutoScalingReplacingUpdate: WillReplace: 'true' ContainerInstances: Type: AWS::AutoScaling::LaunchConfiguration Properties: ImageId: !Ref 'ECSAMI' SecurityGroups: [!Ref 'EcsHostSecurityGroup'] InstanceType: !Ref 'InstanceType' IamInstanceProfile: !Ref 'EC2InstanceProfile' UserData: Fn::Base64: !Sub | #!/bin/bash -xe echo ECS_CLUSTER=${ECSCluster} >> /etc/ecs/ecs.config yum install -y aws-cfn-bootstrap curl /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource ECSAutoScalingGroup --region ${AWS::Region} AutoscalingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: [application-autoscaling.amazonaws.com] Action: ['sts:AssumeRole'] Path: / Policies: - PolicyName: service-autoscaling PolicyDocument: Statement: - Effect: Allow Action: - 'application-autoscaling:*' - 'cloudwatch:DescribeAlarms' - 'cloudwatch:PutMetricAlarm' - 'ecs:DescribeServices' - 'ecs:UpdateService' Resource: '*' EC2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: [!Ref 'EC2Role'] # Role for the EC2 hosts. This allows the ECS agent on the EC2 hosts # to communciate with the ECS control plane, as well as download the docker # images from ECR to run on your host. EC2Role: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: [ec2.amazonaws.com] Action: ['sts:AssumeRole'] Path: / Policies: - PolicyName: ecs-service PolicyDocument: Statement: - Effect: Allow Action: - 'ecs:CreateCluster' - 'ecs:DeregisterContainerInstance' - 'ecs:DiscoverPollEndpoint' - 'ecs:Poll' - 'ecs:RegisterContainerInstance' - 'ecs:StartTelemetrySession' - 'ecs:Submit*' - 'logs:CreateLogStream' - 'logs:PutLogEvents' - 'ecr:GetAuthorizationToken' - 'ecr:BatchGetImage' - 'ecr:GetDownloadUrlForLayer' Resource: '*' # Load balancers for getting traffic to containers. # This sample template creates two load balancers: # # - One public load balancer, hosted in public subnets that is accessible # to the public, and is intended to route traffic to one or more public # facing services. # - One private load balancer, hosted in private subnets, that only # accepts traffic from other containers in the cluster, and is # intended for private services that should not be accessed directly # by the public. # A public facing load balancer, this is used for accepting traffic from the public # internet and directing it to public facing microservices PublicLoadBalancerSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Access to the public facing load balancer VpcId: !Ref 'VPC' SecurityGroupIngress: # Allow access to ALB from anywhere on the internet - CidrIp: 0.0.0.0/0 IpProtocol: -1 PublicLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internet-facing LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: '30' Subnets: # The load balancer is placed into the public subnets, so that traffic # from the internet can reach the load balancer directly via the internet gateway - !Ref PublicSubnetOne - !Ref PublicSubnetTwo SecurityGroups: [!Ref 'PublicLoadBalancerSG'] # A dummy target group is used to setup the ALB to just drop traffic # initially, before any real service target groups have been added. DummyTargetGroupPublic: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckIntervalSeconds: 6 HealthCheckPath: / HealthCheckProtocol: HTTP HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 Name: !Join ['-', [!Ref 'AWS::StackName', 'drop-1']] Port: 80 Protocol: HTTP UnhealthyThresholdCount: 2 VpcId: !Ref 'VPC' PublicLoadBalancerListener: Type: AWS::ElasticLoadBalancingV2::Listener DependsOn: - PublicLoadBalancer Properties: DefaultActions: - TargetGroupArn: !Ref 'DummyTargetGroupPublic' Type: 'forward' LoadBalancerArn: !Ref 'PublicLoadBalancer' Port: 80 Protocol: HTTP # An internal load balancer, this would be used for a service that is not # directly accessible to the public, but instead should only receive traffic # from your other services. PrivateLoadBalancerSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Access to the internal load balancer VpcId: !Ref 'VPC' PrivateLoadBalancerIngressFromECS: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Only accept traffic from a container in the container host security group GroupId: !Ref 'PrivateLoadBalancerSG' IpProtocol: -1 SourceSecurityGroupId: !Ref 'EcsHostSecurityGroup' PrivateLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internal LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: '30' Subnets: # This load balancer is put into the private subnet, so that there is no # route for the public to even be able to access the private load balancer. - !Ref PrivateSubnetOne - !Ref PrivateSubnetTwo SecurityGroups: [!Ref 'PrivateLoadBalancerSG'] # This dummy target group is used to setup the ALB to just drop traffic # initially, before any real service target groups have been added. DummyTargetGroupPrivate: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckIntervalSeconds: 6 HealthCheckPath: / HealthCheckProtocol: HTTP HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 Name: !Join ['-', [!Ref 'AWS::StackName', 'drop-2']] Port: 80 Protocol: HTTP UnhealthyThresholdCount: 2 VpcId: !Ref 'VPC' PrivateLoadBalancerListener: Type: AWS::ElasticLoadBalancingV2::Listener DependsOn: - PrivateLoadBalancer Properties: DefaultActions: - TargetGroupArn: !Ref 'DummyTargetGroupPrivate' Type: 'forward' LoadBalancerArn: !Ref 'PrivateLoadBalancer' Port: 80 Protocol: HTTP # This is an IAM role which authorizes ECS to manage resources on your # account on your behalf, such as updating your load balancer with the # details of where your containers are, so that traffic can reach your # containers. ECSRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: [ecs.amazonaws.com] Action: ['sts:AssumeRole'] Path: / Policies: - PolicyName: ecs-service PolicyDocument: Statement: - Effect: Allow Action: # Rules which allow ECS to attach network interfaces to instances # on your behalf in order for awsvpc networking mode to work right - 'ec2:AttachNetworkInterface' - 'ec2:CreateNetworkInterface' - 'ec2:CreateNetworkInterfacePermission' - 'ec2:DeleteNetworkInterface' - 'ec2:DeleteNetworkInterfacePermission' - 'ec2:Describe*' - 'ec2:DetachNetworkInterface' # Rules which allow ECS to update load balancers on your behalf # with the information sabout how to send traffic to your containers - 'elasticloadbalancing:DeregisterInstancesFromLoadBalancer' - 'elasticloadbalancing:DeregisterTargets' - 'elasticloadbalancing:Describe*' - 'elasticloadbalancing:RegisterInstancesWithLoadBalancer' - 'elasticloadbalancing:RegisterTargets' Resource: '*' # These are the values output by the CloudFormation template. Be careful # about changing any of them, because of them are exported with specific # names so that the other task related CF templates can use them. Outputs: ClusterName: Description: The name of the ECS cluster Value: !Ref 'ECSCluster' Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ClusterName' ] ] InternalUrl: Description: The url of the internal load balancer Value: !Join ['', ['http://', !GetAtt 'PrivateLoadBalancer.DNSName']] Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'InternalUrl' ] ] ExternalUrl: Description: The url of the external load balancer Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName']] Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ExternalUrl' ] ] ECSRole: Description: The ARN of the ECS role Value: !GetAtt 'ECSRole.Arn' Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ECSRole' ] ] PublicListener: Description: The ARN of the public load balancer's Listener Value: !Ref PublicLoadBalancerListener Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicListener' ] ] PrivateListener: Description: The ARN of the public load balancer's Listener Value: !Ref PrivateLoadBalancerListener Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PrivateListener' ] ] VPCId: Description: The ID of the VPC that this stack is deployed in Value: !Ref 'VPC' Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'VPCId' ] ] PublicSubnetOne: Description: Public subnet one Value: !Ref 'PublicSubnetOne' Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetOne' ] ] PublicSubnetTwo: Description: Public subnet two Value: !Ref 'PublicSubnetTwo' Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PublicSubnetTwo' ] ] PrivateSubnetOne: Description: Private subnet one Value: !Ref 'PrivateSubnetOne' Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PrivateSubnetOne' ] ] PrivateSubnetTwo: Description: Private subnet two Value: !Ref 'PrivateSubnetTwo' Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'PrivateSubnetTwo' ] ] EcsHostSecurityGroup: Description: A security group used to allow containers to receive traffic Value: !Ref 'EcsHostSecurityGroup' Export: Name: !Join [ ':', [ !Ref 'AWS::StackName', 'EcsHostSecurityGroup' ] ]