# Stack to create EC2 instances for ECS cluster. # # aws cloudformation deploy \ # --stack-name app-cluster-prod \ # --template-file ./aws-cluster-stack.yaml \ # --parameter-overrides \ # KeyName=DEFAULT \ # SecurityGroups=group1,group2 \ # ImageId=ami-123456 \ # InstanceType=c5.large \ # Subnets=sn-1234,sn-5678 \ # EcsClusterName=myapp-prod \ # --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ # --no-execute-changeset AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: EC2 instances for ECS cluster. Parameters: KeyName: Description: The EC2 Key Pair to allow SSH access to the instance Type: AWS::EC2::KeyPair::KeyName SecurityGroups: Description: Security group ids to use for the instances. Type: CommaDelimitedList ImageId: Description: ECS-optimised AMI ID for your region. http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html Type: String InstanceType: Description: EC2 instance type. Type: String Subnets: Description: Subnet ids for instance placement. Type: CommaDelimitedList EcsClusterName: Description: Name of the ECS cluster. Type: String Resources: ## EC2 InstanceRole: Type: AWS::IAM::Role Properties: Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sts:AssumeRole Principal: Service: ec2.amazonaws.com Policies: - PolicyName: logs PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:* Resource: - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - !Ref InstanceRole AutoscalingLaunchConfig: Type: AWS::AutoScaling::LaunchConfiguration Properties: ImageId: !Ref ImageId InstanceType: !Ref InstanceType KeyName: !Ref KeyName SecurityGroups: !Ref SecurityGroups IamInstanceProfile: !Ref InstanceProfile UserData: !Base64: Fn::Sub: | - Content-Type: multipart/mixed; boundary="==BOUNDARY==" MIME-Version: 1.0 --==BOUNDARY== MIME-Version: 1.0 Content-Type: text/x-shellscript; charset="us-ascii" #!/bin/bash echo ECS_CLUSTER=${ClusterName} >> /etc/ecs/ecs.config STACK_NAME=${AWS::StackName} # Install awslogs and the jq JSON parser yum install -y awslogs jq https://s3-${AWS::Region}.amazonaws.com/amazon-ssm-${AWS::Region}/latest/linux_amd64/amazon-ssm-agent.rpm # Inject the CloudWatch Logs configuration file contents cat > /etc/awslogs/awslogs.conf <<- EOF [general] state_file = /var/lib/awslogs/agent-state [/var/log/dmesg] file = /var/log/dmesg log_group_name = /var/log/dmesg log_stream_name = {cluster}/{container_instance_id} [/var/log/messages] file = /var/log/messages log_group_name = /var/log/messages log_stream_name = {cluster}/{container_instance_id} datetime_format = %b %d %H:%M:%S [/var/log/docker] file = /var/log/docker log_group_name = /var/log/docker log_stream_name = {cluster}/{container_instance_id} 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 = /var/log/ecs/ecs-init.log log_stream_name = {cluster}/{container_instance_id} datetime_format = %Y-%m-%dT%H:%M:%SZ [/var/log/ecs/ecs-agent.log] file = /var/log/ecs/ecs-agent.log.* log_group_name = /var/log/ecs/ecs-agent.log log_stream_name = {cluster}/{container_instance_id} datetime_format = %Y-%m-%dT%H:%M:%SZ [/var/log/ecs/audit.log] file = /var/log/ecs/audit.log.* log_group_name = /var/log/ecs/audit.log log_stream_name = {cluster}/{container_instance_id} datetime_format = %Y-%m-%dT%H:%M:%SZ [/var/log/amazon/ssm/amazon-ssm-agent.log] file = /var/log/amazon/ssm/amazon-ssm-agent.log log_group_name = amazon-ssm log_stream_name = agent-$STACK_NAME/{container_instance_id} datetime_format = %Y-%m-%dT%H:%M:%SZ [/var/log/amazon/ssm/errors.log] file = /var/log/amazon/ssm/errors.log log_group_name = amazon-ssm log_stream_name = errors-$STACK_NAME/{container_instance_id} datetime_format = %Y-%m-%dT%H:%M:%SZ EOF --==BOUNDARY== MIME-Version: 1.0 Content-Type: text/x-shellscript; charset="us-ascii" #!/bin/bash # Set the region to send CloudWatch Logs data to (the region where the container instance is located) region=$(curl 169.254.169.254/latest/meta-data/placement/availability-zone | sed s'/.$//') sed -i -e "s/region = us-east-1/region = $region/g" /etc/awslogs/awscli.conf --==BOUNDARY== MIME-Version: 1.0 Content-Type: text/upstart-job; charset="us-ascii" #upstart-job description "Configure and start CloudWatch Logs agent on Amazon ECS container instance" author "Amazon Web Services" start on started ecs script exec 2>>/var/log/ecs/cloudwatch-logs-start.log set -x until curl -s http://localhost:51678/v1/metadata do sleep 1 done # Grab the cluster and container instance ARN from instance metadata cluster=$(curl -s http://localhost:51678/v1/metadata | jq -r '. | .Cluster') container_instance_id=$(curl -s http://localhost:51678/v1/metadata | jq -r '. | .ContainerInstanceArn' | awk -F/ '{print $2}' ) # Replace the cluster name and container instance ID placeholders with the actual values sed -i -e "s/{cluster}/$cluster/g" /etc/awslogs/awslogs.conf sed -i -e "s/{container_instance_id}/$container_instance_id/g" /etc/awslogs/awslogs.conf service awslogs start chkconfig awslogs on end script --==BOUNDARY==-- - ClusterName: !Ref EcsClusterName AutoscalingGroup: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: !Ref SubnetIds LaunchConfigurationName: !Ref AutoscalingLaunchConfig MinSize: 0 MaxSize: 0 HealthCheckType: EC2 Tags: - Key: !Ref AWS::StackName Value: 'true' PropagateAtLaunch: true - Key: Name Value: !Ref AWS::StackName PropagateAtLaunch: true - Key: role Value: !Ref AWS::StackName PropagateAtLaunch: true LifecycleHookRole: Type: AWS::IAM::Role Properties: Path: / AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sts:AssumeRole Principal: Service: autoscaling.amazonaws.com Policies: - PolicyName: SNSAccess PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sns:Publish Resource: !ImportValue ops-lambdas-prod:EcsLifecycleHookTopicArn AutoscalingGroupInstanceTerminationHook: Type: AWS::AutoScaling::LifecycleHook Properties: AutoScalingGroupName: !Ref AutoscalingGroup HeartbeatTimeout: 600 LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING NotificationTargetARN: !ImportValue ops-lambdas-prod:EcsLifecycleHookTopicArn RoleARN: !GetAtt LifecycleHookRole.Arn InstanceScaleOutPolicy: Type: AWS::AutoScaling::ScalingPolicy Properties: AdjustmentType: PercentChangeInCapacity AutoScalingGroupName: !Ref AutoscalingGroup EstimatedInstanceWarmup: 420 PolicyType: StepScaling StepAdjustments: - MetricIntervalLowerBound: 0 MetricIntervalUpperBound: 10 ScalingAdjustment: 10 - MetricIntervalLowerBound: 10 ScalingAdjustment: 30 InstanceScaleInPolicy: Type: AWS::AutoScaling::ScalingPolicy Properties: AdjustmentType: PercentChangeInCapacity AutoScalingGroupName: !Ref AutoscalingGroup EstimatedInstanceWarmup: 420 PolicyType: StepScaling StepAdjustments: - MetricIntervalUpperBound: 0 MetricIntervalLowerBound: -10 ScalingAdjustment: -10 - MetricIntervalUpperBound: -10 ScalingAdjustment: -30 InstanceCpuAlarmHigh: Type: AWS::CloudWatch::Alarm Properties: EvaluationPeriods: 5 Statistic: Average Threshold: 80 AlarmDescription: Alarm if instance CPU high enough to trigger scale out policy. Period: 60 AlarmActions: - !Ref InstanceScaleOutPolicy Namespace: AWS/EC2 Dimensions: - Name: AutoScalingGroupName Value: !Ref AutoscalingGroup ComparisonOperator: GreaterThanOrEqualToThreshold MetricName: CPUUtilization InstanceCpuAlarmLow: Type: AWS::CloudWatch::Alarm Properties: EvaluationPeriods: 30 Statistic: Average Threshold: 30 AlarmDescription: Alarm if instance CPU low long enough to trigger scale in policy. Period: 60 AlarmActions: - !Ref InstanceScaleInPolicy Namespace: AWS/EC2 Dimensions: - Name: AutoScalingGroupName Value: !Ref AutoscalingGroup ComparisonOperator: LessThanOrEqualToThreshold MetricName: CPUUtilization Outputs: AutoscalingGroupName: Description: Name of ASG. Value: !Ref AutoscalingGroup