Skip to content

Commit 6976e28

Browse files
heitorlessarubenfonseca
authored andcommitted
docs(maintainers): explain framework mechanics
1 parent 06e1ca5 commit 6976e28

File tree

3 files changed

+197
-6
lines changed

3 files changed

+197
-6
lines changed

MAINTAINERS.md

Lines changed: 172 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
- [Drafting release notes](#drafting-release-notes)
1717
- [Run end to end tests](#run-end-to-end-tests)
1818
- [Structure](#structure)
19-
- [Workflow](#workflow)
19+
- [Mechanics](#mechanics)
20+
- [Authoring an E2E test](#authoring-an-e2e-test)
21+
- [Internals](#internals)
22+
- [Parallelization](#parallelization)
23+
- [CDK safe parallelization](#cdk-safe-parallelization)
2024
- [Releasing a documentation hotfix](#releasing-a-documentation-hotfix)
2125
- [Maintain Overall Health of the Repo](#maintain-overall-health-of-the-repo)
2226
- [Manage Roadmap](#manage-roadmap)
@@ -266,7 +270,169 @@ You probably notice we have multiple `conftest.py`, `infrastructure.py`, and `ha
266270
- Feature-level `e2e/<feature>/conftest` deploys stacks in parallel and make them independent of each other.
267271
- **`handlers/`**. Lambda function handlers that will be automatically deployed and exported as PascalCase for later use.
268272

269-
#### Parallelization
273+
#### Mechanics
274+
275+
Under `BaseInfrastructure`, we hide the complexity of handling CDK parallel deployments, exposing CloudFormation Outputs, building Lambda Layer with the latest available code, and creating Lambda functions found in `handlers`.
276+
277+
This allows us to benefit from test and deployment parallelization, use IDE step-through debugging for a single test, run a subset of tests and only deploy their related infrastructure, without any custom configuration.
278+
279+
> Class diagram to understand abstraction built when defining a new stack (`LoggerStack`)
280+
281+
```mermaid
282+
classDiagram
283+
class InfrastructureProvider {
284+
<<interface>>
285+
+deploy() Dict
286+
+delete()
287+
+create_resources()
288+
+create_lambda_functions(function_props: Dict)
289+
}
290+
291+
class BaseInfrastructure {
292+
+deploy() Dict
293+
+delete()
294+
+create_lambda_functions(function_props: Dict) Dict~Functions~
295+
+add_cfn_output(name: str, value: str, arn: Optional[str])
296+
}
297+
298+
class TracerStack {
299+
+create_resources()
300+
}
301+
302+
class LoggerStack {
303+
+create_resources()
304+
}
305+
306+
class MetricsStack {
307+
+create_resources()
308+
}
309+
310+
class EventHandlerStack {
311+
+create_resources()
312+
}
313+
314+
InfrastructureProvider <|-- BaseInfrastructure : implement
315+
BaseInfrastructure <|-- TracerStack : inherit
316+
BaseInfrastructure <|-- LoggerStack : inherit
317+
BaseInfrastructure <|-- MetricsStack : inherit
318+
BaseInfrastructure <|-- EventHandlerStack : inherit
319+
```
320+
321+
#### Authoring an E2E test
322+
323+
Imagine you're going to create E2E for Event Handler feature for the first time.
324+
325+
As a mental model, you'll need to: **(1)** Define infrastructure, **(2)** Deploy/Delete infrastructure when tests run, and **(3)** Expose resources for E2E tests.
326+
327+
**Define infrastructure**
328+
329+
We use CDK as our Infrastructure as Code tool of choice. Before you start using CDK, you need to take the following steps:
330+
331+
1. Create `tests/e2e/event_handler/infrastructure.py` file
332+
2. Create a new class `EventHandlerStack` and inherit from `BaseInfrastructure`
333+
3. Override `create_resources` method and define your infrastructure using CDK
334+
4. (Optional) Create a Lambda function under `handlers/alb_handler.py`
335+
336+
> Excerpt `infrastructure.py` for Event Handler
337+
338+
```python
339+
class EventHandlerStack(BaseInfrastructure):
340+
def create_resources(self):
341+
functions = self.create_lambda_functions()
342+
343+
self._create_alb(function=functions["AlbHandler"])
344+
...
345+
346+
def _create_alb(self, function: Function):
347+
vpc = ec2.Vpc.from_lookup(
348+
self.stack,
349+
"VPC",
350+
is_default=True,
351+
region=self.region,
352+
)
353+
354+
alb = elbv2.ApplicationLoadBalancer(self.stack, "ALB", vpc=vpc, internet_facing=True)
355+
CfnOutput(self.stack, "ALBDnsName", value=alb.load_balancer_dns_name)
356+
...
357+
```
358+
359+
> Excerpt `alb_handler.py` for Event Handler
360+
361+
```python
362+
from aws_lambda_powertools.event_handler import ALBResolver, Response, content_types
363+
364+
app = ALBResolver()
365+
366+
367+
@app.get("/todos")
368+
def hello():
369+
return Response(
370+
status_code=200,
371+
content_type=content_types.TEXT_PLAIN,
372+
body="Hello world",
373+
cookies=["CookieMonster", "MonsterCookie"],
374+
headers={"Foo": ["bar", "zbr"]},
375+
)
376+
377+
378+
def lambda_handler(event, context):
379+
return app.resolve(event, context)
380+
```
381+
382+
**Deploy/Delete infrastructure when tests run**
383+
384+
We need to instruct Pytest to deploy our infrastructure when our tests start, and delete it when they complete (successfully or not).
385+
386+
For this, we create a `test/e2e/event_handler/conftest.py` and create fixture scoped to our test module. This will remain static and will not need any further modification in the future.
387+
388+
> Excerpt `conftest.py` for Event Handler
389+
390+
```python
391+
import pytest
392+
393+
from tests.e2e.event_handler.infrastructure import EventHandlerStack
394+
395+
396+
@pytest.fixture(autouse=True, scope="module")
397+
def infrastructure():
398+
"""Setup and teardown logic for E2E test infrastructure
399+
400+
Yields
401+
------
402+
Dict[str, str]
403+
CloudFormation Outputs from deployed infrastructure
404+
"""
405+
stack = EventHandlerStack()
406+
try:
407+
yield stack.deploy()
408+
finally:
409+
stack.delete()
410+
411+
```
412+
413+
**Expose resources for E2E tests**
414+
415+
Within our tests, we should now have access to the `infrastructure` fixture we defined. We can access any Stack Output using pytest dependency injection.
416+
417+
> Excerpt `test_header_serializer.py` for Event Handler
418+
419+
```python
420+
@pytest.fixture
421+
def alb_basic_listener_endpoint(infrastructure: dict) -> str:
422+
dns_name = infrastructure.get("ALBDnsName")
423+
port = infrastructure.get("ALBBasicListenerPort", "")
424+
return f"http://{dns_name}:{port}"
425+
426+
427+
def test_alb_headers_serializer(alb_basic_listener_endpoint):
428+
# GIVEN
429+
url = f"{alb_basic_listener_endpoint}/todos"
430+
...
431+
```
432+
433+
#### Internals
434+
435+
##### Parallelization
270436

271437
We parallelize our end-to-end tests to benefit from speed and isolate Lambda functions to ease assessing side effects (e.g., traces, logs, etc.). The following diagram demonstrates the process we take every time you use `make e2e`:
272438

@@ -299,6 +465,10 @@ graph TD
299465
ResultCollection --> DeployEnd["Delete Stacks"]
300466
```
301467

468+
##### CDK safe parallelization
469+
470+
Describe CDK App, Stack, synth, etc.
471+
302472
### Releasing a documentation hotfix
303473

304474
You can rebuild the latest documentation without a full release via this [GitHub Actions Workflow](https://github.com/awslabs/aws-lambda-powertools-python/actions/workflows/rebuild_latest_docs.yml). Choose `Run workflow`, keep `develop` as the branch, and input the latest Powertools version available.

tests/e2e/utils/base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Dict, Optional
3+
4+
5+
class InfrastructureProvider(ABC):
6+
@abstractmethod
7+
def create_lambda_functions(self, function_props: Optional[Dict] = None) -> Dict:
8+
pass
9+
10+
@abstractmethod
11+
def deploy(self) -> Dict[str, str]:
12+
pass
13+
14+
@abstractmethod
15+
def delete(self):
16+
pass
17+
18+
@abstractmethod
19+
def create_resources(self):
20+
pass

tests/e2e/utils/infrastructure.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import subprocess
55
import sys
66
import textwrap
7-
from abc import ABC, abstractmethod
87
from pathlib import Path
98
from typing import Callable, Dict, Generator, Optional
109
from uuid import uuid4
@@ -16,13 +15,14 @@
1615
from filelock import FileLock
1716
from mypy_boto3_cloudformation import CloudFormationClient
1817

18+
from tests.e2e.utils.base import InfrastructureProvider
1919
from tests.e2e.utils.constants import CDK_OUT_PATH, PYTHON_RUNTIME_VERSION, SOURCE_CODE_ROOT_PATH
2020
from tests.e2e.utils.lambda_layer.powertools_layer import LocalLambdaPowertoolsLayer
2121

2222
logger = logging.getLogger(__name__)
2323

2424

25-
class BaseInfrastructure(ABC):
25+
class BaseInfrastructure(InfrastructureProvider):
2626
RANDOM_STACK_VALUE: str = f"{uuid4()}"
2727

2828
def __init__(self) -> None:
@@ -103,6 +103,8 @@ def create_lambda_functions(self, function_props: Optional[Dict] = None) -> Dict
103103
code=Code.from_asset(path=layer_build),
104104
)
105105

106+
# NOTE: Agree on a convention if we need to support multi-file handlers
107+
# as we're simply taking any file under `handlers/` to be a Lambda function.
106108
handlers = list(self._handlers_dir.rglob("*.py"))
107109
source = Code.from_asset(f"{self._handlers_dir}")
108110
logger.debug(f"Creating functions for handlers: {handlers}")
@@ -222,7 +224,6 @@ def _create_temp_cdk_app(self):
222224
temp_file.chmod(0o755)
223225
return temp_file
224226

225-
@abstractmethod
226227
def create_resources(self) -> None:
227228
"""Create any necessary CDK resources. It'll be called before deploy
228229
@@ -246,7 +247,7 @@ def created_resources(self):
246247
self.create_lambda_functions()
247248
```
248249
"""
249-
...
250+
raise NotImplementedError()
250251

251252
def add_cfn_output(self, name: str, value: str, arn: str = ""):
252253
"""Create {Name} and optionally {Name}Arn CloudFormation Outputs.

0 commit comments

Comments
 (0)