Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 65 additions & 0 deletions lambda-powertools-secretsmanager-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# AWS Lambda with PowerTools and SecretsManager Integration

This pattern creates an AWS Lambda function to call a protected API endpoint. It is integrated with AWS SecretsManager via Lambda PowerTools, and is deployed with the AWS Cloud Development Kit (AWS CDK) in Python. It includes a test API Gateway deployment to aid in both demonstration and testing. The sample application was developed in Python.

## Requirements

* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [AWS CDK Toolkit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed and configured
* [Python 3.9+](https://www.python.org/downloads/) installed

## Deployment Instructions

1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
```
git clone https://github.com/aws-samples/serverless-patterns

```
1. Change directory to the pattern directory:
```
cd serverless-patterns/lambda-powertools-secretsmanager-cdk/src
```
1. Create a virtual environment for Python:
```
python3 -m venv .venv
```
1. Activate the virtual environment:
```bash
source .venv/bin/activate
```

For a Windows platform, activate the virtualenv like this:
```
.venv\Scripts\activate.bat
```
1. Install the Python required dependencies:
```
pip install -r requirements.txt
```
1. From the command line, use AWS CDK to deploy the AWS resources for the pattern as specified in the app.py file:
```
cdk synth
cdk deploy --all
```
1. Note the outputs from the CDK deployment process. This will contain the deployed Lambda function URL.

## How it works

The deployed Lambda function uses [Lambda Powertools](https://awslabs.github.io/aws-lambda-powertools-python/2.5.0/) to query AWS SecretsManager for an API key. This API key is then used to invoke an API endpoint using [urllib3](https://urllib3.readthedocs.io/en/stable/index.html).

## Testing

Paste the Lambda function URL that is generated by CDK Deploy into your web browser. This endpoint is public for testing and verification purposes only. Make sure to secure/remove the endpoint if using in production systems.

## Cleanup

1. Delete the stack
```bash
cdk destroy --all
```

----
Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
60 changes: 60 additions & 0 deletions lambda-powertools-secretsmanager-cdk/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"title": "Lambda Secrets Manager with PowerTools",
"description": "Lambda function that uses PowerTools to get Secrets Manager secret and call an API.",
"language": "Python",
"level": "200",
"framework": "CDK",
"introBox": {
"headline": "How it works",
"text": [
"This pattern creates an AWS Lambda function to call a protected API endpoint. It is integrated with AWS SecretsManager via AWS PowerTools, and is deployed with the AWS Cloud Development Kit (AWS CDK) in Python."
]
},
"gitHub": {
"template": {
"projectFolder": "lambda-powertools-secretsmanager-cdk/src",
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-powertools-secretsmanager-cdk",
"templateURL": "serverless-patterns/lambda-powertools-secretsmanager-cdk",
"templateFile": "app.py"
}
},
"resources": {
"bullets": [
{
"text": "Lambda Function PowerTools",
"link": "https://awslabs.github.io/aws-lambda-powertools-python/"
}
]
},
"deploy": {
"text": [
"cdk deploy --all"
]
},
"testing": {
"text": [
"See the Github repo for detailed testing instructions."
]
},
"cleanup": {
"text": [
"Delete the stack: <code>cdk destroy --all</code>."
]
},
"authors": [
{
"name": "Luke Viens",
"image": "",
"bio": "Cloud application developer at AWS.",
"linkedin": "",
"twitter": ""
},
{
"name": "Sakthivel Chellapparimanam",
"image": "",
"bio": "AWS - Cloud Application Architect",
"linkedin": "https://www.linkedin.com/in/srcsakthivel",
"twitter": "@srcsakthivel"
}
]
}
10 changes: 10 additions & 0 deletions lambda-powertools-secretsmanager-cdk/src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.swp
package-lock.json
__pycache__
.pytest_cache
.venv
*.egg-info

# CDK asset staging directory
.cdk.staging
cdk.out
101 changes: 101 additions & 0 deletions lambda-powertools-secretsmanager-cdk/src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
import os
from constructs import Construct
from aws_cdk import (
App,
CfnOutput,
Environment,
Stack,
aws_secretsmanager as secretsmanager,
SecretValue as secretvalue,
aws_kms as kms,
aws_lambda as lambda_,
aws_apigateway as apigateway
)


class TestAPIStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)

# Create API Gateway
api = apigateway.RestApi(self, "test-api")

# Add a mock ANY method to the API
api.root.add_method("ANY",
apigateway.MockIntegration(
integration_responses = [
apigateway.IntegrationResponse(
status_code = "200",
response_templates = {
"application/json": "{ \"response\": \"Successfully authenticated using API Key from Secrets Manager.\" }"
}
)
],
passthrough_behavior = apigateway.PassthroughBehavior.NEVER,
request_templates = {
"application/json": "{ \"statusCode\": 200 }"
}
),
method_responses = [
apigateway.MethodResponse(status_code = "200")
],
api_key_required = True
)

# Configure the API key/secret
secret = secretsmanager.Secret(self, 'api_key')

plan = api.add_usage_plan("usage-plan")
key = api.add_api_key("ApiKey", value = secret.secret_value.unsafe_unwrap())
plan.add_api_key(key)
plan.add_api_stage(stage = api.deployment_stage)

self.api_url = api.url
self.secret_name = secret.secret_arn


class LambdaServiceStack(Stack):
def __init__(self, scope: Construct, construct_id: str, secret_arn: str, api_url: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)

region = Stack.of(self).region
secret = secretsmanager.Secret.from_secret_complete_arn(self, "api-key", secret_arn)


# Create lambda that will interface with secrets manager with powertools layer
# Powertools documentation: https://awslabs.github.io/aws-lambda-powertools-python/
layer = lambda_.LayerVersion.from_layer_version_arn(self, "power-tools-layer",
layer_version_arn = "arn:aws:lambda:{region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:17".format(region = region)
)

lambda_handler = lambda_.Function(self, 'lambda-handler',
runtime = lambda_.Runtime.PYTHON_3_9,
handler = 'handler.handler',
code = lambda_.Code.from_asset('lambda'),
layers = [ layer ],
tracing = lambda_.Tracing.ACTIVE,
environment = {
"API_KEY_SECRET": secret_arn,
"API_URL": api_url
}
)

secret.grant_read(lambda_handler) # Assign permissions to read secret


# Output the Lambda's function URL
# Internet facing for testing purposes only, remove/change to private in production
fn_url = lambda_handler.add_function_url(auth_type = lambda_.FunctionUrlAuthType.NONE)
CfnOutput(self, "LambdaURL", value = fn_url.url)


app = App()

api_stack = TestAPIStack(app, "TestAPIStack") # Test Target API
secret_name = api_stack.secret_name # API Key secret
api_url = api_stack.api_url # API URL

LambdaServiceStack(app, "LambdaServiceStack", secret_name, api_url)

app.synth()
Empty file.
37 changes: 37 additions & 0 deletions lambda-powertools-secretsmanager-cdk/src/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"app": "python3 app.py",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"requirements*.txt",
"source.bat",
"**/__init__.py",
"python/__pycache__"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true
}
}
58 changes: 58 additions & 0 deletions lambda-powertools-secretsmanager-cdk/src/lambda/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import os
import urllib3
from aws_lambda_powertools.utilities import parameters
from aws_lambda_powertools import (
Tracer,
Logger
)

