Deploying to AWS ECS using CloudFormation and Spot Instances


This article details the AWS CloudFormation building blocks to deploy a containerised application using the AWS Elastic Container Service (ECS). I use this method to deploy this vary website which was initially running in ECS using an on-demand instance deployed the old fashion way (with many mouse clicks and typing). With this CloudFormation template the entire stack can be created from a single command aws cloudformation create-stack...! and completely blown away and stood up again with minimal effort. The following borrows from an existing CloudFormation template you can find in the References at the bottom of the page.

My main goals when creating the template were to

  • achieve infrastructure as code
  • reduce the operational cost by using Spot instances
  • ensuring the website can auto-heal when said Spot instances are terminated
  • brew my coffee. OK, maybe not brew my coffee but with all that free time I have while CloudFormation does everything for me I can take care of that myself

At a high level, the following things are being created and configured by the Cloudformation template which I have broken up in the following text. For the complete template, jump to the bottom of the page.

  • Create a VPC and basic networking namely an Internet Gateway, public subnet, and route table
  • Create an Elastic IP and associate it to an Elastic Network Interface - this will facilitate reuse of the same public IP when the Spot instances terminate
  • Create security groups for Web and SSH access and access from the EC2 instances to EFS
  • Create an EFS and a mount point
  • Create an ECS Task Definition which defines the required containers and their configurations - this includes mounting the EFS volume into the NGINX container so it can access the Let's Encrypt files (see omission notes below)
  • Create an ECS Cluster and Service
  • Create an Auto Scaling Target and Auto Scaling Group which maintains 1 Spot instance for ECS
  • Create a Launch Template which configures the Spot Instance/s
  • Create the roles required for everything to work correctly
  • Create an SNS topic for email notifications when events occur in the Auto Scaling Group

What's omitted from the template.

  • Some super-secret environment variables defined in the AWS Secrets Manager
  • The initial S3 bucket which contains Let's Encrypt configuration files used by the NGINX container that are seeded into EFS when the CloudFormation Template is run for the first time

Caveats.

  • I intend only to maintain a single EC2 instance running at a time, and so the template and the way it works are configured as such
  • I already had a working Let's Encrypt configuration and certificates which I used to 'pre-load' the S3 seed bucket

For reference, the website has the following container components.

Standard Boiler Plate

The usual boilerplate data for any CloudFormation template.

AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation Template for jasonneurohr.com on ECS

Mappings

In the mappings collection is an ecsOptimizedAmi declaration which indicates what AMI's should be used; specifically this has an ECS optimised AMI in the ap-southeast-2 region. The list of ECS optimised AMI's can be found here.

Mappings:
  ecsOptimizedAmi:
    ap-southeast-2:
      AMI: ami-0c7dea114481e059d

Outputs

In the outputs, collection are references to several objects, nothing fancy.

Outputs:
  awsRegionName:
    Description: The name of the AWS Region your template was launched in
    Value: !Ref AWS::Region
  websiteVpc:
    Description: A reference to the created VPC
    Value: !Ref websiteVpc
  publicSubnet1:
    Description: A reference to the public subnet in the 1st Availability Zone
    Value: !Ref publicSubnet1
  efsFs: 
    Description: Reference ID for the EFS
    Value: !Ref efsFs

Parameters

The parameters collection defines several required parameters required to be either passed to CloudFormation or accept the default where specified. Of note are keyName which defines the EC2 keypair to use for the created instances and asgNotificationEp which is the email address that will receive Auto Scaling Group notifications.

Parameters:
  ecsClusterTargetCapacity:
    Default: 1
    Description: Number of EC2 Spot instances to initially launch in the ECS cluster
    Type: Number
  instanceType:
    AllowedValues:
    - t3.small
    Default: t3.small
    Description: EC2 instance type to use for ECS cluster
    Type: String
  keyName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the EC2 instances
    Type: AWS::EC2::KeyPair::KeyName
  sourceCidr:
    Default: 0.0.0.0/0
    Description: Optional - CIDR/IP range for instance ssh access - defaults to 0.0.0.0/0
    Type: String
  environmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: website-ecs
  vpcCidr:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.192.0.0/16
  publicSubnet1Cidr:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.192.10.0/24
  asgNotificationEp:
    Description: The email address to receive notifications for the Auto Scaling Group
    Type: String

Resources

The resources collection is where the magic happens.

VPC and VPC Networking

The first block of YAML below does the following

  • Creates a VPC containing
  • Creates an Internet Gateway
  • Creates a public subnet
  • Creates a route table and default route
  • Creates a CloudWatch Log Group
Resources:
  websiteVpc:
    Type: AWS::EC2::VPC
    Properties: 
      CidrBlock: !Ref vpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags: 
        - Key: Name
          Value: !Ref environmentName
  internetGateway:
    Type: AWS::EC2::InternetGateway
    DependsOn:
      - websiteVpc
    Properties:
      Tags:
        - Key: Name
          Value: !Ref environmentName
  internetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    DependsOn:
      - websiteVpc
      - internetGateway
    Properties:
      InternetGatewayId: !Ref internetGateway
      VpcId: !Ref websiteVpc
  publicSubnet1:
    Type: AWS::EC2::Subnet
    DependsOn:
      - websiteVpc
    Properties:
      VpcId: !Ref websiteVpc
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref publicSubnet1Cidr
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${environmentName} Public Subnet (AZ1)
  publicRouteTable:
    Type: AWS::EC2::RouteTable
    DependsOn:
      - websiteVpc
    Properties:
      VpcId: !Ref websiteVpc
      Tags:
        - Key: Name
          Value: !Sub ${environmentName} Public Route Table
  defaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: 
    - internetGatewayAttachment
    Properties:
      RouteTableId: !Ref publicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref internetGateway
  publicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref publicRouteTable
      SubnetId: !Ref publicSubnet1
  cloudWatchLogsGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 7

