diff --git a/infra/modules/feature_flags/main.tf b/infra/modules/feature_flags/main.tf new file mode 100644 index 000000000..94d6bafd4 --- /dev/null +++ b/infra/modules/feature_flags/main.tf @@ -0,0 +1,9 @@ +resource "aws_ssm_parameter" "feature_flags" { + # checkov:skip=CKV2_AWS_34:Feature flags values don't need to be encrypted + for_each = var.feature_flags + + name = "/service/${var.service_name}/feature-flag/${each.key}" + value = each.value + type = "String" + description = "Feature flag for ${each.key} in ${var.service_name}" +} diff --git a/infra/modules/feature_flags/outputs.tf b/infra/modules/feature_flags/outputs.tf new file mode 100644 index 000000000..621cefebf --- /dev/null +++ b/infra/modules/feature_flags/outputs.tf @@ -0,0 +1,4 @@ +output "ssm_parameter_arns" { + description = "Map of feature flag keys to their ARNs" + value = { for key, param in aws_ssm_parameter.feature_flags : key => param.arn } +} diff --git a/infra/modules/feature_flags/variables.tf b/infra/modules/feature_flags/variables.tf new file mode 100644 index 000000000..66b2514e9 --- /dev/null +++ b/infra/modules/feature_flags/variables.tf @@ -0,0 +1,9 @@ +variable "feature_flags" { + type = map(string) + description = "A map of feature flags" +} + +variable "service_name" { + type = string + description = "The name of the service that the feature flagging system will be associated with" +} diff --git a/infra/{{app_name}}/app-config/dev.tf b/infra/{{app_name}}/app-config/dev.tf index 4da30dcd9..47e7c0839 100644 --- a/infra/{{app_name}}/app-config/dev.tf +++ b/infra/{{app_name}}/app-config/dev.tf @@ -22,4 +22,9 @@ module "dev_config" { # See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html # Defaults to `false`. Uncomment the next line to enable. # enable_command_execution = true + + # Uncomment to override default feature flag values + # feature_flag_overrides = { + # BAR = true + # } } diff --git a/infra/{{app_name}}/app-config/env-config/feature_flags.tf b/infra/{{app_name}}/app-config/env-config/feature_flags.tf new file mode 100644 index 000000000..32af7c23c --- /dev/null +++ b/infra/{{app_name}}/app-config/env-config/feature_flags.tf @@ -0,0 +1,12 @@ +locals { + # Map from feature flags to their default values (true or false) + feature_flag_defaults = { + # Example feature flags + # FOO = false + # BAR = false + } + feature_flags_config = merge( + local.feature_flag_defaults, + var.feature_flag_overrides + ) +} diff --git a/infra/{{app_name}}/app-config/env-config/outputs.tf b/infra/{{app_name}}/app-config/env-config/outputs.tf index 1acd64b2e..d3dc1d706 100644 --- a/infra/{{app_name}}/app-config/env-config/outputs.tf +++ b/infra/{{app_name}}/app-config/env-config/outputs.tf @@ -2,6 +2,10 @@ output "database_config" { value = local.database_config } +output "feature_flags_config" { + value = local.feature_flags_config +} + output "scheduled_jobs" { value = local.scheduled_jobs } diff --git a/infra/{{app_name}}/app-config/env-config/variables.tf b/infra/{{app_name}}/app-config/env-config/variables.tf index bdeb1c08c..33f457093 100644 --- a/infra/{{app_name}}/app-config/env-config/variables.tf +++ b/infra/{{app_name}}/app-config/env-config/variables.tf @@ -60,6 +60,16 @@ variable "extra_identity_provider_logout_urls" { default = [] } +variable "feature_flag_overrides" { + type = map(string) + description = "Map of overrides for feature flags" + default = {} + validation { + condition = length(setsubtract(keys(var.feature_flag_overrides), keys(local.feature_flag_defaults))) == 0 + error_message = "All features in feature_flag_overrides must be declared in feature_flag_defaults." + } +} + variable "has_database" { type = bool } diff --git a/infra/{{app_name}}/service/feature_flags.tf b/infra/{{app_name}}/service/feature_flags.tf new file mode 100644 index 000000000..5bc1cb873 --- /dev/null +++ b/infra/{{app_name}}/service/feature_flags.tf @@ -0,0 +1,16 @@ +locals { + feature_flags_config = local.environment_config.feature_flags_config + + feature_flags_secrets = [ + for feature_flag in keys(local.feature_flags_config) : { + name = "FF_${feature_flag}" + valueFrom = module.feature_flags.ssm_parameter_arns[feature_flag] + } + ] +} + +module "feature_flags" { + source = "../../modules/feature_flags" + service_name = local.service_name + feature_flags = local.feature_flags_config +} diff --git a/infra/{{app_name}}/service/main.tf b/infra/{{app_name}}/service/main.tf index ffa4c6185..fd122f508 100644 --- a/infra/{{app_name}}/service/main.tf +++ b/infra/{{app_name}}/service/main.tf @@ -105,6 +105,7 @@ module "service" { name = secret_name valueFrom = module.secrets[secret_name].secret_arn }], + local.feature_flags_secrets, module.app_config.enable_identity_provider ? [{ name = "COGNITO_CLIENT_SECRET" valueFrom = module.identity_provider_client[0].client_secret_arn diff --git a/template-only-app/app.py b/template-only-app/app.py index 786b22307..c89b70718 100644 --- a/template-only-app/app.py +++ b/template-only-app/app.py @@ -8,6 +8,7 @@ import notifications import storage from db import get_db_connection +from feature_flags import is_feature_enabled logging.basicConfig() logger = logging.getLogger() @@ -50,6 +51,13 @@ def migrations(): return f"Last migration on {last_migration_date}" +@app.route("/feature-flags") +def feature_flags(): + foo_status = "enabled" if is_feature_enabled("FOO") else "disabled" + bar_status = "enabled" if is_feature_enabled("BAR") else "disabled" + return f"

Feature FOO is {foo_status}

Feature BAR is {bar_status}

" + + @app.route("/document-upload") def document_upload(): path = f"uploads/{datetime.now().date()}/${{filename}}" diff --git a/template-only-app/feature_flags.py b/template-only-app/feature_flags.py new file mode 100644 index 000000000..5b732c5ac --- /dev/null +++ b/template-only-app/feature_flags.py @@ -0,0 +1,5 @@ +import os + +def is_feature_enabled(feature_name: str) -> bool: + value = os.environ.get(f"FF_{feature_name}") + return value == "true" if value else False diff --git a/template-only-app/templates/index.html b/template-only-app/templates/index.html index 15cc4b155..3854d9286 100644 --- a/template-only-app/templates/index.html +++ b/template-only-app/templates/index.html @@ -11,6 +11,7 @@

Hello, world

  • Migrations
  • Document Upload
  • Email Notifications
  • +
  • Feature flags
  • Secrets