AWSTemplateFormatVersion: "2010-09-09"
Description: Avatar API
Parameters:
  Stage:
    Description: Environment name. Must be CAPITALISED, e.g. 'PROD', or 'CODE'.
    Type: String
  Stack:
    Description: Stack name
    Type: String
    Default: discussion
  App:
    Description: App name
    Type: String
    Default: avatar-api
  GithubTeamName:
    Description:
      Github team name to give ssh access (must have been registered first
      with the keys lambda)
    Type: String
    Default: Discussion
  VpcId:
    Description: ID of the VPC onto which to launch the application eg. vpc-1234abcd
    Type: AWS::SSM::Parameter::Value<AWS::EC2::VPC::Id>
    Default: /account/vpc/primary/id
  PrivateVpcSubnets:
    Description: Subnets to use in VPC for public internet-facing ELB eg. subnet-abcd1234
    Type: AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>>
    Default: /account/vpc/primary/subnets/private
  PublicVpcSubnets:
    Description: Subnets to use in VPC for public internet-facing ELB eg. subnet-abcd1234
    Type: AWS::SSM::Parameter::Value<List<AWS::EC2::Subnet::Id>>
    Default: /account/vpc/primary/subnets/public
  TopicSendEmail:
    Description: ARN for an SNS topic, used to send Cloudwatch alerts to discussion dev team
    Type: AWS::SSM::Parameter::Value<String>
  AvatarEventsTopic:
    Description: ARN of an SNS topic used to publish Avatar events
    Type: AWS::SSM::Parameter::Value<String>
  AvatarDeletionQueue:
    Description: ARN of the SQS queue for user deleted events
    Type: AWS::SSM::Parameter::Value<String>
  AvatarDeletionQueueUrl:
    Description: URL of the SQS queue for user deleted events
    Type: AWS::SSM::Parameter::Value<String>
  DynamoTable:
    Description: ARN of an Dynamo table containing Avatar data
    Type: AWS::SSM::Parameter::Value<String>
  IncomingBucket:
    Description: Name of S3 bucket for incoming Avatars
    Type: AWS::SSM::Parameter::Value<String>
  RawBucket:
    Description: Name of S3 bucket for raw Avatars
    Type: AWS::SSM::Parameter::Value<String>
  ProcessedBucket:
    Description: Name of S3 bucket for processed Avatars
    Type: AWS::SSM::Parameter::Value<String>
  OriginBucket:
    Description: Name of S3 bucket for origin Avatars
    Type: AWS::SSM::Parameter::Value<String>
  PrivateBucket:
    Description: Name of S3 bucket containing private credentials
    Type: AWS::SSM::Parameter::Value<String>
    Default: /account/services/private.config.bucket
  FileBucket:
    Description: Name of S3 bucket containing dist files
    Type: AWS::SSM::Parameter::Value<String>
    Default: /account/services/artifact.bucket
  LoggingStream:
    Type: AWS::SSM::Parameter::Value<String>
    Description: SSM parameter containing the Kinesis logging stream ARN
    Default: /account/services/logging.stream
  LoggingStreamName:
    Type: AWS::SSM::Parameter::Value<String>
    Description: SSM parameter containing the name of the Kinesis logging stream
    Default: /account/services/logging.stream.name
  AMI:
    Description: AMI to use for instances
    Type: AWS::EC2::Image::Id
    Default: ami-e9b3858f
  IdentityAccessToken:
    Description: Access token used to authenticate requests to identity API
    Type: AWS::SSM::Parameter::Value<String>
  SSLCert:
    Description: ARN of the SSL certificate to use for the ELB
    Type: AWS::SSM::Parameter::Value<String>
