Skip to content

Commit 55dc77c

Browse files
committed
Added s3-logs-extension-demo-container-image
1 parent 4772094 commit 55dc77c

32 files changed

+826
-3
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ In this repository you'll find a number of different sample projects and demos t
1515

1616
* [AWS AppConfig extension demo](awsappconfig-extension-demo/)
1717
* [Custom runtime extension demo](custom-runtime-extension-demo/)
18-
* [Logs to Amazon S3 extension demo](s3-logs-extension-demo/)
18+
* [Logs to Amazon S3 extension demo: zip archive](s3-logs-extension-demo-zip-archive/)
19+
* [Logs to Amazon S3 extension demo: container image ](s3-logs-extension-demo-container-image/)
1920
* [Extension in Go](go-example-extension/)
2021
* [Extension in Python](python-example-extension/)
2122
* [Extension in Node.js](nodejs-example-extension/)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# S3-Logs extension demo using container image format
2+
3+
This is a demo of the logging functionality available with [AWS Lambda](https://aws.amazon.com/lambda/) Extensions to send logs directly from Lambda to [Amazon Simple Storage Service (S3)](https://aws.amazon.com/s3/).
4+
5+
This example packages the extension and function as separate container images. See the s3-logs-extension-demo-zip-archive version to use the functionality without container images.
6+
7+
For more information on the extensions logs functionality, see the blog post [Using AWS Lambda extensions to send logs to custom destinations](https://aws.amazon.com/blogs/compute/using-aws-lambda-extensions-to-send-logs-to-custom-destinations/)
8+
9+
> This is a simple example extension to help you start investigating the Lambda Runtime Logs API. This code is not production ready, and it has never intended to be. Use it with your own discretion after testing thoroughly.
10+
11+
The demo includes a Lambda function with an extension delivered as a container image.
12+
13+
The extension uses the Extensions API to register for INVOKE and SHUTDOWN events. The extension, using the Logs API, then subscribes to receive platform and function logs, but not extension logs. The extension runs a local HTTP endpoint listening for HTTP POST events. Lambda delivers log batches to this endpoint. The code can be amended (see the comments) to handle each log record in the batch. This can be used to process, filter, and route individual log records to any preferred destination
14+
15+
The example creates an S3 bucket to store the logs. A Lambda function is configured with an environment variable to specify the S3 bucket name. Lambda streams the logs to the extension. The extension copies the logs to the S3 bucket.
16+
17+
The extension uses the Python runtime from the execution environment to show the functionality. The recommended best practice is to compile your extension into an executable binary and not rely on the runtime.
18+
19+
The demo builds a separate container image for the extension.
20+
21+
The [AWS Serverless Application Model (AWS SAM)](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) is used to build the function as another container image, which includes the previously created extension container image.
22+
23+
## Requirements
24+
25+
* [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) - **minimum version 0.48**.
26+
* [Docker CLI](https://docs.docker.com/get-docker/)
27+
28+
[Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and login.
29+
30+
Clone the repo onto your local development machine:
31+
```bash
32+
git clone https://github.com/aws-samples/aws-lambda-extensions
33+
cd s3-logs-extension-demo-container-image
34+
```
35+
36+
## Installation instructions
37+
38+
### Create the extension container image
39+
1. Create an [Amazon Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/) repository for the extension.
40+
```bash
41+
aws ecr create-repository --repository-name log-extension-image
42+
```
43+
Note the `repositoryUri` created.
44+
45+
2. Run the following command to build the container image containing the extension as specified in the `Dockerfile` file.
46+
```bash
47+
cd .\extension\
48+
docker build -t log-extension-image:latest .
49+
```
50+
3. Tag, login, and push the extension container image to an existing ECR repository.
51+
52+
Replace the `repositoryUri` with the one previous created.
53+
54+
Replace the `AccountID` with the your AWS Account ID.
55+
5.
56+
```
57+
docker tag log-extension-image:latest <repositoryUri>:latest
58+
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <AccountID>.dkr.ecr.us-east-1.amazonaws.com
59+
docker push <repositoryUri>:latest
60+
cd ..
61+
```
62+
### Create the Lambda function container image
63+
64+
1. Create an [Amazon Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/) repository for the extension
65+
```bash
66+
aws ecr create-repository --repository-name log-extension-function
67+
```
68+
69+
Note the `repositoryUri` created.
70+
71+
There is no need to run `pip install` for the function as it does not require any dependencies. The function Dockerfile contains the following lines, which add the previously created extension image. Replace the first FROM line with the `repositoryUri` previously created for the extension layer.
72+
73+
```
74+
FROM 123456789012.dkr.ecr.us-east-1.amazonaws.com/log-extension-image:latest AS layer
75+
FROM public.ecr.aws/lambda/python:3.8
76+
# Layer code
77+
WORKDIR /opt
78+
COPY --from=layer /opt/ .
79+
80+
# Function code
81+
WORKDIR /var/task
82+
COPY app.py .
83+
84+
CMD ["app.lambda_handler"]
85+
```
86+
87+
The AWS SAM template specified in the `template.yml` file builds the Lambda function as a container image.
88+
89+
Update the `/function/Dockerfile` with the extension container image location previously created.
90+
91+
1. Run the following commands
92+
93+
```
94+
cd function
95+
sam build
96+
sam deploy --stack-name s3-logs-extension-demo --guided
97+
98+
```
99+
100+
During the prompts:
101+
102+
* Accept the default Stack Name `s3-logs-extension-demo`.
103+
* Enter your preferred Region
104+
* Enter the function container image ECR repository such as: `123456789012.dkr.ecr.us-east-1.amazonaws.com/log-function-image`
105+
* Accept the defaults for the remaining questions.
106+
107+
AWS SAM deploys the application stack which includes the Lambda function and an IAM Role.
108+
109+
Note the outputted S3 Bucket Name.
110+
111+
## Invoke the Lambda function
112+
You can now invoke the Lambda function. Amend the Region and use the following command:
113+
```bash
114+
aws lambda invoke \
115+
--function-name "logs-extension-demo-function-container-image" \
116+
--payload '{"payload": "hello"}' /tmp/invoke-result \
117+
--cli-binary-format raw-in-base64-out \
118+
--log-type Tail \
119+
--region <use your Region>
120+
```
121+
The function should return `"StatusCode": 200`
122+
123+
Browse to the [Amazon CloudWatch Console](https://console.aws.amazon.com/cloudwatch). Navigate to *Logs\Log Groups*. Select the log group **/aws/lambda/logs-extension-demo-function**.
124+
125+
View the log stream to see the platform, function, and extensions each logging while they are processing.
126+
127+
The logging extension also receives the log stream directly from Lambda, and copies the logs to S3.
128+
129+
Browse to the [Amazon S3 Console](https://console.aws.amazon.com/S3). Navigate to the S3 bucket created as part of the SAM deployment.
130+
131+
Downloading the file object containing the copied log stream. The log contains the same platform and function logs, but not the extension logs, as specified during the subscription.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM python:3.8-alpine AS installer
2+
#Layer Code
3+
COPY extensionssrc /opt/
4+
COPY extensionssrc/requirements.txt /opt/
5+
RUN pip install -r /opt/requirements.txt -t /opt/extensions/lib
6+
7+
FROM scratch AS base
8+
WORKDIR /opt/extensions
9+
COPY --from=installer /opt/extensions .
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM 978558897928.dkr.ecr.us-east-1.amazonaws.com/log-extension-image:latest AS layer
2+
FROM public.ecr.aws/lambda/python:3.8
3+
# Layer code
4+
WORKDIR /opt
5+
COPY --from=layer /opt/ .
6+
7+
# Function code
8+
WORKDIR /var/task
9+
COPY app.py .
10+
11+
CMD ["app.lambda_handler"]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: MIT-0
3+
4+
import json
5+
import os
6+
from pathlib import Path
7+
8+
def lambda_handler(event, context):
9+
print(f"Function: Logging something which logging extension will send to S3")
10+
return {
11+
'statusCode': 200,
12+
'body': json.dumps('Hello from Lambda!')
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/bin/sh
2+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
# SPDX-License-Identifier: MIT-0
4+
5+
''''exec python -u -- "$0" ${1+"$@"} # '''
6+
import os
7+
import sys
8+
from pathlib import Path
9+
from datetime import datetime
10+
# Add lib folder to path to import boto3 library.
11+
# Normally with Lambda Layers, python libraries are put into the /python folder which is in the path.
12+
# As this extension is bringing its own Python runtime, and running a separate process, the path is not available.
13+
# Hence, having the files in a different folder and adding it to the path, makes it available.
14+
lib_folder = Path(__file__).parent / "lib"
15+
sys.path.insert(0,str(lib_folder))
16+
import boto3
17+
18+
from logs_api_http_extension.http_listener import http_server_init, RECEIVER_PORT
19+
from logs_api_http_extension.logs_api_client import LogsAPIClient
20+
from logs_api_http_extension.extensions_api_client import ExtensionsAPIClient
21+
22+
from queue import Queue
23+
24+
"""Here is the sample extension code.
25+
- The extension runs two threads. The "main" thread, will register with the Extension API and process its invoke and
26+
shutdown events (see next call). The second "listener" thread listens for HTTP POST events that deliver log batches.
27+
- The "listener" thread places every log batch it receives in a synchronized queue; during each execution slice,
28+
the "main" thread will make sure to process any event in the queue before returning control by invoking next again.
29+
- Note that because of the asynchronous nature of the system, it is possible that logs for one invoke are
30+
processed during the next invoke slice. Likewise, it is possible that logs for the last invoke are processed during
31+
the SHUTDOWN event.
32+
33+
Note:
34+
35+
1. This is a simple example extension to help you understand the Lambda Logs API.
36+
This code is not production ready. Use it with your own discretion after testing it thoroughly.
37+
38+
2. The extension code starts with a shebang. This is to bring Python runtime to the execution environment.
39+
This works if the lambda function is a python3.x function, therefore it brings the python3.x runtime with itself.
40+
It may not work for python 2.7 or other runtimes.
41+
The recommended best practice is to compile your extension into an executable binary and not rely on the runtime.
42+
43+
3. This file needs to be executable, so make sure you add execute permission to the file
44+
`chmod +x logs_api_http_extension.py`
45+
46+
"""
47+
48+
class LogsAPIHTTPExtension():
49+
def __init__(self, agent_name, registration_body, subscription_body):
50+
# print(f"extension.logs_api_http_extension: Initializing LogsAPIExternalExtension {agent_name}")
51+
self.agent_name = agent_name
52+
self.queue = Queue()
53+
self.logs_api_client = LogsAPIClient()
54+
self.extensions_api_client = ExtensionsAPIClient()
55+
56+
# Register early so Runtime could start in parallel
57+
self.agent_id = self.extensions_api_client.register(self.agent_name, registration_body)
58+
59+
# Start listening before Logs API registration
60+
# print(f"extension.logs_api_http_extension: Starting HTTP Server {agent_name}")
61+
http_server_init(self.queue)
62+
self.logs_api_client.subscribe(self.agent_id, subscription_body)
63+
64+
def run_forever(self):
65+
# Configuring S3 Connection
66+
s3_bucket = (os.environ['S3_BUCKET_NAME'])
67+
s3 = boto3.resource('s3')
68+
print(f"extension.logs_api_http_extension: Receiving Logs {self.agent_name}")
69+
while True:
70+
resp = self.extensions_api_client.next(self.agent_id)
71+
# Process the received batches if any.
72+
while not self.queue.empty():
73+
batch = self.queue.get_nowait()
74+
# This following line logs the events received to CloudWatch.
75+
# Replace it to send logs to elsewhere.
76+
# If you've subscribed to extension logs, e.g. "types": ["platform", "function", "extension"],
77+
# you'll receive the logs of this extension back from Logs API.
78+
# And if you log it again with the line below, it will create a cycle since you receive it back again.
79+
# Use `extension` log type if you'll egress it to another endpoint,
80+
# or make sure you've implemented a protocol to handle this case.
81+
# print(f"Log Batch Received from Lambda: {batch}", flush=True)
82+
83+
# There are two options illustrated:
84+
# 1. Sending the entire log batch to S3
85+
# 2. Parsing the batch and sending individual log lines.
86+
# This could be used to parse the log lines and only selectively send logs to S3, or amend for any other destination.
87+
88+
# 1. The following line writes the entire batch to S3
89+
s3_filename = (os.environ['AWS_LAMBDA_FUNCTION_NAME'])+'-'+(datetime.now().strftime('%Y-%m-%d-%H:%M:%S.%f'))+'.log'
90+
try:
91+
response = s3.Bucket(s3_bucket).put_object(Key=s3_filename, Body=str(batch))
92+
except Exception as e:
93+
raise Exception(f"Error sending log to S3 {e}") from e
94+
# 2. The following parses the batch and sends individual log line
95+
# try:
96+
# for item in range(len(batch)):
97+
# s3_filename = (os.environ['AWS_LAMBDA_FUNCTION_NAME'])+'-'+(datetime.now().strftime('%Y-%m-%d-%H:%M:%S.%f'))+'.'+str(item)+'.log'
98+
# content = str(batch[item])
99+
# response = s3.Bucket(s3_bucket).put_object(Key=s3_filename, Body=content)
100+
# except Exception as e:
101+
# raise Exception(f"Error sending log to S3 {e}") from e
102+
103+
# Register for the INVOKE events from the EXTENSIONS API
104+
_REGISTRATION_BODY = {
105+
"events": ["INVOKE", "SHUTDOWN"],
106+
}
107+
108+
# Subscribe to platform logs and receive them on ${local_ip}:4243 via HTTP protocol.
109+
110+
TIMEOUT_MS = 1000 # Maximum time (in milliseconds) that a batch is buffered.
111+
MAX_BYTES = 262144 # Maximum size in bytes that the logs are buffered in memory.
112+
MAX_ITEMS = 10000 # Maximum number of events that are buffered in memory.
113+
114+
_SUBSCRIPTION_BODY = {
115+
"destination":{
116+
"protocol": "HTTP",
117+
"URI": f"http://sandbox:{RECEIVER_PORT}",
118+
},
119+
"types": ["platform", "function"],
120+
"buffering": {
121+
"timeoutMs": TIMEOUT_MS,
122+
"maxBytes": MAX_BYTES,
123+
"maxItems": MAX_ITEMS
124+
}
125+
}
126+
127+
def main():
128+
# print(f"extension.logs_api_http_extension: Starting Extension {_REGISTRATION_BODY} {_SUBSCRIPTION_BODY}")
129+
# Note: Agent name has to be file name to register as an external extension
130+
ext = LogsAPIHTTPExtension(os.path.basename(__file__), _REGISTRATION_BODY, _SUBSCRIPTION_BODY)
131+
ext.run_forever()
132+
133+
if __name__ == "__main__":
134+
main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: MIT-0
3+
4+
import json
5+
import os
6+
import sys
7+
import urllib.request
8+
9+
# Demonstrates code to register as an extension.
10+
11+
LAMBDA_AGENT_NAME_HEADER_KEY = "Lambda-Extension-Name"
12+
LAMBDA_AGENT_IDENTIFIER_HEADER_KEY = "Lambda-Extension-Identifier"
13+
14+
class ExtensionsAPIClient():
15+
def __init__(self):
16+
try:
17+
runtime_api_address = os.environ['AWS_LAMBDA_RUNTIME_API']
18+
self.runtime_api_base_url = f"http://{runtime_api_address}/2020-01-01/extension"
19+
except Exception as e:
20+
raise Exception(f"AWS_LAMBDA_RUNTIME_API is not set {e}") from e
21+
22+
# Register as early as possible - the runtime initialization starts after all extensions have registered.
23+
def register(self, agent_unique_name, registration_body):
24+
try:
25+
print(f"extension.extensions_api_client: Registering Extension at ExtensionsAPI address: {self.runtime_api_base_url}")
26+
req = urllib.request.Request(f"{self.runtime_api_base_url}/register")
27+
req.method = 'POST'
28+
req.add_header(LAMBDA_AGENT_NAME_HEADER_KEY, agent_unique_name)
29+
req.add_header("Content-Type", "application/json")
30+
data = json.dumps(registration_body).encode("utf-8")
31+
req.data = data
32+
resp = urllib.request.urlopen(req)
33+
if resp.status != 200:
34+
print(f"extension.extensions_api_client: /register request to ExtensionsAPI failed. Status: {resp.status}, Response: {resp.read()}")
35+
# Fail the extension
36+
sys.exit(1)
37+
agent_identifier = resp.headers.get(LAMBDA_AGENT_IDENTIFIER_HEADER_KEY)
38+
# print(f"extension.extensions_api_client: received agent_identifier header {agent_identifier}")
39+
return agent_identifier
40+
except Exception as e:
41+
raise Exception(f"Failed to register to ExtensionsAPI: on {self.runtime_api_base_url}/register \
42+
with agent_unique_name:{agent_unique_name} \
43+
and registration_body:{registration_body}\nError: {e}") from e
44+
45+
# Call the following method when the extension is ready to receive the next invocation
46+
# and there is no job it needs to execute beforehand.
47+
def next(self, agent_id):
48+
try:
49+
print(f"extension.extensions_api_client: Requesting /event/next from Extensions API")
50+
req = urllib.request.Request(f"{self.runtime_api_base_url}/event/next")
51+
req.method = 'GET'
52+
req.add_header(LAMBDA_AGENT_IDENTIFIER_HEADER_KEY, agent_id)
53+
req.add_header("Content-Type", "application/json")
54+
resp = urllib.request.urlopen(req)
55+
if resp.status != 200:
56+
print(f"extension.extensions_api_client: /event/next request to ExtensionsAPI failed. Status: {resp.status}, Response: {resp.read()} ")
57+
# Fail the extension
58+
sys.exit(1)
59+
data = resp.read()
60+
print(f"extension.extensions_api_client: Received event from ExtensionsAPI: {data}")
61+
return data
62+
except Exception as e:
63+
raise Exception(f"Failed to get /event/next from ExtensionsAPI: {e}") from e

0 commit comments

Comments
 (0)