Skip to content

feat(parameters): add support for retrieving batch of secrets #7058

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 5 commits into from
Jul 31, 2025
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
3 changes: 2 additions & 1 deletion aws_lambda_powertools/utilities/parameters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .base import BaseProvider, clear_caches
from .dynamodb import DynamoDBProvider
from .exceptions import GetParameterError, TransformParameterError
from .secrets import SecretsProvider, get_secret, set_secret
from .secrets import SecretsProvider, get_secret, get_secrets_by_name, set_secret
from .ssm import SSMProvider, get_parameter, get_parameters, get_parameters_by_name, set_parameter

__all__ = [
Expand All @@ -23,6 +23,7 @@
"get_parameters",
"get_parameters_by_name",
"get_secret",
"get_secrets_by_name",
"set_secret",
"clear_caches",
]
4 changes: 4 additions & 0 deletions aws_lambda_powertools/utilities/parameters/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ class GetParameterError(Exception):
"""When a provider raises an exception on parameter retrieval"""


class GetSecretError(Exception):
"""When a provider raises an exception on secret retrieval"""


class TransformParameterError(Exception):
"""When a provider fails to transform a parameter value"""

Expand Down
243 changes: 237 additions & 6 deletions aws_lambda_powertools/utilities/parameters/secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@
import logging
import os
import warnings
from typing import TYPE_CHECKING, Literal, overload
from typing import TYPE_CHECKING, Any, Literal, overload

import boto3

from aws_lambda_powertools.shared import constants
from aws_lambda_powertools.shared.functions import resolve_max_age
from aws_lambda_powertools.shared.json_encoder import Encoder
from aws_lambda_powertools.utilities.parameters.base import BaseProvider
from aws_lambda_powertools.utilities.parameters.base import BaseProvider, transform_value
from aws_lambda_powertools.utilities.parameters.constants import DEFAULT_MAX_AGE_SECS, DEFAULT_PROVIDERS
from aws_lambda_powertools.utilities.parameters.exceptions import SetSecretError
from aws_lambda_powertools.utilities.parameters.exceptions import (
GetSecretError,
SetSecretError,
TransformParameterError,
)
from aws_lambda_powertools.warnings import PowertoolsDeprecationWarning

if TYPE_CHECKING:
Expand Down Expand Up @@ -126,11 +130,159 @@ def _get(self, name: str, **sdk_options) -> str | bytes:

return secret_value["SecretBinary"]

def _get_multiple(self, path: str, **sdk_options) -> dict[str, str]:
def _get_multiple(self, names: list[str], **sdk_options) -> dict[str, Any]: # type: ignore[override]
"""
Retrieving multiple parameter values is not supported with AWS Secrets Manager
Retrieve multiple secrets using AWS Secrets Manager batch_get_secret_value API

Parameters
----------
names: list[str]
List of secret names to retrieve
sdk_options: dict, optional
Additional options passed to batch_get_secret_value API call

Returns
-------
dict[str, str]
Dictionary mapping secret names to their values

Raises
------
GetParameterError
When the parameter provider fails to retrieve secrets
"""
raise NotImplementedError()

# Merge filters: combine names with any additional filters from sdk_options
filters = sdk_options.get("Filters", [])
name_filter = {"Key": "name", "Values": names}

# Add name filter to existing filters
filters.append(name_filter)
sdk_options["Filters"] = filters

# Remove SecretIdList if present to avoid conflicts
sdk_options.pop("SecretIdList", None)

secrets: dict[str, Any] = {}
next_token = None

# Handle pagination automatically
while True:
if next_token:
sdk_options["NextToken"] = next_token
elif "NextToken" in sdk_options:
# Remove NextToken from first call if it was passed
sdk_options.pop("NextToken") # pragma: no cover

try:
response = self.client.batch_get_secret_value(**sdk_options)
except Exception as exc:
raise GetSecretError(f"Failed to retrieve secrets: {str(exc)}") from exc

# Process successful secrets
for secret in response.get("SecretValues", []):
secret_name = secret["Name"]

# Extract secret value (SecretString or SecretBinary)
if "SecretString" in secret:
secrets[secret_name] = secret["SecretString"]
elif "SecretBinary" in secret:
secrets[secret_name] = secret["SecretBinary"]

# Check if there are more results
next_token = response.get("NextToken")
if not next_token:
break

# If no secrets were found, raise an error
if not secrets:
raise GetSecretError(f"No secrets found matching the provided names: {names}")

return secrets

