Skip to content

Commit 31bee6c

Browse files
authored
feature: idempotency (#654)
1 parent 285d219 commit 31bee6c

File tree

21 files changed

+363
-265
lines changed

21 files changed

+363
-265
lines changed

cdk/my_service/api_construct.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ def __init__(self, scope: Construct, id_: str, appconfig_app_name: str) -> None:
1616
super().__init__(scope, id_)
1717
self.id_ = id_
1818
self.api_db = ApiDbConstruct(self, f'{id_}db')
19-
self.lambda_role = self._build_lambda_role(self.api_db.db)
19+
self.lambda_role = self._build_lambda_role(self.api_db.db, self.api_db.idempotency_db)
2020
self.common_layer = self._build_common_layer()
2121
self.rest_api = self._build_api_gw()
2222
api_resource: aws_apigateway.Resource = self.rest_api.root.add_resource('api').add_resource(constants.GW_RESOURCE)
23-
self._add_post_lambda_integration(api_resource, self.lambda_role, self.api_db.db, appconfig_app_name)
23+
self._add_post_lambda_integration(api_resource, self.lambda_role, self.api_db.db, appconfig_app_name, self.api_db.idempotency_db)
2424

2525
def _build_api_gw(self) -> aws_apigateway.RestApi:
2626
rest_api: aws_apigateway.RestApi = aws_apigateway.RestApi(
@@ -35,7 +35,7 @@ def _build_api_gw(self) -> aws_apigateway.RestApi:
3535
CfnOutput(self, id=constants.APIGATEWAY, value=rest_api.url).override_logical_id(constants.APIGATEWAY)
3636
return rest_api
3737

38-
def _build_lambda_role(self, db: dynamodb.Table) -> iam.Role:
38+
def _build_lambda_role(self, db: dynamodb.Table, idempotency_table: dynamodb.Table) -> iam.Role:
3939
return iam.Role(
4040
self,
4141
constants.SERVICE_ROLE_ARN,
@@ -51,7 +51,19 @@ def _build_lambda_role(self, db: dynamodb.Table) -> iam.Role:
5151
]),
5252
'dynamodb_db':
5353
iam.PolicyDocument(statements=[
54-
iam.PolicyStatement(actions=['dynamodb:PutItem', 'dynamodb:GetItem'], resources=[db.table_arn], effect=iam.Effect.ALLOW)
54+
iam.PolicyStatement(
55+
actions=['dynamodb:PutItem', 'dynamodb:GetItem'],
56+
resources=[db.table_arn],
57+
effect=iam.Effect.ALLOW,
58+
)
59+
]),
60+
'idempotency_table':
61+
iam.PolicyDocument(statements=[
62+
iam.PolicyStatement(
63+
actions=['dynamodb:PutItem', 'dynamodb:GetItem', 'dynamodb:UpdateItem', 'dynamodb:DeleteItem'],
64+
resources=[idempotency_table.table_arn],
65+
effect=iam.Effect.ALLOW,
66+
)
5567
]),
5668
},
5769
managed_policies=[
@@ -68,7 +80,8 @@ def _build_common_layer(self) -> PythonLayerVersion:
6880
removal_policy=RemovalPolicy.DESTROY,
6981
)
7082

