Skip to content

Commit

Permalink
docs(event_handler): add micro function examples (#3056)
Browse files Browse the repository at this point in the history
* docs: add micro function examples

* add sam template

* docs: update example to return correct response code

* Add missing .

Co-authored-by: Leandro Damascena <[email protected]>
Signed-off-by: Tony Sherman <[email protected]>

* Update python version in example template

Co-authored-by: Leandro Damascena <[email protected]>
Signed-off-by: Tony Sherman <[email protected]>

* Apply suggestions from code review

Co-authored-by: Leandro Damascena <[email protected]>
Signed-off-by: Tony Sherman <[email protected]>

* additional updates to review suggestions

* Apply suggestions from code review

Co-authored-by: Leandro Damascena <[email protected]>
Signed-off-by: Tony Sherman <[email protected]>

---------

Signed-off-by: Tony Sherman <[email protected]>
Co-authored-by: Tony Sherman <[email protected]>
Co-authored-by: Leandro Damascena <[email protected]>
  • Loading branch information
3 people authored Sep 15, 2023
1 parent 1d502b6 commit 4e8ca0f
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 2 deletions.
29 changes: 28 additions & 1 deletion docs/core/event_handler/api_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,7 @@ A micro function means that your final code artifact will be different to each f
**Benefits**

* **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.
* **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.
* **Discoverability**. Micro functions are easier to 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.
* **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.

**Downsides**
Expand All @@ -859,6 +859,33 @@ your development, building, deployment tooling need to accommodate the distinct
* **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.
* Automated testing, operational and security reviews are essential to stability in either approaches.

**Example**

Consider a simplified micro function structured REST API that has two routes:

* `/users` - an endpoint that will return all users of the application on `GET` requests
* `/users/<id>` - an endpoint that looks up a single users details by ID on `GET` requests

Each endpoint will be it's own Lambda function that is configured as a [Lambda integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-with-lambda-integration.html){target="_blank"}. This allows you to set different configurations for each lambda (memory size, layers, etc.).

=== "`/users` Endpoint"
```python
--8<-- "examples/event_handler_rest/src/micro_function_all_users_route.py"
```

=== "`/users/<id>` Endpoint"
```python
--8<-- "examples/event_handler_rest/src/micro_function_user_by_id_route.py"
```

=== "Micro Function Example SAM Template"
```yaml
--8<-- "examples/event_handler_rest/sam/micro_function_template.yaml"
```

???+ note
You can see some of the downsides in this example such as some code reuse. If set up with proper build tooling, the `User` class could be shared across functions. This could be accomplished by packaging shared code as a Lambda Layer.

## Testing your code

You can test your routes by passing a proxy event request with required params.
Expand Down
2 changes: 1 addition & 1 deletion docs/utilities/data_classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Use **`APIGatewayAuthorizerRequestEvent`** for type `REQUEST` and **`APIGatewayA
if user.get("isAdmin", False):
policy.allow_all_routes()
else:
policy.allow_route(HttpVerb.GET, "/user-profile")
policy.allow_route(HttpVerb.GET.value, "/user-profile")

return policy.asdict()
```
Expand Down
63 changes: 63 additions & 0 deletions examples/event_handler_rest/sam/micro_function_template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
micro-function-example
Globals:
Api:
TracingEnabled: true
Cors: # see CORS section
AllowOrigin: "'https://example.com'"
AllowHeaders: "'Content-Type,Authorization,X-Amz-Date'"
MaxAge: "'300'"
BinaryMediaTypes: # see Binary responses section
- "*~1*" # converts to */* for any binary type

Function:
Timeout: 5
Runtime: python3.11

Resources:
# Lambda Function Solely For /users endpoint
AllUsersFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
CodeUri: users
Description: Function for /users endpoint
Architectures:
- x86_64
Tracing: Active
Events:
UsersPath:
Type: Api
Properties:
Path: /users
Method: GET
MemorySize: 128 # Each Lambda Function can have it's own memory configuration
Environment:
Variables:
LOG_LEVEL: INFO
Tags:
LambdaPowertools: python

# Lambda Function Solely For /users/{id} endpoint
UserByIdFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
CodeUri: users_by_id
Description: Function for /users/{id} endpoint
Architectures:
- x86_64
Tracing: Active
Events:
UsersByIdPath:
Type: Api
Properties:
Path: /users/{id+}
Method: GET
MemorySize: 128 # Each Lambda Function can have it's own memory configuration
Environment:
Variables:
LOG_LEVEL: INFO
56 changes: 56 additions & 0 deletions examples/event_handler_rest/src/micro_function_all_users_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import json
from dataclasses import dataclass
from http import HTTPStatus

from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = Logger()

# This would likely be a db lookup
users = [
{
"user_id": "b0b2a5bf-ee1e-4c5e-9a86-91074052739e",
"email": "[email protected]",
"active": True,
},
{
"user_id": "3a9df6b1-938c-4e80-bd4a-0c966f4b1c1e",
"email": "[email protected]",
"active": False,
},
{
"user_id": "aa0d3d09-9cb9-42b9-9e63-1fb17ea52981",
"email": "[email protected]",
"active": True,
},
]


@dataclass
class User:
user_id: str
email: str
active: bool


app = APIGatewayRestResolver()


@app.get("/users")
def all_active_users():
"""HTTP Response for all active users"""
all_users = [User(**user) for user in users]
all_active_users = [user.__dict__ for user in all_users if user.active]

return Response(
status_code=HTTPStatus.OK.value,
content_type="application/json",
body=json.dumps(all_active_users),
)


@logger.inject_lambda_context()
def lambda_handler(event: dict, context: LambdaContext) -> dict:
return app.resolve(event, context)
71 changes: 71 additions & 0 deletions examples/event_handler_rest/src/micro_function_user_by_id_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import json
from dataclasses import dataclass
from http import HTTPStatus

from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response
from aws_lambda_powertools.utilities.typing import LambdaContext

logger = Logger()

# This would likely be a db lookup
users = [
{
"user_id": "b0b2a5bf-ee1e-4c5e-9a86-91074052739e",
"email": "[email protected]",
"active": True,
},
{
"user_id": "3a9df6b1-938c-4e80-bd4a-0c966f4b1c1e",
"email": "[email protected]",
"active": False,
},
{
"user_id": "aa0d3d09-9cb9-42b9-9e63-1fb17ea52981",
"email": "[email protected]",
"active": True,
},
]


@dataclass
class User:
user_id: str
email: str
active: bool


def get_user_by_id(user_id: str) -> Union[User, None]:
for user_data in users:
if user_data["user_id"] == user_id:
return User(
user_id=str(user_data["user_id"]),
email=str(user_data["email"]),
active=bool(user_data["active"]),
)

return None


app = APIGatewayRestResolver()


@app.get("/users/<user_id>")
def all_active_users(user_id: str):
"""HTTP Response for all active users"""
user = get_user_by_id(user_id)

if user:
return Response(
status_code=HTTPStatus.OK.value,
content_type="application/json",
body=json.dumps(user.__dict__),
)

else:
return Response(status_code=HTTPStatus.NOT_FOUND)


@logger.inject_lambda_context()
def lambda_handler(event: dict, context: LambdaContext) -> dict:
return app.resolve(event, context)

0 comments on commit 4e8ca0f

Please sign in to comment.