Search code examples
amazon-web-servicesaws-cloudformationamazon-ecsamazon-elbaws-fargate

HTTP to HTTPS redirect in CloudFormation template for ECS Fargate


I have the below template used for creating a stack to do hosting with ECS Fargate on AWS.

AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation template for Storefront SSR hosting.
Parameters:
  VPC:
    Type: AWS::EC2::VPC::Id
  SubnetA:
    Type: AWS::EC2::Subnet::Id
  SubnetB:
    Type: AWS::EC2::Subnet::Id
  Certificate:
    Type: String
    # Update with the certificate ARN from Certificate Manager, which must exist in the same region.
    # In our case, it is storefront.domain.com
    Default: 'arn:aws:acm:us-east-1:5xxxxxxxxx3:certificate/1abedeff-ee6c-46c2-ac44-603dfd14dac1'
  Image:
    Type: String
    # Update with the Docker image. "You can use images in the Docker Hub registry or specify other repositories (repository-url/image:tag)."
    Default: 5xxxxxxxxx3.dkr.ecr.us-east-1.amazonaws.com/storefront-staging:latest
  ServiceName:
    Type: String
    # update with the name of the service
    Default: storefront-staging
  ContainerPort:
    Type: Number
    Default: 3000
  LoadBalancerPort:
    Type: Number
    Default: 443
  HealthCheckPath:
    Type: String
    Default: /categories
  HostedZoneName:
    Type: String
    Default: domain.com
  Subdomain:
    Type: String
    Default: storefront
  # for autoscaling
  MinContainers:
    Type: Number
    Default: 1
  # for autoscaling
  MaxContainers:
    Type: Number
    Default: 2
  # target CPU utilization (%)
  AutoScalingTargetValue:
    Type: Number
    Default: 50
Resources:
  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Join ['', [!Ref ServiceName, Cluster]]
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    # Makes sure the log group is created before it is used.
    DependsOn: LogGroup
    Properties:
      # Name of the task definition. Subsequent versions of the task definition are grouped together under this name.
      Family: !Join ['', [!Ref ServiceName, TaskDefinition]]
      # awsvpc is required for Fargate
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      # 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB
      # 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB
      # 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB
      # 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments
      # 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments
      Cpu: 256
      # 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU)
      # 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU)
      # 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU)
      # Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU)
      # Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU)
      Memory: 0.5GB
      # A role needed by ECS.
      # "The ARN of the task execution role that containers in this task can assume. All containers in this task are granted the permissions that are specified in this role."
      # "There is an optional task execution IAM role that you can specify with Fargate to allow your Fargate tasks to make API calls to Amazon ECR."
      ExecutionRoleArn: !Ref ExecutionRole
      # "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that grants containers in the task permission to call AWS APIs on your behalf."
      TaskRoleArn: !Ref TaskRole
      ContainerDefinitions:
        - Name: !Ref ServiceName
          Image: !Ref Image
          PortMappings:
            - ContainerPort: !Ref ContainerPort
          # Send logs to CloudWatch Logs
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-region: !Ref AWS::Region
              awslogs-group: !Ref LogGroup
              awslogs-stream-prefix: ecs
  # A role needed by ECS
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['', [!Ref ServiceName, ExecutionRole]]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy'
  # A role for the containers
  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['', [!Ref ServiceName, TaskRole]]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: 'sts:AssumeRole'
      # ManagedPolicyArns:
      #   -
      # Policies:
      #   -
  # A role needed for auto scaling
  AutoScalingRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['', [!Ref ServiceName, AutoScalingRole]]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole'
  ContainerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Join ['', [!Ref ServiceName, ContainerSecurityGroup]]
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: !Ref ContainerPort
          ToPort: !Ref ContainerPort
          SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription:
        !Join ['', [!Ref ServiceName, LoadBalancerSecurityGroup]]
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: !Ref LoadBalancerPort
          ToPort: !Ref LoadBalancerPort
          CidrIp: 0.0.0.0/0
  Service:
    Type: AWS::ECS::Service
    # This dependency is needed so that the load balancer is setup correctly in time
    DependsOn:
      - ListenerHTTPS
    Properties:
      ServiceName: !Ref ServiceName
      Cluster: !Ref Cluster
      TaskDefinition: !Ref TaskDefinition
      DeploymentConfiguration:
        MinimumHealthyPercent: 100
        MaximumPercent: 200
      DesiredCount: 2
      # This may need to be adjusted if the container takes a while to start up
      HealthCheckGracePeriodSeconds: 30
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          # change to DISABLED if you're using private subnets that have access to a NAT gateway
          AssignPublicIp: ENABLED
          Subnets:
            - !Ref SubnetA
            - !Ref SubnetB
          SecurityGroups:
            - !Ref ContainerSecurityGroup
      LoadBalancers:
        - ContainerName: !Ref ServiceName
          ContainerPort: !Ref ContainerPort
          TargetGroupArn: !Ref TargetGroup
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 300
      # will look for a 200 status code by default unless specified otherwise
      HealthCheckPath: !Ref HealthCheckPath
      HealthCheckTimeoutSeconds: 5
      UnhealthyThresholdCount: 2
      HealthyThresholdCount: 2
      Name: !Join ['', [!Ref ServiceName, TargetGroup]]
      Port: !Ref ContainerPort
      Protocol: HTTP
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 300 # default is 300
      TargetType: ip
      VpcId: !Ref VPC
  ListenerHTTPS:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn: !Ref LoadBalancer
      Port: !Ref LoadBalancerPort
      Protocol: HTTPS
      Certificates:
        - CertificateArn: !Ref Certificate
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      LoadBalancerAttributes:
        # this is the default, but is specified here in case it needs to be changed
        - Key: idle_timeout.timeout_seconds
          Value: 60
      Name: !Join ['', [!Ref ServiceName, LoadBalancer]]
      # "internal" is also an option
      Scheme: internet-facing
      SecurityGroups:
        - !Ref LoadBalancerSecurityGroup
      Subnets:
        - !Ref SubnetA
        - !Ref SubnetB
  DNSRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: !Join ['', [!Ref HostedZoneName, .]]
      Name: !Join ['', [!Ref Subdomain, ., !Ref HostedZoneName, .]]
      Type: A
      AliasTarget:
        DNSName: !GetAtt LoadBalancer.DNSName
        HostedZoneId: !GetAtt LoadBalancer.CanonicalHostedZoneID
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Join ['', [/ecs/, !Ref ServiceName, TaskDefinition]]
  AutoScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MinCapacity: !Ref MinContainers
      MaxCapacity: !Ref MaxContainers
      ResourceId: !Join ['/', [service, !Ref Cluster, !GetAtt Service.Name]]
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs
      # "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that allows Application Auto Scaling to modify your scalable target."
      RoleARN: !GetAtt AutoScalingRole.Arn
  AutoScalingPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Join ['', [!Ref ServiceName, AutoScalingPolicy]]
      PolicyType: TargetTrackingScaling
      ScalingTargetId: !Ref AutoScalingTarget
      TargetTrackingScalingPolicyConfiguration:
        PredefinedMetricSpecification:
          PredefinedMetricType: ECSServiceAverageCPUUtilization
        ScaleInCooldown: 10
        ScaleOutCooldown: 10
        # Keep things at or lower than 50% CPU utilization, for example
        TargetValue: !Ref AutoScalingTargetValue
