Skip to content

Commit 32a6d69

Browse files
authored
feature: add dynamodb, fix docs (#440)
1 parent 50ec4c0 commit 32a6d69

36 files changed

+521
-330
lines changed

.github/workflows/serverless-service.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
pip install -r lambda_requirements.txt
3636
npm install -g aws-cdk
3737
- name: Configure aws credentials
38-
uses: aws-actions/configure-aws-credentials@v1-node16
38+
uses: aws-actions/configure-aws-credentials@v1
3939
with:
4040
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
4141
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
@@ -53,7 +53,12 @@ jobs:
5353
run: |
5454
make complex
5555
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
56-
- name: Unit and Integration tests
56+
- name: Unit tests
57+
run: |
58+
make unit
59+
- name: Deploy to AWS
60+
run: make deploy
61+
- name: Integration tests
5762
run: |
5863
make pipeline-tests
5964
- name: Codecov
@@ -64,8 +69,6 @@ jobs:
6469
fail_ci_if_error: true # optional (default = false)
6570
verbose: false # optional (default = false)
6671
token: ${{ secrets.CODECOV_TOKEN }}
67-
- name: Deploy to AWS
68-
run: make deploy
6972
- name: Run E2E tests
7073
run: make e2e
7174
- name: Destroy stack

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ integration:
3535
e2e:
3636
pytest tests/e2e --cov-config=.coveragerc --cov=service --cov-report xml
3737

38-
pr: deps yapf sort pre-commit complex lint lint-docs unit integration e2e
38+
pr: deps yapf sort pre-commit complex lint lint-docs unit deploy integration e2e
3939

4040
yapf:
4141
yapf -i -vv --style=./.style --exclude=.venv --exclude=.build --exclude=cdk.out --exclude=.git -r .

Pipfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ setuptools = ">=65.5.1"
2626

2727
[packages]
2828
aws-lambda-powertools= {extras = ["all"],version = "*"}
29+
mypy-boto3-dynamodb = "*"
30+
cachetools = "*"
2931

3032
[requires]
3133
python_version = "3.9"

Pipfile.lock

Lines changed: 102 additions & 88 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,43 @@ This project can serve as a template for new Serverless services - CDK deploymen
1717
**[📜Documentation](https://ran-isenberg.github.io/aws-lambda-handler-cookbook/)** | **[Blogs website](https://www.ranthebuilder.cloud)**
1818
> **Contact details | ran.isenberg@ranthebuilder.cloud**
1919
20-
## Elevate Your Handler's Code
2120

22-
What makes an AWS Lambda handler resilient, traceable and easy to maintain? How do you write such a code?
21+
## **The Problem**
2322

24-
The project is a template project that is based on my AWS Lambda handler cookbook blog series that I publish in [ranthebuilder.cloud](https://www.ranthebuilder.cloud) and attempt to answer those questions.
23+
Starting a Serverless service can be overwhelming. You need to figure out many questions and challenges that have nothing to do with your business domain:
2524

26-
This project provides a working, open source based, AWS Lambda handler skeleton Python code including DEPLOYMENT code with CDK and a pipeline.
25+
- How to deploy to the cloud? What IAC framework do you choose?
26+
- How to write a SaaS-oriented CI/CD pipeline? What does it need to contain?
27+
- How do you handle observability. Logging, tracing, metrics
28+
- How do you handle testing?
29+
- What makes an AWS Lambda handler resilient, traceable, and easy to maintain? How do you write such a code?
30+
31+
32+
## **The Solution**
33+
34+
This project aims to reduce cognitive load and answer these questions for you by providing a skeleton Python Serverless service template
35+
36+
that implements best practices for AWS Lambda, Serverless CI/CD, and AWS CDK in one template project.
37+
38+
### Serverless Service - The Order service
39+
40+
- This project provides a working orders service where customers can create orders of items.
41+
42+
- The project deploys an API GW with an AWS Lambda integration under the path POST /api/orders/ and stores data in a DynamoDB table.
2743

28-
The project deploys an API GW with an AWS Lambda integration under the path POST /api/service/.
44+
### **Features**
2945

30-
The AWS Lambda handler embodies Serverless best practices and has all the bells and whistles for a proper production ready handler.
46+
- Python Serverless service with a recommended file structure.
47+
- CDK infrastructure with infrastructure tests and security tests.
48+
- CI/CD pipelines based on Github actions that deploys to AWS.
49+
- Unit/integration and E2E tests.
50+
- The AWS Lambda handler embodies Serverless best practices and has all the bells and whistles for a proper production ready handler.
3151

52+
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).
3253

3354

3455
## CDK Deployment
35-
The CDK code create an API GW with a path of /api/service which triggers the lambda on 'POST' requests.
56+
The CDK code create an API GW with a path of /api/orders which triggers the lambda on 'POST' requests.
3657

3758
The AWS Lambda handler uses a Lambda layer optimization which takes all the packages under the [packages] section in the Pipfile and downloads them in via a Docker instance.
3859

@@ -45,13 +66,14 @@ Each utility is implemented when a new blog post is published about that utility
4566

4667
The utilities cover multiple aspect of a production-ready service, including:
4768

48-
1. [Logging](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-1-logging)
49-
2. [Observability: Monitoring and Tracing](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-2-observability)
50-
3. [Observability: Business Domain Metrics](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-3-business-domain-observability)
51-
4. [Environment variables](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-environment-variables)
52-
5. [Input validation](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-5-input-validation)
53-
6. [Features flags & dynamic configuration](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-part-6-feature-flags-configuration-best-practices)
54-
7. [Start Your AWS Serverless Service With Two Clicks](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-part-7-how-to-use-the-aws-lambda-cookbook-github-template-project)
69+
- [Logging](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-1-logging)
70+
- [Observability: Monitoring and Tracing](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-2-observability)
71+
- [Observability: Business KPIs Metrics](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-3-business-domain-observability)
72+
- [Environment Variables](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-environment-variables)
73+
- [Input Validation](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-elevate-your-handler-s-code-part-5-input-validation)
74+
- [Dynamic Configuration & feature flags](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-part-6-feature-flags-configuration-best-practices)
75+
- [Start Your AWS Serverless Service With Two Clicks](https://www.ranthebuilder.cloud/post/aws-lambda-cookbook-part-7-how-to-use-the-aws-lambda-cookbook-github-template-project)
76+
- [CDK Best practices](https://github.com/ran-isenberg/aws-lambda-handler-cookbook)
5577

5678

5779
I've written 3 of the mentioned utilities (parser, feature flags and environment variables) and donated two of them, the [parser](https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/parser/) and [feature flags](https://awslabs.github.io/aws-lambda-powertools-python/latest/utilities/feature_flags/) to [AWS Lambda Powertools](https://awslabs.github.io/aws-lambda-powertools-python/latest/).

cdk/my_service/service_stack/configuration/configuration_construct.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ def __init__(self, scope: Construct, id_: str, environment: str, service_name: s
3636

3737
self.config_app = appconfig.CfnApplication(
3838
self,
39-
id=service_name,
40-
name=service_name,
39+
id=f'{id_}{service_name}',
40+
name=f'{id_}{service_name}',
4141
)
4242
self.config_env = appconfig.CfnEnvironment(
4343
self,

cdk/my_service/service_stack/constants.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
SERVICE_ROLE_ARN = 'ServiceRoleArn'
22
LAMBDA_BASIC_EXECUTION_ROLE = 'AWSLambdaBasicExecutionRole'
33
SERVICE_ROLE = 'ServiceRole'
4-
4+
TABLE_NAME = 'orders'
5+
TABLE_NAME_OUTPUT = 'DbOutput'
56
APIGATEWAY = 'Apigateway'
6-
GW_RESOURCE = 'service'
7+
GW_RESOURCE = 'orders'
78
LAMBDA_LAYER_NAME = 'common'
89
API_HANDLER_LAMBDA_MEMORY_SIZE = 128 # MB
910
API_HANDLER_LAMBDA_TIMEOUT = 10 # seconds
1011
POWERTOOLS_SERVICE_NAME = 'POWERTOOLS_SERVICE_NAME'
11-
SERVICE_NAME = 'Example'
12+
SERVICE_NAME = 'Orders'
1213
METRICS_NAMESPACE = 'my_product_kpi'
1314
POWERTOOLS_TRACE_DISABLED = 'POWERTOOLS_TRACE_DISABLED'
1415
POWER_TOOLS_LOG_LEVEL = 'LOG_LEVEL'
Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import my_service.service_stack.constants as constants
22
from aws_cdk import CfnOutput, Duration, RemovalPolicy, aws_apigateway
3+
from aws_cdk import aws_dynamodb as dynamodb
34
from aws_cdk import aws_iam as iam
45
from aws_cdk import aws_lambda as _lambda
56
from aws_cdk import aws_logs
@@ -9,47 +10,66 @@
910

1011
class ApiConstruct(Construct):
1112

12-
def __init__(self, scope: Construct, id_: str) -> None:
13+
def __init__(self, scope: Construct, id_: str, appconfig_app_name: str) -> None:
1314
super().__init__(scope, id_)
1415

15-
self.lambda_role = self._build_lambda_role()
16+
self.db = self._build_db(id_)
17+
self.lambda_role = self._build_lambda_role(self.db)
1618
self.common_layer = self._build_common_layer()
17-
1819
self.rest_api = self._build_api_gw()
1920
api_resource: aws_apigateway.Resource = self.rest_api.root.add_resource('api').add_resource(constants.GW_RESOURCE)
20-
self.__add_post_lambda_integration(api_resource, self.lambda_role)
21+
self.__add_post_lambda_integration(api_resource, self.lambda_role, self.db, appconfig_app_name)
22+
23+
def _build_db(self, id_prefix: str) -> dynamodb.Table:
24+
table_id = f'{id_prefix}{constants.TABLE_NAME}'
25+
table = dynamodb.Table(
26+
self,
27+
table_id,
28+
table_name=table_id,
29+
partition_key=dynamodb.Attribute(name='order_id', type=dynamodb.AttributeType.STRING),
30+
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
31+
point_in_time_recovery=True,
32+
removal_policy=RemovalPolicy.DESTROY,
33+
)
34+
CfnOutput(self, id=constants.TABLE_NAME_OUTPUT, value=table.table_name).override_logical_id(constants.TABLE_NAME_OUTPUT)
35+
return table
2136

2237
def _build_api_gw(self) -> aws_apigateway.LambdaRestApi:
2338
rest_api: aws_apigateway.LambdaRestApi = aws_apigateway.RestApi(
2439
self,
2540
'service-rest-api',
2641
rest_api_name='Service Rest API',
27-
description='This service handles /api/service requests',
42+
description='This service handles /api/orders requests',
2843
deploy_options=aws_apigateway.StageOptions(throttling_rate_limit=2, throttling_burst_limit=10),
2944
)
3045

3146
CfnOutput(self, id=constants.APIGATEWAY, value=rest_api.url).override_logical_id(constants.APIGATEWAY)
3247
return rest_api
3348

34-
def _build_lambda_role(self) -> iam.Role:
49+
def _build_lambda_role(self, db: dynamodb.Table) -> iam.Role:
3550
return iam.Role(
3651
self,
3752
constants.SERVICE_ROLE,
3853
assumed_by=iam.ServicePrincipal('lambda.amazonaws.com'),
3954
inline_policies={
4055
'dynamic_configuration':
4156
iam.PolicyDocument(statements=[
42-
iam.PolicyStatement(actions=['appconfig:GetLatestConfiguration', 'appconfig:StartConfigurationSession'], resources=['*'],
43-
effect=iam.Effect.ALLOW),
57+
iam.PolicyStatement(
58+
actions=['appconfig:GetLatestConfiguration', 'appconfig:StartConfigurationSession'],
59+
resources=['*'],
60+
effect=iam.Effect.ALLOW,
61+
)
4462
]),
63+
'dynamodb_db':
64+
iam.PolicyDocument(
65+
statements=[iam.PolicyStatement(actions=['dynamodb:PutItem'], resources=[db.table_arn], effect=iam.Effect.ALLOW)]),
4566
},
4667
managed_policies=[
4768
iam.ManagedPolicy.from_aws_managed_policy_name(managed_policy_name=(f'service-role/{constants.LAMBDA_BASIC_EXECUTION_ROLE}'))
4869
],
4970
)
5071

5172
def _build_common_layer(self) -> PythonLayerVersion:
52-
5373
return PythonLayerVersion(
5474
self,
5575
'CommonLayer',
@@ -58,22 +78,23 @@ def _build_common_layer(self) -> PythonLayerVersion:
5878
removal_policy=RemovalPolicy.DESTROY,
5979
)
6080

61-
def __add_post_lambda_integration(self, api_name: aws_apigateway.Resource, role: iam.Role):
81+
def __add_post_lambda_integration(self, api_name: aws_apigateway.Resource, role: iam.Role, db: dynamodb.Table, appconfig_app_name: str):
6282
lambda_function = _lambda.Function(
6383
self,
6484
'ServicePost',
6585
runtime=_lambda.Runtime.PYTHON_3_9,
6686
code=_lambda.Code.from_asset(constants.BUILD_FOLDER),
67-
handler='service.handlers.my_handler.my_handler',
87+
handler='service.handlers.create_order.create_order',
6888
environment={
6989
constants.POWERTOOLS_SERVICE_NAME: constants.SERVICE_NAME, # for logger, tracer and metrics
7090
constants.POWER_TOOLS_LOG_LEVEL: 'DEBUG', # for logger
91+
'CONFIGURATION_APP': appconfig_app_name, # for feature flags
92+
'CONFIGURATION_ENV': constants.ENVIRONMENT, # for feature flags
93+
'CONFIGURATION_NAME': constants.CONFIGURATION_NAME, # for feature flags
94+
'CONFIGURATION_MAX_AGE_MINUTES': constants.CONFIGURATION_MAX_AGE_MINUTES, # for feature flags
7195
'REST_API': 'https://www.ranthebuilder.cloud/api', # for env vars example
7296
'ROLE_ARN': 'arn:partition:service:region:account-id:resource-type:resource-id', # for env vars example
73-
'CONFIGURATION_APP': constants.SERVICE_NAME,
74-
'CONFIGURATION_ENV': constants.ENVIRONMENT,
75-
'CONFIGURATION_NAME': constants.CONFIGURATION_NAME,
76-
'CONFIGURATION_MAX_AGE_MINUTES': constants.CONFIGURATION_MAX_AGE_MINUTES,
97+
'TABLE_NAME': db.table_name,
7798
},
7899
tracing=_lambda.Tracing.ACTIVE,
79100
retry_attempts=0,
@@ -84,5 +105,5 @@ def __add_post_lambda_integration(self, api_name: aws_apigateway.Resource, role:
84105
log_retention=aws_logs.RetentionDays.ONE_DAY,
85106
)
86107

87-
# POST /api/service/
108+
# POST /api/orders/
88109
api_name.add_method(http_method='POST', integration=aws_apigateway.LambdaIntegration(handler=lambda_function))

cdk/my_service/service_stack/service_stack.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def get_stack_name() -> str:
1414
try:
1515
username = os.getlogin().replace('.', '-')
1616
except Exception:
17-
username = 'main'
17+
username = 'github'
1818
print(f'username={username}')
1919
try:
2020
return f'{username}-{repo.active_branch}-{SERVICE_NAME}'
@@ -30,5 +30,5 @@ def __init__(self, scope: Construct, id: str, **kwargs) -> None:
3030
# This construct should be deployed in a different repo and have its own pipeline so updates can be decoupled
3131
# from running the service pipeline and without redeploying the service lambdas. For the sake of this template
3232
# example, it is deployed as part of the service stack
33-
self.dynamic_configuration = ConfigurationStore(self, 'dynamic_conf'[0:64], ENVIRONMENT, SERVICE_NAME, CONFIGURATION_NAME)
34-
self.lambdas = ApiConstruct(self, 'Service'[0:64])
33+
self.dynamic_configuration = ConfigurationStore(self, f'{id}dynamic_conf'[0:64], ENVIRONMENT, SERVICE_NAME, CONFIGURATION_NAME)
34+
self.lambdas = ApiConstruct(self, f'{id}Service'[0:64], self.dynamic_configuration.config_app.name)

cdk/setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setup(
99
name='aws-lambda-handler-cookbook',
10-
version='1.0',
10+
version='2.2',
1111
description='CDK code for deploying an AWS Lambda handler that implements the best practices described at https://www.ranthebuilder.cloud',
1212
classifiers=[
1313
'Intended Audience :: Developers',
@@ -25,6 +25,6 @@
2525
'aws-cdk-lib>=2.0.0',
2626
'constructs>=10.0.0',
2727
'cdk-nag>2.0.0',
28-
'aws-cdk.aws-lambda-python-alpha==2.50.0-alpha.0',
28+
'aws-cdk.aws-lambda-python-alpha==2.54.0-alpha.0',
2929
],
3030
)

0 commit comments

Comments
 (0)