Skip to content

Instantly share code, notes, and snippets.

@r0yfire
Created April 30, 2024 12:51
Show Gist options
  • Save r0yfire/2f404d3159ae9c1cab62e8602794a9a9 to your computer and use it in GitHub Desktop.
Save r0yfire/2f404d3159ae9c1cab62e8602794a9a9 to your computer and use it in GitHub Desktop.
CloudFormation template for Flagsmith Platform (https://royfirestein.com/blog/deploy-open-source-feature-flags-on-aws)
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation template for Flagsmith Platform
Parameters:
ApexDomain:
Description: The domain name for the Flagsmith Platform (e.g., "example.com")
Type: String
Default: example.com
StageName:
Description: The environment for the Flagsmith Platform (e.g., "prod")
Type: String
Default: prod
AllowedValues:
- dev
- prod
- staging
ServiceName:
Description: "Service name"
Type: String
Default: "flagsmith"
AllowedHosts:
Type: String
Description: Django allowed hosts (e.g., "*")
Default: "*"
DatabasePassword:
Type: String
Description: Password for the PostgreSQL database
NoEcho: True
DatabasePort:
Type: Number
Description: Port for the PostgreSQL database
Default: 3306
FlagsmithImage:
Type: String
Description: Docker image for the Flagsmith service
Default: flagsmith/flagsmith:latest
FlagsmithServiceSize:
Type: Number
Description: Number of ECS tasks to run for the Flagsmith service
Default: 1
SendgridApiKey:
Type: String
Description: API key for Sendgrid
NoEcho: True
Default: ""
EnableAdminPasswordAccess:
Type: String
Description: Enable admin password access
Default: "False"
AllowedValues:
- "True"
- "False"
Conditions:
CreateProdResources: !Equals
- !Ref StageName
- prod
CreateDevResources: !Not
- !Equals
- !Ref StageName
- prod
Resources:
#
# CloudWatch Logs
#
CloudWatchLogsGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref AWS::StackName
RetentionInDays: 365
#
# TLS Certificate
#
FlagsmithCertificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Sub flagsmith.${ApexDomain}
ValidationMethod: DNS
DomainValidationOptions:
- DomainName: !Sub flagsmith.${ApexDomain}
ValidationDomain: !Sub flagsmith.${ApexDomain}
#
# ALB Target Group and Listener Rules
#
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub ${AWS::StackName}
Scheme: internet-facing
Subnets:
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PublicSubnet1" ] ] }
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PublicSubnet2" ] ] }
- !If
- CreateProdResources
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PublicSubnet3" ] ] }
- !Ref AWS::NoValue
- !If
- CreateProdResources
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PublicSubnet4" ] ] }
- !Ref AWS::NoValue
SecurityGroups:
- { "Fn::ImportValue": !Join [ "-", [ "autohost-app", !Ref StageName, "LoadBalancerSecurityGroup" ] ] }
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: 60
Tags:
- Key: service
Value: !Ref ServiceName
- Key: Environment
Value: !Ref StageName
LoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: redirect
RedirectConfig:
Protocol: "HTTPS"
Port: "443"
Host: "#{host}"
Path: "/#{path}"
Query: "#{query}"
StatusCode: "HTTP_301"
LoadBalancerSecureListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn: !Ref FlagsmithCertificate
DefaultActions:
- Type: forward
TargetGroupArn: !Ref DefaultTargetGroup
DefaultTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub ${AWS::StackName}-1
VpcId: { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "VPC" ] ] }
Port: 80
Protocol: HTTP
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn: LoadBalancer
Properties:
Name: !Sub ${AWS::StackName}-2
VpcId: { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "VPC" ] ] }
Port: 80
Protocol: HTTP
Matcher:
HttpCode: 200-299
HealthCheckIntervalSeconds: 30
HealthCheckPath: /health
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
UnhealthyThresholdCount: 6
TargetType: ip
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 120
ListenerRuleSecure:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref LoadBalancerSecureListener
Priority: 5
Conditions:
- Field: path-pattern
Values:
- "/*"
Actions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
ListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref LoadBalancerListener
Priority: 5
Conditions:
- Field: path-pattern
Values:
- "/*"
Actions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
#
# Roles
#
TaskExecutionRole:
Type: AWS::IAM::Role
Properties:
Path: /
RoleName: !Sub ${AWS::StackName}-task-exec-role-${AWS::Region}
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole'
AssumeRolePolicyDocument: |
{
"Statement": [{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
}
}]
}
Policies:
- PolicyName: !Sub ${AWS::StackName}-task-exec-policy-${AWS::Region}
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Resource: "*"
Action:
- logs:CreateLogStream
- logs:CreateLogGroup
- logs:PutLogEvents
- logs:DescribeLogStreams
- ecr:GetDownloadUrlForLayer
- ecr:GetAuthorizationToken
- ecr:BatchCheckLayerAvailability
- ecr:GetRepositoryPolicy
- ecr:BatchGetImage
- ecs:ListClusters
- ecs:ListContainerInstances
- ecs:DescribeContainerInstances
- Effect: Allow
Resource: !Join [ "", [ "arn:aws:s3:::", { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "AutohostBucket" ] ] }, "/", !Ref StageName, ".env" ] ]
Action:
- s3:GetObject
- Effect: Allow
Resource: !Join [ "", [ "arn:aws:s3:::", { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "AutohostBucket" ] ] } ] ]
Action:
- s3:GetBucketLocation
TaskRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${AWS::StackName}-${AWS::Region}
Path: /
AssumeRolePolicyDocument: |
{
"Statement": [{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
}
}]
}
Policies:
- PolicyName: !Sub ${AWS::StackName}-${AWS::Region}
PolicyDocument:
{
"Statement": [ {
"Effect": "Allow",
"Action": [
"iam:PassRole",
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:*",
],
"Resource": [
{ "Fn::GetAtt": [ "EnvironmentsTable", "Arn" ] },
{ "Fn::GetAtt": [ "IdentitiesTable", "Arn" ] }
]
} ]
}
#
# RDS subnets, security group, and cluster
#
PostgresSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for the PostgreSQL instance
VpcId: { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "VPC" ] ] }
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref DatabasePort
ToPort: !Ref DatabasePort
SourceSecurityGroupId: { "Fn::ImportValue": !Join [ "-", [ "autohost-app", !Ref StageName, "FargateContainerSecurityGroup" ] ] }
- IpProtocol: "-1"
SourceSecurityGroupId: { "Fn::ImportValue": !Join [ "-", [ "autohost-app", !Ref StageName, "FargateContainerSecurityGroup" ] ] }
SecurityGroupEgress:
- IpProtocol: "-1"
CidrIp: "0.0.0.0/0"
PostgresSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnets available for the Flagsmith PostgreSQL instance
DBSubnetGroupName: !Sub ${AWS::StackName}
SubnetIds:
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet1" ] ] }
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet2" ] ] }
- !If
- CreateProdResources
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet3" ] ] }
- !Ref AWS::NoValue
- !If
- CreateProdResources
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet4" ] ] }
- !Ref AWS::NoValue
PostgresCluster:
Type: AWS::RDS::DBInstance
Condition: CreateProdResources
DependsOn:
- PostgresSecurityGroup
Properties:
DBInstanceIdentifier: !Sub ${AWS::StackName}
DBInstanceClass: db.t3.medium
DBName: flagsmith
Engine: postgres
EngineVersion: "15.3"
Port: !Ref DatabasePort
AllocatedStorage: 40
StorageType: gp2
StorageEncrypted: true
MasterUsername: postgres
MasterUserPassword: !Ref DatabasePassword
DBSubnetGroupName: !Ref PostgresSubnetGroup
VPCSecurityGroups:
- !Ref PostgresSecurityGroup
Tags:
- Key: service
Value: !Ref ServiceName
- Key: Environment
Value: !Ref StageName
PostgresInstance:
Type: AWS::RDS::DBInstance
Condition: CreateDevResources
DependsOn:
- PostgresSecurityGroup
Properties:
DBInstanceIdentifier: !Sub ${AWS::StackName}
DBInstanceClass: db.t3.medium
DBName: flagsmith
Engine: postgres
EngineVersion: "15.3"
Port: !Ref DatabasePort
AllocatedStorage: 20
StorageType: gp2
StorageEncrypted: true
MasterUsername: postgres
MasterUserPassword: !Ref DatabasePassword
DBSubnetGroupName: !Ref PostgresSubnetGroup
VPCSecurityGroups:
- !Ref PostgresSecurityGroup
Tags:
- Key: service
Value: !Ref ServiceName
- Key: Environment
Value: !Ref StageName
#
# DynamoDB Tables
#
EnvironmentsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub "${AWS::StackName}-environments"
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
Tags:
- Key: service
Value: !Ref ServiceName
- Key: Environment
Value: !Ref StageName
IdentitiesTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub "${AWS::StackName}-identities"
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
BillingMode: PAY_PER_REQUEST
Tags:
- Key: service
Value: !Ref ServiceName
- Key: Environment
Value: !Ref StageName
#
# Service Discovery
#
FlagsmithDiscoveryService:
Type: AWS::ServiceDiscovery::Service
Properties:
Name: !Sub ${AWS::StackName}
DnsConfig:
DnsRecords: [ { Type: A, TTL: "10" } ]
NamespaceId: { "Fn::ImportValue": !Join [ "-", [ "autohost-app", !Ref StageName, "ServiceDiscoveryNamespaceId" ] ] }
#
# ECS Services
#
FlagsmithService:
Type: AWS::ECS::Service
DependsOn:
- TargetGroup
- ListenerRuleSecure
Properties:
LaunchType: FARGATE
Cluster: { "Fn::ImportValue": !Join [ "-", [ "autohost-app", !Ref StageName, "ECSClusterArn" ] ] }
DesiredCount: !Ref FlagsmithServiceSize
TaskDefinition: !Ref FlagsmithTaskDefinition
EnableExecuteCommand: true
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- { "Fn::ImportValue": !Join [ "-", [ "autohost-app", !Ref StageName, "FargateContainerSecurityGroup" ] ] }
Subnets:
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet1" ] ] }
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet2" ] ] }
- !If
- CreateProdResources
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet3" ] ] }
- !Ref AWS::NoValue
- !If
- CreateProdResources
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet4" ] ] }
- !Ref AWS::NoValue
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 75
LoadBalancers:
- ContainerName: "flagsmith"
ContainerPort: 8000
TargetGroupArn: !Ref TargetGroup
ServiceRegistries:
- RegistryArn: !GetAtt FlagsmithDiscoveryService.Arn
Tags:
- Key: service
Value: !Ref ServiceName
- Key: Environment
Value: !Ref StageName
FlagsmithTaskDefinition:
Type: AWS::ECS::TaskDefinition
DependsOn:
- TargetGroup
- ListenerRuleSecure
Properties:
Family: Flagsmith
ExecutionRoleArn: !Ref TaskExecutionRole
TaskRoleArn: !Ref TaskRole
NetworkMode: awsvpc
Memory: 2048
Cpu: 1024
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: flagsmith
Essential: true
Image: !Ref FlagsmithImage
Environment:
- Name: AWS_REGION
Value: !Ref AWS::Region
- Name: ENV
Value: !Ref StageName
- !If
- CreateProdResources
- Name: ENVIRONMENT
Value: production
- Name: ENVIRONMENT
Value: !Ref StageName
- Name: STAGE_NAME
Value: !Ref StageName
- Name: DJANGO_ALLOWED_HOSTS
Value: !Ref AllowedHosts
- !If
- CreateProdResources
- Name: DATABASE_URL
Value: !Sub "postgresql://postgres:${DatabasePassword}@${PostgresCluster.Endpoint.Address}:${DatabasePort}/flagsmith"
- Name: DATABASE_URL
Value: !Sub "postgresql://postgres:${DatabasePassword}@${PostgresInstance.Endpoint.Address}:${DatabasePort}/flagsmith"
- Name: USE_POSTGRES_FOR_ANALYTICS
Value: "True"
- Name: DJANGO_SECRET_KEY
Value: !Ref DatabasePassword
- Name: USE_X_FORWARDED_HOST
Value: "True"
- Name: SENDGRID_API_KEY
Value: !Ref SendgridApiKey
- Name: SENDER_EMAIL
Value: !Sub "noreply@flagsmith.${ApexDomain}"
- Name: ENABLE_ADMIN_ACCESS_USER_PASS
Value: !Ref EnableAdminPasswordAccess
- Name: ENVIRONMENTS_TABLE_NAME_DYNAMO
Value: !Ref EnvironmentsTable
- Name: IDENTITIES_TABLE_NAME_DYNAMO
Value: !Ref IdentitiesTable
- Name: GUNICORN_KEEP_ALIVE
Value: "90"
- Name: DOMAIN_OVERRIDE
Value: !Sub "flagsmith.${ApexDomain}"
- Name: CSRF_TRUSTED_ORIGINS
Value: !Sub "flagsmith.${ApexDomain}"
Command:
# Run migrations and then start the server (use 'serve' to skip migrations)
- migrate-and-serve
PortMappings:
- ContainerPort: 8000
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Sub ${AWS::StackName}
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: 'service'
Tags:
- Key: service
Value: !Ref ServiceName
- Key: Environment
Value: !Ref StageName
# Autoscaling rules for web service
FlagsmithAutoscalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Condition: CreateProdResources
DependsOn:
- FlagsmithService
- TaskExecutionRole
Properties:
MinCapacity: !Ref FlagsmithServiceSize
MaxCapacity: 5
ResourceId: !Join [ '/', [ service, { "Fn::ImportValue": !Join [ "-", [ "autohost-app", !Ref StageName, "ECSClusterName" ] ] }, !GetAtt FlagsmithService.Name ] ]
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
RoleARN: !GetAtt TaskExecutionRole.Arn
FlagsmithAutoscalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Condition: CreateProdResources
DependsOn:
- FlagsmithService
- FlagsmithAutoscalingTarget
Properties:
PolicyName: !Sub ${AWS::StackName}
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref FlagsmithAutoscalingTarget
TargetTrackingScalingPolicyConfiguration:
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilization
ScaleInCooldown: 900
ScaleOutCooldown: 30
# Keep things at or lower than 50% CPU utilization
TargetValue: 75
# Flagsmith processor service
FlagsmithProcessorService:
Type: AWS::ECS::Service
DependsOn:
- PostgresSecurityGroup
Properties:
LaunchType: FARGATE
Cluster: { "Fn::ImportValue": !Join [ "-", [ "autohost-app", !Ref StageName, "ECSClusterArn" ] ] }
DesiredCount: 1
TaskDefinition: !Ref FlagsmithProcessorTaskDefinition
EnableExecuteCommand: true
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- { "Fn::ImportValue": !Join [ "-", [ "autohost-app", !Ref StageName, "FargateContainerSecurityGroup" ] ] }
Subnets:
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet1" ] ] }
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet2" ] ] }
- !If
- CreateProdResources
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet3" ] ] }
- !Ref AWS::NoValue
- !If
- CreateProdResources
- { "Fn::ImportValue": !Join [ "-", [ "autohost-resources", !Ref StageName, "PrivateSubnet4" ] ] }
- !Ref AWS::NoValue
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 75
Tags:
- Key: service
Value: !Ref ServiceName
- Key: Environment
Value: !Ref StageName
FlagsmithProcessorTaskDefinition:
Type: AWS::ECS::TaskDefinition
DependsOn:
- PostgresSecurityGroup
Properties:
Family: FlagsmithProcessor
ExecutionRoleArn: !Ref TaskExecutionRole
TaskRoleArn: !Ref TaskRole
NetworkMode: awsvpc
Cpu: 512
Memory: 1024
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: flagsmith_processor
Image: !Ref FlagsmithImage
Environment:
- !If
- CreateProdResources
- Name: DATABASE_URL
Value: !Sub "postgresql://postgres:${DatabasePassword}@${PostgresCluster.Endpoint.Address}:${DatabasePort}/flagsmith"
- Name: DATABASE_URL
Value: !Sub "postgresql://postgres:${DatabasePassword}@${PostgresInstance.Endpoint.Address}:${DatabasePort}/flagsmith"
- Name: USE_POSTGRES_FOR_ANALYTICS
Value: "True"
- Name: DJANGO_SECRET_KEY
Value: !Ref DatabasePassword
- Name: USE_X_FORWARDED_HOST
Value: "True"
- Name: SENDGRID_API_KEY
Value: !Ref SendgridApiKey
- Name: SENDER_EMAIL
Value: !Sub "noreply@flagsmith.${ApexDomain}"
- Name: ENABLE_ADMIN_ACCESS_USER_PASS
Value: !Ref EnableAdminPasswordAccess
- Name: ENVIRONMENTS_TABLE_NAME_DYNAMO
Value: !Ref EnvironmentsTable
- Name: IDENTITIES_TABLE_NAME_DYNAMO
Value: !Ref IdentitiesTable
- Name: DOMAIN_OVERRIDE
Value: !Sub "flagsmith.${ApexDomain}"
- Name: CSRF_TRUSTED_ORIGINS
Value: !Sub "flagsmith.${ApexDomain}"
- !If
- CreateProdResources
- Name: ENVIRONMENT
Value: production
- Name: ENVIRONMENT
Value: !Ref StageName
Command:
- run-task-processor
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Sub ${AWS::StackName}
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: 'service'
Tags:
- Key: service
Value: !Ref ServiceName
- Key: Environment
Value: !Ref StageName
#
# Route 53
#
DNS:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName: !Sub ${ApexDomain}.
RecordSets:
- Name: !Sub flagsmith.${ApexDomain}
Type: A
AliasTarget:
HostedZoneId: !GetAtt LoadBalancer.CanonicalHostedZoneID
DNSName: !GetAtt LoadBalancer.DNSName
Outputs:
FlagsmithServiceUrl:
Description: URL for accessing the Flagsmith service
Value: !Sub "https://flagsmith-service.${ApexDomain}"
Export:
Name: !Sub ${AWS::StackName}-ServiceUrl
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment