Testing your Amazon Serverless Application Model (SAM) locally with Neo4j, Lambda and Docker

Testing your Amazon Serverless Application Model (SAM) locally with Neo4j, Lambda and Docker

So this is what I want to achieve. You should be able to do the same by the end of the post:

  • [ ] Install Docker
  • [ ] Install NPM
  • [ ] Create Neo4j Docker container
  • [ ] Docker Network to allow containers to talk to each other
  • [ ] Install SAM Local (beta)
  • [ ] Create template.yaml
    • [ ] Python
    • [ ] JavaScript
    • [ ] Java
  • [ ] Create test handler
    • [ ] Invoke driver
      • [ ] Talk about possible problems and how expensive it is to invoke drives?
    • [ ] Run read query
      • [ ] Link to driver pages and different queries and transactions
      • [ ] Why we won’t close off the connection in case our lambda is reused
  • [ ] Test Lambda function

Install Docker

http://lmgtfy.com/?q=install+docker+on+windows/linux/mac

Install Docker | Docker Documentation

If you’re on a mac:

  1. cmd+space to bring up Spotlight Search, type terminal and press enter

  2. Install Homebrew

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  1. Update Homebrew
$ brew update
  1. Install Docker via Homebrew
$ brew cask install docker
  1. cmd+space to bring up Spotlight Search, type docker and press enter
    • When it starts up, it’ll create everything in the following location
$ ls -l /usr/local/bin/docker*
lrwxr-xr-x  1 laexample  domain Users  67 Apr 12 14:14 /usr/local/bin/docker -> /Users/laexample/Library/Group Containers/group.com.docker/bin/docker
lrwxr-xr-x  1 laexample  domain Users  75 Apr 12 14:14 /usr/local/bin/docker-compose -> /Users/laexample/Library/Group Containers/group.com.docker/bin/docker-compose
lrwxr-xr-x  1 laexample  domain Users  90 Apr 12 14:14 /usr/local/bin/docker-credential-osxkeychain -> /Users/laexample/Library/Group Containers/group.com.docker/bin/docker-credential-osxkeychain
lrwxr-xr-x  1 laexample  domain Users  75 Apr 12 14:14 /usr/local/bin/docker-machine -> /Users/laexample/Library/Group Containers/group.com.docker/bin/docker-machine
  1. Validate version
$ docker version
  • [x] Install Docker

Install NPM

http://lmgtfy.com/?q=install+npm+on+windows/linux/mac
Install Docker | Docker Documentation

Download | Node.js

  1. Install Node / NPM
$ brew install node
  1. Validate Node and NPM
$ node -v 
$ npm -v
  • [x] Install NPM

Create Neo4j Docker container

So we have docker installed, NPM will be used later but now I want to get Neo4j up and running.

Neo4j (by version) Docker Script

  1. Create executable and file
    Paste the following docker run command into an executable file like: neo4j-docker.sh
if [ ! -d "$HOME/neo4j/databases/$1/" ]; then
	echo "Making directory structure"
  mkdir -p $HOME/neo4j/databases/$1/{data,plugins,import,logs,conf}
	echo "Downloading APOC"
curl -o $HOME/neo4j/databases/$1/plugins/apoc-3.3.0.1-all.jar https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.3.0.1/apoc-3.3.0.1-all.jar
	echo "Docker Run"
fi

docker run \
--publish=7474:7474 \
--publish=7687:7687 \
--volume=$HOME/neo4j/databases/$1/data:/data \
--volume=$HOME/neo4j/databases/$1/plugins:/plugins \
--volume=$HOME/neo4j/databases/$1/import:/import \
--volume=$HOME/neo4j/databases/$1/logs:/logs \
--env=NEO4J_dbms_memory_pagecache_size=2G \
--env=NEO4J_dbms_memory_heap_maxSize=4096m \
--env=NEO4J_AUTH=none \
--env=NEO4J_dbms_security_procedures_unrestricted=apoc.\\\* \
--env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \
--ulimit=nofile=40000:40000 \
neo4j:$1-enterprise

So what does it do?

  • If the version directory doesn’t exist:
    • Create the version folder in $HOME/neo4j/databases/
    • Create folders to be mapped to the local machine
    • Download APOC into the plugins folder
  • Download and run a Docker container and start Neo4j
    • Localhost ports 7474 & 7687 are mapped to your the container so you can get access to:

It’s best practise to use either env variables or map the config directory
Add the following line in and remove all —env lines.

--volume=$HOME/neo4j/databases/$1/import:/import \
  1. Change executable permissions
$ chmod +x neo4j-docker.sh

So we can create different docker containers with different versions of Neo4j by running a command similar to ./neo4j-docker.sh

  1. Start Neo4j Docker Container
$ ./neo4j-docker.sh 3.3.2
  1. Head over to http://localhost:7474 and create some data:
CREATE (p:Person{ name: "Luke"}),
       (p2:Person {name:"Mel"}),
       (p)-[pris_m:IS_MARRIED_TO]->(p2)
  1. Test our query we want to use later:
MATCH (p:Person{ name: "Luke"})-[pris_m:IS_MARRIED_TO]->(p2)
RETURN p.name, pris_m.since
  • [x] Create Neo4j Docker container

Install SAM Local (beta)

  1. Install SAM with NPM
$ npm install -g aws-sam-local

Verify the installation worked:

$ sam --version

If you get a permission error when using NPM (such as EACCES: permission denied), please see the instructions on this page of the NPM documentation: https://docs.npmjs.com/getting-started/fixing-npm-permissions.

Upgrading via npm

To update SAM once installed via NPM:

