AWSTemplateFormatVersion: 2010-09-09
Description: 1 Web Container / 2 AZ / EC2
Parameters:
  ApplicationCode:
    Type: String
    Description: Code for application used in object names

  ApplicationDnsZone:
    Type: String
    Description: DNS zone ID to place application record
  ApplicationDnsName:
    Type: String
    Description: Full DNS name to use for application record

  VPC:
    Type: AWS::EC2::VPC::Id
    Description: VPC for deployment
  PublicSubnetA:
    Type: AWS::EC2::Subnet::Id
    Description: Public subnet in Availability Zone 1
  PublicSubnetB:
    Type: AWS::EC2::Subnet::Id
    Description: Public subnet in Availability Zone 2
  PrivateSubnetA:
    Type: AWS::EC2::Subnet::Id
    Description: Private subnet in Availability Zone 1
  PrivateSubnetB:
    Type: AWS::EC2::Subnet::Id
    Description: Private subnet in Availability Zone 2

  ApplicationUrl:
    Type: String
    Description: Container registry and path for application
  RegistryLogin:
    Type: String
    Description: Contains secret manager reference for registry
  ApplicationHealthUrl:
    Type: String
    Default: /status
    Description: Relative URL for ALB health check
  ApplicationMaxCount:
    Type: Number
    Default: 5
    Description: Max number of container instances

  ApplicationVersion:
    Type: String
    Description: Container version of application

  ApplicationConfiguration:
    Type: String
    Description: Configuration variable for application
    Default: ""
  ApplicationSecrets:
    Type: String
    Description: Secrets variable for application
    Default: ""

  LatestAmiId:
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id'
    Description: AMI to use for EC2 instances

  Ec2PoolInstanceType:
    Type: String
    Default: t3a.medium
    Description: Instance type to use for EC2 Pool
  Ec2PoolMaxSize:
    Type: Number
    Default: 5
    Description: Max number of instances in EC2 Pool