def get_multiple( # type: ignore[override]
self,
names: list[str],
max_age: int | None = None,
transform: TransformOptions = None,
raise_on_transform_error: bool = False,
force_fetch: bool = False,
**sdk_options,
) -> dict[str, Any]:
"""
Retrieve multiple secrets by name from AWS Secrets Manager

Parameters
----------
names: list[str]
List of secret names to retrieve
max_age: int, optional
Maximum age of the cached value
transform: str, optional
Optional transformation of the parameter value. Supported values
are "json" for JSON strings and "binary" for base 64 encoded values.
raise_on_transform_error: bool, optional
Raises an exception if any transform fails, otherwise this will
return a None value for each transform that failed
force_fetch: bool, optional
Force update even before a cached item has expired, defaults to False
sdk_options: dict, optional
Arguments that will be passed directly to the underlying API call

Returns
-------
dict[str, str | bytes | dict]
Dictionary mapping secret names to their values

Raises
------
GetParameterError
When the parameter provider fails to retrieve secrets
TransformParameterError
When the parameter provider fails to transform a secret value
"""
if not names:
raise GetSecretError("You must provide at least one secret name")

# Create a unique cache key for this batch of secrets
# Use sorted names to ensure consistent caching regardless of order
cache_key_name = "|".join(sorted(names))
key = self._build_cache_key(name=cache_key_name, transform=transform, is_nested=True)

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, DEFAULT_MAX_AGE_SECS), choice=max_age)

if not force_fetch and self.has_not_expired_in_cache(key):
cached_values = self.fetch_from_cache(key)
# Return only the requested secrets from cache (in case cache has more)
return {name: cached_values[name] for name in names if name in cached_values}

try:
values = self._get_multiple(names, **sdk_options)
except Exception as exc:
raise GetSecretError(str(exc)) from exc

if transform:
# Transform each secret value
transformed_values = {}
for name, value in values.items():
try:
transformed_values[name] = transform_value(
key=name,
value=value,
transform=transform,
raise_on_transform_error=raise_on_transform_error,
)
except TransformParameterError:
if raise_on_transform_error:
raise
transformed_values[name] = None # pragma: no cover
values = transformed_values

# Cache the results
self.add_to_cache(key=key, value=values, max_age=max_age)

return values

def _create_secret(self, name: str, **sdk_options) -> CreateSecretResponseTypeDef:
"""
Expand Down Expand Up @@ -369,6 +521,85 @@ def get_secret(
)


def get_secrets_by_name(
names: list[str],
transform: TransformOptions = None,
force_fetch: bool = False,
max_age: int | None = None,
**sdk_options,
) -> dict[str, str | bytes | dict]:
"""
Retrieve multiple secrets by name from AWS Secrets Manager

Parameters
----------
names: list[str]
List of secret names to retrieve
transform: str, optional
Transforms the content from a JSON object ('json') or base64 binary string ('binary')
force_fetch: bool, optional
Force update even before a cached item has expired, defaults to False
max_age: int, optional
Maximum age of the cached value
sdk_options: dict, optional
Dictionary of options that will be passed to the batch_get_secret_value call

Raises
------
GetParameterError
When the parameter provider fails to retrieve secrets
TransformParameterError
When the parameter provider fails to transform a secret value

Returns
-------
dict[str, str | bytes | dict]
Dictionary mapping secret names to their values

Example
-------
**Retrieves multiple secrets**

>>> from aws_lambda_powertools.utilities.parameters import get_secrets_by_name
>>>
>>> secrets = get_secrets_by_name(["db-password", "api-key", "jwt-secret"])
>>> print(secrets["db-password"])

**Retrieves multiple secrets with JSON transformation**

>>> from aws_lambda_powertools.utilities.parameters import get_secrets_by_name
>>>
>>> secrets = get_secrets_by_name(["config", "settings"], transform="json")
>>> print(secrets["config"]["database_url"])

**Retrieves multiple secrets with additional filters**