Outputs:
  Endpoint:
    Description: Endpoint
    Value: !Join ['', ['https://', !Ref DNSRecord]]

It is working fine, but the template does not have HTTP->HTTPS redirect. After the service is created, I have to manually add a listener on port 80 to ECS Load Balancer, so it would redirect to HTTPS as redirecting to HTTPS://#{host}:443/#{path}?#{query}. Further, my security group currently only supports incoming traffic on port 443, while I need to also allow incoming traffic on 80.

Further, my setup has the below:

  Subdomain:
    Type: String
    Default: storefront

What do I do to support naked domains eg domain.com without a prefix?


Solution

  • To redirect http to https, the following two changes need to be made:

    1. Add port 80 to LoadBalancerSecurityGroup
    2. Add listener ListenerHTTP

    The changes were made in the following version of your template:

    AWSTemplateFormatVersion: 2010-09-09
    Description: CloudFormation template for Storefront SSR hosting.
    Parameters:
      VPC:
        Type: AWS::EC2::VPC::Id
      SubnetA:
        Type: AWS::EC2::Subnet::Id
      SubnetB:
        Type: AWS::EC2::Subnet::Id
      Certificate:
        Type: String
        # Update with the certificate ARN from Certificate Manager, which must exist in the same region.
        # In our case, it is storefront.domain.com
        Default: 'arn:aws:acm:us-east-1:5xxxxxxxxx3:certificate/1abedeff-ee6c-46c2-ac44-603dfd14dac1'
      Image:
        Type: String
        # Update with the Docker image. "You can use images in the Docker Hub registry or specify other repositories (repository-url/image:tag)."
        Default: 5xxxxxxxxx3.dkr.ecr.us-east-1.amazonaws.com/storefront-staging:latest
      ServiceName:
        Type: String
        # update with the name of the service
        Default: storefront-staging
      ContainerPort:
        Type: Number
        Default: 3000
      LoadBalancerPort:
        Type: Number
        Default: 443
      HealthCheckPath:
        Type: String
        Default: /categories
      HostedZoneName:
        Type: String
        Default: domain.com
      Subdomain:
        Type: String
        Default: storefront
      # for autoscaling
      MinContainers:
        Type: Number
        Default: 1
      # for autoscaling
      MaxContainers:
        Type: Number
        Default: 2
      # target CPU utilization (%)
      AutoScalingTargetValue:
        Type: Number
        Default: 50
    Resources:
    
      Cluster:
        Type: AWS::ECS::Cluster
        Properties:
          ClusterName: !Join ['', [!Ref ServiceName, Cluster]]
    
      TaskDefinition:
        Type: AWS::ECS::TaskDefinition
        # Makes sure the log group is created before it is used.
        DependsOn: LogGroup
        Properties:
          # Name of the task definition. Subsequent versions of the task definition are grouped together under this name.
          Family: !Join ['', [!Ref ServiceName, TaskDefinition]]
          # awsvpc is required for Fargate
          NetworkMode: awsvpc
          RequiresCompatibilities:
            - FARGATE
          # 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB
          # 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB
          # 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB
          # 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments
          # 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments
          Cpu: 256
          # 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU)
          # 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU)
          # 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU)
          # Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU)
          # Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU)
          Memory: 0.5GB
          # A role needed by ECS.
          # "The ARN of the task execution role that containers in this task can assume. All containers in this task are granted the permissions that are specified in this role."
          # "There is an optional task execution IAM role that you can specify with Fargate to allow your Fargate tasks to make API calls to Amazon ECR."
          ExecutionRoleArn: !Ref ExecutionRole
          # "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that grants containers in the task permission to call AWS APIs on your behalf."
          TaskRoleArn: !Ref TaskRole
          ContainerDefinitions:
            - Name: !Ref ServiceName
              Image: !Ref Image
              PortMappings:
                - ContainerPort: !Ref ContainerPort
              # Send logs to CloudWatch Logs
              LogConfiguration:
                LogDriver: awslogs
                Options:
                  awslogs-region: !Ref AWS::Region
                  awslogs-group: !Ref LogGroup
                  awslogs-stream-prefix: ecs
    
      # A role needed by ECS
      ExecutionRole:
        Type: AWS::IAM::Role
        Properties:
          RoleName: !Join ['', [!Ref ServiceName, ExecutionRole]]
          AssumeRolePolicyDocument:
            Statement:
              - Effect: Allow
                Principal:
                  Service: ecs-tasks.amazonaws.com
                Action: 'sts:AssumeRole'
          ManagedPolicyArns:
            - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy'
    
      # A role for the containers
      TaskRole:
        Type: AWS::IAM::Role
        Properties:
          RoleName: !Join ['', [!Ref ServiceName, TaskRole]]
          AssumeRolePolicyDocument:
            Statement:
              - Effect: Allow
                Principal:
                  Service: ecs-tasks.amazonaws.com
                Action: 'sts:AssumeRole'
          # ManagedPolicyArns:
          #   -
          # Policies:
          #   -
      # A role needed for auto scaling
    
      AutoScalingRole:
        Type: AWS::IAM::Role
        Properties:
          RoleName: !Join ['', [!Ref ServiceName, AutoScalingRole]]
          AssumeRolePolicyDocument:
            Statement:
              - Effect: Allow
                Principal:
                  Service: ecs-tasks.amazonaws.com
                Action: 'sts:AssumeRole'
          ManagedPolicyArns:
            - 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole'
    
      ContainerSecurityGroup:
        Type: AWS::EC2::SecurityGroup
        Properties:
          GroupDescription: !Join ['', [!Ref ServiceName, ContainerSecurityGroup]]
          VpcId: !Ref VPC
          SecurityGroupIngress:
            - IpProtocol: tcp
              FromPort: !Ref ContainerPort
              ToPort: !Ref ContainerPort
              SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
    
      LoadBalancerSecurityGroup:
        Type: AWS::EC2::SecurityGroup
        Properties:
          GroupDescription:
            !Join ['', [!Ref ServiceName, LoadBalancerSecurityGroup]]
          VpcId: !Ref VPC
          SecurityGroupIngress:
            - IpProtocol: tcp
              FromPort: !Ref LoadBalancerPort
              ToPort: !Ref LoadBalancerPort
              CidrIp: 0.0.0.0/0
            - IpProtocol: tcp
              FromPort: 80
              ToPort: 80
              CidrIp: 0.0.0.0/0  
    
      Service:
        Type: AWS::ECS::Service
        # This dependency is needed so that the load balancer is setup correctly in time
        DependsOn:
          - ListenerHTTPS
        Properties:
          ServiceName: !Ref ServiceName
          Cluster: !Ref Cluster
          TaskDefinition: !Ref TaskDefinition
          DeploymentConfiguration:
            MinimumHealthyPercent: 100
            MaximumPercent: 200
          DesiredCount: 2
          # This may need to be adjusted if the container takes a while to start up
          HealthCheckGracePeriodSeconds: 30
          LaunchType: FARGATE
          NetworkConfiguration:
            AwsvpcConfiguration:
              # change to DISABLED if you're using private subnets that have access to a NAT gateway
              AssignPublicIp: ENABLED
              Subnets:
                - !Ref SubnetA
                - !Ref SubnetB
              SecurityGroups:
                - !Ref ContainerSecurityGroup
          LoadBalancers:
            - ContainerName: !Ref ServiceName
              ContainerPort: !Ref ContainerPort
              TargetGroupArn: !Ref TargetGroup
    
      TargetGroup:
        Type: AWS::ElasticLoadBalancingV2::TargetGroup
        Properties:
          HealthCheckIntervalSeconds: 300
          # will look for a 200 status code by default unless specified otherwise
          HealthCheckPath: !Ref HealthCheckPath
          HealthCheckTimeoutSeconds: 5
          UnhealthyThresholdCount: 2
          HealthyThresholdCount: 2
          Name: !Join ['', [!Ref ServiceName, TargetGroup]]
          Port: !Ref ContainerPort
          Protocol: HTTP
          TargetGroupAttributes:
            - Key: deregistration_delay.timeout_seconds
              Value: 300 # default is 300
          TargetType: ip
          VpcId: !Ref VPC
    
      ListenerHTTP:
        Type: AWS::ElasticLoadBalancingV2::Listener
        Properties:
          DefaultActions:
            - Type: "redirect"
              RedirectConfig:
                Protocol: "HTTPS"
                Port: 443
                Host: "#{host}"
                Path: "/#{path}"
                Query: "#{query}"
                StatusCode: "HTTP_301"
          LoadBalancerArn: !Ref LoadBalancer
          Port: 80
          Protocol: HTTP
    
      ListenerHTTPS:
        Type: AWS::ElasticLoadBalancingV2::Listener
        Properties:
          DefaultActions:
            - TargetGroupArn: !Ref TargetGroup
              Type: forward
          LoadBalancerArn: !Ref LoadBalancer
          Port: !Ref LoadBalancerPort
          Protocol: HTTPS
          Certificates:
            - CertificateArn: !Ref Certificate
    
      LoadBalancer:
        Type: AWS::ElasticLoadBalancingV2::LoadBalancer
        Properties:
          LoadBalancerAttributes:
            # this is the default, but is specified here in case it needs to be changed
            - Key: idle_timeout.timeout_seconds
              Value: 60
          Name: !Join ['', [!Ref ServiceName, LoadBalancer]]
          # "internal" is also an option
          Scheme: internet-facing
          SecurityGroups:
            - !Ref LoadBalancerSecurityGroup
          Subnets:
            - !Ref SubnetA
            - !Ref SubnetB
    
      DNSRecord:
        Type: AWS::Route53::RecordSet
        Properties:
          HostedZoneName: !Join ['', [!Ref HostedZoneName, .]]
          Name: !Join ['', [!Ref Subdomain, ., !Ref HostedZoneName, .]]
          Type: A
          AliasTarget:
            DNSName: !GetAtt LoadBalancer.DNSName
            HostedZoneId: !GetAtt LoadBalancer.CanonicalHostedZoneID
    
      LogGroup:
        Type: AWS::Logs::LogGroup
        Properties:
          LogGroupName: !Join ['', [/ecs/, !Ref ServiceName, TaskDefinition]]
    
      AutoScalingTarget:
        Type: AWS::ApplicationAutoScaling::ScalableTarget
        Properties:
          MinCapacity: !Ref MinContainers
          MaxCapacity: !Ref MaxContainers
          ResourceId: !Join ['/', [service, !Ref Cluster, !GetAtt Service.Name]]
          ScalableDimension: ecs:service:DesiredCount
          ServiceNamespace: ecs
          # "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that allows Application Auto Scaling to modify your scalable target."
          RoleARN: !GetAtt AutoScalingRole.Arn
    
      AutoScalingPolicy:
        Type: AWS::ApplicationAutoScaling::ScalingPolicy
        Properties:
          PolicyName: !Join ['', [!Ref ServiceName, AutoScalingPolicy]]
          PolicyType: TargetTrackingScaling
          ScalingTargetId: !Ref AutoScalingTarget
          TargetTrackingScalingPolicyConfiguration:
            PredefinedMetricSpecification:
              PredefinedMetricType: ECSServiceAverageCPUUtilization
            ScaleInCooldown: 10
            ScaleOutCooldown: 10
            # Keep things at or lower than 50% CPU utilization, for example
            TargetValue: !Ref AutoScalingTargetValue
    
    Outputs:
      Endpoint:
        Description: Endpoint
        Value: !Join ['', ['https://', !Ref DNSRecord]]
    

    To support naked domain you would have to get ACM certificate for such domain and remove Subdomain from DNSRecord.