Skip to content

Commit c92c1c0

Browse files
authored
docs(apigateway): re-add sample layout, add considerations (#826)
1 parent bda42a8 commit c92c1c0

File tree

4 files changed

+211
-9
lines changed

4 files changed

+211
-9
lines changed

docs/core/event_handler/api_gateway.md

+209-9
Original file line numberDiff line numberDiff line change
@@ -896,14 +896,16 @@ Let's assume you have `app.py` as your Lambda function entrypoint and routes in
896896

897897
We use `include_router` method and include all user routers registered in the `router` global object.
898898

899-
```python hl_lines="6 8-9"
899+
```python hl_lines="7 10-11"
900900
from typing import Dict
901901

902+
from aws_lambda_powertools import Logger
902903
from aws_lambda_powertools.event_handler import ApiGatewayResolver
903904
from aws_lambda_powertools.utilities.typing import LambdaContext
904905

905906
import users
906907

908+
logger = Logger()
907909
app = ApiGatewayResolver()
908910
app.include_router(users.router)
909911

@@ -960,22 +962,220 @@ When necessary, you can set a prefix when including a router object. This means
960962
# many other related /users routing
961963
```
962964

965+
#### Sample layout
966+
967+
!!! info "We use ALB to demonstrate that the UX remains the same"
968+
969+
This sample project contains an Users function with two distinct set of routes, `/users` and `/health`. The layout optimizes for code sharing, no custom build tooling, and it uses [Lambda Layers](../../index.md#lambda-layer) to install Lambda Powertools.
970+
971+
=== "Project layout"
972+
973+
974+
```python hl_lines="6 8 10-13"
975+
.
976+
├── Pipfile # project app & dev dependencies; poetry, pipenv, etc.
977+
├── Pipfile.lock
978+
├── mypy.ini # namespace_packages = True
979+
├── .env # VSCode only. PYTHONPATH="users:${PYTHONPATH}"
980+
├── users
981+
│ ├── requirements.txt # sam build detect it automatically due to CodeUri: users, e.g. pipenv lock -r > users/requirements.txt
982+
│ ├── lambda_function.py # this will be our users Lambda fn; it could be split in folders if we want separate fns same code base
983+
│ ├── constants.py
984+
│ └── routers # routers module
985+
│ ├── __init__.py
986+
│ ├── users.py # /users routes, e.g. from routers import users; users.router
987+
│ ├── health.py # /health routes, e.g. from routers import health; health.router
988+
├── template.yaml # SAM template.yml, CodeUri: users, Handler: users.main.lambda_handler
989+
└── tests
990+
├── __init__.py
991+
├── unit
992+
│ ├── __init__.py
993+
│ └── test_users.py # unit tests for the users router
994+
│ └── test_health.py # unit tests for the health router
995+
└── functional
996+
├── __init__.py
997+
├── conftest.py # pytest fixtures for the functional tests
998+
└── test_lambda_function.py # functional tests for the main lambda handler
999+
```
1000+
1001+
=== "template.yml"
1002+
1003+
```yaml hl_lines="20-21"
1004+
AWSTemplateFormatVersion: '2010-09-09'
1005+
Transform: AWS::Serverless-2016-10-31
1006+
Description: Example service with multiple routes
1007+
Globals:
1008+
Function:
1009+
Timeout: 10
1010+
MemorySize: 512
1011+
Runtime: python3.9
1012+
Tracing: Active
1013+
Environment:
1014+
Variables:
1015+
LOG_LEVEL: INFO
1016+
POWERTOOLS_LOGGER_LOG_EVENT: true
1017+
POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication
1018+
POWERTOOLS_SERVICE_NAME: users
1019+
Resources:
1020+
UsersService:
1021+
Type: AWS::Serverless::Function
1022+
Properties:
1023+
Handler: lambda_function.lambda_handler
1024+
CodeUri: users
1025+
Layers:
1026+
# Latest version: https://awslabs.github.io/aws-lambda-powertools-python/latest/#lambda-layer
1027+
- !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:3
1028+
Events:
1029+
ByUser:
1030+
Type: Api
1031+
Properties:
1032+
Path: /users/{name}
1033+
Method: GET
1034+
AllUsers:
1035+
Type: Api
1036+
Properties:
1037+
Path: /users
1038+
Method: GET
1039+
HealthCheck:
1040+
Type: Api
1041+
Properties:
1042+
Path: /status
1043+
Method: GET
1044+
Outputs:
1045+
UsersApiEndpoint:
1046+
Description: "API Gateway endpoint URL for Prod environment for Users Function"
1047+
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"
1048+
AllUsersURL:
1049+
Description: "URL to fetch all registered users"
1050+
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/users"
1051+
ByUserURL:
1052+
Description: "URL to retrieve details by user"
1053+
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/users/test"
1054+
UsersServiceFunctionArn:
1055+
Description: "Users Lambda Function ARN"
1056+
Value: !GetAtt UsersService.Arn
1057+
```
1058+
1059+
=== "users/lambda_function.py"
1060+
1061+
```python hl_lines="9 15-16"
1062+
from typing import Dict
1063+
1064+
from aws_lambda_powertools import Logger, Tracer
1065+
from aws_lambda_powertools.event_handler import ApiGatewayResolver
1066+
from aws_lambda_powertools.event_handler.api_gateway import ProxyEventType
1067+
from aws_lambda_powertools.logging.correlation_paths import APPLICATION_LOAD_BALANCER
1068+
from aws_lambda_powertools.utilities.typing import LambdaContext
1069+
1070+
from routers import health, users
1071+
1072+
tracer = Tracer()
1073+
logger = Logger()
1074+
app = ApiGatewayResolver(proxy_type=ProxyEventType.ALBEvent)
1075+
1076+
app.include_router(health.router)
1077+
app.include_router(users.router)
1078+
1079+
1080+
@logger.inject_lambda_context(correlation_id_path=APPLICATION_LOAD_BALANCER)
1081+
@tracer.capture_lambda_handler
1082+
def lambda_handler(event: Dict, context: LambdaContext):
1083+
return app.resolve(event, context)
1084+
```
1085+
1086+
=== "users/routers/health.py"
1087+
1088+
```python hl_lines="4 6-7 10"
1089+
from typing import Dict
1090+
1091+
from aws_lambda_powertools import Logger
1092+
from aws_lambda_powertools.event_handler.api_gateway import Router
1093+
1094+
router = Router()
1095+
logger = Logger(child=True)
1096+
1097+
1098+
@router.get("/status")
1099+
def health() -> Dict:
1100+
logger.debug("Health check called")
1101+
return {"status": "OK"}
1102+
```
1103+
1104+
=== "tests/functional/test_users.py"
1105+
1106+
```python hl_lines="3"
1107+
import json
1108+
1109+
from users import main # follows namespace package from root
1110+
1111+
1112+
def test_lambda_handler(apigw_event, lambda_context):
1113+
ret = main.lambda_handler(apigw_event, lambda_context)
1114+
expected = json.dumps({"message": "hello universe"}, separators=(",", ":"))
1115+
1116+
assert ret["statusCode"] == 200
1117+
assert ret["body"] == expected
1118+
```
1119+
1120+
=== ".env"
1121+
1122+
> Note: It is not needed for PyCharm (select folder as source).
1123+
1124+
This is necessary for Visual Studio Code, so integrated tooling works without failing import.
1125+
1126+
```bash
1127+
PYTHONPATH="users:${PYTHONPATH}"
1128+
```
1129+
1130+
### Considerations
1131+
1132+
This utility is optimized for fast startup, minimal feature set, and to quickly on-board customers familiar with frameworks like Flask — it's not meant to be a fully fledged framework.
1133+
1134+
Event Handler naturally leads to a single Lambda function handling multiple routes for a given service, which can be eventually broken into multiple functions.
1135+
1136+
Both single (monolithic) and multiple functions (micro) offer different set of trade-offs worth knowing.
1137+
1138+
!!! tip "TL;DR. Start with a monolithic function, add additional functions with new handlers, and possibly break into micro functions if necessary."
1139+
1140+
#### Monolithic function
1141+
1142+
![Monolithic function sample](./../../media/monolithic-function.png)
1143+
1144+
A monolithic function means that your final code artifact will be deployed to a single function. This is generally the best approach to start.
1145+
1146+
**Benefits**
1147+
1148+
* **Code reuse**. It's easier to reason about your service, modularize it and reuse code as it grows. Eventually, it can be turned into a standalone library.
1149+
* **No custom tooling**. Monolithic functions are treated just like normal Python packages; no upfront investment in tooling.
1150+
* **Faster deployment and debugging**. Whether you use all-at-once, linear, or canary deployments, a monolithic function is a single deployable unit. IDEs like PyCharm and VSCode have tooling to quickly profile, visualize, and step through debug any Python package.
1151+
1152+
**Downsides**
9631153

964-
#### Trade-offs
1154+
* **Cold starts**. Frequent deployments and/or high load can diminish the benefit of monolithic functions depending on your latency requirements, due to [Lambda scaling model](https://docs.aws.amazon.com/lambda/latest/dg/invocation-scaling.html){target="_blank"}. Always load test to pragmatically balance between your customer experience and development cognitive load.
1155+
* **Granular security permissions**. The micro function approach enables you to use fine-grained permissions & access controls, separate external dependencies & code signing at the function level. Conversely, you could have multiple functions while duplicating the final code artifact in a monolithic approach.
1156+
- Regardless, least privilege can be applied to either approaches.
1157+
* **Higher risk per deployment**. A misconfiguration or invalid import can cause disruption if not caught earlier in automated testing. Multiple functions can mitigate misconfigurations but they would still share the same code artifact. You can further minimize risks with multiple environments in your CI/CD pipeline.
9651158

966-
!!! tip "TL;DR. Balance your latency requirements, cognitive overload, least privilege, and operational overhead to decide between one, few, or many single purpose functions."
1159+
#### Micro function
9671160

968-
Route splitting feature helps accommodate customers familiar with popular frameworks and practices found in the Python community.
1161+
![Micro function sample](./../../media/micro-function.png)
9691162

970-
It can help better organize your code and reason
1163+
A micro function means that your final code artifact will be different to each function deployed. This is generally the approach to start if you're looking for fine-grain control and/or high load on certain parts of your service.
9711164

972-
This can also quickly lead to discussions whether it facilitates a monolithic vs single-purpose function. To this end, these are common trade-offs you'll encounter as you grow your Serverless service, specifically synchronous functions.
1165+
**Benefits**
9731166

974-
**Least privilege**. Start with a monolithic function, then split them as their data access & boundaries become clearer. Treat Lambda functions as separate logical resources to more easily scope permissions.
1167+
* **Granular scaling**. A micro function can benefit from the [Lambda scaling model](https://docs.aws.amazon.com/lambda/latest/dg/invocation-scaling.html){target="_blank"} to scale differently depending on each part of your application. Concurrency controls and provisioned concurrency can also be used at a granular level for capacity management.
1168+
* **Discoverability**. Micro functions are easier do visualize when using distributed tracing. Their high-level architectures can be self-explanatory, and complexity is highly visible — assuming each function is named to the business purpose it serves.
1169+
* **Package size**. An independent function can be significant smaller (KB vs MB) depending on external dependencies it require to perform its purpose. Conversely, a monolithic approach can benefit from [Lambda Layers](https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html){target="_blank"} to optimize builds for external dependencies.
9751170

976-
**Package size**. Consider Lambda Layers for third-party dependencies and service-level shared code. Treat third-party dependencies as dev dependencies, and Lambda Layers as a mechanism to speed up build and deployments.
1171+
**Downsides**
9771172

978-
**Cold start**. High load can diminish the benefit of monolithic functions depending on your latency requirements. Always load test to pragmatically balance between your customer experience and development cognitive load.
1173+
* **Upfront investment**. Python ecosystem doesn't use a bundler — you need a custom build tooling to ensure each function only has what it needs and account for [C bindings for runtime compatibility](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html){target="_blank"}. Operations become more elaborate — you need to standardize tracing labels/annotations, structured logging, and metrics to pinpoint root causes.
1174+
- Engineering discipline is necessary for both approaches. Micro-function approach however requires further attention in consistency as the number of functions grow, just like any distributed system.
1175+
* **Harder to share code**. Shared code must be carefully evaluated to avoid unnecessary deployments when that changes. Equally, if shared code isn't a library,
1176+
your development, building, deployment tooling need to accommodate the distinct layout.
1177+
* **Slower safe deployments**. Safely deploying multiple functions require coordination — AWS CodeDeploy deploys and verifies each function sequentially. This increases lead time substantially (minutes to hours) depending on the deployment strategy you choose. You can mitigate it by selectively enabling it in prod-like environments only, and where the risk profile is applicable.
1178+
- Automated testing, operational and security reviews are essential to stability in either approaches.
9791179

9801180
## Testing your code
9811181

docs/core/event_handler/appsync.md

+2
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,8 @@ You can subclass `AppSyncResolverEvent` to bring your own set of methods to hand
713713

714714
### Split operations with Router
715715

716+
!!! tip "Read the **[considerations section for trade-offs between monolithic and micro functions](./api_gateway.md#considerations){target="_blank"}**, as it's also applicable here."
717+
716718
As you grow the number of related GraphQL operations a given Lambda function should handle, it is natural to split them into separate files to ease maintenance - That's where the `Router` feature is useful.
717719

718720
Let's assume you have `app.py` as your Lambda function entrypoint and routes in `users.py`, this is how you'd use the `Router` feature.

docs/media/micro-function.png

84.8 KB
Loading

docs/media/monolithic-function.png

79.8 KB
Loading

0 commit comments

Comments
 (0)