Skip to content

feat(parameters): Configure max_age and decrypt parameters via environment variables #2088

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

Merged
merged 7 commits into from
Apr 14, 2023
3 changes: 3 additions & 0 deletions aws_lambda_powertools/shared/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@

IDEMPOTENCY_DISABLED_ENV: str = "POWERTOOLS_IDEMPOTENCY_DISABLED"

PARAMETERS_DEFAULT_DECRYPT: str = "POWERTOOLS_PARAMETERS_SSM_DECRYPT"
PARAMETERS_MAX_AGE: str = "POWERTOOLS_PARAMETERS_MAX_AGE"

LOGGER_LAMBDA_CONTEXT_KEYS = [
"function_arn",
"function_memory_size",
Expand Down
5 changes: 5 additions & 0 deletions aws_lambda_powertools/shared/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ def resolve_truthy_env_var_choice(env: str, choice: Optional[bool] = None) -> bo
return choice if choice is not None else strtobool(env)


def resolve_max_age(env: str, choice: Optional[int]) -> int:
"""Resolve max age value"""
return choice if choice is not None else int(env)


@overload
def resolve_env_var_choice(env: Optional[str], choice: float) -> float:
...
Expand Down
12 changes: 9 additions & 3 deletions aws_lambda_powertools/utilities/parameters/appconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
if TYPE_CHECKING:
from mypy_boto3_appconfigdata import AppConfigDataClient

from ...shared import constants
from ...shared.functions import resolve_env_var_choice
from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import (
resolve_env_var_choice,
resolve_max_age,
)

from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider


Expand Down Expand Up @@ -136,7 +140,7 @@ def get_app_config(
application: Optional[str] = None,
transform: TransformOptions = None,
force_fetch: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
max_age: Optional[int] = None,
**sdk_options
) -> Union[str, list, dict, bytes]:
"""
Expand Down Expand Up @@ -187,6 +191,8 @@ def get_app_config(
>>> print(value)
My configuration's JSON value
"""
# Resolving if will use the default value (5), the value passed by parameter or the environment variable
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE, DEFAULT_MAX_AGE_SECS), choice=max_age)

