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
2 changes: 1 addition & 1 deletion .github/workflows/serverless-service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: pre commit
run: |
make pre-commit
- name: Lint with flake8
- name: Lint with flake8 and mypy
run: |
make lint
- name: file format
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ dev:
lint:
@echo "Running flake8"
flake8 service/* cdk/* tests/* docs/examples/* --exclude patterns='build,cdk.json,cdk.context.json,.yaml'
@echo "Running mypy"
make mypy-lint

complex:
@echo "Running Radon"
Expand All @@ -22,6 +24,9 @@ sort:
pre-commit:
pre-commit run -a --show-diff-on-failure

mypy-lint:
mypy --pretty service docs/examples cdk tests

deps:
pipenv requirements --dev > dev_requirements.txt
pipenv requirements > lambda_requirements.txt
Expand All @@ -47,10 +52,10 @@ deploy:
make deps
mkdir -p .build/lambdas ; cp -r service .build/lambdas
mkdir -p .build/common_layer ; pipenv requirements > .build/common_layer/requirements.txt
cdk deploy --app="python3 ${PWD}/cdk/my_service/app.py" --require-approval=never
cdk deploy --app="python3 ${PWD}/app.py" --require-approval=never

destroy:
cdk destroy --app="python3 ${PWD}/cdk/my_service/app.py" --force
cdk destroy --app="python3 ${PWD}/app.py" --force

docs:
mkdocs serve
Expand Down
3 changes: 3 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ boto3 = "*"
mkdocs-material = "*"
mkdocs-git-revision-date-plugin = "*"
setuptools = ">=65.5.1"
types-cachetools = "*"
mypy = "*"
types-requests = "*"

[packages]
aws-lambda-powertools= {extras = ["all"],version = "*"}
Expand Down
216 changes: 139 additions & 77 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ This project aims to reduce cognitive load and answer these questions for you by

- Python Serverless service with a recommended file structure.
- CDK infrastructure with infrastructure tests and security tests.
- CI/CD pipelines based on Github actions that deploys to AWS with python linters, complexity checks and style formatters.
- CI/CD pipelines based on Github actions that deploys to AWS with python linters, static code analysis, complexity checks and style formatters.
- The AWS Lambda handler embodies Serverless best practices and has all the bells and whistles for a proper production ready handler.
- AWS Lambda handler uses [AWS Lambda Powertools](https://awslabs.github.io/aws-lambda-powertools-python/).
- Unit, integration and E2E tests.
Expand Down
18 changes: 18 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env python3
import os

from aws_cdk import App, Environment
from boto3 import client, session

from cdk.my_service.service_stack import ServiceStack, get_stack_name

account = client('sts').get_caller_identity()['Account']
region = session.Session().region_name
app = App()
my_stack = ServiceStack(
app,
get_stack_name(),
env=Environment(account=os.environ.get('AWS_DEFAULT_ACCOUNT', account), region=os.environ.get('AWS_DEFAULT_REGION', region)),
)

app.synth()
1 change: 1 addition & 0 deletions cdk.context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
File renamed without changes.
14 changes: 0 additions & 14 deletions cdk/my_service/app.py

This file was deleted.

3 changes: 0 additions & 3 deletions cdk/my_service/cdk.context.json

This file was deleted.

File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

import aws_cdk.aws_appconfig as appconfig
from constructs import Construct
from my_service.service_stack.configuration.schema import FeatureFlagsConfiguration

from cdk.my_service.configuration.schema import FeatureFlagsConfiguration

DEFAULT_DEPLOYMENT_STRATEGY = 'AppConfig.AllAtOnce'

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import my_service.service_stack.constants as constants
from aws_cdk import CfnOutput, Duration, RemovalPolicy, aws_apigateway
from aws_cdk import aws_dynamodb as dynamodb
from aws_cdk import aws_iam as iam
Expand All @@ -7,6 +6,8 @@
from aws_cdk.aws_lambda_python_alpha import PythonLayerVersion
from constructs import Construct

import cdk.my_service.constants as constants


class ApiConstruct(Construct):

Expand Down Expand Up @@ -34,8 +35,8 @@ def _build_db(self, id_prefix: str) -> dynamodb.Table:
CfnOutput(self, id=constants.TABLE_NAME_OUTPUT, value=table.table_name).override_logical_id(constants.TABLE_NAME_OUTPUT)
return table

def _build_api_gw(self) -> aws_apigateway.LambdaRestApi:
rest_api: aws_apigateway.LambdaRestApi = aws_apigateway.RestApi(
def _build_api_gw(self) -> aws_apigateway.RestApi:
rest_api: aws_apigateway.RestApi = aws_apigateway.RestApi(
self,
'service-rest-api',
rest_api_name='Service Rest API',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from aws_cdk import Stack
from constructs import Construct
from git import Repo
from my_service.service_stack.configuration.configuration_construct import ConfigurationStore
from my_service.service_stack.constants import CONFIGURATION_NAME, ENVIRONMENT, SERVICE_NAME
from my_service.service_stack.service_construct import ApiConstruct

from cdk.my_service.configuration.configuration_construct import ConfigurationStore
from cdk.my_service.constants import CONFIGURATION_NAME, ENVIRONMENT, SERVICE_NAME
from cdk.my_service.service_construct import ApiConstruct


def get_stack_name() -> str:
Expand Down
4 changes: 2 additions & 2 deletions cdk/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
here = path.abspath(path.dirname(__file__))

setup(
name='aws-lambda-handler-cookbook',
name='service-cdk',
version='2.2',
description='CDK code for deploying an AWS Lambda handler that implements the best practices described at https://www.ranthebuilder.cloud',
classifiers=[
Expand All @@ -25,6 +25,6 @@
'aws-cdk-lib>=2.0.0',
'constructs>=10.0.0',
'cdk-nag>2.0.0',
'aws-cdk.aws-lambda-python-alpha==2.54.0-alpha.0',
'aws-cdk.aws-lambda-python-alpha==2.58.1-alpha.0',
],
)
28 changes: 16 additions & 12 deletions dev_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
-i https://pypi.org/simple
attrs==22.2.0; python_version >= '3.6'
aws-cdk-lib==2.56.1; python_version ~= '3.7'
aws-cdk.asset-awscli-v1==2.2.42; python_version ~= '3.7'
aws-cdk-lib==2.58.1; python_version ~= '3.7'
aws-cdk.asset-awscli-v1==2.2.46; python_version ~= '3.7'
aws-cdk.asset-kubectl-v20==2.1.1; python_version ~= '3.7'
aws-cdk.asset-node-proxy-agent-v5==2.0.38; python_version ~= '3.7'
aws-cdk.aws-lambda-python-alpha==2.54.0a0; python_version ~= '3.7'
boto3==1.26.37
botocore==1.29.37; python_version >= '3.7'
aws-cdk.aws-lambda-python-alpha==2.58.1a0; python_version ~= '3.7'
boto3==1.26.41
botocore==1.29.41; python_version >= '3.7'
cattrs==22.2.0; python_version >= '3.7'
-e ./cdk
cdk-nag==2.21.43; python_version ~= '3.7'
cdk-nag==2.21.47; python_version ~= '3.7'
certifi==2022.12.7; python_version >= '3.6'
cfgv==3.3.1; python_full_version >= '3.6.1'
charset-normalizer==2.1.1; python_version >= '3.6'
click==8.1.3; python_version >= '3.7'
colorama==0.4.6; python_version >= '3.5'
constructs==10.1.201; python_version ~= '3.7'
constructs==10.1.205; python_version ~= '3.7'
coverage[toml]==7.0.1; python_version >= '3.7'
distlib==0.3.6
exceptiongroup==1.1.0; python_version < '3.11'
filelock==3.8.2; python_version >= '3.7'
filelock==3.9.0; python_version >= '3.7'
flake8==6.0.0
future==0.18.2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
ghp-import==2.1.0
gitdb==4.0.10; python_version >= '3.7'
gitpython==3.1.29
gitpython==3.1.30
identify==2.5.11; python_version >= '3.7'
idna==3.4; python_version >= '3.5'
importlib-metadata==5.2.0; python_version < '3.10'
Expand All @@ -42,9 +41,11 @@ mkdocs==1.4.2; python_version >= '3.7'
mkdocs-git-revision-date-plugin==0.3.2
mkdocs-material==8.5.11
mkdocs-material-extensions==1.1.1; python_version >= '3.7'
mypy==0.991
mypy-extensions==0.4.3
nodeenv==1.7.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
packaging==22.0; python_version >= '3.7'
platformdirs==2.6.0; python_version >= '3.7'
platformdirs==2.6.2; python_version >= '3.7'
pluggy==1.0.0; python_version >= '3.6'
pre-commit==2.21.0
publication==0.0.3
Expand All @@ -71,6 +72,9 @@ six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3
smmap==5.0.0; python_version >= '3.6'
tomli==2.0.1; python_version < '3.11'
typeguard==2.13.3; python_full_version >= '3.5.3'
types-cachetools==5.2.1
types-requests==2.28.11.7
types-urllib3==1.26.25.4
typing-extensions==4.4.0; python_version >= '3.7'
urllib3==1.26.13; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
virtualenv==20.17.1; python_version >= '3.6'
Expand All @@ -83,5 +87,5 @@ aws-xray-sdk==2.11.0
cachetools==5.2.0
fastjsonschema==2.16.2
mypy-boto3-dynamodb==1.26.24
pydantic==1.10.2
pydantic==1.10.4
wrapt==1.14.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
4 changes: 2 additions & 2 deletions docs/best_practices/dynamic_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Let's review its advantages:
1. Application (service)
2. Environment
3. Custom deployment strategy - immediate deploy, 0 minutes wait, no validations or AWS CloudWatch alerts
4. The JSON configuration. It uploads the files ‘cdk/my_service/service_stack/configuration/json/{environment}_configuration.json’, where environment is a construct argument (default is 'dev')
4. The JSON configuration. It uploads the files ‘cdk/my_service/configuration/json/{environment}_configuration.json’, where environment is a construct argument (default is 'dev')

The construct **validates** the JSON file and verifies that feature flags syntax is valid and exists under the 'features' key. Feature flags are optional.

Expand All @@ -72,7 +72,7 @@ Args:
--8<-- "docs/examples/best_practices/dynamic_configuration/cdk_appconfig.py"
```

The JSON configuration that is uploaded to AWS AppConfig resides under ``cdk/my_service/service_stack/configuration/json/dev_configuration.json``
The JSON configuration that is uploaded to AWS AppConfig resides under ``cdk/my_service/configuration/json/dev_configuration.json``

``dev`` represents the default environment. You can add multiple configurations for different environments.

Expand Down
6 changes: 3 additions & 3 deletions docs/cdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ In order to add a new dependency, add it to the Pipfile under the [packages] sec

### **CDK Constants**

All ASW Lambda function configurations are saved as constants at the `cdk.my_service.service_stack.constants.py` file and can easily be changed.
All ASW Lambda function configurations are saved as constants at the `cdk.my_service.constants.py` file and can easily be changed.

- Memory size
- Timeout in seconds
Expand All @@ -35,8 +35,8 @@ All ASW Lambda function configurations are saved as constants at the `cdk.my_ser

### **Deployed Resources**

- AWS Cloudformation stack: **cdk.my_service.service_stack.service_stack.py** which is consisted of one construct
- Construct: **cdk.my_service.service_stack.service_construct.py** which includes:
- AWS Cloudformation stack: **cdk.my_service.service_stack.py** which is consisted of one construct
- Construct: **cdk.my_service.service_construct.py** which includes:
- **Lambda Layer** - deployment optimization meant to be used with multiple handlers under the same API GW, sharing code logic and dependencies. You can read more about it in Yan - Cui's [blog](https://medium.com/theburningmonk-com/lambda-layer-not-a-package-manager-but-a-deployment-optimization-85ddcae40a96){:target="_blank" rel="noopener"}
- **Lambda Function** - The Lambda handler function itself. Handler code is taken from the service `folder`.
- **Lambda Role** - The role of the Lambda function.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from aws_cdk import Stack
from constructs import Construct
from my_service.service_stack.configuration.configuration_construct import ConfigurationStore
from my_service.service_stack.constants import CONFIGURATION_NAME, ENVIRONMENT, SERVICE_NAME

from cdk.my_service.configuration.configuration_construct import ConfigurationStore
from cdk.my_service.constants import CONFIGURATION_NAME, ENVIRONMENT, SERVICE_NAME


class DynamicConfigurationStack(Stack):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@
@init_environment_variables(model=MyHandlerEnvVars)
def my_handler(event: Dict[str, Any], context: LambdaContext) -> Dict[str, Any]:
try:
my_configuration: MyConfiguration = parse_configuration(model=MyConfiguration)
my_configuration: MyConfiguration = parse_configuration(model=MyConfiguration) # type: ignore
logger.debug('fetched dynamic configuration', extra={'configuration': my_configuration.dict()})
except (SchemaValidationError, ConfigurationStoreError) as exc:
logger.exception(f'dynamic configuration error, error={str(exc)}')
return build_response(http_status=HTTPStatus.INTERNAL_SERVER_ERROR, body={})

campaign: bool = get_dynamic_configuration_store().evaluate(
campaign = get_dynamic_configuration_store().evaluate(
name='ten_percent_off_campaign',
context={},
default=False,
)
logger.debug('campaign feature flag value', extra={'campaign': campaign})

premium: bool = get_dynamic_configuration_store().evaluate(
premium = get_dynamic_configuration_store().evaluate(
name='premium_features',
context={'customer_name': 'RanTheBuilder'},
default=False,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import my_service.service_stack.constants as constants
from aws_cdk import Duration, aws_apigateway
from aws_cdk import aws_iam as iam
from aws_cdk import aws_lambda as _lambda

import cdk.my_service.constants as constants


def _build_lambda_role(self) -> iam.Role:
return iam.Role(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@init_environment_variables(model=MyHandlerEnvVars)
def my_handler(event: Dict[str, Any], context: LambdaContext) -> Dict[str, Any]:
try:
my_configuration: MyConfiguration = parse_configuration(model=MyConfiguration)
my_configuration: MyConfiguration = parse_configuration(model=MyConfiguration) # type: ignore
except (SchemaValidationError, ConfigurationStoreError) as exc:
logger.exception(f'dynamic configuration error, error={str(exc)}')
return build_response(http_status=HTTPStatus.INTERNAL_SERVER_ERROR, body={})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Literal
from typing import Annotated, Literal

from pydantic import BaseModel, HttpUrl, constr
from pydantic import BaseModel, Field, HttpUrl


class MyHandlerEnvVars(BaseModel):
REST_API: HttpUrl
ROLE_ARN: constr(min_length=20, max_length=2048)
POWERTOOLS_SERVICE_NAME: constr(min_length=1)
ROLE_ARN: Annotated[str, Field(min_length=20, max_length=2048)]
POWERTOOLS_SERVICE_NAME: Annotated[str, Field(min_length=1)]
LOG_LEVEL: Literal['DEBUG', 'INFO', 'ERROR', 'CRITICAL', 'WARNING', 'EXCEPTION']
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@

@init_environment_variables(model=MyHandlerEnvVars)
def my_handler(event: Dict[str, Any], context: LambdaContext) -> Dict[str, Any]:
env_vars: MyHandlerEnvVars = get_environment_variables(model=MyHandlerEnvVars) # noqa: F841
env_vars = get_environment_variables(model=MyHandlerEnvVars) # noqa: F841
return {'statusCode': HTTPStatus.OK, 'headers': {'Content-Type': 'application/json'}, 'body': json.dumps({'message': 'success'})}
Empty file.
Empty file.
Empty file.
Empty file.
1 change: 1 addition & 0 deletions docs/pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ All steps can be run locally using the makefile. See details below:
- Install dev dependencies
- Run pre-commit checks as defined in `.pre-commit-config.yaml`
- Lint with flake8 as defined in `.flake8` - run `make lint` in the IDE
- Static type check with mypy as defined in `.mypy.ini` - run `make mypy-lint` in the IDE
- Verify that Python imports are sorted according to standard - run `make sort` in the IDE
- Python formatter Yapf as defined in `.style` - run `make yapf` in the IDE
- Python complexity checks: radon and xenon - run `make complex` in the IDE
Expand Down
4 changes: 2 additions & 2 deletions lambda_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
-i https://pypi.org/simple
aws-lambda-powertools[all]==2.5.0
aws-xray-sdk==2.11.0
botocore==1.29.37; python_version >= '3.7'
botocore==1.29.41; python_version >= '3.7'
cachetools==5.2.0
fastjsonschema==2.16.2
jmespath==1.0.1; python_version >= '3.7'
mypy-boto3-dynamodb==1.26.24
pydantic==1.10.2
pydantic==1.10.4
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
typing-extensions==4.4.0; python_version >= '3.7'
Expand Down
Loading