Elastic IP and Elastic Network Interface

The next portion of the resources collection creates an Elastic IP (EIP) and an Elastic Network Interface (ENI). These are used later in the LaunchTemplate which facilitates EC2 instances maintaining the same IP when they are replaced.

elasticIp:
    Type: AWS::EC2::EIP
    DependsOn:
      - internetGatewayAttachment
    Properties: 
      Domain: vpc
  elasticNetworkInterface:
    Type: AWS::EC2::NetworkInterface
    Properties: 
      Description: ENI for spot instance
      SourceDestCheck: true
      SubnetId: !Ref publicSubnet1
      GroupSet: 
        - !Ref ecsInstanceSecurityGroup
      Tags: 
        - Key: Name
          Value: !Ref environmentName
  elasticIpAssociation:
    Type: AWS::EC2::EIPAssociation
    DependsOn:
      - elasticIp
      - elasticNetworkInterface
    Properties:
      AllocationId: !GetAtt elasticIp.AllocationId 
      NetworkInterfaceId: !Ref elasticNetworkInterface

Security Groups

The next portion of the template creates the required security groups for access to the EC2 instance and access from EC2 to the Elastic File System (EFS) created later.

  ecsInstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    DependsOn:
      - websiteVpc
    Properties:
      GroupName: ecs-access-from-internet
      GroupDescription: Web and SSH from anywhere to ECS Auto Scaling Instances
      SecurityGroupIngress:
      - CidrIp: !Ref sourceCidr
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22
      - CidrIp: !Ref sourceCidr
        FromPort: 443
        IpProtocol: tcp
        ToPort: 443
      - CidrIp: !Ref sourceCidr
        FromPort: 80
        IpProtocol: tcp
        ToPort: 80
      VpcId: !Ref websiteVpc
  efsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    DependsOn:
      - websiteVpc
    Properties:
      GroupName: efs-access-from-ecs
      GroupDescription: Security Group for EFS access from ECS
      SecurityGroupIngress:
      - IpProtocol: tcp
        SourceSecurityGroupId: !Ref ecsInstanceSecurityGroup
        FromPort: 2049
        ToPort: 2049
      VpcId: !Ref websiteVpc

Elastic File System

The Elastic File System (EFS) and EFS mount point are then created. The Let's Encrypt configuration directory is stored in EFS and referenced in the Launch Template user data defined later such that if an EC2 instance gets terminated and replaced it can come back online without human intervention by being remounted at the host and attached to the NGINX container inside ECS.

  efsFs:
    Type: AWS::EFS::FileSystem
    Properties: 
      Encrypted: false
      FileSystemTags: 
        - Key: Name
          Value: !Ref environmentName
      PerformanceMode: generalPurpose
      ThroughputMode: bursting
  efsMountPoint:
    Type: AWS::EFS::MountTarget
    DependsOn:
      - efsFs
    Properties: 
      FileSystemId: !Ref efsFs
      SecurityGroups: 
        - !Ref efsSecurityGroup
      SubnetId: !Ref publicSubnet1

Elastic Container Service

The next collection defines the Elastic Container Service (ECS) infrastructure, specifically

  • the ECS Task Definition which includes the EFS volume to be mounted into the NGINX container
  • the ECS Cluster
  • the ECS Service

