AWSTemplateFormatVersion: '2010-09-09' Description: "Creates a complete mesh environment including: VPC with 2 AZs, ECS Cluster, Service Discovery Namespace, App Mesh, and mesh virtual gateway / ingress fronted by NLB." Parameters: EnvironmentName: Type: String Description: "A friendly environment name that will be used for namespacing all cluster resources. Example: staging, qa, or production" ClusterLogGroupRetentionInDays: Type: Number Default: 7 EnvoyImage: Type: String Description: Image location for Envoy proxy Default: public.ecr.aws/appmesh/aws-appmesh-envoy:v1.18.3.0-prod 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.100.0/24' PrivateTwo: CIDR: '10.0.101.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'] Tags: - Key: Name Value: !Ref EnvironmentName # Two public subnets, where containers can have public IP addresses PublicSubnetOne: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select - 0 - Fn::GetAZs: !Ref 'AWS::Region' VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PublicOne', 'CIDR'] MapPublicIpOnLaunch: true PublicSubnetTwo: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !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: !Select - 0 - Fn::GetAZs: !Ref 'AWS::Region' VpcId: !Ref 'VPC' CidrBlock: !FindInMap ['SubnetConfig', 'PrivateOne', 'CIDR'] PrivateSubnetTwo: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !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 GatewayAttachment: 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: GatewayAttachment 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: GatewayAttachment Properties: Domain: vpc NatGatewayTwoAttachment: Type: AWS::EC2::EIP DependsOn: GatewayAttachment 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 # ECS Resources ECSCluster: Type: AWS::ECS::Cluster Properties: ClusterName: !Ref EnvironmentName # A security group for the containers we will run in ECS. Allows all traffic within the VPC CIDR range. ContainerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Access to the ECS hosts that run containers VpcId: !Ref VPC SecurityGroupIngress: - CidrIp: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] IpProtocol: -1 TaskExecutionRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: | { "Statement": [{ "Effect": "Allow", "Principal": { "Service": [ "ecs-tasks.amazonaws.com" ]}, "Action": [ "sts:AssumeRole" ] }] } ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess DefaultTaskIamRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: | { "Statement": [{ "Effect": "Allow", "Principal": { "Service": [ "ecs-tasks.amazonaws.com" ]}, "Action": [ "sts:AssumeRole" ] }] } ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchFullAccess - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess - arn:aws:iam::aws:policy/AWSAppMeshPreviewEnvoyAccess - arn:aws:iam::aws:policy/AWSAppMeshEnvoyAccess ### Service discovery namespace ServiceDiscoveryNamespace: Type: AWS::ServiceDiscovery::PrivateDnsNamespace Properties: Name: !Sub ${EnvironmentName}.local Vpc: !Ref VPC ### Log group for the cluster ClusterLogGroup: Type: 'AWS::Logs::LogGroup' Properties: LogGroupName: !Sub "${EnvironmentName}-cluster" RetentionInDays: !Ref ClusterLogGroupRetentionInDays ### Mesh Mesh: Type: AWS::AppMesh::Mesh Properties: MeshName: !Ref EnvironmentName Spec: EgressFilter: Type: DROP_ALL ### Virtual gateway for ingress into mesh VirtualGateway: Type: AWS::AppMesh::VirtualGateway Properties: MeshName: !GetAtt Mesh.MeshName Spec: Listeners: - PortMapping: Port: 9080 Protocol: http VirtualGatewayName: vg-ingress IngressTaskIamRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: | { "Statement": [{ "Effect": "Allow", "Principal": { "Service": [ "ecs-tasks.amazonaws.com" ]}, "Action": [ "sts:AssumeRole" ] }] } Policies: - PolicyName: ACMExportCertificateAccess PolicyDocument: | { "Statement": [{ "Effect": "Allow", "Action": ["acm:ExportCertificate"], "Resource": ["*"] }] } - PolicyName: ACMCertificateAuthorityAccess PolicyDocument: | { "Statement": [{ "Effect": "Allow", "Action": ["acm-pca:GetCertificateAuthorityCertificate"], "Resource": ["*"] }] } ManagedPolicyArns: - arn:aws:iam::aws:policy/CloudWatchFullAccess - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess - arn:aws:iam::aws:policy/AWSAppMeshPreviewEnvoyAccess - arn:aws:iam::aws:policy/AWSAppMeshEnvoyAccess IngressServiceDiscoveryRecord: Type: 'AWS::ServiceDiscovery::Service' Properties: Name: "ingress" DnsConfig: NamespaceId: !Ref ServiceDiscoveryNamespace DnsRecords: - Type: A TTL: 300 HealthCheckCustomConfig: FailureThreshold: 1 IngressTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: RequiresCompatibilities: - 'FARGATE' Family: !Sub '${EnvironmentName}-Ingress' NetworkMode: 'awsvpc' Cpu: 256 Memory: 512 TaskRoleArn: !Ref IngressTaskIamRole ExecutionRoleArn: !Ref TaskExecutionRole ContainerDefinitions: - Name: 'envoy' Image: !Ref EnvoyImage Essential: true StopTimeout: 5 Ulimits: - Name: "nofile" HardLimit: 15000 SoftLimit: 15000 PortMappings: - ContainerPort: 9901 Protocol: 'tcp' - ContainerPort: 9080 Protocol: 'tcp' HealthCheck: Command: - 'CMD-SHELL' - 'curl -s http://localhost:9901/server_info | grep state | grep -q LIVE' Interval: 5 Timeout: 2 Retries: 3 StartPeriod: 60 LogConfiguration: LogDriver: 'awslogs' Options: awslogs-group: !Ref ClusterLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: 'ingress-envoy' Environment: - Name: 'APPMESH_RESOURCE_ARN' Value: !Ref VirtualGateway - Name: 'ENABLE_ENVOY_STATS_TAGS' Value: '1' - Name: 'ENABLE_ENVOY_DOG_STATSD' Value: '1' - Name: 'STATSD_PORT' Value: '8125' - Name: 'cw-agent' Image: 'amazon/cloudwatch-agent:latest' Essential: true StopTimeout: 5 PortMappings: - ContainerPort: 8125 Protocol: 'udp' Environment: - Name: CW_CONFIG_CONTENT Value: Fn::Sub: - "{ \"metrics\": { \"namespace\":\"${MetricNamespace}\", \"metrics_collected\": { \"statsd\": { \"metrics_aggregation_interval\": 0}}}}" - MetricNamespace: Fn::Join: - '/' - - !Ref EnvironmentName - gateway-envoy - StatsD LogConfiguration: LogDriver: 'awslogs' Options: awslogs-group: !Ref ClusterLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: 'ingress-cw-agent' IngressService: Type: AWS::ECS::Service DependsOn: - PublicLoadBalancerListener Properties: Cluster: !Ref ECSCluster DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 100 DesiredCount: 1 LaunchType: FARGATE ServiceRegistries: - RegistryArn: !GetAtt IngressServiceDiscoveryRecord.Arn NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: DISABLED SecurityGroups: - !Ref ContainerSecurityGroup Subnets: - !Ref PrivateSubnetOne - !Ref PrivateSubnetTwo TaskDefinition: !Ref IngressTaskDefinition LoadBalancers: - ContainerName: envoy ContainerPort: 9080 TargetGroupArn: !Ref IngressTargetGroup PublicLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internet-facing Subnets: - !Ref PublicSubnetOne - !Ref PublicSubnetTwo Type: network IngressTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckIntervalSeconds: 10 HealthCheckPort: 9080 HealthCheckProtocol: TCP HealthCheckTimeoutSeconds: 10 HealthyThresholdCount: 2 TargetType: ip Name: !Sub "${EnvironmentName}-ingress" Port: 80 Protocol: TCP UnhealthyThresholdCount: 2 TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 5 VpcId: !Ref VPC PublicLoadBalancerListener: Type: AWS::ElasticLoadBalancingV2::Listener DependsOn: - PublicLoadBalancer Properties: DefaultActions: - TargetGroupArn: !Ref IngressTargetGroup Type: 'forward' LoadBalancerArn: !Ref PublicLoadBalancer Port: 80 Protocol: TCP # 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: MeshName: Description: The name of the App MeshName Value: !GetAtt Mesh.MeshName Export: Name: !Sub ${EnvironmentName}:MeshName EnvoyImage: Value: !Ref EnvoyImage Export: Name: !Sub ${EnvironmentName}:EnvoyImage IngressEndpoint: Value: !GetAtt PublicLoadBalancer.DNSName Export: Name: !Sub ${EnvironmentName}:IngressEndpoint ClusterName: Description: The name of the ECS cluster Value: !Ref 'ECSCluster' Export: Name: !Sub ${EnvironmentName}:ClusterName ContainerSecurityGroup: Description: A security group used to allow containers to receive traffic Value: !Ref ContainerSecurityGroup Export: Name: !Sub ${EnvironmentName}:ContainerSecurityGroup TaskExecutionRoleArn: Description: ECS Task Execution role Value: !GetAtt TaskExecutionRole.Arn Export: Name: !Sub ${EnvironmentName}:TaskExecutionRoleArn DefaultTaskIamRoleArn: Description: ECS Task role Value: !GetAtt DefaultTaskIamRole.Arn Export: Name: !Sub ${EnvironmentName}:DefaultTaskIamRoleArn ServiceDiscoveryNamespaceId: Description: Namespace for service discovery Value: !Ref ServiceDiscoveryNamespace Export: Name: !Sub ${EnvironmentName}:ServiceDiscoveryNamespaceId ServiceDiscoveryNamespaceArn: Description: Namespace for service discovery Value: !GetAtt ServiceDiscoveryNamespace.Arn Export: Name: !Sub ${EnvironmentName}:ServiceDiscoveryNamespaceArn ServiceDiscoveryNamespaceName: Description: Namespace for service discovery Value: !Sub ${EnvironmentName}.local Export: Name: !Sub ${EnvironmentName}:ServiceDiscoveryNamespaceName ClusterLogGroup: Description: Log group for cluster Value: !Ref ClusterLogGroup Export: Name: !Sub ${EnvironmentName}:ClusterLogGroup VpcId: Description: The ID of the VPC that this stack is deployed in Value: !Ref 'VPC' Export: Name: !Sub ${EnvironmentName}:VpcId VpcCIDR: Description: The CIDR of the VPC that this stack is deployed in Value: !FindInMap ['SubnetConfig', 'VPC', 'CIDR'] Export: Name: !Sub ${EnvironmentName}:VpcCIDR PublicSubnetOne: Description: Public subnet one Value: !Ref 'PublicSubnetOne' Export: Name: !Sub ${EnvironmentName}:PublicSubnetOne PublicSubnetTwo: Description: Public subnet two Value: !Ref 'PublicSubnetTwo' Export: Name: !Sub ${EnvironmentName}:PublicSubnetTwo PrivateSubnetOne: Description: Private subnet one Value: !Ref 'PrivateSubnetOne' Export: Name: !Sub ${EnvironmentName}:PrivateSubnetOne PrivateSubnetTwo: Description: Private subnet two Value: !Ref 'PrivateSubnetTwo' Export: Name: !Sub ${EnvironmentName}:PrivateSubnetTwo