Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add healthcheck path to app manifest #739

Merged
merged 1 commit into from
Mar 13, 2020

Conversation

SoManyHs
Copy link
Contributor

Closes #597.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@SoManyHs SoManyHs requested a review from a team as a code owner March 13, 2020 04:36
@SoManyHs SoManyHs requested a review from kohidave March 13, 2020 04:36
@SoManyHs
Copy link
Contributor Author

Note: Should probably add the healthcheck route in app show, but will add that pending #733!

@SoManyHs
Copy link
Contributor Author

app init now writes the following manifest:

$ cat ecs-project/brownies/manifest.yml 
# The manifest for the "brownies" application.
# Read the full specification for the "Load Balanced Web App" type at:
#  https://github.com/aws/amazon-ecs-cli-v2/wiki/Manifests#load-balanced-web-app

# Your application name will be used in naming your resources like log groups, services, etc.
name: brownies
# The "architecture" of the application you're running.
type: Load Balanced Web App

image:
  # Path to your application's Dockerfile.
  build: trivia-backend/Dockerfile
  # Port exposed through your container to route traffic to it.
  port: 80

http:
  # Requests to this path will be forwarded to your service.
  path: 'brownies'
  # You can specify a custom health check path. The default is "/"
  healthcheck: '/'

# Number of CPU units for the task.
cpu: 256
# Amount of memory in MiB used by the task.
memory: 512
# Number of tasks that should be running in your service.
count: 1

# Optional fields for more advanced use-cases.
#
#variables:                    # Pass environment variables as key value pairs.
#  LOG_LEVEL: info
#
#secrets:                      # Pass secrets from AWS Systems Manager (SSM) Parameter Store.
#  GITHUB_TOKEN: GITHUB_TOKEN  # The key is the name of the environment variable, the value is the name of the SSM parameter.
#
#scaling:                      # Optional configuration for scaling your service.
#  minCount: 1                   # Minimum number of tasks that should be running in your service.
#  maxCount: 3                   # Maximum number of tasks that should be running in your service.
#
#  # If the target value is crossed, ECS starts adding or removing tasks.
#  targetCPU: 75.0               # Target average CPU utilization percentage.

# You can override any of the values defined above by environment.
#environments:
#  test:
#    count: 2               # Number of tasks to run for the "test" environment.

@SoManyHs
Copy link
Contributor Author

app deploy results in the following CFN:

AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation template that represents a load balanced web application on Amazon ECS.
Parameters:
  ProjectName:
    Type: String
    Default: backend
  EnvName:
    Type: String
    Default: hotdog
  AppName:
    Type: String
    Default: brownies
  ContainerImage:
    Type: String
    Default: 794715269151.dkr.ecr.us-east-2.amazonaws.com/backend/brownies:fd52b49
  ContainerPort:
    Type: Number
    Default: 80
  RulePath:
    Type: String
    Default: 'brownies'
  TaskCPU:
    Type: String
    Default: '256'
  TaskMemory:
    Type: String
    Default: '512'
  TaskCount:
    Type: Number
    Default: 1
  HTTPSEnabled:
    Type: String
    AllowedValues: [true, false]
    Default: 'false'
  LogRetention:
    Type: Number
    Default: 30
  AddonsTemplateURL:
    Description: 'URL of the addons nested stack template within the S3 bucket.'
    Type: String
    Default: ""
  HealthCheckPath:
    Type: String
    Default: '/'
Conditions:
  HTTPLoadBalancer:
    !Not
      - !Condition HTTPSLoadBalancer
  HTTPSLoadBalancer:
    !Equals [!Ref HTTPSEnabled, true]
  HasAddons: # If a bucket URL is specified, that means the template exists.
    !Not [!Equals [!Ref AddonsTemplateURL, ""]]
