Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parameters): migrate AppConfig to new APIs due to API deprecation #1553

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions aws_lambda_powertools/utilities/parameters/appconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def __init__(
self.environment = environment
self.current_version = ""

self.next_call = ""
self._next_token = "" # nosec - token for get_latest_configuration executions
self.last_returned_value = ""

def _get(self, name: str, **sdk_options) -> str:
Expand All @@ -100,21 +100,21 @@ def _get(self, name: str, **sdk_options) -> str:
name: str
Name of the configuration
sdk_options: dict, optional
Dictionary of options that will be passed to the client's start_configuration_session API call
SDK options to propagate to `start_configuration_session` API call
"""
if not self.next_call:
if not self._next_token:
sdk_options["ConfigurationProfileIdentifier"] = name
sdk_options["ApplicationIdentifier"] = self.application
sdk_options["EnvironmentIdentifier"] = self.environment
response_configuration = self.client.start_configuration_session(**sdk_options)
self.next_call = response_configuration["InitialConfigurationToken"]
self._next_token = response_configuration["InitialConfigurationToken"]

# The new AppConfig APIs require two API calls to return the configuration
# First we start the session and after that we retrieve the configuration
# We need to store the token to use in the next execution
response = self.client.get_latest_configuration(ConfigurationToken=self.next_call)
response = self.client.get_latest_configuration(ConfigurationToken=self._next_token)
return_value = response["Configuration"].read()
self.next_call = response["NextPollConfigurationToken"]
self._next_token = response["NextPollConfigurationToken"]

if return_value:
self.last_returned_value = return_value
Expand Down Expand Up @@ -155,7 +155,7 @@ def get_app_config(
max_age: int
Maximum age of the cached value
sdk_options: dict, optional
Dictionary of options that will be passed to the boto client start_configuration_session API call
SDK options to propagate to `start_configuration_session` API call

Raises
------
Expand Down
6 changes: 3 additions & 3 deletions docs/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Changes at a glance:
* The API for **event handler's `Response`** has minor changes to support multi value headers and cookies.
* The **legacy SQS batch processor** was removed.
* The **Idempotency key** format changed slightly, invalidating all the existing cached results.
* The **Feature Flags and AppConfig Parameter utility** API calls have changed and you must add new IAM permissions to Lambda.
* The **Feature Flags and AppConfig Parameter utility** API calls have changed and you must update your IAM permissions.

???+ important
Powertools for Python v2 drops suport for Python 3.6, following the Python 3.6 End-Of-Life (EOL) reached on December 23, 2021.
Expand Down Expand Up @@ -158,6 +158,6 @@ Using qualified names prevents distinct functions with the same name to contend

## Feature Flags and AppConfig Parameter utility
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved

The current API is [deprecated](https://docs.aws.amazon.com/appconfig/2019-10-09/APIReference/API_GetConfiguration.html) for new accounts and we had to change this.
AWS AppConfig deprecated the current API (GetConfiguration) - [more details here](https://github.com/awslabs/aws-lambda-powertools-python/issues/1506#issuecomment-1266645884).

No changes are required to your code, but you must add `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession` IAM permissions for this to work in your Lambdas.
You must update your IAM permissions to allow `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession`. There are no code changes required.
12 changes: 6 additions & 6 deletions docs/utilities/feature_flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ title: Feature flags
description: Utility
---

???+ note
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.

???+ info
We currently only support AppConfig using [freeform configuration profile](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html#appconfig-creating-configuration-and-profile-free-form-configurations).
We are planning to add support for the [feature flag configuration profile](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html#appconfig-creating-configuration-and-profile-feature-flags) in future releases.
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved

## Terminology

Feature flags are used to modify behaviour without changing the application's code. These flags can be **static** or **dynamic**.
Expand All @@ -27,10 +28,9 @@ If you want to learn more about feature flags, their variations and trade-offs,
* [Feature Toggles (aka Feature Flags) - Pete Hodgson](https://martinfowler.com/articles/feature-toggles.html)
* [AWS Lambda Feature Toggles Made Simple - Ran Isenberg](https://isenberg-ran.medium.com/aws-lambda-feature-toggles-made-simple-580b0c444233)
* [Feature Flags Getting Started - CloudBees](https://www.cloudbees.com/blog/ultimate-feature-flag-guide)
* [Best Practices for validating AWS AppConfig Feature Flags and Configuration Data](https://aws.amazon.com/pt/blogs/mt/best-practices-for-validating-aws-appconfig-feature-flags-and-configuration-data/)

???+ note
Optimize your Lambda execution time by putting your feature settings in a single [Configuration](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html).
AWS AppConfig requires two API calls to fetch configuration for the first time. You can improve latency by consolidating your feature settings in a single [Configuration](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile.html).

## Key features

Expand All @@ -42,7 +42,7 @@ If you want to learn more about feature flags, their variations and trade-offs,

### IAM Permissions

Your Lambda function must have `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession` IAM permissions in order to fetch configuration from AWS AppConfig.
Your Lambda function IAM Role must have `appconfig:GetLatestConfiguration` and `appconfig:StartConfigurationSession` IAM permissions before using this feature.

### Required resources

Expand Down
9 changes: 0 additions & 9 deletions tests/e2e/parameters/handlers/parameter_string_handler.py

This file was deleted.

36 changes: 19 additions & 17 deletions tests/e2e/parameters/infrastructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,31 @@
from aws_cdk import CfnOutput
from aws_cdk import aws_appconfig as appconfig
from aws_cdk import aws_iam as iam
from aws_cdk import aws_ssm as ssm

from tests.e2e.utils.data_builder import build_service_name
from tests.e2e.utils.infrastructure import BaseInfrastructure


class ParametersStack(BaseInfrastructure):
def create_resources(self):
functions = self.create_lambda_functions()
self._create_parameter_string(function=functions["ParameterStringHandler"])
self._create_app_config(function=functions["ParameterAppconfigFreeformHandler"])

def _create_parameter_string(self, function: Function):
parameter = ssm.StringParameter(
self.stack, id="string_parameter", parameter_name="sample_string", string_value="Lambda Powertools"
)

parameter.grant_read(function)

CfnOutput(self.stack, "ParameterString", value=parameter.parameter_name)
CfnOutput(self.stack, "ParameterStringValue", value=parameter.string_value)

def _create_app_config(self, function: Function):

cfn_application = appconfig.CfnApplication(self.stack, id="appconfig-app", name="appe2e", description="appe2e")
service_name = build_service_name()

cfn_application = appconfig.CfnApplication(
self.stack, id="appconfig-app", name=f"appe2e{service_name}", description="appe2e"
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
)
CfnOutput(self.stack, "AppConfigApplication", value=cfn_application.name)

cfn_environment = appconfig.CfnEnvironment(
self.stack, "appconfig-env", application_id=cfn_application.ref, name="enve2e", description="enve2e"
self.stack,
"appconfig-env",
application_id=cfn_application.ref,
name=f"enve2e{service_name}",
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
description="enve2e",
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
)
CfnOutput(self.stack, "AppConfigEnvironment", value=cfn_environment.name)

Expand All @@ -40,14 +37,18 @@ def _create_app_config(self, function: Function):
deployment_duration_in_minutes=0,
final_bake_time_in_minutes=0,
growth_factor=100,
name="deploymente2e",
name=f"deploymente2e{service_name}",
description="deploymente2e",
replicate_to="NONE",
growth_type="LINEAR",
)

self._create_app_config_freeform(
app=cfn_application, environment=cfn_environment, strategy=cfn_deployment_strategy, function=function
app=cfn_application,
environment=cfn_environment,
strategy=cfn_deployment_strategy,
function=function,
service_name=service_name,
)

def _create_app_config_freeform(
Expand All @@ -56,6 +57,7 @@ def _create_app_config_freeform(
environment: appconfig.CfnEnvironment,
strategy: appconfig.CfnDeploymentStrategy,
function: Function,
service_name: str,
):

cfn_configuration_profile = appconfig.CfnConfigurationProfile(
Expand All @@ -64,7 +66,7 @@ def _create_app_config_freeform(
application_id=app.ref,
location_uri="hosted",
type="AWS.Freeform",
name="profilee2e",
name=f"profilee2e{service_name}",
description="profilee2e",
)
CfnOutput(self.stack, "AppConfigProfile", value=cfn_configuration_profile.name)
Expand Down
29 changes: 27 additions & 2 deletions tests/e2e/parameters/test_appconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,36 @@ def parameter_appconfig_freeform_value(infrastructure: dict) -> str:
return infrastructure.get("AppConfigConfigurationValue", "")


leandrodamascena marked this conversation as resolved.
Show resolved Hide resolved
@pytest.fixture
def parameter_appconfig_freeform_application(infrastructure: dict) -> str:
return infrastructure.get("AppConfigApplication", "")


@pytest.fixture
def parameter_appconfig_freeform_environment(infrastructure: dict) -> str:
return infrastructure.get("AppConfigEnvironment", "")


@pytest.fixture
def parameter_appconfig_freeform_profile(infrastructure: dict) -> str:
return infrastructure.get("AppConfigProfile", "")


def test_get_parameter_appconfig_freeform(
parameter_appconfig_freeform_handler_fn_arn: str, parameter_appconfig_freeform_value: str
parameter_appconfig_freeform_handler_fn_arn: str,
parameter_appconfig_freeform_value: str,
parameter_appconfig_freeform_application: str,
parameter_appconfig_freeform_environment: str,
parameter_appconfig_freeform_profile: str,
):
# GIVEN
payload = json.dumps({"name": "profilee2e", "environment": "enve2e", "application": "appe2e"})
payload = json.dumps(
{
"name": parameter_appconfig_freeform_profile,
"environment": parameter_appconfig_freeform_environment,
"application": parameter_appconfig_freeform_application,
}
)
expected_return = parameter_appconfig_freeform_value

# WHEN
Expand Down
27 changes: 0 additions & 27 deletions tests/e2e/parameters/test_ssm.py

This file was deleted.

24 changes: 12 additions & 12 deletions tests/functional/test_utilities_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1640,15 +1640,15 @@ def test_appconf_provider_get_configuration_json_content_type(mock_name, config)
mock_value = StreamingBody(BytesIO(encoded_message), len(encoded_message))

stubber = stub.Stubber(provider.client)
response_start = {"InitialConfigurationToken": "initial_token"}
stubber.add_response("start_configuration_session", response_start)
response_start_config_session = {"InitialConfigurationToken": "initial_token"}
stubber.add_response("start_configuration_session", response_start_config_session)

response_latest = {
response_get_latest_config = {
"Configuration": mock_value,
"NextPollConfigurationToken": "initial_token",
"ContentType": "application/json",
}
stubber.add_response("get_latest_configuration", response_latest)
stubber.add_response("get_latest_configuration", response_get_latest_config)
stubber.activate()

try:
Expand Down Expand Up @@ -1679,15 +1679,15 @@ def test_appconf_provider_get_configuration_json_content_type_with_custom_client
mock_value = StreamingBody(BytesIO(encoded_message), len(encoded_message))

stubber = stub.Stubber(provider.client)
response_start = {"InitialConfigurationToken": "initial_token"}
stubber.add_response("start_configuration_session", response_start)
response_start_config_session = {"InitialConfigurationToken": "initial_token"}
stubber.add_response("start_configuration_session", response_start_config_session)

response_latest = {
response_get_latest_config = {
"Configuration": mock_value,
"NextPollConfigurationToken": "initial_token",
"ContentType": "application/json",
}
stubber.add_response("get_latest_configuration", response_latest)
stubber.add_response("get_latest_configuration", response_get_latest_config)
stubber.activate()

try:
Expand Down Expand Up @@ -1716,15 +1716,15 @@ def test_appconf_provider_get_configuration_no_transform(mock_name, config):
mock_value = StreamingBody(BytesIO(encoded_message), len(encoded_message))

stubber = stub.Stubber(provider.client)
response_start = {"InitialConfigurationToken": "initial_token"}
stubber.add_response("start_configuration_session", response_start)
response_start_config_session = {"InitialConfigurationToken": "initial_token"}
stubber.add_response("start_configuration_session", response_start_config_session)

response_latest = {
response_get_latest_config = {
"Configuration": mock_value,
"NextPollConfigurationToken": "initial_token",
"ContentType": "application/json",
}
stubber.add_response("get_latest_configuration", response_latest)
stubber.add_response("get_latest_configuration", response_get_latest_config)
stubber.activate()

try:
Expand Down