Resources:
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Join ['-', [!Ref ApplicationCode, lb]]
      Type: application
      IpAddressType: ipv4
      Scheme: internet-facing
      SecurityGroups:
        - !Ref LoadBalancerSecurityGroup
      Subnets:
        - !Ref PublicSubnetA
        - !Ref PublicSubnetB

  ApplicationDns:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref ApplicationDnsZone
      Name: !Ref ApplicationDnsName
      Type: A
      AliasTarget:
        DNSName: !GetAtt "ApplicationLoadBalancer.DNSName"
        HostedZoneId: !GetAtt 'ApplicationLoadBalancer.CanonicalHostedZoneID'

  ApplicationCertificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Ref ApplicationDnsName
      ValidationMethod: DNS
      DomainValidationOptions:
        - DomainName: !Ref ApplicationDnsName
          HostedZoneId: !Ref ApplicationDnsZone

  ApplicationTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Join ['-', [!Ref ApplicationCode, tg]]
      Protocol: HTTP
      TargetType: ip
      ProtocolVersion: HTTP1
      Port: 8080
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: !Ref HealthCheckUrl
      HealthCheckPort: traffic-port
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 3
      UnhealthyThresholdCount: 2
      VpcId: !Ref VPC
      TargetGroupAttributes:
        - Key: load_balancing.algorithm.type
          Value: round_robin
        - Key: stickiness.enabled
          Value: true
        - Key: stickiness.type
          Value: app_cookie
        - Key: stickiness.app_cookie.cookie_name
          Value: AFLSID
        - Key: stickiness.app_cookie.duration_seconds
          Value: 86400

  ApplicationListener80:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ApplicationLoadBalancer
      Protocol: HTTP
      Port: 80
      DefaultActions:
        - Type: redirect
          RedirectConfig:
            Protocol: "HTTPS"
            Port: 443
            Host: "#{host}"
            Path: "/#{path}"
            Query: "#{query}"
            StatusCode: "HTTP_301"

  ApplicationListener443:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ApplicationLoadBalancer
      Protocol: HTTPS
      Port: 443
      SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
      Certificates:
        - CertificateArn: !Ref ApplicationCertificate
      DefaultActions:
        - Type: forward
          ForwardConfig:
            TargetGroups:
              - TargetGroupArn: !Ref ApplicationTargetGroup
                Weight: 100

  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Join ['-', [!Ref ApplicationCode, lb]]
      GroupDescription: !Join ['-', [!Ref ApplicationCode, lb]]
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: 0.0.0.0/0

  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['-', [!Ref ApplicationCode, taskexec]]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Policies:
        - PolicyName: !Join ['-', [!Ref ApplicationCode, taskexec]]
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - secretsmanager:GetSecretValue
                Resource: !Ref RegistryLogin

  ApplicationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['-', [!Ref ApplicationCode, app]]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: !Join ['-', [!Ref ApplicationCode, app]]
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - appconfig:StartConfigurationSession
                  - appconfig:GetLatestConfiguration
                  - secretsmanager:GetSecretValue
                Resource: "*"

  ApplicationSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Join ['-', [!Ref ApplicationCode, application]]
      GroupDescription: !Join ['-', [!Ref ApplicationCode, application]]
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0

  CloudWatchLogsGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Ref ApplicationCode
      RetentionInDays: 14

  ApplicationDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Name: application
          Image: !Sub "${ApplicationUrl}:${ApplicationVersion}"
          RepositoryCredentials:
            CredentialsParameter: !Ref RegistryLogin 
          PortMappings:
            - ContainerPort: 8080
              Protocol: tcp
          Environment:
            - Name: CONFIGURATION
              Value: !Ref ApplicationConfiguration
            - Name: SECRETS
              Value: !Ref ApplicationSecrets
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref CloudWatchLogsGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: application
      Family: !Join ['-', [!Ref ApplicationCode, application]]
      NetworkMode: awsvpc
      TaskRoleArn: !Ref ApplicationRole
      ExecutionRoleArn: !Ref ExecutionRole
      Cpu: 256
      Memory: 1024
      RuntimePlatform:
        CpuArchitecture: X86_64
        OperatingSystemFamily: LINUX

  Ec2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Join ['-', [!Ref ApplicationCode, ec2]]
      GroupDescription: !Join ['-', [!Ref ApplicationCode, ec2]]
      VpcId: !Ref VPC
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0

  Ec2InstanceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['-', [!Ref ApplicationCode, ec2]]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

  Ec2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Join ['-', [!Ref ApplicationCode, ec2]]
      Roles:
        - !Ref Ec2InstanceRole

  Ec2Launch1:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: !Join ['-', [!Ref ApplicationCode, 1]]
      LaunchTemplateData:
        ImageId: !Ref LatestAmiId
        InstanceType: !Ref Ec2PoolInstanceType
        SecurityGroupIds:
          - !GetAtt Ec2SecurityGroup.GroupId
        IamInstanceProfile:
          Arn: !GetAtt Ec2InstanceProfile.Arn
        PrivateDnsNameOptions:
          EnableResourceNameDnsAAAARecord: false
          EnableResourceNameDnsARecord: false
        UserData:
          Fn::Base64: !Sub |
            #!/bin/bash -xe
            echo ECS_CLUSTER=${Cluster} >> /etc/ecs/ecs.config

  Ec2ScalingGroup1:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AutoScalingGroupName: !Join ['-', [!Ref ApplicationCode, 1]]
      HealthCheckGracePeriod: 60
      LaunchTemplate:
        LaunchTemplateId: !Ref Ec2Launch1
        Version: !GetAtt Ec2Launch1.LatestVersionNumber
      VPCZoneIdentifier:
        - !Ref PrivateSubnetA
        - !Ref PrivateSubnetB
      NewInstancesProtectedFromScaleIn: true
      MaxSize: !Ref Ec2PoolMaxSize
      MinSize: 0
      DesiredCapacity: 1
      Cooldown: 60
      Tags:
        - Key: Name
          Value: !Join ['-', [!Ref ApplicationCode, autoscale, 1]]
          PropagateAtLaunch: "true"

  Ec2CapacityProvider1:
    Type: AWS::ECS::CapacityProvider
    Properties:
      Name: !Join ['-', [!Ref ApplicationCode, 1]]
      AutoScalingGroupProvider:
        AutoScalingGroupArn: !Ref Ec2ScalingGroup1
        ManagedScaling:
          Status: ENABLED
          TargetCapacity: 100
          MaximumScalingStepSize: 1
          InstanceWarmupPeriod: 60
        # Bug https://github.com/aws/aws-cdk/issues/14732
        ManagedTerminationProtection: DISABLED

  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Ref ApplicationCode

  ClusterCapacityProviderAssociation:
    Type: AWS::ECS::ClusterCapacityProviderAssociations
    Properties:
      Cluster: !Ref Cluster
      CapacityProviders:
        - !Ref Ec2CapacityProvider1
      DefaultCapacityProviderStrategy:
        - CapacityProvider: !Ref Ec2CapacityProvider1
          Base: 1
          Weight: 1

  ApplicationService:
    Type: AWS::ECS::Service
    DependsOn:
      - ApplicationLoadBalancer
      - ApplicationListener443
      - ClusterCapacityProviderAssociation

    Properties:
      ServiceName: application
      Cluster: !Ref Cluster
      DesiredCount: 1
      PlacementStrategies:
        - Field: MEMORY
          Type: binpack
      DeploymentController:
        Type: ECS
      TaskDefinition: !Ref ApplicationDefinition
      LoadBalancers:
        - ContainerName: application
          ContainerPort: 8080
          TargetGroupArn: !Ref ApplicationTargetGroup
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            - !Ref ApplicationSecurityGroup
          Subnets:
            - !Ref PrivateSubnetA
            - !Ref PrivateSubnetB

  AutoScalingRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['-', [!Ref ApplicationCode, autoscale]]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - application-autoscaling.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: "/"
      Policies:
        - PolicyName: !Join ['-', [!Ref ApplicationCode, autoscale]]
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - ecs:UpdateService
                  - ecs:DescribeServices
                  - application-autoscaling:*
                  - cloudwatch:DescribeAlarms
                  - cloudwatch:GetMetricStatistics
                Resource: "*"

  ApplicationAutoScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MaxCapacity: !Ref ApplicationMaxCount
      MinCapacity: 1
      ResourceId: !Join ['/', [ service, !Ref ApplicationCode, !GetAtt ApplicationService.Name ]]
      RoleARN: !GetAtt AutoScalingRole.Arn
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs

  ApplicationAutoScalingPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Join ['-', [ !Ref ApplicationCode, application, autoscale ]]
      PolicyType: TargetTrackingScaling
      ScalingTargetId: !Ref ApplicationAutoScalingTarget
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs
      TargetTrackingScalingPolicyConfiguration:
        TargetValue: 50
        ScaleInCooldown: 300
        ScaleOutCooldown: 300
        PredefinedMetricSpecification:
          PredefinedMetricType: ECSServiceAverageCPUUtilization