API_KEY_SECRET = "API_KEY_SECRET"
API_URL = "API_URL"

tracer = Tracer()
logger = Logger()


@tracer.capture_method(capture_response=False, capture_error=False)
def get_secret(secret_name):
try:
return parameters.get_secret(secret_name) # Plaintext API key

except parameters.exceptions.GetParameterError as e:
logger.exception("Error getting paramaeter \"" + secret_name + "\ from secrets manager")
raise RuntimeError("Error getting parameter from secrets manager") from e


@tracer.capture_method
def call_api(api_url, api_key):
try:
http = urllib3.PoolManager()
headers = {
"x-api-key": api_key
}

response = http.request(
"POST",
url = api_url,
headers = headers
)

return response

except urllib3.HTTPError as e:
logger.exception("Received HTTP error")
raise RuntimeError("Received HTTP error while calling API") from e


@tracer.capture_lambda_handler
@logger.inject_lambda_context(log_event=False)
def handler(event, context):
api_url = os.getenv(API_URL)
api_key = get_secret(os.getenv(API_KEY_SECRET))

response = call_api(api_url, api_key)

return {
'statusCode': 200,
'body': response.data
}
2 changes: 2 additions & 0 deletions lambda-powertools-secretsmanager-cdk/src/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
aws-cdk-lib==2.54.0
constructs>=10.0.0,<11.0.0