DEV Community

Matt Martz
Matt Martz

Posted on

AWS SAM local with postgres

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

$ sam --version
SAM CLI, version 0.45.0
$ docker --version
Docker version 19.03.8, build afacb8b
$ node --version
v12.13.1
Enter fullscreen mode Exit fullscreen mode

The code for this post lives here:

Hello, Lambda

hello

Hello

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
Enter fullscreen mode Exit fullscreen mode

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\"}"}
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Connecting the Lambda to Postgres

connecting

🤔

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
};
Enter fullscreen mode Exit fullscreen mode

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\"}"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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\"}"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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\"}"}
Enter fullscreen mode Exit fullscreen mode

Improvements

For brevity I removed the tests 😳... maybe the topic of a future blog.

improvements

  • Use environment variables for the database connections
  • Make everything typescript
  • Write integration tests

Top comments (0)