Resources:
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Join ['', [/ecs/, !Ref ProjectName, '-', !Ref EnvName, '-', !Ref AppName]]
      RetentionInDays: !Ref LogRetention

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    DependsOn: LogGroup
    Properties:
      Family: !Join ['', [!Ref ProjectName, '-', !Ref EnvName, '-', !Ref AppName]]
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      Cpu: !Ref TaskCPU
      Memory: !Ref TaskMemory
      ExecutionRoleArn: !Ref ExecutionRole
      TaskRoleArn: !Ref TaskRole
      ContainerDefinitions:
        - Name: !Ref AppName
          Image: !Ref ContainerImage
          PortMappings:
            - ContainerPort: !Ref ContainerPort
          # We pipe certain environment variables directly into the task definition.
          # This lets customers have access to, for example, their LB endpoint - which they'd
          # have no way of otherwise determining.
          Environment:
          - Name: ECS_CLI_PROJECT_NAME
            Value: !Sub '${ProjectName}'
          - Name: ECS_APP_DISCOVERY_ENDPOINT
            Value: !Sub '${ProjectName}.local'
          - Name: ECS_CLI_ENVIRONMENT_NAME
            Value: !Sub '${EnvName}'
          - Name: ECS_CLI_APP_NAME
            Value: !Sub '${AppName}'
          - Name: ECS_CLI_LB_DNS
            Value:
              Fn::ImportValue:
                !Sub "${ProjectName}-${EnvName}-PublicLoadBalancerDNS" 
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-region: !Ref AWS::Region
              awslogs-group: !Ref LogGroup
              awslogs-stream-prefix: ecs

  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: !Join ['', [!Ref ProjectName, '-', !Ref EnvName, '-', !Ref AppName, SecretsPolicy]]
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'ssm:GetParameters'
                  - 'secretsmanager:GetSecretValue'
                  - 'kms:Decrypt'
                Resource:
                  - !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*'
                  - !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:*'
                  - !Sub 'arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/*'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy'

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: 'DenyIAMExceptTaggedRoles'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Deny'
                Action: 'iam:*'
                Resource: '*'
              - Effect: 'Allow'
                Action: 'sts:AssumeRole'
                Resource:
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:role/*'
                Condition:
                  StringEquals:
                    'iam:ResourceTag/ecs-project': !Sub '${ProjectName}'
                    'iam:ResourceTag/ecs-environment': !Sub '${EnvName}'
        - PolicyName: 'AllowPrefixedResources'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action: '*'
                Resource:
                  - !Sub 'arn:aws:s3:::${ProjectName}-${EnvName}-*'
                  - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:*/${ProjectName}-${EnvName}-*'
                  - !Sub 'arn:aws:elasticache:${AWS::Region}:${AWS::AccountId}:*/${ProjectName}-${EnvName}-*'
                  - !Sub 'arn:aws:redshift:${AWS::Region}:${AWS::AccountId}:*:${ProjectName}-${EnvName}-*'
                  - !Sub 'arn:aws:rds:${AWS::Region}:${AWS::AccountId}:*:${ProjectName}-${EnvName}-*'
                  - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:*/${ProjectName}-${EnvName}-*'

                  - !Sub 'arn:aws:sns:${AWS::Region}:${AWS::AccountId}:${ProjectName}-${EnvName}-*'
                  - !Sub 'arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${ProjectName}-${EnvName}-*'
                  - !Sub 'arn:aws:kinesis:${AWS::Region}:${AWS::AccountId}:*/${ProjectName}-${EnvName}-*'
                  - !Sub 'arn:aws:firehose:${AWS::Region}:${AWS::AccountId}:*/${ProjectName}-${EnvName}-*'
                  - !Sub 'arn:aws:kinesisanalytics:${AWS::Region}:${AWS::AccountId}:*/${ProjectName}-${EnvName}-*'
        - PolicyName: 'AllowTaggedResources' # See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_actions-resources-contextkeys.html
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action: '*'
                Resource: '*'
                Condition:
                  StringEquals:
                    'aws:ResourceTag/ecs-project': !Sub '${ProjectName}'
                    'aws:ResourceTag/ecs-environment': !Sub '${EnvName}'
              - Effect: 'Allow'
                Action: '*'
                Resource: '*'
                Condition:
                  StringEquals:
                    'secretsmanager:ResourceTag/ecs-project': !Sub '${ProjectName}'
                    'secretsmanager:ResourceTag/ecs-environment': !Sub '${EnvName}'
        - PolicyName: 'CloudWatchMetricsAndDashboard'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'cloudwatch:PutMetricData'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'cloudwatch:GetDashboard'
                  - 'cloudwatch:ListDashboards'
                  - 'cloudwatch:PutDashboard'
                  - 'cloudwatch:ListMetrics'
                Resource: '*'
  DiscoveryService:
    Type: AWS::ServiceDiscovery::Service
    Properties:
      Description: Discovery Service for the ECS CLI V2 services
      DnsConfig:
        RoutingPolicy: MULTIVALUE
        DnsRecords:
          - TTL: 60
            Type: A
          - TTL: 60
            Type: SRV
      HealthCheckCustomConfig:
        FailureThreshold: 1
      Name:  !Ref AppName
      NamespaceId:
        Fn::ImportValue:
          !Sub '${ProjectName}-${EnvName}-ServiceDiscoveryNamespaceID'
  Service:
    Type: AWS::ECS::Service
    DependsOn: WaitUntilListenerRuleIsCreated
    Properties:
      Cluster:
        Fn::ImportValue:
          !Sub '${ProjectName}-${EnvName}-ClusterId'
      TaskDefinition: !Ref TaskDefinition
      DeploymentConfiguration:
        MinimumHealthyPercent: 100
        MaximumPercent: 200
      DesiredCount: !Ref TaskCount
      # This may need to be adjusted if the container takes a while to start up
      HealthCheckGracePeriodSeconds: 60
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets:
            - Fn::Select:
              - 0
              - Fn::Split:
                - ','
                - Fn::ImportValue: !Sub '${ProjectName}-${EnvName}-PublicSubnets'
            - Fn::Select:
              - 1
              - Fn::Split:
                - ','
                - Fn::ImportValue: !Sub '${ProjectName}-${EnvName}-PublicSubnets'
          SecurityGroups:
            - Fn::ImportValue: !Sub '${ProjectName}-${EnvName}-EnvironmentSecurityGroup'
      LoadBalancers:
        - ContainerName: !Ref AppName
          ContainerPort: !Ref ContainerPort
          TargetGroupArn: !Ref TargetGroup
      ServiceRegistries:
        - RegistryArn: !GetAtt DiscoveryService.Arn
          Port: !Ref ContainerPort

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      #  Check if your application is healthy within 20 = 10*2 seconds, compared to 2.5 mins = 30*5 seconds.
      HealthCheckIntervalSeconds: 10 # Default is 30.
      HealthyThresholdCount: 2       # Default is 5.
      HealthCheckTimeoutSeconds: 5
      HealthCheckPath: !Ref HealthCheckPath
      Port: !Ref ContainerPort
      Protocol: HTTP
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 60                  # Default is 300.
      TargetType: ip
      VpcId:
        Fn::ImportValue:
          !Sub "${ProjectName}-${EnvName}-VpcId"

  LoadBalancerDNSAlias:
    Type: AWS::Route53::RecordSetGroup
    Condition: HTTPSLoadBalancer
    Properties:
      HostedZoneId:
        Fn::ImportValue:
          !Sub "${ProjectName}-${EnvName}-HostedZone"
      Comment: !Sub "LoadBalancer alias for app ${AppName}"
      RecordSets:
      - Name:
          !Join
            - '.'
            - - !Ref AppName
              - Fn::ImportValue:
                  !Sub "${ProjectName}-${EnvName}-SubDomain"
              - ""
        Type: A
        AliasTarget:
          HostedZoneId:
            Fn::ImportValue:
              !Sub "${ProjectName}-${EnvName}-CanonicalHostedZoneID"
          DNSName:
            Fn::ImportValue:
              !Sub "${ProjectName}-${EnvName}-PublicLoadBalancerDNS"

  RulePriorityFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          'use strict';const aws=require("aws-sdk");let defaultResponseURL,report=function(a,b,c,d,e,f){return new Promise((g,h)=>{const i=require("https"),{URL:j}=require("url");var k=JSON.stringify({Status:c,Reason:f,PhysicalResourceId:d||b.logStreamName,StackId:a.StackId,RequestId:a.RequestId,LogicalResourceId:a.LogicalResourceId,Data:e});const l=new j(a.ResponseURL||defaultResponseURL),m={hostname:l.hostname,port:443,path:l.pathname+l.search,method:"PUT",headers:{"Content-Type":"","Content-Length":k.length}};i.request(m).on("error",h).on("response",a=>{a.resume(),400<=a.statusCode?h(new Error(`Error ${a.statusCode}: ${a.statusMessage}`)):g()}).end(k,"utf8")})};const calculateNextRulePriority=async function(a){var b,c=new aws.ELBv2,d=[];do{const e=await c.describeRules({ListenerArn:a,Marker:b}).promise();d=d.concat(e.Rules),b=e.NextMarker}while(b);let e=1;if(0<d.length){const a=d.map(a=>"default"===a.Priority?0:parseInt(a.Priority)),b=Math.max(...a);e=b+1}return e};exports.nextAvailableRulePriorityHandler=async function(a,b){var c,d,e={};try{switch(a.RequestType){case"Create":d=await calculateNextRulePriority(a.ResourceProperties.ListenerArn),e.Priority=d,c=`alb-rule-priority-${a.LogicalResourceId}`;break;case"Update":case"Delete":c=a.PhysicalResourceId;break;default:throw new Error(`Unsupported request type ${a.RequestType}`);}await report(a,b,"SUCCESS",c,e)}catch(d){console.log(`Caught error ${d}.`),await report(a,b,"FAILED",c,null,d.message)}},exports.withDefaultResponseURL=function(a){defaultResponseURL=a};
      Handler: "index.nextAvailableRulePriorityHandler"
      Timeout: 600
      MemorySize: 512
      Role: !GetAtt 'CustomResourceRole.Arn'
      Runtime: nodejs10.x

  CustomResourceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: "DNSandACMAccess"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
                - elasticloadbalancing:DescribeRules
              Resource: "*"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  HTTPSRulePriorityAction:
    Condition: HTTPSLoadBalancer
    Type: Custom::RulePriorityFunction
    Properties:
      ServiceToken: !GetAtt RulePriorityFunction.Arn
      ListenerArn:
        Fn::ImportValue:
          !Sub "${ProjectName}-${EnvName}-HTTPSListenerArn"

  HTTPSListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Condition: HTTPSLoadBalancer
    Properties:
      Actions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      Conditions:
        - Field: 'host-header'
          HostHeaderConfig:
            Values:
              - Fn::Join:
                - '.'
                - - !Ref AppName
                  - Fn::ImportValue:
                      !Sub "${ProjectName}-${EnvName}-SubDomain"
      ListenerArn:
        Fn::ImportValue:
          !Sub "${ProjectName}-${EnvName}-HTTPSListenerArn"
      Priority: !GetAtt HTTPSRulePriorityAction.Priority

  HTTPRulePriorityAction:
    Condition: HTTPLoadBalancer
    Type: Custom::RulePriorityFunction
    Properties:
      ServiceToken: !GetAtt RulePriorityFunction.Arn
      ListenerArn:
        Fn::ImportValue:
          !Sub "${ProjectName}-${EnvName}-HTTPListenerArn"

  HTTPListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Condition: HTTPLoadBalancer
    Properties:
      Actions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward
      Conditions:
        - Field: 'path-pattern'
          PathPatternConfig:
            Values:
            - !Sub "/${RulePath}"
            - !Sub "/${RulePath}/*"
      ListenerArn:
        Fn::ImportValue:
          !Sub "${ProjectName}-${EnvName}-HTTPListenerArn"
      Priority: !GetAtt HTTPRulePriorityAction.Priority

  # Force a conditional dependency from the ECS service on the listener rules.
  # Our service depends on our HTTP/S listener to be set up before it can
  # be created. But, since our environment is either HTTPS or not, we
  # have a conditional dependency (we have to wait for the HTTPS listener
  # to be created or the HTTP listener to be created). In order to have a
  # conditional dependency, we use the WaitHandle resource as a way to force
  # a single dependency. The Ref in the WaitCondition implicitly creates a conditional
  # dependency - if the condition is satisfied (HTTPLoadBalancer) - the ref resolves
  # the HTTPWaitHandle, which depends on the HTTPListenerRule.

  HTTPSWaitHandle:
    Condition: HTTPSLoadBalancer
    DependsOn: HTTPSListenerRule
    Type: AWS::CloudFormation::WaitConditionHandle

  HTTPWaitHandle:
    Condition: HTTPLoadBalancer
    DependsOn: HTTPListenerRule
    Type: AWS::CloudFormation::WaitConditionHandle

  # We don't actually need to wait for the condition to
  # be completed, that's why we set a count of 0. The timeout
  # is a required field, but useless, so we set it to one.
  WaitUntilListenerRuleIsCreated:
    Type: AWS::CloudFormation::WaitCondition
    Properties:
      Handle: !If [HTTPLoadBalancer, !Ref HTTPWaitHandle, !Ref HTTPSWaitHandle]
      Timeout: "1"
      Count: 0

  AddonsStack:
    Type: AWS::CloudFormation::Stack
    Condition: HasAddons
    Properties:
      Parameters:
        Project: !Ref ProjectName
        Env: !Ref EnvName
        App: !Ref AppName
      TemplateURL:
        !Ref AddonsTemplateURL

@efekarakus efekarakus added this to In review in Sprint 🏃‍♀️ via automation Mar 13, 2020
Copy link
Contributor

@efekarakus efekarakus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 Looks good! A suggestion below:

templates/lb-fargate-service/manifest.yml Outdated Show resolved Hide resolved
Copy link
Contributor

@kohidave kohidave left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! One suggestion and a plus one to Efe’s point! THANK YOU 🌿

internal/pkg/manifest/lb_fargate_manifest.go Show resolved Hide resolved
@SoManyHs SoManyHs merged commit 454b596 into aws:master Mar 13, 2020
Sprint 🏃‍♀️ automation moved this from In review to Pending release Mar 13, 2020
@SoManyHs SoManyHs deleted the healthcheck branch April 10, 2020 04:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Sprint 🏃‍♀️
  
Pending release
Development

Successfully merging this pull request may close these issues.

Expose health check path in manifest
3 participants