# Only create the provider if this function is called at least once
if "appconfig" not in DEFAULT_PROVIDERS:
Expand Down
17 changes: 14 additions & 3 deletions aws_lambda_powertools/utilities/parameters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import base64
import json
import os
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from typing import (
Expand All @@ -24,6 +25,8 @@
import boto3
from botocore.config import Config

from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import resolve_max_age
from aws_lambda_powertools.utilities.parameters.types import TransformOptions

from .exceptions import GetParameterError, TransformParameterError
Expand All @@ -35,7 +38,9 @@
from mypy_boto3_ssm import SSMClient


DEFAULT_MAX_AGE_SECS = 5
# If the environment variable is not set, the default value is 5
DEFAULT_MAX_AGE_SECS = "5"

# These providers will be dynamically initialized on first use of the helper functions
DEFAULT_PROVIDERS: Dict[str, Any] = {}
TRANSFORM_METHOD_JSON = "json"
Expand Down Expand Up @@ -77,7 +82,7 @@ def has_not_expired_in_cache(self, key: Tuple[str, TransformOptions]) -> bool:
def get(
self,
name: str,
max_age: int = DEFAULT_MAX_AGE_SECS,
max_age: Optional[int] = None,
transform: TransformOptions = None,
force_fetch: bool = False,
**sdk_options,
Expand Down Expand Up @@ -121,6 +126,9 @@ def get(
value: Optional[Union[str, bytes, dict]] = None
key = (name, transform)

# Resolving if will use the default value (5), the value passed by parameter or the environment variable
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE, DEFAULT_MAX_AGE_SECS), choice=max_age)

if not force_fetch and self.has_not_expired_in_cache(key):
return self.store[key].value

Expand Down Expand Up @@ -149,7 +157,7 @@ def _get(self, name: str, **sdk_options) -> Union[str, bytes]:
def get_multiple(
self,
path: str,
max_age: int = DEFAULT_MAX_AGE_SECS,
max_age: Optional[int] = None,
transform: TransformOptions = None,
raise_on_transform_error: bool = False,
force_fetch: bool = False,
Expand Down Expand Up @@ -186,6 +194,9 @@ def get_multiple(
"""
key = (path, transform)

# Resolving if will use the default value (5), the value passed by parameter or the environment variable
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE, DEFAULT_MAX_AGE_SECS), choice=max_age)

if not force_fetch and self.has_not_expired_in_cache(key):
return self.store[key].value # type: ignore # need to revisit entire typing here

Expand Down
13 changes: 8 additions & 5 deletions aws_lambda_powertools/utilities/parameters/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""


import os
from typing import TYPE_CHECKING, Any, Dict, Optional, Union

import boto3
Expand All @@ -11,6 +12,9 @@
if TYPE_CHECKING:
from mypy_boto3_secretsmanager import SecretsManagerClient

from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import resolve_max_age

from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider


Expand Down Expand Up @@ -111,11 +115,7 @@ def _get_multiple(self, path: str, **sdk_options) -> Dict[str, str]:


def get_secret(
name: str,
transform: Optional[str] = None,
force_fetch: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
**sdk_options
name: str, transform: Optional[str] = None, force_fetch: bool = False, max_age: Optional[int] = None, **sdk_options
) -> Union[str, dict, bytes]:
"""
Retrieve a parameter value from AWS Secrets Manager
Expand Down Expand Up @@ -162,6 +162,9 @@ def get_secret(
>>> get_secret("my-secret", VersionId="f658cac0-98a5-41d9-b993-8a76a7799194")
"""

# Resolving if will use the default value (5), the value passed by parameter or the environment variable
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE, DEFAULT_MAX_AGE_SECS), choice=max_age)

# Only create the provider if this function is called at least once
if "secrets" not in DEFAULT_PROVIDERS:
DEFAULT_PROVIDERS["secrets"] = SecretsProvider()
Expand Down
85 changes: 66 additions & 19 deletions aws_lambda_powertools/utilities/parameters/ssm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
"""
from __future__ import annotations

import os
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, overload

import boto3
from botocore.config import Config
from typing_extensions import Literal

from aws_lambda_powertools.shared.functions import slice_dictionary
from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import (
resolve_max_age,
resolve_truthy_env_var_choice,
slice_dictionary,
)

from .base import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS, BaseProvider, transform_value
from .exceptions import GetParameterError
Expand Down Expand Up @@ -110,9 +116,9 @@ def __init__(
def get( # type: ignore[override]
self,
name: str,
max_age: int = DEFAULT_MAX_AGE_SECS,
max_age: Optional[int] = None,
transform: TransformOptions = None,
decrypt: bool = False,
decrypt: Optional[bool] = None,
force_fetch: bool = False,
**sdk_options,
) -> Optional[Union[str, dict, bytes]]:
Expand Down Expand Up @@ -145,6 +151,14 @@ def get( # type: ignore[override]
When the parameter provider fails to transform a parameter value.
"""

# Resolving if will use the default value (5), the value passed by parameter or the environment variable
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE, DEFAULT_MAX_AGE_SECS), choice=max_age)

# Resolving if will use the default value (False), the value passed by parameter or the environment variable
decrypt = resolve_truthy_env_var_choice(
env=os.getenv(constants.PARAMETERS_DEFAULT_DECRYPT, "false"), choice=decrypt
)

# Add to `decrypt` sdk_options to we can have an explicit option for this
sdk_options["decrypt"] = decrypt

Expand Down Expand Up @@ -212,8 +226,8 @@ def get_parameters_by_name(
self,
parameters: Dict[str, Dict],
transform: TransformOptions = None,
decrypt: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
decrypt: Optional[bool] = None,
max_age: Optional[int] = None,
raise_on_error: bool = True,
) -> Dict[str, str] | Dict[str, bytes] | Dict[str, dict]:
"""
Expand Down Expand Up @@ -259,6 +273,15 @@ def get_parameters_by_name(

When "_errors" reserved key is in parameters to be fetched from SSM.
"""

# Resolving if will use the default value (5), the value passed by parameter or the environment variable
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE, DEFAULT_MAX_AGE_SECS), choice=max_age)

# Resolving if will use the default value (False), the value passed by parameter or the environment variable
decrypt = resolve_truthy_env_var_choice(
env=os.getenv(constants.PARAMETERS_DEFAULT_DECRYPT, "false"), choice=decrypt
)

# Init potential batch/decrypt batch responses and errors
batch_ret: Dict[str, Any] = {}
decrypt_ret: Dict[str, Any] = {}
Expand Down Expand Up @@ -487,9 +510,9 @@ def _raise_if_errors_key_is_present(parameters: Dict, reserved_parameter: str, r
def get_parameter(
name: str,
transform: Optional[str] = None,
decrypt: bool = False,
decrypt: Optional[bool] = None,
force_fetch: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
max_age: Optional[int] = None,
**sdk_options,
) -> Union[str, dict, bytes]:
"""
Expand Down Expand Up @@ -543,6 +566,14 @@ def get_parameter(
if "ssm" not in DEFAULT_PROVIDERS:
DEFAULT_PROVIDERS["ssm"] = SSMProvider()

# Resolving if will use the default value (5), the value passed by parameter or the environment variable
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE, DEFAULT_MAX_AGE_SECS), choice=max_age)

# Resolving if will use the default value (False), the value passed by parameter or the environment variable
decrypt = resolve_truthy_env_var_choice(
env=os.getenv(constants.PARAMETERS_DEFAULT_DECRYPT, "false"), choice=decrypt
)

# Add to `decrypt` sdk_options to we can have an explicit option for this
sdk_options["decrypt"] = decrypt

Expand All @@ -555,9 +586,9 @@ def get_parameters(
path: str,
transform: Optional[str] = None,
recursive: bool = True,
decrypt: bool = False,
decrypt: Optional[bool] = None,
force_fetch: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
max_age: Optional[int] = None,
raise_on_transform_error: bool = False,
**sdk_options,
) -> Union[Dict[str, str], Dict[str, dict], Dict[str, bytes]]:
Expand Down Expand Up @@ -617,6 +648,14 @@ def get_parameters(
if "ssm" not in DEFAULT_PROVIDERS:
DEFAULT_PROVIDERS["ssm"] = SSMProvider()

# Resolving if will use the default value (5), the value passed by parameter or the environment variable
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE, DEFAULT_MAX_AGE_SECS), choice=max_age)

# Resolving if will use the default value (False), the value passed by parameter or the environment variable
decrypt = resolve_truthy_env_var_choice(
env=os.getenv(constants.PARAMETERS_DEFAULT_DECRYPT, "false"), choice=decrypt
)

sdk_options["recursive"] = recursive
sdk_options["decrypt"] = decrypt

Expand All @@ -634,8 +673,8 @@ def get_parameters(
def get_parameters_by_name(
parameters: Dict[str, Dict],
transform: None = None,
decrypt: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
decrypt: Optional[bool] = None,
max_age: Optional[int] = None,
raise_on_error: bool = True,
) -> Dict[str, str]:
...
Expand All @@ -645,8 +684,8 @@ def get_parameters_by_name(
def get_parameters_by_name(
parameters: Dict[str, Dict],
transform: Literal["binary"],
decrypt: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
decrypt: Optional[bool] = None,
max_age: Optional[int] = None,
raise_on_error: bool = True,
) -> Dict[str, bytes]:
...
Expand All @@ -656,8 +695,8 @@ def get_parameters_by_name(
def get_parameters_by_name(
parameters: Dict[str, Dict],
transform: Literal["json"],
decrypt: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
decrypt: Optional[bool] = None,
max_age: Optional[int] = None,
raise_on_error: bool = True,
) -> Dict[str, Dict[str, Any]]:
...
Expand All @@ -667,8 +706,8 @@ def get_parameters_by_name(
def get_parameters_by_name(
parameters: Dict[str, Dict],
transform: Literal["auto"],
decrypt: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
decrypt: Optional[bool] = None,
max_age: Optional[int] = None,
raise_on_error: bool = True,
) -> Union[Dict[str, str], Dict[str, dict]]:
...
Expand All @@ -677,8 +716,8 @@ def get_parameters_by_name(
def get_parameters_by_name(
parameters: Dict[str, Any],
transform: TransformOptions = None,
decrypt: bool = False,
max_age: int = DEFAULT_MAX_AGE_SECS,
decrypt: Optional[bool] = None,
max_age: Optional[int] = None,
raise_on_error: bool = True,
) -> Union[Dict[str, str], Dict[str, bytes], Dict[str, dict]]:
"""
Expand Down Expand Up @@ -732,6 +771,14 @@ def get_parameters_by_name(
# NOTE: Decided against using multi-thread due to single-thread outperforming in 128M and 1G + timeout risk
# see: https://github.com/awslabs/aws-lambda-powertools-python/issues/1040#issuecomment-1299954613

# Resolving if will use the default value (5), the value passed by parameter or the environment variable
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE, DEFAULT_MAX_AGE_SECS), choice=max_age)

# Resolving if will use the default value (False), the value passed by parameter or the environment variable
decrypt = resolve_truthy_env_var_choice(
env=os.getenv(constants.PARAMETERS_DEFAULT_DECRYPT, "false"), choice=decrypt
)

# Only create the provider if this function is called at least once
if "ssm" not in DEFAULT_PROVIDERS:
DEFAULT_PROVIDERS["ssm"] = SSMProvider()
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,8 @@ Core utilities such as Tracing, Logging, Metrics, and Event Handler will be avai
| **POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | [Logging](./core/logger) | `false` |
| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | [Logging](./core/logger) | `0` |
| **POWERTOOLS_LOG_DEDUPLICATION_DISABLED** | Disables log deduplication filter protection to use Pytest Live Log feature | [Logging](./core/logger) | `false` |
| **POWERTOOLS_PARAMETERS_MAX_AGE** | Adjust how long values are kept in cache (in seconds) | [Parameters](.//utilities/parameters/#adjusting-cache-ttl) | `5` |
| **POWERTOOLS_PARAMETERS_SSM_DECRYPT** | Sets whether to decrypt or not values retrieved from AWS SSM Parameters Store | [Parameters](.//utilities/parameters/#ssmprovider) | `false` |
| **POWERTOOLS_DEV** | Increases verbosity across utilities | Multiple; see [POWERTOOLS_DEV effect below](#increasing-verbosity-across-utilities) | `false` |
| **LOG_LEVEL** | Sets logging level | [Logging](./core/logger) | `INFO` |

Expand Down
5 changes: 4 additions & 1 deletion docs/utilities/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ The following will retrieve the latest version and store it in the cache.
???+ tip
`max_age` parameter is also available in underlying provider functions like `get()`, `get_multiple()`, etc.

By default, we cache parameters retrieved in-memory for 5 seconds.
By default, we cache parameters retrieved in-memory for 5 seconds. If you want to change this default value and set the same TTL for all parameters, you can set the `POWERTOOLS_PARAMETERS_MAX_AGE` environment variable. **This will override the default TTL of 5 seconds but can be overridden by the `maxAge` parameter**.

You can adjust how long we should keep values in cache by using the param `max_age`, when using `get_parameter()`, `get_parameters()` and `get_secret()` methods across all providers.

Expand Down Expand Up @@ -179,6 +179,9 @@ The AWS Systems Manager Parameter Store provider supports two additional argumen

You can create `SecureString` parameters, which are parameters that have a plaintext parameter name and an encrypted parameter value. If you don't use the `decrypt` argument, you will get an encrypted value. Read [here](https://docs.aws.amazon.com/kms/latest/developerguide/services-parameter-store.html) about best practices using KMS to secure your parameters.

???+ tip
If you want to always decrypt parameters, you can set the `POWERTOOLS_PARAMETERS_SSM_DECRYPT=true` environment variable. **This will override the default value of `false` but can be overridden by the `decrypt` parameter**.

=== "builtin_provider_ssm_with_decrypt.py"
```python hl_lines="6 10 16"
--8<-- "examples/parameters/src/builtin_provider_ssm_with_decrypt.py"
Expand Down
Loading