Feature flags
This is currently in Beta, as we might change Store parameters in the next release.
The feature flags utility provides a simple rule engine to define when one or multiple features should be enabled depending on the input.
Terminology
Feature flags are used to modify behaviour without changing the application's code. These flags can be static or dynamic .
Static flags . Indicates something is simply on
or off
, for example TRACER_ENABLED=True
.
Dynamic flags . Indicates something can have varying states, for example enable a premium feature for customer X not Y.
You can use Parameters utility for static flags while this utility can do both static and dynamic feature flags.
Be mindful that feature flags can increase the complexity of your application over time; use them sparingly.
If you want to learn more about feature flags, their variations and trade-offs, check these articles:
Key features
Define simple feature flags to dynamically decide when to enable a feature
Fetch one or all feature flags enabled for a given application context
Support for static feature flags to simply turn on/off a feature without rules
Getting started
IAM Permissions
Your Lambda function must have appconfig:GetConfiguration
IAM permission in order to fetch configuration from AWS AppConfig.
Required resources
By default, this utility provides AWS AppConfig as a configuration store.
The following sample infrastructure will be used throughout this documentation:
template.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60 AWSTemplateFormatVersion : "2010-09-09"
Description : Lambda Powertools Feature flags sample template
Resources :
FeatureStoreApp :
Type : AWS::AppConfig::Application
Properties :
Description : "AppConfig Application for feature toggles"
Name : product-catalogue
FeatureStoreDevEnv :
Type : AWS::AppConfig::Environment
Properties :
ApplicationId : !Ref FeatureStoreApp
Description : "Development Environment for the App Config Store"
Name : dev
FeatureStoreConfigProfile :
Type : AWS::AppConfig::ConfigurationProfile
Properties :
ApplicationId : !Ref FeatureStoreApp
Name : features
LocationUri : "hosted"
HostedConfigVersion :
Type : AWS::AppConfig::HostedConfigurationVersion
Properties :
ApplicationId : !Ref FeatureStoreApp
ConfigurationProfileId : !Ref FeatureStoreConfigProfile
Description : 'A sample hosted configuration version'
Content : |
{
"premium_features": {
"default": false,
"rules": {
"customer tier equals premium": {
"when_match": true,
"conditions": [
{
"action": "EQUALS",
"key": "tier",
"value": "premium"
}
]
}
}
},
"ten_percent_off_campaign": {
"default": false
}
}
ContentType : 'application/json'
ConfigDeployment :
Type : AWS::AppConfig::Deployment
Properties :
ApplicationId : !Ref FeatureStoreApp
ConfigurationProfileId : !Ref FeatureStoreConfigProfile
ConfigurationVersion : !Ref HostedConfigVersion
DeploymentStrategyId : "AppConfig.AllAtOnce"
EnvironmentId : !Ref FeatureStoreDevEnv
CDK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 import json
import aws_cdk.aws_appconfig as appconfig
from aws_cdk import core
class SampleFeatureFlagStore ( core . Construct ):
def __init__ ( self , scope : core . Construct , id_ : str ) -> None :
super () . __init__ ( scope , id_ )
features_config = {
"premium_features" : {
"default" : False ,
"rules" : {
"customer tier equals premium" : {
"when_match" : True ,
"conditions" : [{ "action" : "EQUALS" , "key" : "tier" , "value" : "premium" }],
}
},
},
"ten_percent_off_campaign" : { "default" : True },
}
self . config_app = appconfig . CfnApplication (
self ,
id = "app" ,
name = "product-catalogue" ,
)
self . config_env = appconfig . CfnEnvironment (
self ,
id = "env" ,
application_id = self . config_app . ref ,
name = "dev-env" ,
)
self . config_profile = appconfig . CfnConfigurationProfile (
self ,
id = "profile" ,
application_id = self . config_app . ref ,
location_uri = "hosted" ,
name = "features" ,
)
self . hosted_cfg_version = appconfig . CfnHostedConfigurationVersion (
self ,
"version" ,
application_id = self . config_app . ref ,
configuration_profile_id = self . config_profile . ref ,
content = json . dumps ( features_config ),
content_type = "application/json" ,
)
self . app_config_deployment = appconfig . CfnDeployment (
self ,
id = "deploy" ,
application_id = self . config_app . ref ,
configuration_profile_id = self . config_profile . ref ,
configuration_version = self . hosted_cfg_version . ref ,
deployment_strategy_id = "AppConfig.AllAtOnce" ,
environment_id = self . config_env . ref ,
)
Evaluating a single feature flag
To get started, you'd need to initialize AppConfigStore
and FeatureFlags
. Then call FeatureFlags
evaluate
method to fetch, validate, and evaluate your feature.
The evaluate
method supports two optional parameters:
context : Value to be evaluated against each rule defined for the given feature
default : Sentinel value to use in case we experience any issues with our store, or feature doesn't exist
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 from aws_lambda_powertools.utilities.feature_flags import FeatureFlags , AppConfigStore
app_config = AppConfigStore (
environment = "dev" ,
application = "product-catalogue" ,
name = "features"
)
feature_flags = FeatureFlags ( store = app_config )
def lambda_handler ( event , context ):
# Get customer's tier from incoming request
ctx = { "tier" : event . get ( "tier" , "standard" ) }
# Evaluate whether customer's tier has access to premium features
# based on `has_premium_features` rules
has_premium_features : bool = feature_flags . evaluate ( name = "premium_features" ,
context = ctx , default = False )
if has_premium_features :
# enable premium features
...
event.json
{
"username" : "lessa" ,
"tier" : "premium" ,
"basked_id" : "random_id"
}
features.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 {
"premium_features" : {
"default" : false ,
"rules" : {
"customer tier equals premium" : {
"when_match" : true ,
"conditions" : [
{
"action" : "EQUALS" ,
"key" : "tier" ,
"value" : "premium"
}
]
}
}
},
"ten_percent_off_campaign" : {
"default" : false
}
}
Static flags
We have a static flag named ten_percent_off_campaign
. Meaning, there are no conditional rules, it's either ON or OFF for all customers.
In this case, we could omit the context
parameter and simply evaluate whether we should apply the 10% discount.
Getting all enabled features
As you might have noticed, each evaluate
call means an API call to the Store and the more features you have the more costly this becomes.
You can use get_enabled_features
method for scenarios where you need a list of all enabled features according to the input context.
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
from aws_lambda_powertools.utilities.feature_flags import FeatureFlags , AppConfigStore
app = ApiGatewayResolver ()
app_config = AppConfigStore (
environment = "dev" ,
application = "product-catalogue" ,
name = "features"
)
feature_flags = FeatureFlags ( store = app_config )
@app . get ( "/products" )
def list_products ():
ctx = {
** app . current_event . headers ,
** app . current_event . json_body
}
# all_features is evaluated to ["geo_customer_campaign", "ten_percent_off_campaign"]
all_features : list [ str ] = feature_flags . get_enabled_features ( context = ctx )
if "geo_customer_campaign" in all_features :
# apply discounts based on geo
...
if "ten_percent_off_campaign" in all_features :
# apply additional 10% for all customers
...
def lambda_handler ( event , context ):
return app . resolve ( event , context )
event.json
{
"body" : ' { "username" : "lessa" , "tier" : "premium" , "basked_id" : "random_id" } ' ,
"resource" : "/products" ,
"path" : "/products" ,
"httpMethod" : "GET" ,
"isBase64Encoded" : false ,
"headers" : {
"CloudFront-Viewer-Country" : "NL" ,
}
}
features.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 {
"premium_features" : {
"default" : false ,
"rules" : {
"customer tier equals premium" : {
"when_match" : true ,
"conditions" : [
{
"action" : "EQUALS" ,
"key" : "tier" ,
"value" : "premium"
}
]
}
}
},
"ten_percent_off_campaign" : {
"default" : true
},
"geo_customer_campaign" : {
"default" : false ,
"rules" : {
"customer in temporary discount geo" : {
"when_match" : true ,
"conditions" : [
{
"action" : "IN" ,
"key" : "CloudFront-Viewer-Country" ,
"value" : [ "NL" , "IE" , "UK" , "PL" , "PT" },
}
]
}
}
}
}
Advanced
Schema
This utility expects a certain schema to be stored as JSON within AWS AppConfig.
Features
A feature can simply have its name and a default
value. This is either on or off, also known as a static flag .
If you need more control and want to provide context such as user group, permissions, location, etc., you need to add rules to your feature flag configuration.
Rules
When adding rules
to a feature, they must contain:
A rule name as a key
when_match
boolean value that should be used when conditions match
A list of conditions
for evaluation
You can have multiple rules with different names. The rule engine will return the first result when_match
of the matching rule configuration, or default
value when none of the rules apply.
Conditions
The conditions
block is a list of conditions that contain action
, key
, and value
keys:
The action
configuration can have 5 different values: EQUALS
, STARTSWITH
, ENDSWITH
, IN
, NOT_IN
.
The key
and value
will be compared to the input from the context parameter.
For multiple conditions , we will evaluate the list of conditions as a logical AND
, so all conditions needs to match to return when_match
value.
Rule engine flowchart
Now that you've seen all properties of a feature flag schema, this flowchart describes how the rule engines makes a decision on when to return True
or False
.
Adjusting in-memory cache
By default, we cache configuration retrieved from the Store for 5 seconds for performance and reliability reasons.
You can override max_age
parameter when instantiating the store.
from aws_lambda_powertools.utilities.feature_flags import FeatureFlags , AppConfigStore
app_config = AppConfigStore (
environment = "dev" ,
application = "product-catalogue" ,
name = "features" ,
max_age = 300
)
Envelope
There are scenarios where you might want to include feature flags as part of an existing application configuration.
For this to work, you need to use a JMESPath expression via the envelope
parameter to extract that key as the feature flags configuration.
app.py
from aws_lambda_powertools.utilities.feature_flags import FeatureFlags , AppConfigStore
app_config = AppConfigStore (
environment = "dev" ,
application = "product-catalogue" ,
name = "configuration" ,
envelope = "feature_flags"
)
configuration.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 {
"logging" : {
"level" : "INFO" ,
"sampling_rate" : 0.1
},
"feature_flags" : {
"premium_feature" : {
"default" : false ,
"rules" : {
"customer tier equals premium" : {
"when_match" : true ,
"conditions" : [
{
"action" : "EQUALS" ,
"key" : "tier" ,
"value" : "premium"
}
]
}
}
},
"feature2" : {
"default" : false
}
}
}
Built-in store provider
For GA, you'll be able to bring your own store.
AppConfig
AppConfig store provider fetches any JSON document from AWS AppConfig.
These are the available options for further customization.
Parameter
Default
Description
environment
""
AWS AppConfig Environment, e.g. test
application
""
AWS AppConfig Application
name
""
AWS AppConfig Configuration name
envelope
None
JMESPath expression to use to extract feature flags configuration from AWS AppConfig configuration
max_age
5
Number of seconds to cache feature flags configuration fetched from AWS AppConfig
sdk_config
None
Botocore Config object
jmespath_options
None
For advanced use cases when you want to bring your own JMESPath functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 from botocore.config import Config
import jmespath
boto_config = Config ( read_timeout = 10 , retries = { "total_max_attempts" : 2 })
# Custom JMESPath functions
class CustomFunctions ( jmespath . functions . Functions ):
@jmespath . functions . signature ({ 'types' : [ 'string' ]})
def _func_special_decoder ( self , s ):
return my_custom_decoder_logic ( s )
custom_jmespath_options = { "custom_functions" : CustomFunctions ()}
app_config = AppConfigStore (
environment = "dev" ,
application = "product-catalogue" ,
name = "configuration" ,
max_age = 120 ,
envelope = "features" ,
sdk_config = boto_config ,
jmespath_options = custom_jmespath_options
)
Testing your code
You can unit test your feature flags locally and independently without setting up AWS AppConfig.
AppConfigStore
only fetches a JSON document with a specific schema. This allows you to mock the response and use it to verify the rule evaluation.
This excerpt relies on pytest
and pytest-mock
dependencies
test_feature_flags_independently.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 from typing import Dict , List , Optional
from aws_lambda_powertools.utilities.feature_flags import FeatureFlags , AppConfigStore , RuleAction
def init_feature_flags ( mocker , mock_schema , envelope = "" ) -> FeatureFlags :
"""Mock AppConfig Store get_configuration method to use mock schema instead"""
method_to_mock = "aws_lambda_powertools.utilities.feature_flags.AppConfigStore.get_configuration"
mocked_get_conf = mocker . patch ( method_to_mock )
mocked_get_conf . return_value = mock_schema
app_conf_store = AppConfigStore (
environment = "test_env" ,
application = "test_app" ,
name = "test_conf_name" ,
envelope = envelope ,
)
return FeatureFlags ( store = app_conf_store )
def test_flags_condition_match ( mocker ):
# GIVEN
expected_value = True
mocked_app_config_schema = {
"my_feature" : {
"default" : expected_value ,
"rules" : {
"tenant id equals 12345" : {
"when_match" : True ,
"conditions" : [
{
"action" : RuleAction . EQUALS . value ,
"key" : "tenant_id" ,
"value" : "12345" ,
}
],
}
},
}
}
# WHEN
ctx = { "tenant_id" : "12345" , "username" : "a" }
feature_flags = init_feature_flags ( mocker = mocker , mock_schema = mocked_app_config_schema )
flag = feature_flags . evaluate ( name = "my_feature" , context = ctx , default = False )
# THEN
assert flag == expected_value
Feature flags vs Parameters vs env vars
Method
When to use
Requires new deployment on changes
Supported services
Environment variables
Simple configuration that will rarely if ever change, because changing it requires a Lambda function deployment.
Yes
Lambda
Parameters utility
Access to secrets, or fetch parameters in different formats from AWS System Manager Parameter Store or Amazon DynamoDB.
No
Parameter Store, DynamoDB, Secrets Manager, AppConfig
Feature flags utility
Rule engine to define when one or multiple features should be enabled depending on the input.
No
AppConfig
Last update: 2021-08-11