Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(event_handler): add micro function examples #3056

Merged
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
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
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)
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)