Skip to content

Commit 9161cb6

Browse files
authored
feat(new): Added AWS Lambda module (#655)
As part of the effort described, detailed and presented on #559 This is the 4th (and final in this track) PR that should provide support for AWS Lambda containers. This module will add the ability to test and run Amazon Lambdas (using the built-in runtime interface emulator) For example: ```python from testcontainers.aws import AWSLambdaContainer from testcontainers.core.waiting_utils import wait_for_logs from testcontainers.core.image import DockerImage with DockerImage(path="./modules/aws/tests/lambda_sample", tag="test-lambda:latest") as image: with AWSLambdaContainer(image=image, port=8080) as func: response = func.send_request(data={'payload': 'some data'}) assert response.status_code == 200 assert "Hello from AWS Lambda using Python" in response.json() delay = wait_for_logs(func, "START RequestId:") ``` This can (and probably will) be used with the provided [LocalStackContainer](https://testcontainers-python.readthedocs.io/en/latest/modules/localstack/README.html) to help simulate more advance AWS cases. --- Based on the work done on: - #585 - #595 - #612 Expended from issue #83
1 parent 068c431 commit 9161cb6

File tree

8 files changed

+151
-1
lines changed

8 files changed

+151
-1
lines changed

modules/aws/README.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
:code:`testcontainers-aws` is a set of AWS containers modules that can be used to create AWS containers.
2+
3+
.. autoclass:: testcontainers.aws.AWSLambdaContainer
4+
.. title:: testcontainers.aws.AWSLambdaContainer
5+
6+
The following environment variables are used by the AWS Lambda container:
7+
8+
+-------------------------------+--------------------------+------------------------------+
9+
| Env Variable | Default | Notes |
10+
+===============================+==========================+==============================+
11+
| ``AWS_DEFAULT_REGION`` | ``us-west-1`` | Fetched from os environment |
12+
+-------------------------------+--------------------------+------------------------------+
13+
| ``AWS_ACCESS_KEY_ID`` | ``testcontainers-aws`` | Fetched from os environment |
14+
+-------------------------------+--------------------------+------------------------------+
15+
| ``AWS_SECRET_ACCESS_KEY`` | ``testcontainers-aws`` | Fetched from os environment |
16+
+-------------------------------+--------------------------+------------------------------+
17+
18+
Each one of the environment variables is expected to be set in the host machine where the test is running.
19+
20+
Make sure you are using an image based on :code:`public.ecr.aws/lambda/python`
21+
22+
Please checkout https://docs.aws.amazon.com/lambda/latest/dg/python-image.html for more information on how to run AWS Lambda functions locally.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .aws_lambda import AWSLambdaContainer # noqa: F401
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import os
2+
from typing import Union
3+
4+
import httpx
5+
6+
from testcontainers.core.image import DockerImage
7+
from testcontainers.generic.server import ServerContainer
8+
9+
RIE_PATH = "/2015-03-31/functions/function/invocations"
10+
# AWS OS-only base images contain an Amazon Linux distribution and the runtime interface emulator (RIE) for Lambda.
11+
12+
13+
class AWSLambdaContainer(ServerContainer):
14+
"""
15+
AWS Lambda container that is based on a custom image.
16+
17+
Example:
18+
19+
.. doctest::
20+
21+
>>> from testcontainers.aws import AWSLambdaContainer
22+
>>> from testcontainers.core.waiting_utils import wait_for_logs
23+
>>> from testcontainers.core.image import DockerImage
24+
25+
>>> with DockerImage(path="./modules/aws/tests/lambda_sample", tag="test-lambda:latest") as image:
26+
... with AWSLambdaContainer(image=image, port=8080) as func:
27+
... response = func.send_request(data={'payload': 'some data'})
28+
... assert response.status_code == 200
29+
... assert "Hello from AWS Lambda using Python" in response.json()
30+
... delay = wait_for_logs(func, "START RequestId:")
31+
32+
:param image: Docker image to be used for the container.
33+
:param port: Port to be exposed on the container (default: 8080).
34+
"""
35+
36+
def __init__(self, image: Union[str, DockerImage], port: int = 8080) -> None:
37+
super().__init__(port, str(image))
38+
self.with_env("AWS_DEFAULT_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-west-1"))
39+
self.with_env("AWS_ACCESS_KEY_ID", os.environ.get("AWS_ACCESS_KEY_ID", "testcontainers-aws"))
40+
self.with_env("AWS_SECRET_ACCESS_KEY", os.environ.get("AWS_SECRET_ACCESS_KEY", "testcontainers-aws"))
41+
42+
def get_api_url(self) -> str:
43+
return self._create_connection_url() + RIE_PATH
44+
45+
def send_request(self, data: dict) -> httpx.Response:
46+
"""
47+
Send a request to the AWS Lambda function.
48+
49+
:param data: Data to be sent to the AWS Lambda function.
50+
:return: Response from the AWS Lambda function.
51+
"""
52+
client = self.get_client()
53+
return client.post(self.get_api_url(), json=data)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM public.ecr.aws/lambda/python:3.9
2+
3+
RUN pip install boto3
4+
5+
COPY lambda_function.py ${LAMBDA_TASK_ROOT}
6+
7+
EXPOSE 8080
8+
9+
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
10+
CMD [ "lambda_function.handler" ]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import sys
2+
3+
4+
def handler(event, context):
5+
return "Hello from AWS Lambda using Python" + sys.version + "!"

