Skip to content

Commit e5860e8

Browse files
committed
initial commit
1 parent be903e3 commit e5860e8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+3401
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
2+
# API Gateway direct integration to DynamoDB
3+
4+
This pattern shows how to create an API Gateway with direct integration to DynamoDB.
5+
The pettern showcase transformation of request/response using VTL and CDK and implement examples for using Cognito, Lambda authorizer and API keys.
6+
7+
Learn more about this pattern at Serverless Land Patterns: [Serverless Land Patterns](https://serverlessland.com/patterns/apigw-dynamodb-python-cdk).
8+
9+
Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.
10+
11+
![alt text](image.png)
12+
13+
## Requirements
14+
15+
* [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.
16+
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
17+
* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) (AWS CDK) installed
18+
19+
## Deployment Instructions
20+
21+
1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
22+
```
23+
git clone https://github.com/aws-samples/serverless-patterns/
24+
```
25+
2. Change directory
26+
```
27+
cd serverless-patterns/apigw-dynamodb-python-cdk
28+
```
29+
3. To manually create a virtualenv on MacOS and Linux:
30+
```
31+
$ python3 -m venv .venv
32+
```
33+
4. After the init process completes and the virtualenv is created, you can use the following to activate virtualenv.
34+
```
35+
$ source .venv/bin/activate
36+
```
37+
6. After activating your virtual environment for the first time, install the app's standard dependencies:
38+
```
39+
python -m pip install -r requirements.txt
40+
```
41+
7. To generate a cloudformation templates (optional)
42+
```
43+
cdk synth
44+
```
45+
8. To deploy AWS resources as a CDK project
46+
```
47+
cdk deploy
48+
```
49+
50+
## How it works
51+
At the end of the deployment the CDK output will list stack outputs, and an API Gateway URL. In the customer's AWS account, a REST API along with an authorizer, Cognito user pool, and a DynamoDB table will be created.
52+
Put resource - uses Lambda authorizer to authenticate the client and send allow/deny to API Gateway.
53+
Get resource - uses API key to control the rate limit. Need to provide valid key for the request with x-api-key header.
54+
Delete resource - uses Cognito to authenticate the client. Cognito token need to be provided with Authorization header.
55+
56+
## Testing
57+
1. Run pytest
58+
```
59+
pytest tests/test_apigw_dynamodb_python_stack.py
60+
```
61+
## Cleanup
62+
63+
1. Delete the stack
64+
```bash
65+
cdk destroy
66+
```
67+
1. Confirm the stack has been deleted
68+
```bash
69+
cdk list
70+
```
71+
----
72+
Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
73+
74+
SPDX-License-Identifier: MIT-0

apigw-dynamodb-python-cdk/apigw_dynamodb_python_cdk/__init__.py

Whitespace-only changes.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from constructs import Construct
2+
import aws_cdk.aws_apigateway as apigateway
3+
4+
5+
class UsagePlanConstruct(Construct):
6+
def __init__(self, scope: Construct, id: str, apigateway_construct, plan_name, plan_config ,**kwargs) -> None:
7+
super().__init__(scope, id, **kwargs)
8+
9+
# Map the period of the usage plan from the config to apigateway.Period.XXX
10+
period_enum = self.get_period_enum(plan_config['quota']['period'])
11+
12+
# Create usage plan dynamically using the context data
13+
usage_plan = apigateway_construct.api.add_usage_plan(plan_name,
14+
name=plan_name,
15+
throttle=apigateway.ThrottleSettings(
16+
rate_limit=plan_config['throttle']['rate_limit'],
17+
burst_limit=plan_config['throttle']['burst_limit']
18+
),
19+
quota=apigateway.QuotaSettings(
20+
limit=plan_config['quota']['limit'],
21+
period=period_enum
22+
)
23+
)
24+
25+
# Create API key
26+
api_key = apigateway.ApiKey(self, f"ApiKey-{plan_name}",
27+
api_key_name=f"ApiKey-{plan_name}")
28+
self.api_key_id = api_key.key_id
29+
usage_plan.add_api_key(api_key)
30+
31+
# If method is configured in the context assign the API key to the relevant API method
32+
if plan_config['method']:
33+
def get_method(method_name):
34+
method_mapping = { # Change the method to fit your API
35+
"GET": apigateway_construct.get_method,
36+
"POST": apigateway_construct.put_method,
37+
"DELETE": apigateway_construct.delete_method
38+
}
39+
return method_mapping.get(method_name.upper())
40+
usage_plan.add_api_stage(
41+
stage=apigateway_construct.api.deployment_stage,
42+
throttle=[apigateway.ThrottlingPerMethod(
43+
method=get_method(plan_config['method']),
44+
throttle=apigateway.ThrottleSettings(
45+
rate_limit=100,
46+
burst_limit=1
47+
))]
48+
)
49+
50+
51+
52+
53+
@staticmethod
54+
def get_period_enum(period: str) -> apigateway.Period:
55+
period_mapping = {
56+
"DAY": apigateway.Period.DAY,
57+
"WEEK": apigateway.Period.WEEK,
58+
"MONTH": apigateway.Period.MONTH
59+
}
60+
return period_mapping.get(period.upper())
61+
62+
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
from constructs import Construct
2+
import aws_cdk.aws_apigateway as apigateway
3+
import aws_cdk.aws_iam as iam
4+
import os
5+
6+
class ApiGatewayConstruct(Construct):
7+
def __init__(self, scope: Construct, id: str, cognito_construct, dynamodb_construct, lambda_construct, vtl_dir ,**kwargs) -> None:
8+
super().__init__(scope, id, **kwargs)
9+
10+
self.vtl_dir = vtl_dir
11+
12+
# Define the Cognito Authorizer
13+
cognito_authorizer = apigateway.CognitoUserPoolsAuthorizer(self, "CognitoAuthorizer",
14+
cognito_user_pools=[cognito_construct.user_pool]
15+
)
16+
17+
# Define lambda authorizer
18+
lambda_authorizer = apigateway.RequestAuthorizer(self, "LambdaAuthorizer",
19+
handler=lambda_construct.lambda_function,
20+
identity_sources=[apigateway.IdentitySource.header("Authorization")]
21+
)
22+
23+
# Create IAM role
24+
api_gateway_role = iam.Role(self, "ApiGatewayDynamoDBRole",
25+
assumed_by=iam.ServicePrincipal("apigateway.amazonaws.com"),
26+
inline_policies={
27+
"DynamoDBAccess": iam.PolicyDocument(
28+
statements=[
29+
iam.PolicyStatement(
30+
actions=["dynamodb:PutItem","dynamodb:DeleteItem", "dynamodb:Scan", "dynamodb:Query", "dynamodb:DescribeTable"],
31+
resources=[dynamodb_construct.table.table_arn]
32+
)
33+
]
34+
)
35+
}
36+
)
37+
38+
# Define API Gateway
39+
self.api = apigateway.RestApi(self, "MyApi",
40+
rest_api_name="My Service",
41+
description="This service serves my DynamoDB table.",
42+
cloud_watch_role=True,
43+
deploy_options=apigateway.StageOptions(
44+
stage_name="prod",
45+
logging_level=apigateway.MethodLoggingLevel.INFO,
46+
data_trace_enabled=True,
47+
metrics_enabled=True,
48+
variables={
49+
"TableName": dynamodb_construct.table.table_name}
50+
)
51+
)
52+
53+
# Change default response for Bad Request Body
54+
self.api.add_gateway_response(
55+
"BadRequestBody",
56+
type=apigateway.ResponseType.BAD_REQUEST_BODY,
57+
templates={
58+
"application/json": '{"message": "Invalid Request Body: $context.error.validationErrorString"}'
59+
}
60+
)
61+
62+
# Create request model schema
63+
request_model_schema = apigateway.JsonSchema(
64+
type=apigateway.JsonSchemaType.OBJECT,
65+
required=["ID","FirstName", "Age"],
66+
properties={
67+
"ID": {"type": apigateway.JsonSchemaType.STRING},
68+
"FirstName": {"type": apigateway.JsonSchemaType.STRING},
69+
"Age": {"type": apigateway.JsonSchemaType.NUMBER}
70+
},
71+
# Allow to send additional properites - handled in putItem.vtl to construct them to the request
72+
additional_properties=True
73+
)
74+
75+
# Create a request validator
76+
request_validator = apigateway.RequestValidator(self, "RequestValidator",
77+
rest_api=self.api,
78+
validate_request_body=True,
79+
validate_request_parameters=False
80+
)
81+
82+
# Create the request model
83+
request_model = apigateway.Model(self, "RequestModel",
84+
rest_api=self.api,
85+
content_type="application/json",
86+
schema=request_model_schema,
87+
model_name="PutObjectRequestModel"
88+
)
89+
90+
# Create integration request
91+
integration_request = apigateway.AwsIntegration(
92+
service="dynamodb",
93+
action="PutItem",
94+
options=apigateway.IntegrationOptions(
95+
credentials_role=api_gateway_role,
96+
request_templates={
97+
"application/json":
98+
self.get_vtl_template("putItem.vtl")
99+
},
100+
integration_responses=[
101+
apigateway.IntegrationResponse(
102+
status_code="200",
103+
response_templates={
104+
"application/json": self.get_vtl_template("response.vtl")
105+
}
106+
),
107+
]
108+
)
109+
)
110+
111+
# Create a resource and method for the API Gateway
112+
put_resource = self.api.root.add_resource("put")
113+
self.put_method = put_resource.add_method(
114+
"POST",
115+
integration_request,
116+
authorization_type=apigateway.AuthorizationType.CUSTOM,
117+
authorizer=lambda_authorizer,
118+
request_validator=request_validator,
119+
request_models={"application/json": request_model},
120+
method_responses=[
121+
apigateway.MethodResponse(status_code="200",response_models={
122+
"application/json": apigateway.Model.EMPTY_MODEL
123+
} ),
124+
]
125+
)
126+
127+
# Add GET method with response mapping
128+
get_integration = apigateway.AwsIntegration(
129+
service="dynamodb",
130+
action="Scan",
131+
options=apigateway.IntegrationOptions(
132+
credentials_role=api_gateway_role,
133+
request_templates={
134+
"application/json": self.get_vtl_template('scan_request.vtl')
135+
},
136+
integration_responses=[
137+
apigateway.IntegrationResponse(
138+
status_code="200",
139+
response_templates={
140+
"application/json": self.get_vtl_template('scan.vtl')
141+
}
142+
),
143+
]
144+
)
145+
)
146+
147+
get_resource = self.api.root.add_resource('get')
148+
self.get_method = get_resource.add_method(
149+
"GET", get_integration,
150+
api_key_required=True,
151+
method_responses=[
152+
apigateway.MethodResponse(
153+
status_code="200",
154+
response_models={
155+
"application/json": apigateway.Model.EMPTY_MODEL
156+
}
157+
),
158+
]
159+
)
160+
161+
delete_resource = self.api.root.add_resource('delete')
162+
delete_resource_id = delete_resource.add_resource('{id}')
163+
self.delete_method = delete_resource_id.add_method(
164+
"POST",
165+
apigateway.AwsIntegration(
166+
service="dynamodb",
167+
action="DeleteItem",
168+
options=apigateway.IntegrationOptions(
169+
credentials_role=api_gateway_role,
170+
request_templates={
171+
"application/json":
172+
self.get_vtl_template("deleteItem.vtl")
173+
},
174+
integration_responses=[
175+
apigateway.IntegrationResponse(
176+
status_code="200",
177+
response_templates={
178+
"application/json": '{"message": "Item deleted"}'
179+
}
180+
),
181+
]
182+
)
183+
),
184+
authorization_type=apigateway.AuthorizationType.COGNITO,
185+
authorizer=cognito_authorizer,
186+
request_validator=request_validator,
187+
method_responses=[
188+
apigateway.MethodResponse(
189+
status_code="200",
190+
response_models={
191+
"application/json": apigateway.Model.EMPTY_MODEL
192+
}
193+
),
194+
]
195+
)
196+
197+
198+
def get_vtl_template(self, filename: str) -> str:
199+
"""
200+
Reads a VTL template from a file and returns its contents as a string.
201+
"""
202+
template_path = os.path.join(self.vtl_dir, filename)
203+
with open(template_path, "r") as f:
204+
return f.read()

0 commit comments

Comments
 (0)