$ npm update -g aws-sam-local
  • [x] Install SAM Local (beta)

Container Talk - Docker Network

In order to get our containers (SAM Local and Neo4j) speaking to each other, we need to create a network which they will share:

  1. Create a network
docker network create {network-name}

Now let's connect the neo4j we started running before to the network.
We'll need it's container id or name:

  1. Get the container ID or name
docker ps

Use the network you created and the neo4j container name

  1. Connect the Neo4j container to the network
docker network connect {network-name} {neo4j-container-id/name}

Now lets find the IP address for the neo4j container, use that in your code.

  1. Get Neo4j container IP
docker inspect {neo4j-container-id} 

At the bottom of the page you’ll find within the Array of JSON objects, a “NetworkSettings” object, you’ll want NetworkSettings.Networks.{network-name}

  • [x] Docker Network to allow containers to talk to each other

Create template.yaml

What does a template.yaml file need?

  • [ ] Create template.yaml

Python

AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: My first serverless application.
Resources:
  GraphHandler:
    Type: AWS::Serverless::Function
    Properties:
      Handler: main.lambda_handler
      Runtime: python3.6
      CodeUri: .
      Timeout: 10
- [ ] Python

JavaScript

AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: My first serverless application.
Resources:
  GraphHandler:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      Runtime: nodejs6.10
      CodeUri: .
      Timeout: 10
- [ ] JavaScript

Java

AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Writing my first serverless application.
Resources:
  GraphHandler:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.nineteen.neo4j.GraphHandler
      CodeUri: ./target/lambda-1.0-jar-with-dependencies.jar
      Runtime: java8
      Timeout: 999

Note: you have to provide the path of the compiled jar.
- [ ] Java

dotNetCore

Unfortunately we’re waiting for the SAM team to support .NetCore, sorry Chris Skardon :sadparrot:

Create Test Handler

Python

Within main.py paste the following:

import null;

#TODO: Pad this out a bit
def lambda_handler(event, context):
    return ""
- [ ] Python

JavaScript

'use strict';

exports.handler = (event, context, callback) => {
    console.log(event);
}

Props to @AdamCowley’s and his mad javascript skills. I don’t like being tortured by languages.
- [ ] JavaScript

Java

package com.nineteen.neo4j.example;

import com.amazonaws.services.lambda.runtime.Context;

private Driver driver;

public class Hello {

    public String myHandler(String name, Context context) {

        System.out.println("log data to cloudwatch from stdout");
        System.err.println("log data tp cloudwatch from stderr");
        return String.format("Hello %s.", name);
    }
}  
- [ ] Java
  • [ ] Create test handlers

Invoke driver

Java

package com.nineteen.neo4j.example;

import com.amazonaws.services.lambda.runtime.Context;

public class Hello {
	private Driver driver;
	public String myHandler(String name, Context context) {
		if (driver == null)
		{
			driver = GraphDatabase.driver("{URL_OF_NEO4J_CONTAINER}", AuthTokens.basic("neo4j", ""));
		}
    }
}  
- [ ] Java
  • [ ] Invoke Driver

Talk about possible problems and how expensive it is to invoke drives?

Run read query

  • [ ] Run read query

Python

JavaScript

'use strict';



exports.handler = (event, context, callback) => {
    const neo4j = require('neo4j-driver').v1;
    const driver = new neo4j.driver('bolt://172.17.0.2:7687', neo4j.auth.basic('neo4j', 'neo') );

    const session = driver.session();

    session.run(`MATCH (p:Person{ id: {id} }),
                (p)-[prlive:LIVES_AT]->(a:Address),
                (p)-[pris_p:IS_PARTNER_OF]->(p2:Person),
                (p2)-[p2ris_i:IS_IDENTIFIED_BY]->(g:GovID)
        WHERE p.name= {p_name} AND prlive.since> {prlive_since} AND a.addressLine1 = {addressLine1} AND p2.name = {p2_name}
        RETURN p.name, a.postCode, p2.name as p2name, g.type`, params)
        .then(res => {
            console.log(res.records.length);

            return res.records.map(row => {
                return [ row.get('p.name'), row.get('a.postCode'), row.get('p2name'), row.get('g.type') ];
            });
        })
        .then(json => {
            session.close();

            return json;
        })
        .then(json => {
            console.log(json);

            const end = new Date().getTime();

            console.log('-- END',  end);
            console.log('-- END', end - start);

            callback(null, {
                statusCode: 200,
                headers: { "x-custom-header" : "my custom header value" },
                body: json
            });
        })
        .catch(e => {
            callback(e);
        })
        .then(() => {
            return driver.close();
        });

}

## Java

package com.nineteen.neo4j.example;

import com.amazonaws.services.lambda.runtime.Context;

public class Hello {
	private Driver driver;
	public String myHandler(String name, Context context) {
		if (driver == null)
		{
			driver = GraphDatabase.driver("{URL_OF_NEO4J_CONTAINER}", AuthTokens.basic("neo4j", ""));
		}

		try (Session session = driver.session(AccessMode.READ))
      {
			sr = session.run(query);
			List resultList = sr.list();
			//TODO: add return: return resultList;
        }
        catch (ClientException ce)
        {
            ce.printStackTrace();
        }

      //return String.format("Hello %s.", name);
    }
}  

Link to driver pages and different queries and transactions

Why we won’t close off the connection in case our lambda is reused

  • [ ] Why we won’t close off the connection in case our lambda is reused

Test Lambda function

  • [ ] Test Lambda function

Didn’t even break a sweat. BIG SHAQ - MANS NOT HOT

Links