The ECS Service defines one desired instance of the Task Definition should be running. It also stipulates a MinimumHealthyPercent of 0 and a MaximumPercent of 100 which facilitates an Azure DevOps pipeline instructing ECS to force a new deployment of the task.

  ecsTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    DependsOn:
      - efsFs
      - efsMountPoint
    Properties: 
      ContainerDefinitions: 
        - Name: nginx
          Hostname: nginx
          Cpu: 128
          Memory: 256
          MemoryReservation: 256
          Image: jasonneurohr/private:nginx
          Essential: true
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
              Protocol: tcp
            - ContainerPort: 443
              HostPort: 443
              Protocol: tcp
          Links:
            - web:web
          MountPoints:
            - ContainerPath: /etc/letsencrypt
              SourceVolume: nginx-letsencrypt
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
        - Name: web
          Hostname: web
          Cpu: 256
          Memory: 256
          MemoryReservation: 256
          Image: jasonneurohr/private:web
          Essential: true
          PortMappings:
            - ContainerPort: 5000
              HostPort: 5000
              Protocol: tcp
          Links:
            - webapi:webapi
            - contactsvc:contactsvc
            - notifysvc:notifysvc
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
          Environment:
            - Name: ApiLocation
              Value: http://webapi:55000
            - Name: contactsvcEndpoint
              Value: contactsvc:5001
            - Name: notifysvcEndpoint
              Value: notifysvc:5002
            - Name: SendGridApiKey
              Value: asdf
        - Name: webapi
          Hostname: webapi
          Cpu: 128
          Memory: 256
          MemoryReservation: 256
          Image: jasonneurohr/private:webapi
          Essential: true
          PortMappings:
            - ContainerPort: 55000
              HostPort: 55000
              Protocol: tcp
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
          Environment:
            - Name: dbName
              Value: asdf
            - Name: dbServer
              Value: asdf
            - Name: dbUser
              Value: asdf
          Secrets:
            - Name: dbPassword
              ValueFrom: asdf
        - Name: notifysvc
          Hostname: notifysvc
          Cpu: 128
          Memory: 128
          Image: jasonneurohr/private:notifysvc
          Essential: true
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
        - Name: contactsvc
          Hostname: contactsvc
          Cpu: 128
          Memory: 128
          Image: jasonneurohr/private:contactsvc
          Essential: true
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
      ExecutionRoleArn: arn:aws:iam::asdf:role/ecsTaskExecutionRole
      Family: website-ecs
      RequiresCompatibilities: 
        - EC2
      Tags: 
        - Key: Name
          Value: !Ref environmentName
      Volumes: 
        - Name: nginx-letsencrypt
          Host:
            SourcePath: /mnt/efs
  ecsCluster:
    Type: AWS::ECS::Cluster
    Properties: 
      ClusterName: website-cluster
      Tags: 
        - Key: Name
          Value: !Ref environmentName
  ecsService:
    Type: AWS::ECS::Service
    DependsOn:
      - ecsAutoScalingGroup
    Properties: 
      Cluster: !Ref ecsCluster
      DeploymentConfiguration: 
        MaximumPercent: 100
        MinimumHealthyPercent: 0
      DesiredCount: 1
      EnableECSManagedTags: true
      LaunchType: EC2
      PlacementStrategies: 
        - Field: attribute:ecs.availability-zone
          Type: spread
        - Field: instanceId
          Type: spread
      SchedulingStrategy: REPLICA
      ServiceName: website-service
      Tags: 
        - Key: Name
          Value: !Ref environmentName
      TaskDefinition: !Ref ecsTaskDefinition

ECS Auto Scaling

An ApplicationAutoScaling collection defines a MaxCapacity of one and a MinCapacity of zero in line with the service definition. This facilitates auto-scaling of the ECS service though only to one right now.

  ecsAutoScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    DependsOn:
      - autoScalingRole
      - ecsCluster
      - ecsService
    Properties: 
      MaxCapacity: 1
      MinCapacity: 0
      ResourceId: !Join [ "/", [ "service", !Ref ecsCluster, !GetAtt ecsService.Name ] ]
      RoleARN: !GetAtt autoScalingRole.Arn
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs

Launch Template

The Launch Template collection is where the Spot price is defined and includes user data to configure several things. Of note is the use of the ENI created above and the last several lines which setup the EFS volume on the instance for use in the NGINX container.

websiteLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    DependsOn:
      - elasticNetworkInterface
      - instanceProfile
      - ecsCluster
    Properties: 
      LaunchTemplateData: 
        InstanceType: !Ref instanceType
        KeyName: !Ref keyName
        CreditSpecification:
          CpuCredits: standard
        ImageId:
          Fn::FindInMap:
          - ecsOptimizedAmi
          - Ref: AWS::Region
          - AMI
        IamInstanceProfile: 
          Arn: !GetAtt instanceProfile.Arn
        NetworkInterfaces:
          - NetworkInterfaceId: !Ref elasticNetworkInterface
            DeviceIndex: 0
        InstanceMarketOptions:
            MarketType: spot
            SpotOptions: 
              InstanceInterruptionBehavior: terminate
              MaxPrice: 0.01
              SpotInstanceType: one-time
        UserData:
          Fn::Base64: !Sub |
            #!/bin/bash
            export PATH=/usr/local/bin:$PATH
            yum -y --security update
            yum -y install jq
            easy_install pip
            pip install awscli
            aws configure set default.region ${AWS::Region}
            echo ECS_CLUSTER=${ecsCluster} >> /etc/ecs/ecs.config
            echo ECS_BACKEND_HOST= >> /etc/ecs/ecs.config

            cat <<EOF > /tmp/awslogs.conf
            [general]
            state_file = /var/awslogs/state/agent-state

            [/var/log/dmesg]
            file = /var/log/dmesg
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/dmesg
            initial_position = start_of_file

            [/var/log/messages]
            file = /var/log/messages
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/messages
            datetime_format = %b %d %H:%M:%S
            initial_position = start_of_file

            [/var/log/docker]
            file = /var/log/docker
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/docker
            datetime_format = %Y-%m-%dT%H:%M:%S.%f
            initial_position = start_of_file

            [/var/log/ecs/ecs-init.log]
            file = /var/log/ecs/ecs-init.log.*
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/ecs/ecs-init.log
            datetime_format = %Y-%m-%dT%H:%M:%SZ
            initial_position = start_of_file

            [/var/log/ecs/ecs-agent.log]
            file = /var/log/ecs/ecs-agent.log.*
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/ecs/ecs-agent.log
            datetime_format = %Y-%m-%dT%H:%M:%SZ
            initial_position = start_of_file

            [/var/log/ecs/audit.log]
            file = /var/log/ecs/audit.log.*
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/ecs/audit.log
            datetime_format = %Y-%m-%dT%H:%M:%SZ
            initial_position = start_of_file
            EOF

            cd /tmp && curl -sO https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py
            python /tmp/awslogs-agent-setup.py -n -r ${AWS::Region} -c /tmp/awslogs.conf

            cat <<EOF > /etc/init/cloudwatch-logs-start.conf
            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/cloudwatch-logs-start.log
            set -x
            until curl -s http://localhost:51678/v1/metadata; do sleep 1; done
            ECS_CLUSTER=\$(curl -s http://localhost:51678/v1/metadata | jq .Cluster | tr -d \")
            CONTAINER_INSTANCE=\$(curl -s http://localhost:51678/v1/metadata | jq .ContainerInstanceArn | tr -d \")
            sed -i "s|%ECS_CLUSTER|\$ECS_CLUSTER|g" /var/awslogs/etc/awslogs.conf
            sed -i "s|%CONTAINER_INSTANCE|\$CONTAINER_INSTANCE|g" /var/awslogs/etc/awslogs.conf
            chkconfig awslogs on
            service awslogs start
            end script
            EOF

            cat <<EOF > /etc/init/spot-instance-termination-notice-handler.conf
            description "Start spot instance termination handler monitoring script"
            author "Amazon Web Services"
            start on started ecs
            script
            echo \$\$ > /var/run/spot-instance-termination-notice-handler.pid
            exec /usr/local/bin/spot-instance-termination-notice-handler.sh
            end script
            pre-start script
            logger "[spot-instance-termination-notice-handler.sh]: spot instance termination notice handler started"
            end script
            EOF

            cat <<EOF > /usr/local/bin/spot-instance-termination-notice-handler.sh
            #!/bin/bash
            while sleep 5; do
            if [ -z \$(curl -Isf http://169.254.169.254/latest/meta-data/spot/termination-time)];
            then
            /bin/false
            else
            logger "[spot-instance-termination-notice-handler.sh]: spot instance termination notice detected"
            STATUS=DRAINING
            ECS_CLUSTER=\$(curl -s http://localhost:51678/v1/metadata | jq .Cluster | tr -d \")
            CONTAINER_INSTANCE=\$(curl -s http://localhost:51678/v1/metadata | jq .ContainerInstanceArn | tr -d \")
            logger "[spot-instance-termination-notice-handler.sh]: putting instance in state $STATUS"
            logger "[spot-instance-termination-notice-handler.sh]: running: /bin/aws ecs update-container-instances-state --cluster \$ECS_CLUSTER --container-instances $CONTAINER_INSTANCE --status \$STATUS"
            /bin/aws ecs update-container-instances-state --cluster $ECS_CLUSTER --container-instances \$CONTAINER_INSTANCE --status \$STATUS
            logger "[spot-instance-termination-notice-handler.sh]: running: \"/bin/aws sns publish --topic-arn ${snsTopicForSpotInstanceMonitorScript} --message \"Spot instance termination notice detected. Details: cluster: \$ECS_CLUSTER, container_instance: \$CONTAINER_INSTANCE. Putting instance in state \$STATUS.\""
            /bin/aws sns publish --topic-arn ${snsTopicForSpotInstanceMonitorScript} --message "Spot instance termination notice detected. Details: cluster: \$ECS_CLUSTER, container_instance: \$CONTAINER_INSTANCE. Putting instance in state \$STATUS."
            logger "[spot-instance-termination-notice-handler.sh]: putting myself to sleep..."
            sleep 120
            fi
            done
            EOF

            chmod +x /usr/local/bin/spot-instance-termination-notice-handler.sh
            mkdir /mnt/efs
            yum install -y amazon-efs-utils
            cp /etc/fstab /etc/fstab.bak
            echo "${efsFs}:/ /mnt/efs efs defaults,_netdev 0 0" | tee -a /etc/fstab
            mount -a
            # aws s3 cp s3://temp-jasonneurohr/letsencrypt /mnt/efs/ --recursive

Auto Scaling Group

The next collection defines the Auto Scaling Group, which maintains one EC2 instance and uses the above Launch Template. It also specifies the SNS Topic, which will receive notifications.

ecsAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    DependsOn:
      - efsMountPoint
      - snsTopicForAutoScalingGroup
    Properties: 
      AutoScalingGroupName: ecs-website
      AvailabilityZones: 
        - !Select [ 0, !GetAZs '' ]
      Cooldown: 300
      DesiredCapacity: 1
      HealthCheckGracePeriod: 0
      HealthCheckType: EC2
      LaunchTemplate: 
        LaunchTemplateId: !Ref websiteLaunchTemplate
        Version: !GetAtt websiteLaunchTemplate.LatestVersionNumber
      MaxSize: 1
      MinSize: 1
      NotificationConfigurations: 
        - TopicARN: !Ref snsTopicForAutoScalingGroup
          NotificationTypes:
            - autoscaling:EC2_INSTANCE_LAUNCH
            - autoscaling:EC2_INSTANCE_LAUNCH_ERROR
            - autoscaling:EC2_INSTANCE_TERMINATE
            - autoscaling:EC2_INSTANCE_TERMINATE_ERROR
            - autoscaling:TEST_NOTIFICATION
      Tags: 
        - Key: Name
          Value: !Ref environmentName
          PropagateAtLaunch: true
      TerminationPolicies: 
        - Default

Roles

Roles are setup were required for various actions in ECS, auto-scaling, CloudWatch, and SNS.

  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: "*"
  instanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action:
          - sts:AssumeRole
          Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
        Version: 2012-10-17
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
      Path: /
      Policies:
      - PolicyDocument:
          Statement:
          - Action:
            - ecs:UpdateContainerInstancesState
            Effect: Allow
            Resource: '*'
          Version: 2012-10-17
        PolicyName: ecsUpdateContainerInstancesStatePolicy
      - PolicyDocument:
          Statement:
          - Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            - logs:DescribeLogStreams
            Effect: Allow
            Resource: arn:aws:logs:*:*:*
          Version: 2012-10-17
        PolicyName: cloudWatchLogsPolicy
      - PolicyName: ec2-read-temp-jasonneurohr-s3-bucket
        PolicyDocument:
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Action:
              - s3:Get*
              - s3:List*
            Resource: 
              - arn:aws:s3:::temp-jasonneurohr
              - arn:aws:s3:::temp-jasonneurohr/*
      - PolicyDocument:
          Statement:
          - Action:
            - sns:Publish
            Effect: Allow
            Resource: !Ref snsTopicForSpotInstanceMonitorScript
          Version: 2012-10-17
        PolicyName: snsPublishPolicy
  instanceProfile:
    Type: AWS::IAM::InstanceProfile
    DependsOn:
    - instanceRole
    Properties:
      Path: /
      Roles:
      - Ref: instanceRole
  snsTopicForAutoScalingGroup:
    Type: AWS::SNS::Topic
    Properties: 
      DisplayName: ecs-website-autoscalingroup
      Subscription: 
        - Endpoint: !Ref asgNotificationEp
          Protocol: email
      TopicName: ecs-website-autoscalingroup

Simple Notification Service

An SNS collection is created which will send Auto Scaling event notifications to the email address provided as a parameter to CloudFormation.

  snsTopicForSpotInstanceMonitorScript:
    Type: AWS::SNS::Topic
    Properties: 
      DisplayName: ecs-website-spotinstancemonitor
      Subscription: 
        - Endpoint: !Ref asgNotificationEp
          Protocol: email
      TopicName: ecs-website-spotinstancemonitor

Full CloudFormation Template YAML

The complete CloudFormation Template YAML is located below for reference.

AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation Template for jasonneurohr.com on ECS
Mappings:
  # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-optimized_AMI.html
  ecsOptimizedAmi:
    ap-southeast-2:
      AMI: ami-0c7dea114481e059d

Outputs:
  awsRegionName:
    Description: The name of the AWS Region your template was launched in
    Value: !Ref AWS::Region
  websiteVpc:
    Description: A reference to the created VPC
    Value: !Ref websiteVpc
  publicSubnet1:
    Description: A reference to the public subnet in the 1st Availability Zone
    Value: !Ref publicSubnet1
  efsFs: 
    Description: Reference ID for the EFS stack
    Value: !Ref efsFs

Parameters:
  ecsClusterTargetCapacity:
    Default: 1
    Description: Number of EC2 Spot instances to initially launch in the ECS cluster
    Type: Number
  instanceType:
    AllowedValues:
    - t3.small
    Default: t3.small
    Description: EC2 instance type to use for ECS cluster
    Type: String
  keyName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the EC2 instances
    Type: AWS::EC2::KeyPair::KeyName
  sourceCidr:
    Default: 0.0.0.0/0
    Description: Optional - CIDR/IP range for instance ssh access - defaults to 0.0.0.0/0
    Type: String
  environmentName:
    Description: An environment name that will be prefixed to resource names
    Type: String
    Default: website-ecs
  vpcCidr:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.192.0.0/16
  publicSubnet1Cidr:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.192.10.0/24
  asgNotificationEp:
    Description: The email address to receive notifications for the Auto Scaling Group
    Type: String

Resources:
  websiteVpc:
    Type: AWS::EC2::VPC
    Properties: 
      CidrBlock: !Ref vpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags: 
        - Key: Name
          Value: !Ref environmentName
  internetGateway:
    Type: AWS::EC2::InternetGateway
    DependsOn:
      - websiteVpc
    Properties:
      Tags:
        - Key: Name
          Value: !Ref environmentName
  internetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    DependsOn:
      - websiteVpc
      - internetGateway
    Properties:
      InternetGatewayId: !Ref internetGateway
      VpcId: !Ref websiteVpc
  publicSubnet1:
    Type: AWS::EC2::Subnet
    DependsOn:
      - websiteVpc
    Properties:
      VpcId: !Ref websiteVpc
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: !Ref publicSubnet1Cidr
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${environmentName} Public Subnet (AZ1)
  publicRouteTable:
    Type: AWS::EC2::RouteTable
    DependsOn:
      - websiteVpc
    Properties:
      VpcId: !Ref websiteVpc
      Tags:
        - Key: Name
          Value: !Sub ${environmentName} Public Route Table
  defaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: 
    - internetGatewayAttachment
    Properties:
      RouteTableId: !Ref publicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref internetGateway
  publicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref publicRouteTable
      SubnetId: !Ref publicSubnet1
  elasticIp:
    Type: AWS::EC2::EIP
    DependsOn:
      - internetGatewayAttachment
    Properties: 
      Domain: vpc
  elasticNetworkInterface:
    Type: AWS::EC2::NetworkInterface
    Properties: 
      Description: ENI for spot instance
      SourceDestCheck: true
      SubnetId: !Ref publicSubnet1
      GroupSet: 
        - !Ref ecsInstanceSecurityGroup
      Tags: 
        - Key: Name
          Value: !Ref environmentName
  elasticIpAssociation:
    Type: AWS::EC2::EIPAssociation
    DependsOn:
      - elasticIp
      - elasticNetworkInterface
    Properties:
      AllocationId: !GetAtt elasticIp.AllocationId 
      NetworkInterfaceId: !Ref elasticNetworkInterface
  ecsInstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    DependsOn:
      - websiteVpc
    Properties:
      GroupName: ecs-access-from-internet
      GroupDescription: Web and SSH from anywhere to ECS Auto Scaling Instances
      SecurityGroupIngress:
      - CidrIp: !Ref sourceCidr
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22
      - CidrIp: !Ref sourceCidr
        FromPort: 443
        IpProtocol: tcp
        ToPort: 443
      - CidrIp: !Ref sourceCidr
        FromPort: 80
        IpProtocol: tcp
        ToPort: 80
      VpcId: !Ref websiteVpc
  efsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    DependsOn:
      - websiteVpc
    Properties:
      GroupName: efs-access-from-ecs
      GroupDescription: Security Group for EFS access from ECS
      SecurityGroupIngress:
      - IpProtocol: tcp
        SourceSecurityGroupId: !Ref ecsInstanceSecurityGroup
        FromPort: 2049
        ToPort: 2049
      VpcId: !Ref websiteVpc
  efsFs:
    Type: AWS::EFS::FileSystem
    Properties: 
      Encrypted: false
      FileSystemTags: 
        - Key: Name
          Value: !Ref environmentName
      PerformanceMode: generalPurpose
      ThroughputMode: bursting
  efsMountPoint:
    Type: AWS::EFS::MountTarget
    DependsOn:
      - efsFs
    Properties: 
      FileSystemId: !Ref efsFs
      SecurityGroups: 
        - !Ref efsSecurityGroup
      SubnetId: !Ref publicSubnet1
  ecsTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    DependsOn:
      - efsFs
      - efsMountPoint
    Properties: 
      ContainerDefinitions: 
        - Name: nginx
          Hostname: nginx
          Cpu: 128
          Memory: 256
          MemoryReservation: 256
          Image: jasonneurohr/private:nginx
          Essential: true
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
              Protocol: tcp
            - ContainerPort: 443
              HostPort: 443
              Protocol: tcp
          Links:
            - web:web
          MountPoints:
            - ContainerPath: /etc/letsencrypt
              SourceVolume: nginx-letsencrypt
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
        - Name: web
          Hostname: web
          Cpu: 256
          Memory: 256
          MemoryReservation: 256
          Image: jasonneurohr/private:web
          Essential: true
          PortMappings:
            - ContainerPort: 5000
              HostPort: 5000
              Protocol: tcp
          Links:
            - webapi:webapi
            - contactsvc:contactsvc
            - notifysvc:notifysvc
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
          Environment:
            - Name: ApiLocation
              Value: http://webapi:55000
            - Name: contactsvcEndpoint
              Value: contactsvc:5001
            - Name: notifysvcEndpoint
              Value: notifysvc:5002
            - Name: SendGridApiKey
              Value: asdf
        - Name: webapi
          Hostname: webapi
          Cpu: 128
          Memory: 256
          MemoryReservation: 256
          Image: jasonneurohr/private:webapi
          Essential: true
          PortMappings:
            - ContainerPort: 55000
              HostPort: 55000
              Protocol: tcp
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
          Environment:
            - Name: dbName
              Value: asdf
            - Name: dbServer
              Value: asdf
            - Name: dbUser
              Value: asdf
          Secrets:
            - Name: dbPassword
              ValueFrom: asdf
        - Name: notifysvc
          Hostname: notifysvc
          Cpu: 128
          Memory: 128
          Image: jasonneurohr/private:notifysvc
          Essential: true
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
        - Name: contactsvc
          Hostname: contactsvc
          Cpu: 128
          Memory: 128
          Image: jasonneurohr/private:contactsvc
          Essential: true
          RepositoryCredentials:
            CredentialsParameter: asdf
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/website
              awslogs-region: ap-southeast-2
              awslogs-stream-prefix: ecs
      ExecutionRoleArn: arn:aws:iam::asdf:role/ecsTaskExecutionRole
      Family: website-ecs
      RequiresCompatibilities: 
        - EC2
      Tags: 
        - Key: Name
          Value: !Ref environmentName
      Volumes: 
        - Name: nginx-letsencrypt
          Host:
            SourcePath: /mnt/efs
  ecsCluster:
    Type: AWS::ECS::Cluster
    Properties: 
      ClusterName: website-cluster
      Tags: 
        - Key: Name
          Value: !Ref environmentName
  ecsService:
    Type: AWS::ECS::Service
    DependsOn:
      - ecsAutoScalingGroup
    Properties: 
      Cluster: !Ref ecsCluster
      DeploymentConfiguration: 
        MaximumPercent: 100
        MinimumHealthyPercent: 0
      DesiredCount: 1
      EnableECSManagedTags: true
      LaunchType: EC2
      PlacementStrategies: 
        - Field: attribute:ecs.availability-zone
          Type: spread
        - Field: instanceId
          Type: spread
      SchedulingStrategy: REPLICA
      ServiceName: website-service
      Tags: 
        - Key: Name
          Value: !Ref environmentName
      TaskDefinition: !Ref ecsTaskDefinition
  ecsAutoScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    DependsOn:
      - autoScalingRole
      - ecsCluster
      - ecsService
    Properties: 
      MaxCapacity: 1
      MinCapacity: 0
      ResourceId: !Join [ "/", [ "service", !Ref ecsCluster, !GetAtt ecsService.Name ] ]
      RoleARN: !GetAtt autoScalingRole.Arn
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs
  cloudWatchLogsGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 7
  websiteLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    DependsOn:
      - elasticNetworkInterface
      - instanceProfile
      - ecsCluster
    Properties: 
      LaunchTemplateData: 
        InstanceType: !Ref instanceType
        KeyName: !Ref keyName
        CreditSpecification:
          CpuCredits: standard
        ImageId:
          Fn::FindInMap:
          - ecsOptimizedAmi
          - Ref: AWS::Region
          - AMI
        IamInstanceProfile: 
          Arn: !GetAtt instanceProfile.Arn
        NetworkInterfaces:
          - NetworkInterfaceId: !Ref elasticNetworkInterface
            DeviceIndex: 0
        InstanceMarketOptions:
            MarketType: spot
            SpotOptions: 
              InstanceInterruptionBehavior: terminate
              MaxPrice: 0.01
              SpotInstanceType: one-time
        UserData:
          Fn::Base64: !Sub |
            #!/bin/bash
            export PATH=/usr/local/bin:$PATH
            yum -y --security update
            yum -y install jq
            easy_install pip
            pip install awscli
            aws configure set default.region ${AWS::Region}
            echo ECS_CLUSTER=${ecsCluster} >> /etc/ecs/ecs.config
            echo ECS_BACKEND_HOST= >> /etc/ecs/ecs.config

            cat <<EOF > /tmp/awslogs.conf
            [general]
            state_file = /var/awslogs/state/agent-state

            [/var/log/dmesg]
            file = /var/log/dmesg
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/dmesg
            initial_position = start_of_file

            [/var/log/messages]
            file = /var/log/messages
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/messages
            datetime_format = %b %d %H:%M:%S
            initial_position = start_of_file

            [/var/log/docker]
            file = /var/log/docker
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/docker
            datetime_format = %Y-%m-%dT%H:%M:%S.%f
            initial_position = start_of_file

            [/var/log/ecs/ecs-init.log]
            file = /var/log/ecs/ecs-init.log.*
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/ecs/ecs-init.log
            datetime_format = %Y-%m-%dT%H:%M:%SZ
            initial_position = start_of_file

            [/var/log/ecs/ecs-agent.log]
            file = /var/log/ecs/ecs-agent.log.*
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/ecs/ecs-agent.log
            datetime_format = %Y-%m-%dT%H:%M:%SZ
            initial_position = start_of_file

            [/var/log/ecs/audit.log]
            file = /var/log/ecs/audit.log.*
            log_group_name = ${cloudWatchLogsGroup}
            log_stream_name = %ECS_CLUSTER/%CONTAINER_INSTANCE/var/log/ecs/audit.log
            datetime_format = %Y-%m-%dT%H:%M:%SZ
            initial_position = start_of_file
            EOF

            cd /tmp && curl -sO https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py
            python /tmp/awslogs-agent-setup.py -n -r ${AWS::Region} -c /tmp/awslogs.conf

            cat <<EOF > /etc/init/cloudwatch-logs-start.conf
            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/cloudwatch-logs-start.log
            set -x
            until curl -s http://localhost:51678/v1/metadata; do sleep 1; done
            ECS_CLUSTER=\$(curl -s http://localhost:51678/v1/metadata | jq .Cluster | tr -d \")
            CONTAINER_INSTANCE=\$(curl -s http://localhost:51678/v1/metadata | jq .ContainerInstanceArn | tr -d \")
            sed -i "s|%ECS_CLUSTER|\$ECS_CLUSTER|g" /var/awslogs/etc/awslogs.conf
            sed -i "s|%CONTAINER_INSTANCE|\$CONTAINER_INSTANCE|g" /var/awslogs/etc/awslogs.conf
            chkconfig awslogs on
            service awslogs start
            end script
            EOF

            cat <<EOF > /etc/init/spot-instance-termination-notice-handler.conf
            description "Start spot instance termination handler monitoring script"
            author "Amazon Web Services"
            start on started ecs
            script
            echo \$\$ > /var/run/spot-instance-termination-notice-handler.pid
            exec /usr/local/bin/spot-instance-termination-notice-handler.sh
            end script
            pre-start script
            logger "[spot-instance-termination-notice-handler.sh]: spot instance termination notice handler started"
            end script
            EOF

            cat <<EOF > /usr/local/bin/spot-instance-termination-notice-handler.sh
            #!/bin/bash
            while sleep 5; do
            if [ -z \$(curl -Isf http://169.254.169.254/latest/meta-data/spot/termination-time)];
            then
            /bin/false
            else
            logger "[spot-instance-termination-notice-handler.sh]: spot instance termination notice detected"
            STATUS=DRAINING
            ECS_CLUSTER=\$(curl -s http://localhost:51678/v1/metadata | jq .Cluster | tr -d \")
            CONTAINER_INSTANCE=\$(curl -s http://localhost:51678/v1/metadata | jq .ContainerInstanceArn | tr -d \")
            logger "[spot-instance-termination-notice-handler.sh]: putting instance in state $STATUS"
            logger "[spot-instance-termination-notice-handler.sh]: running: /bin/aws ecs update-container-instances-state --cluster \$ECS_CLUSTER --container-instances $CONTAINER_INSTANCE --status \$STATUS"
            /bin/aws ecs update-container-instances-state --cluster $ECS_CLUSTER --container-instances \$CONTAINER_INSTANCE --status \$STATUS
            logger "[spot-instance-termination-notice-handler.sh]: running: \"/bin/aws sns publish --topic-arn ${snsTopicForSpotInstanceMonitorScript} --message \"Spot instance termination notice detected. Details: cluster: \$ECS_CLUSTER, container_instance: \$CONTAINER_INSTANCE. Putting instance in state \$STATUS.\""
            /bin/aws sns publish --topic-arn ${snsTopicForSpotInstanceMonitorScript} --message "Spot instance termination notice detected. Details: cluster: \$ECS_CLUSTER, container_instance: \$CONTAINER_INSTANCE. Putting instance in state \$STATUS."
            logger "[spot-instance-termination-notice-handler.sh]: putting myself to sleep..."
            sleep 120
            fi
            done
            EOF

            chmod +x /usr/local/bin/spot-instance-termination-notice-handler.sh
            mkdir /mnt/efs
            yum install -y amazon-efs-utils
            cp /etc/fstab /etc/fstab.bak
            echo "${efsFs}:/ /mnt/efs efs defaults,_netdev 0 0" | tee -a /etc/fstab
            mount -a
            # aws s3 cp s3://temp-jasonneurohr/letsencrypt /mnt/efs/ --recursive
            
  # add to the bottom of the launch template user data for initial EFS seeing only
  # aws s3 cp s3://temp-jasonneurohr/letsencrypt /mnt/efs/ --recursive

  ecsAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    DependsOn:
      - efsMountPoint
      - snsTopicForAutoScalingGroup
    Properties: 
      AutoScalingGroupName: ecs-website
      AvailabilityZones: 
        - !Select [ 0, !GetAZs '' ]
      Cooldown: 300
      DesiredCapacity: 1
      HealthCheckGracePeriod: 0
      HealthCheckType: EC2
      LaunchTemplate: 
        LaunchTemplateId: !Ref websiteLaunchTemplate
        Version: !GetAtt websiteLaunchTemplate.LatestVersionNumber
      MaxSize: 1
      MinSize: 1
      NotificationConfigurations: 
        - TopicARN: !Ref snsTopicForAutoScalingGroup
          NotificationTypes:
            - autoscaling:EC2_INSTANCE_LAUNCH
            - autoscaling:EC2_INSTANCE_LAUNCH_ERROR
            - autoscaling:EC2_INSTANCE_TERMINATE
            - autoscaling:EC2_INSTANCE_TERMINATE_ERROR
            - autoscaling:TEST_NOTIFICATION
      Tags: 
        - Key: Name
          Value: !Ref environmentName
          PropagateAtLaunch: true
      TerminationPolicies: 
        - Default
  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: "*"
  instanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action:
          - sts:AssumeRole
          Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
        Version: 2012-10-17
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
      Path: /
      Policies:
      - PolicyDocument:
          Statement:
          - Action:
            - ecs:UpdateContainerInstancesState
            Effect: Allow
            Resource: '*'
          Version: 2012-10-17
        PolicyName: ecsUpdateContainerInstancesStatePolicy
      - PolicyDocument:
          Statement:
          - Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            - logs:DescribeLogStreams
            Effect: Allow
            Resource: arn:aws:logs:*:*:*
          Version: 2012-10-17
        PolicyName: cloudWatchLogsPolicy
      - PolicyName: ec2-read-temp-jasonneurohr-s3-bucket
        PolicyDocument:
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Action:
              - s3:Get*
              - s3:List*
            Resource: 
              - arn:aws:s3:::temp-jasonneurohr
              - arn:aws:s3:::temp-jasonneurohr/*
      - PolicyDocument:
          Statement:
          - Action:
            - sns:Publish
            Effect: Allow
            Resource: !Ref snsTopicForSpotInstanceMonitorScript
          Version: 2012-10-17
        PolicyName: snsPublishPolicy
  instanceProfile:
    Type: AWS::IAM::InstanceProfile
    DependsOn:
    - instanceRole
    Properties:
      Path: /
      Roles:
      - Ref: instanceRole
  snsTopicForAutoScalingGroup:
    Type: AWS::SNS::Topic
    Properties: 
      DisplayName: ecs-website-autoscalingroup
      Subscription: 
        - Endpoint: !Ref asgNotificationEp
          Protocol: email
      TopicName: ecs-website-autoscalingroup
  snsTopicForSpotInstanceMonitorScript:
    Type: AWS::SNS::Topic
    Properties: 
      DisplayName: ecs-website-spotinstancemonitor
      Subscription: 
        - Endpoint: !Ref asgNotificationEp
          Protocol: email
      TopicName: ecs-website-spotinstancemonitor

References