Skip to main content

Deploy Flagsmith on AWS using CloudFormation

· 17 min read
Roy Firestein

In this post, we'll deploy Flagsmith on AWS using CloudFormation. Flagsmith is an open-source feature flag and remote configuration service. It allows you to manage feature flags and remote configuration for your applications.

What are feature flags?

Feature flags, also known as feature toggles, are a software development practice that allows teams to modify system behavior without changing code. They act as switches that can turn features on or off in a live environment, providing a high level of control over a software's functionality.

Imagine a light switch in your home; you can easily turn the lights on or off without rewiring the electrical system. Similarly, feature flags enable developers to toggle features for users without deploying new code. This approach is especially useful for testing new features, performing A/B testing, or enabling a gradual rollout to users.

At their core, feature flags are implemented through conditional statements in the code that check the status of a flag. If the flag is active, the new feature is presented to the user; if not, the system defaults to the existing functionality. The status of these flags is typically controlled through a feature management system that can be updated in real-time.

Example use cases for feature flags include:

  • Testing in Production: Feature flags allow developers to test new features in the production environment with a subset of users. This is known as canary releasing. By exposing the feature to a small group, you can gather valuable feedback and ensure stability before a wider release.
  • Gradual Rollouts: Instead of launching a feature to all users at once, feature flags enable a gradual rollout. This helps in monitoring the feature's impact on system performance and user experience, and quickly rolling it back if issues arise.
  • User Segmentation: Feature flags can be used to enable features for specific user segments. For example, you might roll out a new feature only to premium users or to users in a particular geographic location.
  • A/B Testing: By employing feature flags, teams can perform A/B testing to compare different user experiences and determine which version performs better in terms of user engagement, conversion rates, and other metrics.
  • Emergency Kill Switch: If a newly released feature causes unexpected issues, feature flags serve as an emergency kill switch. Developers can quickly disable the problematic feature, mitigating any negative impact on the user experience.

Architecture

We'll deploy Flagsmith on AWS using a CloudFormation. The architecture consists of the following components:

  • Amazon Certificate Manager: We'll use ACM to provision SSL/TLS certificates for securing the application's traffic.
  • Amazon Route 53: We'll use Route 53 to manage the DNS records for the application's domain name.
  • Amazon RDS (Relational Database Service): We'll use RDS to host the PostgreSQL database for storing feature flags and configuration data. We are using RDS PostgreSQL Cluster with Multi-AZ deployment for high availability and fault tolerance, deployed in private subnets.
  • Amazon Application Load Balancer: We'll use an ALB to route incoming traffic to the ECS service. The ALB will be configured with a listener for HTTPS traffic, deployed in a public subnet.
  • Flagsmith API: The Flagsmith API is a RESTful service that provides endpoints for managing feature flags and configurations. This service is deployed as a Docker container on ECS in a private subnet.
  • Flagsmith Processor: The Flagsmith Processor is a background worker that processes feature flag evaluations and updates the database. This service is also deployed as a Docker container on ECS in a private subnet, subscribing to events from the RDS database.
  • Auto-scaling: Both the API and Processor services are deployed as ECS tasks behind an Application Load Balancer. The ECS service is configured to auto-scale based on CPU utilization.

The following diagram illustrates the architecture:

Now, let's deploy Flagsmith on AWS using CloudFormation.

Prerequisites

Before we begin, make sure you have the following prerequisites:

  • An AWS account with the necessary permissions to create resources using CloudFormation.
  • The AWS CLI installed and configured with your AWS account credentials.

Deployment Steps

Elastic Container Registry (ECR)

We must begin by creating an ECR repository for the Flagsmith Docker image. We need to do this in order to avoid throttling issues when pulling the image from Docker Hub.

Type the following command in your terminal to create the ECR repository:

aws ecr create-repository --repository-name flagsmith
warning

The following build command must be run on an X86 system. If you are using an M1 Mac, you can use a virtual machine to run the command.

Let's pull the flagsmith image from Docker Hub:

docker pull flagsmith/flagsmith:latest

Tag the image with the ECR repository URI:

docker push {account-id}.dkr.ecr.us-east-1.amazonaws.com/flagsmith:latest
info

Replace {account-id} with your AWS account ID and us-east-1 with your preferred AWS region.

Login to ECR:

aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin {account-id}.dkr.ecr.us-east-1.amazonaws.com

And finally, push the image to ECR:

docker push {account-id}.dkr.ecr.us-east-1.amazonaws.com/flagsmith:latest

Great! We have successfully mirrored the flagsmith image on ECR. Now, let's deploy the CloudFormation template.

CloudFormation

Create a new file named stack-params.json with the following content:

[
{
"ParameterKey": "AllowedHosts",
"ParameterValue": "*"
},
{
"ParameterKey": "DatabasePassword",
"ParameterValue": "{password}"
},
{
"ParameterKey": "DatabasePort",
"ParameterValue": "5432"
},
{
"ParameterKey": "FlagsmithImage",
"ParameterValue": "{account-id}.dkr.ecr.us-east-1.amazonaws.com/flagsmith:latest"
},
{
"ParameterKey": "FlagsmithServiceSize",
"ParameterValue": "1"
},
{
"ParameterKey": "ServiceName",
"ParameterValue": "flagsmith"
},
{
"ParameterKey": "StageName",
"ParameterValue": "prod"
},
{
"ParameterKey": "ApexDomain",
"ParameterValue": "{domain.tld}"
},
{
"ParameterKey": "SendgridApiKey",
"ParameterValue": "{api-key}"
}
]

Replace the placeholders with the appropriate values:

  • {password}: A strong password for the RDS database.
  • {account-id}: Your AWS account ID.
  • {domain.tld}: Your domain name.
  • {api-key}: Your SendGrid API key.

The CloudFormation template

Create a new file named cloudformation.yml with the following content:
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.${ApexDomain}"
Export:
Name: !Sub ${AWS::StackName}-ServiceUrl

This CloudFormation template creates the necessary resources for deploying Flagsmith on AWS, including the RDS database, ECS services, ALB, Route 53 records, and more.

warning

This template makes use of the autohost CloudFormation stack outputs that define the VPC, subnets, and security groups. You must replace these references with your own VPC and subnet IDs.

tip

You can use tools such as ChatGPT to replace imported values with your own VPC and subnet IDs, or create the missing resources.

Now, deploy the CloudFormation stack using the following command:

aws cloudformation create-stack \
--stack-name flagsmith \
--capabilities CAPABILITY_NAMED_IAM \
--tags Key=service,Value=flagsmith Key=environment,Value=dev \
--parameters file://$(pwd)/stack-params.json \
--template-body file://$(pwd)/cloudformation.yml \
--profile default

After the stack is created, you can access the Flagsmith service using the URL provided in the CloudFormation stack outputs.

Retrieve the API URL:

aws cloudformation describe-stacks \
--stack-name flagsmith \
--query "Stacks[].Outputs[?OutputKey=='ServiceUrl'][] | [0].OutputValue"

You can now access the Flagsmith service using the URL provided.

When you access the URL, you should see the Flagsmith login page.

Usage

To start using Flagsmith, you need to create an account and log in. Once logged in, you can create feature flags, manage user segments, and more.

You can use this sample code to create an API client for Flagsmith:

import Flagsmith from 'flagsmith-nodejs';

/**
* Export the Flagsmith client
*/

let flagsmithInstance = null;
export const client = () => {
if (flagsmithInstance) {
return flagsmithInstance;
}

if (!process.env.FLAGSMITH_ENVIRONMENT_KEY || !process.env.FLAGSMITH_API_URL) {
console.log('WARNING: Flagsmith environment key or API URL is missing');
return null;
} else {
// initialize Flagsmith client
flagsmithInstance = new Flagsmith({
environmentKey: process.env.FLAGSMITH_ENVIRONMENT_KEY,
apiUrl: process.env.FLAGSMITH_API_URL,
});

return flagsmithInstance;
}
};

export const isKeyConfigured = () => {
return !!process.env.FLAGSMITH_ENVIRONMENT_KEY || !!process.env.FLAGSMITH_API_URL;
};

The expected environment variables are:

  • FLAGSMITH_ENVIRONMENT_KEY: The environment key for Flagsmith.
  • FLAGSMITH_API_URL: The API URL for the Flagsmith service (e.g., https://flagsmith.example.com/api/v1/).

Conclusion

In this tutorial, we deployed Flagsmith on AWS using CloudFormation. We created an ECR repository for the Flagsmith Docker image, and then deployed the CloudFormation stack to create the necessary resources for running the Flagsmith service. We also retrieved the API URL to access the Flagsmith service. You can now start using Flagsmith to manage feature flags and configurations for your applications.