toolshed-ecs-cluster.yaml
---

Generic ECS Cluster Template

This CloudFormation template will build an ECS cluster.

AWSTemplateFormatVersion: '2010-09-09' Description: ECS Cluster Base

Parameters

These are the input parameters for this template. All of these parameters must be supplied for this template to be deployed.

Parameters: AppSlug: MinLength: '3' Type: String Description: Short application slug, ie 'toolshed'. Lowercase letters, numbers and dashes only AllowedPattern: "[a-z0-9-]*" EnvSlug: MinLength: '2' Type: String Description: Short environment slug, ie 'dev', or 'markdev'. Lowercase letters, numbers and dashes only AllowedPattern: "[a-z0-9]*" VPCID: Description: Target VPC Type: AWS::EC2::VPC::Id HostedZoneName: Type: String Description: Name of the Route53 Hosted Zone Default: "uits-prod-aws.arizona.edu"

Load Balancer Settings

InstanceSubnets: Type: List<AWS::EC2::Subnet::Id> Description: private subnets for instances

Launch Template Version

LaunchTemplateVersion: Description: Launch Template Version Type: String Default: 1

Load Balancer Settings

LBSubnets: Type: List<AWS::EC2::Subnet::Id> Description: public subnets for load balancer SSLCertificateARN: Type: String Description: Full ARN of the SSL Certificate to use on the load balancer DockerImage: Description: 'Default Site Docker Image, i.e.: toolshed/toolshed-default:latest' Type: String KeyName: Description: Amazon EC2 Key Pair Type: AWS::EC2::KeyPair::KeyName EcsImageId: Description: Latest AMI Amazon built specifically for ECS Type : AWS::SSM::Parameter::Value<String> Default: /aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id AllowedValues: - /aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id ServiceTag: Description: Refers to the application (Uaccess Learning, Uaccess Employee, Uaccess Student) Type: String EnvironmentTag: Description: Type of environment that is using this resource, such as 'dev', 'tst', 'prd'. Type: String ContactNetidTag: Description: NetID of person most familiar with resource Type: String AccountNumberTag: Description: Identifies the financial system account number Type: String SubAccountTag: Description: Identifies the financial system subaccount number Type: String TicketNumberTag: Description: Jira Ticket Number Type: String

Metadata

Metadata is mostly for organizing and presenting Parameters in a better way when using CloudFormation in the AWS Web UI.

Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Application Information Parameters: - AppSlug - EnvSlug - DockerImage - Label: default: Instance Settings Parameters: - VPCID - InstanceSubnets - LaunchTemplateVersion - KeyName - EcsImageId - Label: default: Load Balancer Settings Parameters: - LBSubnets - HostedZoneName - SSLCertificateARN - Label: default: Tags Parameters: - ServiceTag - SubAccountTag - EnvironmentTag - AccountNumberTag - ContactNetidTag - TicketNumberTag

Resources

These are all of the actual AWS resources created for this application.

Resources:

Instance Role

This is the IAM role that will be applied to the ECS Instances. Any AWS specific permissions that the node might need should be defined here.

EnvInstanceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM

Instance Profile

This is just a little construct to connect a set of roles together into a profile. The profile is referenced by ec2 instances.

EnvInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: "/" Roles: - !Ref EnvInstanceRole

Instance Security Group

Security group for the host nodes themselves. Needs to permit incoming traffice from the ELB, and any other authorized incoming sources.

InstanceSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Allow Load Balancer and SSH to host VpcId: !Ref VPCID SecurityGroupIngress: - Description: CC 317 Wired Ports IpProtocol: tcp FromPort: '22' ToPort: '22' CidrIp: 128.196.135.64/26 - Description: InfraDev VPN IpProtocol: tcp FromPort: '22' ToPort: '22' CidrIp: 150.135.112.0/24 - Description: ben.uits bastion IpProtocol: tcp FromPort: '22' ToPort: '22' CidrIp: 128.196.130.211/32 - Description: "Incoming Traffic from Load Balancer" IpProtocol: tcp FromPort: '1024' ToPort: '65535' SourceSecurityGroupId: !Ref AlbSecurityGroup Tags: - Key: service Value: !Ref ServiceTag - Key: environment Value: !Ref EnvironmentTag - Key: contactnetid Value: !Ref ContactNetidTag - Key: accountnumber Value: !Ref AccountNumberTag - Key: ticketnumber Value: !Ref TicketNumberTag - Key: subaccount Value: !Ref SubAccountTag

Launch Template

Create an EC2 Launch Template for toolshed hosts

InstanceLaunchTemplate: Type: "AWS::EC2::LaunchTemplate" Properties: LaunchTemplateName: !Sub "${AWS::StackName}-launchtemplate" LaunchTemplateData: ImageId: !Ref EcsImageId KeyName: !Ref KeyName InstanceType: t3.micro IamInstanceProfile: Name: !Ref EnvInstanceProfile NetworkInterfaces: - AssociatePublicIpAddress: "false" DeviceIndex: "0" Groups: - !Ref InstanceSecurityGroup TagSpecifications: - ResourceType: volume Tags: - Key: service Value: !Ref ServiceTag - Key: environment Value: !Ref EnvironmentTag - Key: contactnetid Value: !Ref ContactNetidTag - Key: accountnumber Value: !Ref AccountNumberTag - Key: ticketnumber Value: !Ref TicketNumberTag UserData: Fn::Base64: !Sub | #!/bin/bash echo ECS_CLUSTER=${AppSlug}-${EnvSlug}-ECS >> /etc/ecs/ecs.config EcsInstanceAsg: Type: AWS::AutoScaling::AutoScalingGroup DependsOn: EcsCluster Properties: VPCZoneIdentifier: !Ref InstanceSubnets LaunchTemplate: LaunchTemplateId: !Ref InstanceLaunchTemplate Version: !Ref LaunchTemplateVersion MinSize: 0 MaxSize: 1 DesiredCapacity: 1 Tags: - Key: Name Value: !Sub "${AWS::StackName} ECS Host" PropagateAtLaunch: 'true' - Key: Description Value: "This instance is the part of the Auto Scaling group which was created through CloudFormation" PropagateAtLaunch: 'true' - Key: service Value: !Ref ServiceTag PropagateAtLaunch: 'true' - Key: environment Value: !Ref EnvironmentTag PropagateAtLaunch: 'true' - Key: contactnetid Value: !Ref ContactNetidTag PropagateAtLaunch: 'true' - Key: accountnumber Value: !Ref AccountNumberTag PropagateAtLaunch: 'true' - Key: ticketnumber Value: !Ref "TicketNumberTag" PropagateAtLaunch: 'true' - Key: subaccount Value: !Ref SubAccountTag PropagateAtLaunch: 'true' #ECS Cluster EcsCluster: Type: "AWS::ECS::Cluster" #Need to make sure the LB is created before the ECS cluster is created Properties: ClusterName: !Sub "${AppSlug}-${EnvSlug}-ECS"

Load Balancer

The load balancer (ALB) constructor along with the Security Group that allows client traffic to the ALB on ports 80 & 443

LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internet-facing Subnets: !Ref LBSubnets LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: '120' SecurityGroups: - !Ref AlbSecurityGroup Tags: - Key: service Value: !Ref ServiceTag - Key: environment Value: !Ref EnvironmentTag - Key: contactnetid Value: !Ref ContactNetidTag - Key: accountnumber Value: !Ref AccountNumberTag - Key: ticketnumber Value: !Ref TicketNumberTag

Route53 DNS Record

DNS name to point at the load balancer

DnsRecord: Type: AWS::Route53::RecordSet Properties: HostedZoneName: !Sub "${HostedZoneName}." Name: !Sub "${AppSlug}.${HostedZoneName}." Type: "CNAME" TTL: "200" ResourceRecords: - !GetAtt LoadBalancer.DNSName

Add to Load Balancer

Target Group

Define the Target Group for adding Instances to the ALB as well as the health checks for those Instances

AlbTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckIntervalSeconds: 60 UnhealthyThresholdCount: 10 HealthCheckPath: / Matcher: HttpCode: "200-399" Port: 80 Protocol: HTTP VpcId: !Ref VPCID Tags: - Key: service Value: !Ref ServiceTag - Key: environment Value: !Ref EnvironmentTag - Key: contactnetid Value: !Ref ContactNetidTag - Key: accountnumber Value: !Ref AccountNumberTag - Key: ticketnumber Value: !Ref TicketNumberTag

ALB Security Group

Create the Security Group for the ALB here in the base template so it can be referenced here. Exported for use in the host template.

AlbSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: 'Allow external traffic to load balancer' VpcId: !Ref VPCID SecurityGroupIngress: - CidrIp: 0.0.0.0/0 IpProtocol: "tcp" FromPort: "80" ToPort: "80" - CidrIp: 0.0.0.0/0 IpProtocol: "tcp" FromPort: "443" ToPort: "443" Tags: - Key: "Name" Value: !Sub "${AWS::StackName}-alb-sg" - Key: service Value: !Ref ServiceTag - Key: environment Value: !Ref EnvironmentTag - Key: contactnetid Value: !Ref ContactNetidTag - Key: accountnumber Value: !Ref AccountNumberTag - Key: ticketnumber Value: !Ref TicketNumberTag

ALB Listeners definitions

AlbListener80: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: redirect RedirectConfig: Host: '#{host}' Path: '/#{path}' Port: 443 Protocol: HTTPS Query: '#{query}' StatusCode: HTTP_301 LoadBalancerArn: !Ref LoadBalancer Port: 80 Protocol: HTTP AlbListener443: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: Ref: AlbTargetGroup LoadBalancerArn: !Ref LoadBalancer Port: 443 Protocol: HTTPS SslPolicy: 'ELBSecurityPolicy-TLS-1-2-2017-01' Certificates: - CertificateArn: !Ref SSLCertificateARN #ECS Task Definition EcsTask: Type: "AWS::ECS::TaskDefinition" Properties: Family: !Sub "${AppSlug}-default" NetworkMode: "bridge" ContainerDefinitions: - Name: !Sub "${AppSlug}-default" Essential: "true" Image: !Ref DockerImage PortMappings: - ContainerPort: "80" Hostname: !Sub "toolshed-default.${HostedZoneName}" Cpu: "10" MemoryReservation: "32" Privileged: "true" LogConfiguration: LogDriver: "awslogs" Options: awslogs-group: !Ref EcsLogGroup awslogs-region: !Ref "AWS::Region" awslogs-stream-prefix: toolshed-default

ECS Service

EcsService: Type: "AWS::ECS::Service" #Waiting for the DNS record to be created beause we know the LB has been #created if the DNS record is created, and the LB needs to be created #before the ECS service is created DependsOn: - DnsRecord - EcsLogGroup Properties: ServiceName: !Sub "${AppSlug}-default" Cluster: !Ref EcsCluster TaskDefinition: !Ref EcsTask DesiredCount: 1 Role: !Ref EcsServiceRole LoadBalancers: - ContainerName: !Sub "${AppSlug}-default" ContainerPort: "80" TargetGroupArn: !Ref AlbTargetGroup #For now we will spread across AZs PlacementStrategies: - Field: "attribute:ecs.availability-zone" Type: "spread" DeploymentConfiguration: MaximumPercent: "200" #Need to create a LogGroup in order for the ECS service to log details of the build #If this does not exist the ECS Service will not come up EcsLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "${AppSlug}-${EnvSlug}-ecs-lg" RetentionInDays: 30

ECS Service Role

EcsServiceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ecs.amazonaws.com Action: - sts:AssumeRole Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole

Outputs

Output values that can be viewed from the AWS CloudFormation console.

Outputs: VPCID: Description: Target VPC Value: !Ref VPCID Export: Name: !Sub "${AWS::StackName}-vpcid" InstanceSecurityGroup: Description: The Instance Security Group Value: !Ref InstanceSecurityGroup Export: Name: !Sub "${AWS::StackName}-instance-sg" LoadBalancer: Description: The ALB arn Value: !Ref LoadBalancer Export: Name: !Sub "${AWS::StackName}-alb-arn" AlbSecurityGroup: Description: The ALB Security Group Value: !Ref AlbSecurityGroup Export: Name: !Sub "${AWS::StackName}-alb-sg" AlbTargetGroup: Description: The ALB Target Group Value: !Ref AlbTargetGroup Export: Name: !Sub "${AWS::StackName}-alb-tg" AlbDns: Description: The ALB DNS Entry Value: !GetAtt LoadBalancer.DNSName Export: Name: !Sub "${AWS::StackName}-alb-dns" AlbListener: Description: SSL Listener Value: !Ref AlbListener443 Export: Name: !Sub "${AWS::StackName}-alb-listener" EcsCluster: Description: The ECS Cluster Value: !Ref EcsCluster Export: Name: !Sub "${AWS::StackName}-ecs"