Mappings:
  Stage:
    CODE:
      StageLower: code
      ApiUrl: https://avatar.code.dev-theguardian.com
      HostedZone: avatar-aws.code.dev-guardianapis.com

      InstanceType: t4g.micro
      MinSize: "1"
      MaxSize: "3"

      IdentityApiUrl: https://idapi.code.dev-theguardian.com
      IdentityOktaIssuer: https://profile.code.dev-theguardian.com/oauth2/aus3v9gla95Toj0EE0x7
      IdentityOktaAudience: https://profile.code.dev-theguardian.com/
    PROD:
      StageLower: prod
      ApiUrl: https://avatar.theguardian.com
      HostedZone: avatar-aws.guardianapis.com

      InstanceType: t4g.small
      MinSize: "2"
      MaxSize: "8"

      IdentityApiUrl: https://idapi.theguardian.com
      IdentityOktaIssuer: https://profile.theguardian.com/oauth2/aus3xgj525jYQRowl417
      IdentityOktaAudience: https://profile.theguardian.com/

Conditions:
  IsProd: !Equals
    - !Ref "Stage"
    - PROD
Resources:
  InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref "Role"
  Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM
      Path: /
      Policies:
        - PolicyName: AvatarPolicy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - s3:Get*
                  - s3:List*
                Resource:
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "PrivateBucket"
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "PrivateBucket"
                      - /
                      - !Ref "Stage"
                      - /*
              - Resource:
                  - !Ref "DynamoTable"
                  - !Join
                    - ""
                    - - !Ref "DynamoTable"
                      - /index/*
                Action:
                  - dynamodb:*
                Effect: Allow
              - Effect: Allow
                Action:
                  - ec2:DescribeInstances
                  - ec2:DescribeTags
                Resource: "*"
              - Effect: Allow
                Action:
                  - s3:ListBucket
                Resource:
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "RawBucket"
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "ProcessedBucket"
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "OriginBucket"
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:DeleteObject
                Resource:
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "RawBucket"
                      - /*
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "ProcessedBucket"
                      - /*
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "OriginBucket"
                      - /*
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "IncomingBucket"
                      - /*
              - Effect: Allow
                Action:
                  - cloudwatch:PutMetricData
                Resource: "*"
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref "AvatarEventsTopic"
              - Effect: Allow
                Action:
                  - sqs:ReceiveMessage*
                  - sqs:DeleteMessage*
                  - sqs:SendMessage*
                Resource: !Ref "AvatarDeletionQueue"
        - PolicyName: GetArtifactPolicy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                Resource:
                  - !Join
                    - ""
                    - - "arn:aws:s3:::"
                      - !Ref "FileBucket"
                      - /*
              - Effect: Allow
                Action:
                  - s3:ListBucket
                Resource: !Join
                  - ""
                  - - "arn:aws:s3:::"
                    - !Ref "FileBucket"
        - PolicyName: ELKKinesisPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - kinesis:PutRecord
                  - kinesis:PutRecords
                  - kinesis:DescribeStream
                Resource: !Ref LoggingStream
        - PolicyName: describe-ec2-policy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Resource: "*"
                Action:
                  - ec2:DescribeTags
                  - ec2:DescribeInstances
                  - autoscaling:DescribeAutoScalingGroups
                  - autoscaling:DescribeAutoScalingInstances
  ElasticLoadBalancer:
    Type: AWS::ElasticLoadBalancing::LoadBalancer
    Properties:
      CrossZone: true
      Policies:
        - PolicyName: ELBSecurityPolicy
          Attributes:
            - Name: Reference-Security-Policy
              Value: ELBSecurityPolicy-TLS-1-2-2017-01
          PolicyType: SSLNegotiationPolicyType
      Listeners:
        - Protocol: HTTPS
          PolicyNames: [ELBSecurityPolicy]
          LoadBalancerPort: "443"
          InstancePort: "8080"
          SSLCertificateId: !Ref "SSLCert"
      HealthCheck:
        Target: HTTP:8080/v1/service/healthcheck
        Timeout: "10"
        Interval: "20"
        UnhealthyThreshold: "10"
        HealthyThreshold: "2"
      Subnets: !Ref "PublicVpcSubnets"
      SecurityGroups:
        - !Ref "ElasticLoadBalancerSecurityGroup"
      Tags:
        - Key: App
          Value: !Ref "App"
  AppServerGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AvailabilityZones: !GetAZs ""
      VPCZoneIdentifier: !Ref "PrivateVpcSubnets"
      LaunchTemplate:
        LaunchTemplateId: !Ref "LaunchTemplate"
        Version: !GetAtt "LaunchTemplate.LatestVersionNumber"
      MinSize: !FindInMap
        - Stage
        - !Ref "Stage"
        - MinSize
      MaxSize: !FindInMap
        - Stage
        - !Ref "Stage"
        - MaxSize
      HealthCheckType: ELB
      HealthCheckGracePeriod: 300
      LoadBalancerNames:
        - !Ref "ElasticLoadBalancer"
      Tags:
        - Key: Stage
          Value: !Ref "Stage"
          PropagateAtLaunch: "true"
        - Key: Stack
          Value: !Ref "Stack"
          PropagateAtLaunch: "true"
        - Key: App
          Value: !Ref "App"
          PropagateAtLaunch: "true"
        - Key: Name
          Value: !Join
            - ":"
            - - !Ref "Stage"
              - avatar-api
          PropagateAtLaunch: "true"
        - Key: LogKinesisStreamName
          Value: !Ref LoggingStreamName
          PropagateAtLaunch: true
        - Key: SystemdUnit
          Value: avatar-api.service
          PropagateAtLaunch: true
  DnsRecord:
    Type: AWS::Route53::RecordSetGroup
    Properties:
      HostedZoneName: !Join
        - ""
        - - !FindInMap [ Stage, !Ref Stage, HostedZone ]
          - .
      Comment: Alias to avatar api ELB
      RecordSets:
        - Name: !Join
            - .
            - - avatar
              - !FindInMap [ Stage, !Ref Stage, HostedZone ]
          Type: CNAME
          TTL: "300"
          ResourceRecords:
            - !GetAtt "ElasticLoadBalancer.DNSName"
  LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: !Sub "${Stage}-${Stack}-avatar-api"
      LaunchTemplateData:
        ImageId: !Ref "AMI"
        SecurityGroupIds:
          - !Ref "AppSecurityGroup"
        InstanceType: !FindInMap
          - Stage
          - !Ref "Stage"
          - InstanceType
        IamInstanceProfile:
          Arn: !GetAtt "InstanceProfile.Arn"
        KeyName: aws-discussion
        MetadataOptions:
          HttpTokens: required
        UserData: !Base64
          Fn::Join:
            - "\n"
            - - "#!/bin/bash"
              - |
                # Configure disk monitoring
                cat > /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json <<__END__
                {
                  "metrics": {
                    "append_dimensions": {
                      "AutoScalingGroupName": "\${aws:AutoScalingGroupName}",
                      "ImageId": "\${aws:ImageId}",
                      "InstanceId": "\${aws:InstanceId}",
                      "InstanceType": "\${aws:InstanceType}"
                    },
                    "aggregation_dimensions": [
                      ["AutoScalingGroupName"]
                    ],
                    "metrics_collected": {
                      "disk": {
                        "resources": [
                          "/"
                        ],
                        "measurement": [
                          "disk_used_percent"
                        ]
                      }
                    }
                  }
                }
                __END__
                amazon-cloudwatch-agent-ctl -s -a append-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
              - !Join
                - ""
                - - aws s3 cp s3://
                  - !Ref "FileBucket"
                  - /
                  - !Ref "Stack"
                  - /
                  - !Ref "Stage"
                  - /
                  - !Ref "App"
                  - /avatar-api.tgz
                  - " avatar-api.tar.gz"
              - mkdir /opt
              - mv avatar-api.tar.gz /opt/
              - cd /opt
              - tar vxzf avatar-api.tar.gz
              - cd avatar-api/
              - !Join
                - ""
                - - aws s3 cp s3://
                  - !Ref "PrivateBucket"
                  - /
                  - !Ref "Stage"
                  - /avatar-api-keys-echo.sh .
              - cat >/etc/default/avatar-api << EOF
              - !Join
                - ""
                - - API_URL=
                  - !FindInMap [ Stage, !Ref Stage, ApiUrl ]
              - !Join
                - ""
                - - STAGE=
                  - !Ref "Stage"
              - !Join
                - ""
                - - STAGE_LOWER=
                  - !FindInMap [ Stage, !Ref Stage, StageLower ]
              - !Join
                - ""
                - - SNS_TOPIC_ARN=
                  - !Ref "AvatarEventsTopic"
              - !Join
                - ""
                - - SQS_DELETED_URL=
                  - !Ref "AvatarDeletionQueueUrl"
              - !Join
                - ""
                - - IDENTITY_API_URL=
                  - !FindInMap
                    - Stage
                    - !Ref Stage
                    - IdentityApiUrl
              - !Join
                - ""
                - - IDENTITY_ACCESS_TOKEN=
                  - !Ref IdentityAccessToken
              - !Join
                - ""
                - - IDENTITY_OKTA_ISSUER=
                  - !FindInMap
                    - Stage
                    - !Ref Stage
                    - IdentityOktaIssuer
              - !Join
                - ""
                - - IDENTITY_OKTA_AUDIENCE=
                  - !FindInMap
                    - Stage
                    - !Ref Stage
                    - IdentityOktaAudience
              - $(source avatar-api-keys-echo.sh)
              - EOF
              - rm avatar-api-keys-echo.sh
              - adduser --disabled-password avatar-api
              - chown -R avatar-api /opt/avatar-api
              - mv /opt/avatar-api/conf/avatar-api.service /etc/systemd/system/avatar-api.service
              - systemctl enable avatar-api && systemctl start avatar-api
  ElasticLoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow public access over HTTP(S)
      VpcId: !Ref "VpcId"
      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"
  AppSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SSH and HTTP
      VpcId: !Ref "VpcId"
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: "8080"
          ToPort: "8080"
          SourceSecurityGroupId: !Ref "ElasticLoadBalancerSecurityGroup"
  HighLatencyAlarmAlert:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "discussion-avatar-api-${Stage} high load balancer latency"
      AlarmDescription: Load balancer latency is at least three seconds for the last three minutes
      Namespace: AWS/ELB
      Dimensions:
        - Name: LoadBalancerName
          Value: !Ref "ElasticLoadBalancer"
      MetricName: Latency
      Statistic: Average
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Threshold: "3"
      Period: "60"
      EvaluationPeriods: "3"
      AlarmActions: !If
        - IsProd
        - - !Ref "TopicSendEmail"
        - !Ref "AWS::NoValue"
  HighDiskSpaceUtilizationAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "discussion-avatar-api-${Stage} low disk space"
      AlarmDescription: Disk space utilization is more than 70% for the last minute
      Namespace: CWAgent
      Dimensions:
        - Name: AutoScalingGroupName
          Value: !Ref "AppServerGroup"
      MetricName: disk_used_percent
      Statistic: Maximum
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Threshold: "70"
      Period: "300"
      EvaluationPeriods: "1"
      AlarmActions: !If
        - IsProd
        - - !Ref "TopicSendEmail"
        - !Ref "AWS::NoValue"
      InsufficientDataActions: !If
        - IsProd
        - - !Ref "TopicSendEmail"
        - !Ref "AWS::NoValue"
  High5xxAlarmAlert:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub "discussion-avatar-api-${Stage} high 5XX errors"
      AlarmDescription: Avatar API returned at least 100 5XX errors in the last minute
      Namespace: AWS/ELB
      Dimensions:
        - Name: LoadBalancerName
          Value: !Ref "ElasticLoadBalancer"
      MetricName: HTTPCode_Backend_5XX
      Statistic: Sum
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Threshold: "100"
      Period: "60"
      EvaluationPeriods: "1"
      AlarmActions: !If
        - IsProd
        - - !Ref "TopicSendEmail"
        - !Ref "AWS::NoValue"
Outputs:
  DnsRecord:
    Value: !Ref "DnsRecord"
  DNSName:
    Value: !GetAtt "ElasticLoadBalancer.DNSName"
