Description: > This template deploys an ECS cluster to the provided VPC and subnets using an Auto Scaling Group Parameters: EnvironmentName: Description: An environment name that will be prefixed to resource names Type: String InstanceType: Description: Which instance type should we use to build the ECS cluster? Type: String Default: c4.large ClusterSize: Description: How many ECS hosts do you want to initially deploy? Type: Number Default: 5 KeyName: Description: The EC2 Key Pair to allow SSH access to the instances Type: AWS::EC2::KeyPair::KeyName ECSServiceLogGroupRetentionInDays: Type: Number Default: 30 ECSServicesDomain: Type: String Description: "Domain name registerd under Route-53 that will be used for Service Discovery" Default: default.svc.cluster.local ECSAmi: Description: AMI ID Type: AWS::SSM::Parameter::Value Default: "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" EC2Ami: Description: AMI ID Type: AWS::SSM::Parameter::Value Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" Resources: ECSCluster: Type: AWS::ECS::Cluster Properties: ClusterName: !Ref EnvironmentName ECSInstancesSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "Security group for the instances" VpcId: 'Fn::ImportValue': !Sub "${EnvironmentName}:VPC" SecurityGroupIngress: - CidrIp: 'Fn::ImportValue': !Sub "${EnvironmentName}:VpcCIDR" IpProtocol: -1 ECSInstancesSecurityGroupIngressFromPublicALB: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Ingress from the public ALB GroupId: !Ref 'ECSInstancesSecurityGroup' IpProtocol: -1 SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG' #Bastion host Security group BastionSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Allow http to client host VpcId: 'Fn::ImportValue': !Sub "${EnvironmentName}:VPC" SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 0.0.0.0/0 #Bastion Host to SSH into the instance BastionHost: Type: AWS::EC2::Instance Properties: ImageId: !Ref EC2Ami KeyName: !Ref KeyName InstanceType: t2.micro SecurityGroupIds: - !Ref BastionSecurityGroup SubnetId: 'Fn::ImportValue': !Sub "${EnvironmentName}:PublicSubnet1" Tags: - Key: Name Value: 'Bastion-host-Demo' # ECS AutoScaling Group. ECSAutoScalingGroup: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: - 'Fn::ImportValue': !Sub "${EnvironmentName}:PrivateSubnet1" - 'Fn::ImportValue': !Sub "${EnvironmentName}:PrivateSubnet2" LaunchConfigurationName: !Ref ECSLaunchConfiguration MinSize: !Ref ClusterSize MaxSize: !Ref ClusterSize DesiredCapacity: !Ref ClusterSize Tags: - Key: Name Value: !Sub ${EnvironmentName} ECS host PropagateAtLaunch: true CreationPolicy: ResourceSignal: Timeout: PT15M UpdatePolicy: AutoScalingRollingUpdate: MinInstancesInService: 1 MaxBatchSize: 1 PauseTime: PT15M SuspendProcesses: - HealthCheck - ReplaceUnhealthy - AZRebalance - AlarmNotification - ScheduledActions WaitOnResourceSignals: true ECSLaunchConfiguration: Type: AWS::AutoScaling::LaunchConfiguration Properties: ImageId: !Ref ECSAmi InstanceType: !Ref InstanceType KeyName: !Ref KeyName SecurityGroups: - !Ref ECSInstancesSecurityGroup IamInstanceProfile: !Ref ECSInstanceProfile UserData: "Fn::Base64": !Sub | #!/bin/bash yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm yum install -y aws-cfn-bootstrap hibagent /opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource ECSLaunchConfiguration /opt/aws/bin/cfn-signal -e $? --region ${AWS::Region} --stack ${AWS::StackName} --resource ECSAutoScalingGroup /usr/bin/enable-ec2-spot-hibernation Metadata: AWS::CloudFormation::Init: config: packages: yum: awslogs: [] commands: 01_add_instance_to_cluster: command: !Sub echo ECS_CLUSTER=${ECSCluster} >> /etc/ecs/ecs.config files: "/home/ec2-user/xray-tracing.yaml": content: !Sub | static_resources: clusters: - name: xray connect_timeout: 5s type: STRICT_DNS lb_policy: round_robin hosts: - socket_address: protocol: udp address: xray port_value: 2000 tracing: http: name: envoy.xray config: collector_cluster: xray collector_endpoint: "127.0.0.1:2000" "/etc/cfn/cfn-hup.conf": mode: 000400 owner: root group: root content: !Sub | [main] stack=${AWS::StackId} region=${AWS::Region} "/etc/cfn/hooks.d/cfn-auto-reloader.conf": content: !Sub | [cfn-auto-reloader-hook] triggers=post.update path=Resources.ECSLaunchConfiguration.Metadata.AWS::CloudFormation::Init action=/opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource ECSLaunchConfiguration "/etc/awslogs/awscli.conf": content: !Sub | [plugins] cwlogs = cwlogs [default] region = ${AWS::Region} "/etc/awslogs/awslogs.conf": content: !Sub | [general] state_file = /var/lib/awslogs/agent-state [/var/log/dmesg] file = /var/log/dmesg log_group_name = ${ECSCluster}-/var/log/dmesg log_stream_name = ${ECSCluster} [/var/log/messages] file = /var/log/messages log_group_name = ${ECSCluster}-/var/log/messages log_stream_name = ${ECSCluster} datetime_format = %b %d %H:%M:%S [/var/log/docker] file = /var/log/docker log_group_name = ${ECSCluster}-/var/log/docker log_stream_name = ${ECSCluster} datetime_format = %Y-%m-%dT%H:%M:%S.%f [/var/log/ecs/ecs-init.log] file = /var/log/ecs/ecs-init.log.* log_group_name = ${ECSCluster}-/var/log/ecs/ecs-init.log log_stream_name = ${ECSCluster} datetime_format = %Y-%m-%dT%H:%M:%SZ [/var/log/ecs/ecs-agent.log] file = /var/log/ecs/ecs-agent.log.* log_group_name = ${ECSCluster}-/var/log/ecs/ecs-agent.log log_stream_name = ${ECSCluster} datetime_format = %Y-%m-%dT%H:%M:%SZ [/var/log/ecs/audit.log] file = /var/log/ecs/audit.log.* log_group_name = ${ECSCluster}-/var/log/ecs/audit.log log_stream_name = ${ECSCluster} datetime_format = %Y-%m-%dT%H:%M:%SZ services: sysvinit: cfn-hup: enabled: true ensureRunning: true files: - /etc/cfn/cfn-hup.conf - /etc/cfn/hooks.d/cfn-auto-reloader.conf awslogs: enabled: true ensureRunning: true files: - /etc/awslogs/awslogs.conf - /etc/awslogs/awscli.conf # This IAM Role is attached to all of the ECS hosts. It is based on the default role # published here: # http://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html # # You can add other IAM policy statements here to allow access from your ECS hosts # to other AWS services. Please note that this role will be used by ALL containers # running on the ECS host. ECSInstanceRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: | { "Statement": [{ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" } }] } 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:BatchCheckLayerAvailability", "ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer", "ecr:GetAuthorizationToken", "ssm:DescribeAssociation", "ssm:GetDeployablePatchSnapshotForInstance", "ssm:GetDocument", "ssm:GetManifest", "ssm:GetParameters", "ssm:ListAssociations", "ssm:ListInstanceAssociations", "ssm:PutInventory", "ssm:PutComplianceItems", "ssm:PutConfigurePackageResult", "ssm:UpdateAssociationStatus", "ssm:UpdateInstanceAssociationStatus", "ssm:UpdateInstanceInformation", "ec2messages:AcknowledgeMessage", "ec2messages:DeleteMessage", "ec2messages:FailMessage", "ec2messages:GetEndpoint", "ec2messages:GetMessages", "ec2messages:SendReply", "cloudwatch:PutMetricData", "ec2:DescribeInstanceStatus", "ds:CreateComputer", "ds:DescribeDirectories", "logs:CreateLogGroup", "logs:CreateLogStream", "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:PutLogEvents", "s3:PutObject", "s3:GetObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts", "s3:ListBucket", "s3:ListBucketMultipartUploads" ], "Resource": "*" }] } ECSInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - !Ref ECSInstanceRole ECSServiceAutoScalingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: Action: - 'sts:AssumeRole' Effect: Allow Principal: Service: - application-autoscaling.amazonaws.com Path: / Policies: - PolicyName: ecs-service-autoscaling PolicyDocument: Statement: Effect: Allow Action: - application-autoscaling:* - cloudwatch:DescribeAlarms - cloudwatch:PutMetricAlarm - ecs:DescribeServices - ecs:UpdateService Resource: "*" ECSServiceSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "Security group for the service" VpcId: 'Fn::ImportValue': !Sub "${EnvironmentName}:VPC" SecurityGroupIngress: - CidrIp: 'Fn::ImportValue': !Sub "${EnvironmentName}:VpcCIDR" IpProtocol: -1 TaskIamRole: 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 TaskExecutionIamRole: 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/AmazonEC2ContainerRegistryReadOnly - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess Policies: - PolicyName: AllowPutMetricData PolicyDocument: Statement: - Effect: Allow Action: [ "cloudwatch:PutMetricData" ] Resource: [ "*" ] ECSServiceLogGroup: Type: 'AWS::Logs::LogGroup' Properties: RetentionInDays: Ref: ECSServiceLogGroupRetentionInDays ECSServiceDiscoveryNamespace: Type: AWS::ServiceDiscovery::PrivateDnsNamespace Properties: Vpc: 'Fn::ImportValue': !Sub "${EnvironmentName}:VPC" Name: { Ref: ECSServicesDomain } # Load balancers for getting traffic to containers. # This sample template creates one load balancer: # # - 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. # 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: 'Fn::ImportValue': !Sub "${EnvironmentName}: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 - 'Fn::ImportValue': !Sub "${EnvironmentName}:PublicSubnet1" - 'Fn::ImportValue': !Sub "${EnvironmentName}:PublicSubnet2" 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: !Sub "${EnvironmentName}-drop-1" Port: 80 Protocol: HTTP UnhealthyThresholdCount: 2 VpcId: 'Fn::ImportValue': !Sub "${EnvironmentName}:VPC" PublicLoadBalancerListener: Type: AWS::ElasticLoadBalancingV2::Listener DependsOn: - PublicLoadBalancer Properties: DefaultActions: - TargetGroupArn: !Ref 'DummyTargetGroupPublic' Type: 'forward' LoadBalancerArn: !Ref 'PublicLoadBalancer' Port: 80 Protocol: HTTP Outputs: Cluster: Description: A reference to the ECS cluster Value: !Ref ECSCluster Export: Name: !Sub "${EnvironmentName}:ECSCluster" BastionHostIP: Description: Bastion Host Ip address to SSH Value: !GetAtt BastionHost.PublicIp ECSServiceAutoScalingRole: Description: A reference to ECS service auto scaling role Value: !GetAtt ECSServiceAutoScalingRole.Arn ECSAutoScalingGroupName: Description: A reference to ECS AutoScaling Group Name Value: !Ref ECSAutoScalingGroup ECSServiceDiscoveryNamespace: Description: A SDS namespace that will be used by all services in this cluster Value: !Ref ECSServiceDiscoveryNamespace Export: Name: !Sub "${EnvironmentName}:ECSServiceDiscoveryNamespace" ECSServiceLogGroup: Description: Log group for services to publish logs Value: !Ref ECSServiceLogGroup Export: Name: !Sub "${EnvironmentName}:ECSServiceLogGroup" ECSServiceSecurityGroup: Description: Security group to be used by all services in the cluster Value: !Ref ECSServiceSecurityGroup Export: Name: !Sub "${EnvironmentName}:ECSServiceSecurityGroup" ECSInstancesSecurityGroup: Description: Security group to be used by all ECS instance in the cluster Value: !Ref ECSInstancesSecurityGroup Export: Name: !Sub "${EnvironmentName}:ECSInstancesSecurityGroup" TaskExecutionIamRoleArn: Description: Task Executin IAM role used by ECS tasks Value: { "Fn::GetAtt": TaskExecutionIamRole.Arn } Export: Name: !Sub "${EnvironmentName}:TaskExecutionIamRoleArn" TaskIamRole: Description: IAM role to be used by ECS task Value: !Ref TaskIamRole Export: Name: !Sub "${EnvironmentName}:TaskIamRole" #Load balancer PublicLoadBalancer: Description: The ARN of the public load balancer Value: !Ref PublicLoadBalancer Export: Name: !Sub "${EnvironmentName}:PublicLoadBalancer" PublicListener: Description: The ARN of the public load balancer's Listener Value: !Ref PublicLoadBalancerListener Export: Name: !Sub "${EnvironmentName}:PublicListener" ExternalUrl: Description: The url of the external load balancer Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName']] Export: Name: !Sub "${EnvironmentName}:ExternalUrl"