Skip to content

Commit 4ef7b62

Browse files
Code for caching data from parameter store and dynamodb using Lambda Extensions (#24)
* Code for caching data from parameter store and dynamodb using Lambda extensions * Incorporating code review comments * Updated to README.md and pretty printing error message Co-authored-by: Hari Ohm Prasath <[email protected]>
1 parent 0bc338f commit 4ef7b62

File tree

17 files changed

+876
-0
lines changed

17 files changed

+876
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ 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+
* [Cache Extension demo](cache-extension-demo/)
1819
* [Logs to Amazon S3 extension demo: zip archive](s3-logs-extension-demo-zip-archive/)
1920
* [Logs to Amazon S3 extension demo: container image ](s3-logs-extension-demo-container-image/)
2021
* [Extension in Go](go-example-extension/)

cache-extension-demo/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bin/*
2+
.idea/*
3+
go.sum
4+
run.sh

cache-extension-demo/README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Cache Extension Demo in Go
2+
3+
The provided code sample demo's a cache extension written in Go that acts as a companion process which the AWS Lambda function runtime can communicate.
4+
5+
# Introduction
6+
Having a caching layer inside the Lambda function is a very common use case. It would allow the lambda function to process requests quicker and avoid the additional cost incurred by calling the AWS services over and over again. They are two types of cache:
7+
- Data cache (caching data from databases like RDS, dynamodb, etc.)
8+
- Configuration cache (caching data from a configuration system like parameter store, app config, secrets, etc.)
9+
10+
This extension demo's the Lambda layer that enables both data cache (using dynamodb) and configuration cache (using parameter store).
11+
Here is how it works:
12+
- Uses `config.yaml` defined part of the lambda function to determine the items that needs to be cached
13+
- All the data are cached in memory before the request gets handled to the lambda function. So no cold start problems
14+
- Starts a local HTTP server at port `3000` that replies to request for reading items from the cache depending upon path variables
15+
- Uses `"CACHE_EXTENSION_TTL"` Lambda environment variable to let users define cache refresh interval (defined based on Go time format, ex: 30s, 3m, etc)
16+
- Uses `"CACHE_EXTENSION_INIT_STARTUP"` Lambda environment variable used to specify whether to load all items specified in `"cache.yml"` into cache part of extension startup (takes boolean value, ex: true and false)
17+
18+
Here are some advantages of having the cache layer part of Lambda extension instead of having it inside the function
19+
- Reuse the code related to cache in multiple Lambda functions
20+
- Common dependencies like SDK are packaged part of the Lambda layers
21+
22+
## Architecture
23+
Here is the high level view of all the components
24+
25+
![architecture](img/architecture.svg)
26+
27+
## Initialize extension and reading secrets from the cache
28+
Below sequence diagram explains the initialization of lambda extension and how lambda function
29+
reads cached items using HTTP server hosted inside the extension
30+
![init](img/Sequence.svg)
31+
32+
## Pre-requisites
33+
- Zip utility needs to be installed in the local system
34+
- AWS CLI needs to be installed in the local system, for more information click [here](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html)
35+
36+
## Deploy
37+
### Parameter Store
38+
Create a new parameter using the following command
39+
40+
```bash
41+
aws ssm put-parameter \
42+
--name "Parameter1" \
43+
--type "String" \
44+
--value "Parameter_value"
45+
```
46+
47+
### DynamoDB
48+
- Create a new dynamodb table with a partition key compassing of hash and sort key
49+
50+
```bash
51+
aws dynamodb create-table \
52+
--table-name DynamoDbTable \
53+
--attribute-definitions AttributeName=pKey,AttributeType=S AttributeName=sKey,AttributeType=S \
54+
--key-schema AttributeName=pKey,KeyType=HASH AttributeName=sKey,KeyType=RANGE \
55+
--billing-mode PAY_PER_REQUEST
56+
```
57+
- Save the following JSON in a file named as `"item.json"`
58+
59+
```json
60+
{
61+
"pKey": {"S": "pKey1"},
62+
"sKey": {"S": "sKey1"},
63+
"Data": {"S": "Data goes here"}
64+
}
65+
```
66+
67+
- Add a new record to the `DynamoDbTable` table, by running the following command
68+
```bash
69+
aws dynamodb put-item \
70+
--table-name DynamoDbTable \
71+
--item file://item.json \
72+
--return-consumed-capacity TOTAL \
73+
--return-item-collection-metrics SIZE
74+
```
75+
76+
## Compile package and dependencies
77+
To run this example, you will need to ensure that your build architecture matches that of the Lambda execution environment by compiling with `GOOS=linux` and `GOARCH=amd64` if you are not running in a Linux environment.
78+
79+
Building and saving package into a `bin/extensions` directory:
80+
```bash
81+
$ cd cache-extension-demo
82+
$ GOOS=linux GOARCH=amd64 go build -o bin/extensions/cache-extension-demo main.go
83+
$ chmod +x bin/extensions/cache-extension-demo
84+
```
85+
86+
## Layer Setup Process
87+
The extensions .zip file should contain a root directory called `extensions/`, where the extension executables are located. In this sample project we must include the `cache-extension-demo` binary.
88+
89+
Creating zip package for the extension:
90+
```bash
91+
$ cd bin
92+
$ zip -r extension.zip extensions/
93+
```
94+
95+
Ensure that you have aws-cli v2 for the commands below.
96+
Publish a new layer using the `extension.zip`. The output of the following command should provide you a layer arn.
97+
```bash
98+
aws lambda publish-layer-version \
99+
--layer-name "cache-extension-demo" \
100+
--region <use your region> \
101+
--zip-file "fileb://extension.zip"
102+
```
103+
Note the LayerVersionArn that is produced in the output.
104+
eg. `"LayerVersionArn": "arn:aws:lambda:<region>:123456789012:layer:<layerName>:1"`
105+
106+
Add the newly created layer version to a Lambda function.
107+
- You can use the provided `index.js` (nodeJS extension) in the `example/` directory
108+
- Make sure to have a `config.yaml` in the root of the lambda function's directory and updated with the correct region information. You can use the provided `config.yaml` in the the `example/` directory
109+
- Make sure to increase the default timeout to 2 mins and memory to 512 MB
110+
111+
>Note: Make sure to have`'AmazonDynamoDBReadOnlyAccess'` & `'AmazonSSMReadOnlyAccess'` IAM policies assigned to the IAM role associated with the Lambda function
112+
113+
Here is the AWS CLI command that can update the layers on the existing AWS Lambda function
114+
115+
```bash
116+
aws lambda update-function-configuration \
117+
--function-name <<function-name>> \
118+
--layers $(aws lambda list-layer-versions --layer-name cache-extension-demo \
119+
--max-items 1 --no-paginate --query 'LayerVersions[0].LayerVersionArn' \
120+
--output text)
121+
```
122+
123+
>Note: Make sure to replace `function-name` with the actual lambda function name
124+
125+
## Function Invocation and Extension Execution
126+
You can invoke the Lambda function using the following CLI command
127+
```bash
128+
aws lambda invoke \
129+
--function-name "<<function-name>>" \
130+
--payload '{"payload": "hello"}' /tmp/invoke-result \
131+
--cli-binary-format raw-in-base64-out \
132+
--log-type Tail
133+
```
134+
>Note: Make sure to replace `function-name` with the actual lambda function name
135+
136+
The function should return ```"StatusCode": 200```.
137+
138+
When invoking the function, you should now see log messages from the example extension similar to the following:
139+
```
140+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX START RequestId: 9ca08945-de9b-46ec-adc6-3fe9ef0d2e8d Version: $LATEST
141+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX [cache-extension-demo] Register response: {
142+
"functionName": "my-function",
143+
"functionVersion": "$LATEST",
144+
"handler": "function.handler"
145+
}
146+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX [cache-extension-demo] Cache successfully loaded
147+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX [cache-extension-demo] Waiting for event...
148+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX [cache-extension-demo] Starting Httpserver on port 3000
149+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX EXTENSION Name: cache-extension-demo State: Ready Events: [INVOKE,SHUTDOWN]
150+
...
151+
...
152+
Function logs...
153+
...
154+
...
155+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX START RequestId: d94434eb-705d-4c22-8600-c7f53a0c2204 Version: $LATEST
156+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX [cache-extension-demo] Waiting for event...
157+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX INFO Finally got some response here: "{\"Data\":\"Data goes here\",\"pKey\":\"pKey1\",\"sKey\":\"sKey1\"}"
158+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX END RequestId: d94434eb-705d-4c22-8600-c7f53a0c2204
159+
XXXX-XX-XXTXX:XX:XX.XXX-XX:XX REPORT RequestId: d94434eb-705d-4c22-8600-c7f53a0c2204 Duration: 17.09 ms Billed Duration: 18 ms Memory Size: 1472 MB Max Memory Used: 89 MB Init Duration: 289.40 ms
160+
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#SPDX-License-Identifier: MIT-0
3+
parameters:
4+
- region: us-west-2
5+
names:
6+
- Parameter1
7+
dynamodb:
8+
- table: DynamoDbTable
9+
hashkey: pKey
10+
hashkeytype: S
11+
hashkeyvalue: pKey1
12+
sortkey: sKey
13+
sortkeytype: S
14+
sortkeyvalue: sKey1
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
exports.handler = function(event, context, callback) {
4+
5+
const https = require('http')
6+
const options = {
7+
hostname: 'localhost',
8+
port: 3000,
9+
path: '/dynamodb/DynamoDbTable-pKey1-sKey1',
10+
method: 'GET'
11+
}
12+
13+
const req = https.request(options, res => {
14+
res.on('data', d => {
15+
console.log("Finally got some response here: "+d);
16+
return d;
17+
})
18+
})
19+
20+
req.on('error', error => {
21+
console.error(error)
22+
})
23+
24+
req.end()
25+
};
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
4+
package extension
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"encoding/json"
10+
"fmt"
11+
"io/ioutil"
12+
"net/http"
13+
)
14+
15+
// RegisterResponse is the body of the response for /register
16+
type RegisterResponse struct {
17+
FunctionName string `json:"functionName"`
18+
FunctionVersion string `json:"functionVersion"`
19+
Handler string `json:"handler"`
20+
}
21+
22+
// NextEventResponse is the response for /event/next
23+
type NextEventResponse struct {
24+
EventType EventType `json:"eventType"`
25+
DeadlineMs int64 `json:"deadlineMs"`
26+
RequestID string `json:"requestId"`
27+
InvokedFunctionArn string `json:"invokedFunctionArn"`
28+
Tracing Tracing `json:"tracing"`
29+
}
30+
31+
// Tracing is part of the response for /event/next
32+
type Tracing struct {
33+
Type string `json:"type"`
34+
Value string `json:"value"`
35+
}
36+
37+
// EventType represents the type of events recieved from /event/next
38+
type EventType string
39+
40+
const (
41+
// Invoke is a lambda invoke
42+
Invoke EventType = "INVOKE"
43+
44+
// Shutdown is a shutdown event for the environment
45+
Shutdown EventType = "SHUTDOWN"
46+
47+
extensionNameHeader = "Lambda-Extension-Name"
48+
extensionIdentiferHeader = "Lambda-Extension-Identifier"
49+
)
50+
51+
// Client is a simple client for the Lambda Extensions API
52+
type Client struct {
53+
baseURL string
54+
httpClient *http.Client
55+
extensionID string
56+
}
57+
58+
// NewClient returns a Lambda Extensions API client
59+
func NewClient(awsLambdaRuntimeAPI string) *Client {
60+
baseURL := fmt.Sprintf("http://%s/2020-01-01/extension", awsLambdaRuntimeAPI)
61+
return &Client{
62+
baseURL: baseURL,
63+
httpClient: &http.Client{},
64+
}
65+
}
66+
67+
// Register will register the extension with the Extensions API
68+
func (e *Client) Register(ctx context.Context, filename string) (*RegisterResponse, error) {
69+
const action = "/register"
70+
url := e.baseURL + action
71+
72+
reqBody, err := json.Marshal(map[string]interface{}{
73+
"events": []EventType{Invoke, Shutdown},
74+
})
75+
if err != nil {
76+
return nil, err
77+
}
78+
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(reqBody))
79+
if err != nil {
80+
return nil, err
81+
}
82+
httpReq.Header.Set(extensionNameHeader, filename)
83+
httpRes, err := e.httpClient.Do(httpReq)
84+
if err != nil {
85+
return nil, err
86+
}
87+
if httpRes.StatusCode != 200 {
88+
return nil, fmt.Errorf("request failed with status %s", httpRes.Status)
89+
}
90+
defer httpRes.Body.Close()
91+
body, err := ioutil.ReadAll(httpRes.Body)
92+
if err != nil {
93+
return nil, err
94+
}
95+
res := RegisterResponse{}
96+
err = json.Unmarshal(body, &res)
97+
if err != nil {
98+
return nil, err
99+
}
100+
e.extensionID = httpRes.Header.Get(extensionIdentiferHeader)
101+
return &res, nil
102+
}
103+
104+
// NextEvent blocks while long polling for the next lambda invoke or shutdown
105+
func (e *Client) NextEvent(ctx context.Context) (*NextEventResponse, error) {
106+
const action = "/event/next"
107+
url := e.baseURL + action
108+
109+
httpReq, err := http.NewRequestWithContext(ctx, "GET", url, nil)
110+
if err != nil {
111+
return nil, err
112+
}
113+
httpReq.Header.Set(extensionIdentiferHeader, e.extensionID)
114+
httpRes, err := e.httpClient.Do(httpReq)
115+
if err != nil {
116+
return nil, err
117+
}
118+
if httpRes.StatusCode != 200 {
119+
return nil, fmt.Errorf("request failed with status %s", httpRes.Status)
120+
}
121+
defer httpRes.Body.Close()
122+
body, err := ioutil.ReadAll(httpRes.Body)
123+
if err != nil {
124+
return nil, err
125+
}
126+
res := NextEventResponse{}
127+
err = json.Unmarshal(body, &res)
128+
if err != nil {
129+
return nil, err
130+
}
131+
return &res, nil
132+
}

0 commit comments

Comments
 (0)