In this post we're going to create two super-basic lambda functions that read and write to a postgres table using AWS Severless Application Model (SAM), Docker(-compose) and postgres for fully local testing.
SAM + docker-compose enables fully local testing of lambda functions that interact with postgres (or anything else in docker-compose). Another great example of this is
Table of Contents
- Versions and Code
- Hello, Lambda
- Initializing Postgres
- Connecting the Lambda to Postgres
- Improvements
Versions and Code
$ sam --version
SAM CLI, version 0.45.0
$ docker --version
Docker version 19.03.8, build afacb8b
$ node --version
v12.13.1
The code for this post lives here:
Hello, Lambda
In your project directory run sam init
with the following options:
$ sam init
SAM CLI now collects telemetry to better understand customer needs.
You can OPT OUT and disable telemetry collection by setting the
environment variable SAM_CLI_TELEMETRY=0 in your shell.
Thanks for your help!
Learn More: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-telemetry.html
Which template source would you like to use?
1 - AWS Quick Start Templates
2 - Custom Template Location
Choice: 1
Which runtime would you like to use?
1 - nodejs12.x
2 - python3.8
3 - ruby2.7
4 - go1.x
5 - java11
6 - dotnetcore2.1
7 - nodejs10.x
8 - python3.7
9 - python3.6
10 - python2.7
11 - ruby2.5
12 - java8
13 - dotnetcore2.0
14 - dotnetcore1.0
Runtime: 1
Project name [sam-app]: sam-with-postgres
Cloning app templates from https://github.com/awslabs/aws-sam-cli-app-templates.git
-----------------------
Generating application:
-----------------------
Name: sam-with-postgres
Runtime: nodejs12.x
Dependency Manager: npm
Application Template: hello-world
Output Directory: .
Next steps can be found in the README file at ./sam-with-postgres/README.md
To verify it's running you can follow the directions in the README... generally I prefer to use sam local start-lambda
and in another terminal...
$ sam local invoke HelloWorldFunction --event events/event.json
Invoking app.lambdaHandler (nodejs12.x)
Fetching lambci/lambda:nodejs12.x Docker container image..........................................................................................................................................................
Mounting /Users/mattmartz/Development/sam-postgres/sam-with-postgres/hello-world as /var/task:ro,delegated inside runtime container
START RequestId: cfad24b9-7ed9-1eac-d063-c0430d0c8862 Version: $LATEST
END RequestId: cfad24b9-7ed9-1eac-d063-c0430d0c8862
REPORT RequestId: cfad24b9-7ed9-1eac-d063-c0430d0c8862 Init Duration: 86.84 ms Duration: 3.04 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 39 MB
{"statusCode":200,"body":"{\"message\":\"hello world\"}"}
Initializing Postgres
Now that we've got a basic HelloWorld lambda... let's get postgres up and running.
Let's create a docker-compose.yml file that includes:
version: '2'
services:
sam-postgres-db:
image: postgres:10.5-alpine
container_name: sam-postgres-db
environment:
- POSTGRES_PASSWORD=martzcodesshouldhaveabetterpassword
ports:
- '5432:5432'
networks:
martzcodes:
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
martzcodes:
With an init.sql file that contains:
CREATE TABLE hello_table (
id serial NOT NULL PRIMARY KEY,
hello_source varchar(20),
hello_target varchar(20)
);
INSERT INTO hello_table (hello_source, hello_target)
VALUES ('Matt', 'Whit');
When you run docker-compose up -d
postgres will install and initialize the postgres image with that sql script (this is a feature of the postgres image)... to verify you can check with docker exec -it sam-postgres-db psql -U postgres
and then running select * from hello_table;
$ docker exec -it sam-postgres-db psql -U postgres
psql (10.5)
Type "help" for help.
postgres=# select * from hello_table;
id | hello_source | hello_target
----+--------------+--------------
1 | Matt | Whit
(1 row)
postgres=# \q
Connecting the Lambda to Postgres
Now let's get the lambda communicating with postgres. cd hello-world
into the lambda directory and uninstall axios (we don't need it)... npm uninstall axios --save
. Now install the node-postgres library using npm install pg --save
.
Basic SELECT
Add node-postgres to our hello-world/app.js
and connect to the db. My app.js file now looks like:
const { Client } = require("pg");
let response;
/**
...
*/
exports.lambdaHandler = async (event, context) => {
const client = new Client({
user: "postgres",
host: "localhost",
password: "martzcodesshouldhaveabetterpassword"
});
await client.connect();
const res = await client.query("SELECT * from hello_table ORDER BY id DESC LIMIT 1");
const hello = `${res.rows[0].hello_source} says hello to ${res.rows[0].hello_target}`;
console.log(hello); // Shows "Matt says hello to Whit""
await client.end();
try {
response = {
'statusCode': 200,
'body': JSON.stringify({
message: hello,
})
}
} catch (err) {
console.log(err);
return err;
}
return response
};
To test this, make sure postgres is still running (docker-compose up -d
) and start the lambda (sam local start-lambda
) and then you can invoke the lambda to get:
sam local invoke HelloWorldFunction --event events/event.json --docker-network host
Invoking app.lambdaHandler (nodejs12.x)
Fetching lambci/lambda:nodejs12.x Docker container image......
Mounting /Users/mattmartz/Development/sam-postgres/sam-with-postgres/hello-world as /var/task:ro,delegated inside runtime container
START RequestId: 08885bb9-71db-1ef7-fdfe-6421b6bbbf1a Version: $LATEST
2020-03-19T17:56:05.307Z 08885bb9-71db-1ef7-fdfe-6421b6bbbf1a INFO Matt says hello to Whit
END RequestId: 08885bb9-71db-1ef7-fdfe-6421b6bbbf1a
REPORT RequestId: 08885bb9-71db-1ef7-fdfe-6421b6bbbf1a Init Duration: 163.82 ms Duration: 16.32 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 45 MB
{"statusCode":200,"body":"{\"message\":\"Matt says hello to Whit\"}"}
Basic INSERT
Let's make it a little more interesting and have a write to the database using the lambda event.
Tweak the body of events/event.json to be: "body": "{\"source\": \"whit\", \"target\": \"matt\"}",
.
Then add a new resource to the template.yml file in the project root:
SayFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello-world/
Handler: app.sayHandler
Runtime: nodejs12.x
Along with the corresponding code... I put mine in hello-world/app.js
but I generally prefer to keep everything in separate files (and have them be typescript...).
exports.sayHandler = async (event, context) => {
const client = new Client({
user: "postgres",
host: "localhost",
password: "martzcodesshouldhaveabetterpassword"
});
await client.connect();
const body = JSON.parse(event.body);
const queryText =
"INSERT INTO hello_table(hello_source, hello_target) VALUES($1, $2) RETURNING (hello_source, hello_target)";
await client.query(queryText, [body.source, body.target]);
await client.end();
return exports.lambdaHandler(event, context);
};
Now restart sam local start-lambda
and run the invoker for the SayFunction:
$ sam local invoke SayFunction --event events/event.json --docker-network host
Invoking app.sayHandler (nodejs12.x)
Fetching lambci/lambda:nodejs12.x Docker container image......
Mounting /Users/mattmartz/Development/sam-postgres/sam-with-postgres/hello-world as /var/task:ro,delegated inside runtime container
START RequestId: 1abdf1fa-7962-1589-7682-cf939c2cf391 Version: $LATEST
2020-03-19T18:20:02.749Z 1abdf1fa-7962-1589-7682-cf939c2cf391 INFO whit says hello to matt
END RequestId: 1abdf1fa-7962-1589-7682-cf939c2cf391
REPORT RequestId: 1abdf1fa-7962-1589-7682-cf939c2cf391 Init Duration: 155.42 ms Duration: 25.71 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 46 MB
{"statusCode":200,"body":"{\"message\":\"whit says hello to matt\"}"}
If you docker exec -it sam-postgres-db psql -U postgres
into the database again and select... you'll see the newly added row:
docker exec -it sam-postgres-db psql -U postgres
psql (10.5)
Type "help" for help.
postgres=# select * from hello_table;
id | hello_source | hello_target
----+--------------+--------------
1 | Matt | Whit
2 | whit | matt
(2 rows)
postgres=# \q
And since we set up the HelloFunction to return the last entry its response will now reflect the right row:
sam local invoke HelloWorldFunction --event events/event.json --docker-network host
Invoking app.lambdaHandler (nodejs12.x)
Fetching lambci/lambda:nodejs12.x Docker container image......
Mounting /Users/mattmartz/Development/sam-postgres/sam-with-postgres/hello-world as /var/task:ro,delegated inside runtime container
START RequestId: c6be69ec-8aa0-1cc4-5ae2-69f41c5dd836 Version: $LATEST
2020-03-19T18:22:24.383Z c6be69ec-8aa0-1cc4-5ae2-69f41c5dd836 INFO whit says hello to matt
END RequestId: c6be69ec-8aa0-1cc4-5ae2-69f41c5dd836
REPORT RequestId: c6be69ec-8aa0-1cc4-5ae2-69f41c5dd836 Init Duration: 183.15 ms Duration: 18.55 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 45 MB
{"statusCode":200,"body":"{\"message\":\"whit says hello to matt\"}"}
Improvements
For brevity I removed the tests 😳... maybe the topic of a future blog.
- Use environment variables for the database connections
- Make everything typescript
- Write integration tests
Top comments (0)