modules/aws/tests/test_aws.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import re
2+
import os
3+
4+
import pytest
5+
from unittest.mock import patch
6+
7+
from testcontainers.core.image import DockerImage
8+
from testcontainers.aws import AWSLambdaContainer
9+
from testcontainers.aws.aws_lambda import RIE_PATH
10+
11+
DOCKER_FILE_PATH = "./modules/aws/tests/lambda_sample"
12+
IMAGE_TAG = "lambda:test"
13+
14+
15+
def test_aws_lambda_container():
16+
with DockerImage(path=DOCKER_FILE_PATH, tag="test-lambda:latest") as image:
17+
with AWSLambdaContainer(image=image, port=8080) as func:
18+
assert func.get_container_host_ip() == "localhost"
19+
assert func.internal_port == 8080
20+
assert func.env["AWS_DEFAULT_REGION"] == "us-west-1"
21+
assert func.env["AWS_ACCESS_KEY_ID"] == "testcontainers-aws"
22+
assert func.env["AWS_SECRET_ACCESS_KEY"] == "testcontainers-aws"
23+
assert re.match(rf"http://localhost:\d+{RIE_PATH}", func.get_api_url())
24+
response = func.send_request(data={"payload": "test"})
25+
assert response.status_code == 200
26+
assert "Hello from AWS Lambda using Python" in response.json()
27+
for log_str in ["START RequestId", "END RequestId", "REPORT RequestId"]:
28+
assert log_str in func.get_stdout()
29+
30+
31+
def test_aws_lambda_container_external_env_vars():
32+
vars = {
33+
"AWS_DEFAULT_REGION": "region",
34+
"AWS_ACCESS_KEY_ID": "id",
35+
"AWS_SECRET_ACCESS_KEY": "key",
36+
}
37+
with patch.dict(os.environ, vars):
38+
with DockerImage(path=DOCKER_FILE_PATH, tag="test-lambda-env-vars:latest") as image:
39+
with AWSLambdaContainer(image=image, port=8080) as func:
40+
assert func.env["AWS_DEFAULT_REGION"] == "region"
41+
assert func.env["AWS_ACCESS_KEY_ID"] == "id"
42+
assert func.env["AWS_SECRET_ACCESS_KEY"] == "key"
43+
44+
45+
def test_aws_lambda_container_no_port():
46+
with DockerImage(path=DOCKER_FILE_PATH, tag="test-lambda-no-port:latest") as image:
47+
with AWSLambdaContainer(image=image) as func:
48+
response = func.send_request(data={"payload": "test"})
49+
assert response.status_code == 200
50+
51+
52+
def test_aws_lambda_container_no_path():
53+
with pytest.raises(TypeError):
54+
with DockerImage(path=DOCKER_FILE_PATH, tag="test-lambda-no-path:latest") as image:
55+
with AWSLambdaContainer() as func: # noqa: F841
56+
pass

poetry.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ classifiers = [
2929
packages = [
3030
{ include = "testcontainers", from = "core" },
3131
{ include = "testcontainers", from = "modules/arangodb" },
32+
{ include = "testcontainers", from = "modules/aws"},
3233
{ include = "testcontainers", from = "modules/azurite" },
3334
{ include = "testcontainers", from = "modules/cassandra" },
3435
{ include = "testcontainers", from = "modules/chroma" },
@@ -116,6 +117,7 @@ trino = { version = "*", optional = true }
116117

117118
[tool.poetry.extras]
118119
arangodb = ["python-arango"]
120+
aws = ["boto3", "httpx"]
119121
azurite = ["azure-storage-blob"]
120122
cassandra = []
121123
clickhouse = ["clickhouse-driver"]

0 commit comments

Comments
 (0)