71-
def _add_post_lambda_integration(self, api_name: aws_apigateway.Resource, role: iam.Role, db: dynamodb.Table, appconfig_app_name: str):
83+
def _add_post_lambda_integration(self, api_name: aws_apigateway.Resource, role: iam.Role, db: dynamodb.Table, appconfig_app_name: str,
84+
idempotency_table: dynamodb.Table):
7285
lambda_function = _lambda.Function(
7386
self,
7487
constants.CREATE_LAMBDA,
@@ -85,6 +98,7 @@ def _add_post_lambda_integration(self, api_name: aws_apigateway.Resource, role:
8598
'REST_API': 'https://www.ranthebuilder.cloud/api', # for env vars example
8699
'ROLE_ARN': 'arn:partition:service:region:account-id:resource-type:resource-id', # for env vars example
87100
'TABLE_NAME': db.table_name,
101+
'IDEMPOTENCY_TABLE_NAME': idempotency_table.table_name,
88102
},
89103
tracing=_lambda.Tracing.ACTIVE,
90104
retry_attempts=0,

cdk/my_service/api_db_construct.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,23 @@ def __init__(self, scope: Construct, id_: str) -> None:
1111
super().__init__(scope, id_)
1212

1313
self.db: dynamodb.Table = self._build_db(id_)
14+
self.idempotency_db: dynamodb.Table = self._build_idempotency_table(id_)
15+
16+
def _build_idempotency_table(self, id_: str) -> dynamodb.Table:
17+
table_id = f'{id_}{constants.IDEMPOTENCY_TABLE_NAME}'
18+
table = dynamodb.Table(
19+
self,
20+
table_id,
21+
table_name=table_id,
22+
partition_key=dynamodb.Attribute(name='id', type=dynamodb.AttributeType.STRING),
23+
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
24+
removal_policy=RemovalPolicy.DESTROY,
25+
time_to_live_attribute='expiration',
26+
point_in_time_recovery=True,
27+
)
28+
CfnOutput(self, id=constants.IDEMPOTENCY_TABLE_NAME_OUTPUT,
29+
value=table.table_name).override_logical_id(constants.IDEMPOTENCY_TABLE_NAME_OUTPUT)
30+
return table
1431

1532
def _build_db(self, id_prefix: str) -> dynamodb.Table:
1633
table_id = f'{id_prefix}{constants.TABLE_NAME}'

cdk/my_service/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
SERVICE_ROLE = 'ServiceRole'
44
CREATE_LAMBDA = 'CreateOrder'
55
TABLE_NAME = 'orders'
6+
IDEMPOTENCY_TABLE_NAME = 'IdempotencyTable'
67
TABLE_NAME_OUTPUT = 'DbOutput'
8+
IDEMPOTENCY_TABLE_NAME_OUTPUT = 'IdempotencyDbOutput'
79
APIGATEWAY = 'Apigateway'
810
GW_RESOURCE = 'orders'
911
LAMBDA_LAYER_NAME = 'common'

docs/cdk.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,17 @@ All ASW Lambda function configurations are saved as constants at the `cdk.my_ser
4444
- **Lambda Function** - The Lambda handler function itself. Handler code is taken from the service `folder`.
4545
- **Lambda Role** - The role of the Lambda function.
4646
- **API GW with Lambda Integration** - API GW with a Lambda integration POST /api/orders that triggers the Lambda function.
47-
- **AWS DynamoDB table** - stores request data. Created in its own construct: api_db_construct.py
47+
- **AWS DynamoDB table** - stores request data. Created in the `api_db_construct.py` construct.
48+
- **AWS DynamoDB table** - stores idempotency data. Created in the `api_db_construct.py` construct.
4849
- Construct: **cdk.my_service.configuration.configuration_construct.py** which includes:
49-
- AWS AppConfig configuration with an environment, application, configuration and deployment strategy. You can read more about it [here.](best_practices/dynamic_configuration.md)
50+
- AWS AppConfig configuration with an environment, application, configuration and deployment strategy. You can read more about it [here](best_practices/dynamic_configuration.md).
5051

5152
### **Infrastructure CDK & Security Tests**
5253

5354
Under tests there is an `infrastructure` folder for CDK infrastructure tests.
5455

55-
The first test, 'test_cdk' uses CDK's testing framework which asserts that required resources exists so the application will not break anything upon deployment.
56+
The first test, `test_cdk` uses CDK's testing framework which asserts that required resources exists so the application will not break anything upon deployment.
5657

57-
The security tests are based on 'cdk_nag'. It checks your cloudformation output for security best practices. It can be found in the 'service_stack.py' as part of the stack definition. It will fail the deployment when there is a security issue.
58+
The security tests are based on `cdk_nag`. It checks your cloudformation output for security best practices. It can be found in the `service_stack.py` as part of the stack definition. It will fail the deployment when there is a security issue.
5859

5960
For more information click [here](https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/check-aws-cdk-applications-or-cloudformation-templates-for-best-practices-by-using-cdk-nag-rule-packs.html){:target="_blank" rel="noopener"}.

docs/examples/best_practices/input_validation/my_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from aws_lambda_powertools.utilities.parser.envelopes import ApiGatewayEnvelope
66
from aws_lambda_powertools.utilities.typing import LambdaContext
77

8-
from service.handlers.schemas.input import Input
8+
from .schema import Input
99

1010

1111
def my_handler(event: Dict[str, Any], context: LambdaContext):

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This project aims to reduce cognitive load and answer these questions for you by
3737
- AWS Lambda handler uses [AWS Lambda Powertools](https://awslabs.github.io/aws-lambda-powertools-python/){:target="_blank" rel="noopener"}.
3838
- AWS Lambda handler 3 layer architecture: handler layer, logic layer and data access layer
3939
- Features flags and configuration based on AWS AppConfig
40+
- Idempotent API
4041
- Unit, infrastructure, security, integration and E2E tests.
4142

4243
The GitHub template project can be found at [https://github.com/ran-isenberg/aws-lambda-handler-cookbook](https://github.com/ran-isenberg/aws-lambda-handler-cookbook){:target="_blank" rel="noopener"}.

docs/media/design.png

-35.3 KB
Loading

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ nav:
1616
- best_practices/environment_variables.md
1717
- best_practices/input_validation.md
1818
- best_practices/dynamic_configuration.md
19+
- Idempotency: https://awslabs.github.io/aws-lambda-powertools-python/2.16.2/utilities/idempotency/" target="_blank"
1920
- CDK Best practices: https://www.ranthebuilder.cloud/post/aws-cdk-best-practices-from-the-trenches" target="_blank"
2021
- Testing Best practices: https://www.ranthebuilder.cloud/post/guide-to-serverless-lambda-testing-best-practices-part-1" target="_blank"
2122

0 commit comments

Comments
 (0)