>>> from aws_lambda_powertools.utilities.parameters import get_secrets_by_name
>>>
>>> secrets = get_secrets_by_name(
... names=["app-secret"],
... Filters=[{"Key": "primary-region", "Values": ["us-east-1"]}]
... )
"""
if not names:
raise GetSecretError("You must provide at least one secret name")

# If max_age is not set, resolve it from the environment variable, defaulting to DEFAULT_MAX_AGE_SECS
max_age = resolve_max_age(env=os.getenv(constants.PARAMETERS_MAX_AGE_ENV, 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()

return DEFAULT_PROVIDERS["secrets"].get_multiple(
names=names,
max_age=max_age,
transform=transform,
force_fetch=force_fetch,
**sdk_options,
)


def set_secret(
name: str,
value: str | bytes,
Expand Down
32 changes: 32 additions & 0 deletions docs/utilities/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ This utility requires additional permissions to work as expected.
| SSM | If using **`decrypt=True`** | You must add an additional permission **`kms:Decrypt`** |
| Secrets | **`get_secret`**, **`SecretsProvider.get`** | **`secretsmanager:GetSecretValue`** |
| Secrets | **`set_secret`**, **`SecretsProvider.set`** | **`secretsmanager:PutSecretValue`** and **`secretsmanager:CreateSecret`** (if creating secrets) |
| Secrets | **`get_secrets_by_name`**, **`SecretsProvider.get_multiple`** | **`secretsmanager:BatchGetSecretValue`**, **`secretsmanager:GetSecretValue`** and **`secretsmanager:ListSecrets`** |
| DynamoDB | **`DynamoDBProvider.get`** | **`dynamodb:GetItem`** |
| DynamoDB | **`DynamoDBProvider.get_multiple`** | **`dynamodb:Query`** |
| AppConfig | **`get_app_config`**, **`AppConfigProvider.get_app_config`** | **`appconfig:GetLatestConfiguration`** and **`appconfig:StartConfigurationSession`** |
Expand Down Expand Up @@ -111,6 +112,30 @@ You can fetch secrets stored in Secrets Manager using `get_secret`.
--8<-- "examples/parameters/src/getting_started_secret.py"
```

### Fetching multiple secrets

You can fetch multiple secrets from Secrets Manager in a single API call using `get_secrets_by_name`. This reduces the number of API calls and improves performance when you need to retrieve several secrets at once.

???+ info "Batch retrieval benefits"
- **Performance**: Retrieve up to 20 secrets in one API call
- **Cost optimization**: Fewer API calls reduce AWS costs
- **Error resilience**: Partial failures don't break the entire operation
- **Advanced filtering**: Use additional filters beyond secret names

=== "getting_started_batch_secrets.py"
```python hl_lines="1 7"
--8<-- "examples/parameters/src/getting_started_batch_secrets.py"
```

#### Advanced filtering

You can combine secret name filtering with additional AWS Secrets Manager filters for more precise results:

=== "batch_secrets_with_filters.py"
```python hl_lines="2 10-16"
--8<-- "examples/parameters/src/batch_secrets_with_filters.py"
```

### Setting secrets

You can set secrets stored in Secrets Manager using `set_secret`.
Expand Down Expand Up @@ -251,6 +276,11 @@ You can create `SecureString` parameters, which are parameters that have a plain
--8<-- "examples/parameters/src/builtin_provider_secret.py"
```

=== "batch_secrets_provider.py"
```python hl_lines="2-12"
--8<-- "examples/parameters/src/batch_secrets_provider.py"
```

#### DynamoDBProvider

The DynamoDB Provider does not have any high-level functions, as it needs to know the name of the DynamoDB table containing the parameters.
Expand Down Expand Up @@ -445,7 +475,9 @@ Here is the mapping between this utility's functions and methods and the underly
| SSM Parameter Store | `SSMProvider.get` | `ssm` | [get_parameter](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameter){target="_blank"} |
| SSM Parameter Store | `SSMProvider.get_multiple` | `ssm` | [get_parameters_by_path](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameters_by_path){target="_blank"} |
| Secrets Manager | `get_secret` | `secretsmanager` | [get_secret_value](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager.html#SecretsManager.Client.get_secret_value){target="_blank"} |
| Secrets Manager | `get_secrets_by_name` | `secretsmanager` | [batch_get_secret_value](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager.html#SecretsManager.Client.batch_get_secret_value){target="_blank"} |
| Secrets Manager | `SecretsProvider.get` | `secretsmanager` | [get_secret_value](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager.html#SecretsManager.Client.get_secret_value){target="_blank"} |
| Secrets Manager | `SecretsProvider.get_multiple` | `secretsmanager` | [batch_get_secret_value](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager.html#SecretsManager.Client.batch_get_secret_value){target="_blank"} |
| DynamoDB | `DynamoDBProvider.get` | `dynamodb` | ([Table resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#table){target="_blank"}) |
| DynamoDB | `DynamoDBProvider.get_multiple` | `dynamodb` | ([Table resource](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#table){target="_blank"}) |
| App Config | `get_app_config` | `appconfigdata` | [start_configuration_session](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/appconfigdata.html#AppConfigData.Client.start_configuration_session){target="_blank"} and [get_latest_configuration](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/appconfigdata.html#AppConfigData.Client.get_latest_configuration){target="_blank"} |
Expand Down
31 changes: 31 additions & 0 deletions examples/parameters/src/batch_secrets_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.parameters import SecretsProvider
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = Logger()
# Create provider instance for more control
secrets_provider = SecretsProvider()


def lambda_handler(event, context: LambdaContext):
# Retrieve secrets with custom settings
secrets = secrets_provider.get_multiple(
names=["service/auth-token", "service/encryption-key"],
max_age=600, # Cache for 10 minutes
transform="json", # Parse JSON secrets
raise_on_transform_error=False, # Don't fail on transform errors
)

# Handle potential transform failures
auth_token = secrets.get("service/auth-token")
encryption_key = secrets.get("service/encryption-key")

if auth_token is None:
logger.info("Warning: auth-token failed to parse as JSON")
if encryption_key is None:
logger.info("Warning: encryption-key failed to parse as JSON")

return {
"statusCode": 200,
"body": f"Retrieved {len([s for s in secrets.values() if s is not None])} valid secrets",
}
Loading