cloudformation.yaml (569 lines of code) (raw):
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"