Skip to content

Commit c37b426

Browse files
authored
feature: best practice dynamic configuration and feature flags (#25)
1 parent 943e28f commit c37b426

34 files changed

+941
-84
lines changed

.github/workflows/comment_issues.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: Comment when opened
2+
on:
3+
issues:
4+
types:
5+
- opened
6+
jobs:
7+
comment:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- run: gh issue comment $ISSUE --body "Thank you for opening this issue, we'll review it ASAP."
11+
env:
12+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13+
ISSUE: ${{ github.event.issue.html_url }}

.github/workflows/dependabot.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "github-actions"
4+
directory: "/"
5+
schedule:
6+
interval: "daily"
7+
commit-message:
8+
prefix: chore
9+
include: scope
10+
11+
- package-ecosystem: "pip"
12+
directory: "/"
13+
schedule:
14+
interval: "daily"
15+
target-branch: "main"
16+
commit-message:
17+
prefix: chore
18+
include: scope

.github/workflows/python-app.yml

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ jobs:
1818
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
1919
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
2020
- name: Check out repository code
21-
uses: actions/checkout@v2
21+
uses: actions/checkout@v3
2222
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
23-
- uses: actions/checkout@v2
23+
- uses: actions/checkout@v3
2424
- name: Set up Python 3.8
25-
uses: actions/setup-python@v2
25+
uses: actions/setup-python@v3
2626
with:
2727
python-version: "3.8"
2828
- name: Install dependencies
@@ -54,9 +54,27 @@ jobs:
5454
fail_ci_if_error: true # optional (default = false)
5555
verbose: false # optional (default = false)
5656
token: ${{ secrets.CODECOV_TOKEN }}
57-
- run: echo "🍏 This job's status is ${{ job.status }}."
5857
- run: echo "Run CDK deploy to your AWS account"
5958
- run: echo "Run E2E"
6059
- run: echo "Update documentation"
61-
- run: pip install mkdocs-material mkdocs-git-revision-date-plugin
62-
- run: mkdocs gh-deploy --force
60+
- run: echo "🍏 This job's status is ${{ job.status }}."
61+
62+
generate_docs_on_main:
63+
name: generate_docs_on_main
64+
runs-on: ubuntu-latest
65+
needs: [build]
66+
if: contains('refs/heads/main', github.ref)
67+
steps:
68+
- name: Check out repository code
69+
uses: actions/checkout@v3
70+
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
71+
- uses: actions/checkout@v3
72+
- name: Set up Python 3.8
73+
uses: actions/setup-python@v3
74+
with:
75+
python-version: "3.8"
76+
- name: Generate docs
77+
run: |
78+
python -m pip install --upgrade pip
79+
pip install mkdocs-material mkdocs-git-revision-date-plugin
80+
mkdocs gh-deploy --force

.github/workflows/semantic.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# conventional commit types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
2+
types:
3+
- feature
4+
- fix
5+
- docs
6+
- refactor
7+
- perf
8+
- test
9+
- chore
10+
- revert
11+
12+
# Always validate the PR title
13+
titleOnly: true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,4 @@ $RECYCLE.BIN/
245245
.coverage
246246
cdk.out
247247
.build
248+
.vscode

cdk/aws_lambda_handler_cookbook/service_stack/configuration/__init__.py

Whitespace-only changes.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import json
2+
from pathlib import Path
3+
from typing import Any, Dict, Optional
4+
5+
import aws_cdk.aws_appconfig as appconfig
6+
from aws_lambda_handler_cookbook.service_stack.configuration.schema import FeatureFlagsConfiguration
7+
from constructs import Construct
8+
9+
DEFAULT_DEPLOYMENT_STRATEGY = 'AppConfig.AllAtOnce'
10+
11+
CUSTOM_ZERO_TIME_STRATEGY = 'zero'
12+
13+
14+
class ConfigurationStore(Construct):
15+
16+
def __init__(self, scope: Construct, id_: str, environment: str, service_name: str, configuration_name: str,
17+
deployment_strategy_id: Optional[str] = None) -> None:
18+
"""
19+
This construct should be deployed in a different repo and have its own pipeline so updates can be decoupled from
20+
running the service pipeline and without redeploying the service lambdas.
21+
22+
Args:
23+
scope (Construct): The scope in which to define this construct.
24+
id_ (str): The scoped construct ID. Must be unique amongst siblings. If the ID includes a path separator (``/``), then it will be replaced by double dash ``--``.
25+
environment (str): environment name. Used for loading the corresponding JSON file to upload under 'configuration/json/{environment}_configuration.json'
26+
service_name (str): application name.
27+
configuration_name (str): configuration name
28+
deployment_strategy_id (str, optional): AWS AppConfig deployment strategy.
29+
See https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-deployment-strategy.html
30+
Defaults to DEFAULT_DEPLOYMENT_STRATEGY.
31+
"""
32+
super().__init__(scope, id_)
33+
34+
configuration_str = self._get_and_validate_configuration(environment)
35+
36+
self.config_app = appconfig.CfnApplication(
37+
self,
38+
id=service_name,
39+
name=service_name,
40+
)
41+
self.config_env = appconfig.CfnEnvironment(
42+
self,
43+
id='env',
44+
application_id=self.config_app.ref,
45+
name=environment,
46+
)
47+
self.config_profile = appconfig.CfnConfigurationProfile(
48+
self,
49+
id='profile',
50+
application_id=self.config_app.ref,
51+
location_uri='hosted',
52+
name=configuration_name,
53+
)
54+
self.hosted_cfg_version = appconfig.CfnHostedConfigurationVersion(
55+
self,
56+
'version',
57+
application_id=self.config_app.ref,
58+
configuration_profile_id=self.config_profile.ref,
59+
content=configuration_str,
60+
content_type='application/json',
61+
)
62+
63+
self.cfn_deployment_strategy = appconfig.CfnDeploymentStrategy(
64+
self,
65+
CUSTOM_ZERO_TIME_STRATEGY,
66+
deployment_duration_in_minutes=0,
67+
growth_factor=100,
68+
name=CUSTOM_ZERO_TIME_STRATEGY,
69+
replicate_to='NONE',
70+
description='zero minutes, zero bake, 100 growth all at once',
71+
final_bake_time_in_minutes=0,
72+
)
73+
74+
self.app_config_deployment = appconfig.CfnDeployment(
75+
self,
76+
id='deploy',
77+
application_id=self.config_app.ref,
78+
configuration_profile_id=self.config_profile.ref,
79+
configuration_version=self.hosted_cfg_version.ref,
80+
deployment_strategy_id=self.cfn_deployment_strategy.ref,
81+
environment_id=self.config_env.ref,
82+
)
83+
84+
def _get_and_validate_configuration(self, environment: str) -> str:
85+
current = Path(__file__).parent
86+
conf_filepath = current / (f'json/{environment}_configuration.json')
87+
configuration_str = conf_filepath.read_text()
88+
# validate configuration (check feature flags schema structure if exists)
89+
parsed_configuration = FeatureFlagsConfiguration.parse_raw(configuration_str)
90+
return configuration_str
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"features": {
3+
"premium_features": {
4+
"default": false,
5+
"rules": {
6+
"enable premium features for this specific customer name": {
7+
"when_match": true,
8+
"conditions": [
9+
{
10+
"action": "EQUALS",
11+
"key": "customer_name",
12+
"value": "RanTheBuilder"
13+
}
14+
]
15+
}
16+
}
17+
},
18+
"ten_percent_off_campaign": {
19+
"default": true
20+
}
21+
},
22+
"countries": [
23+
"ISRAEL",
24+
"USA"
25+
]
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Any, Dict, Optional
2+
3+
from aws_lambda_powertools.utilities.feature_flags import SchemaValidator
4+
from pydantic import BaseModel, validator
5+
6+
7+
class FeatureFlagsConfiguration(BaseModel):
8+
features: Optional[Dict[str, Any]]
9+
10+
@validator('features', pre=True)
11+
def validate_features(cls, value):
12+
validator = SchemaValidator(value)
13+
try:
14+
validator.validate()
15+
except Exception as exc:
16+
raise ValueError(str(exc))
17+
return value

cdk/aws_lambda_handler_cookbook/service_stack/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
POWER_TOOLS_LOG_LEVEL = 'LOG_LEVEL'
1515
BUILD_FOLDER = '.build/lambdas/'
1616
COMMION_LAYER_BUILD_FOLDER = '.build/common_layer'
17+
ENVIRONMENT = 'dev'
18+
CONFIGURATION_NAME = 'my_conf'
19+
CONFIGURATION_MAX_AGE_MINUTES = '5' # time to store appconfig conf in the cache before refetching it
1720

1821

1922
def get_stack_name() -> str:

0 commit comments

Comments
 (0)