├── .env ├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── build.sh ├── deploy-swarm.md ├── docker-compose.yml └── functions ├── api-create ├── Dockerfile ├── handler.js └── package.json ├── api-delete ├── Dockerfile ├── handler.js └── package.json ├── api-read ├── Dockerfile ├── handler.js └── package.json └── api-update ├── Dockerfile ├── handler.js └── package.json /.env: -------------------------------------------------------------------------------- 1 | POSTGRES_HOST=movies-db 2 | POSTGRES_USER=postgres 3 | POSTGRES_PASS=postgres 4 | POSTGRES_DB=movies 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | template 2 | build 3 | env.yml 4 | .DS_Store 5 | .video 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "esnext": true, 4 | "jasmine": false, 5 | "spyOn": false, 6 | "it": false, 7 | "console": false, 8 | "describe": false, 9 | "expect": false, 10 | "beforeEach": false, 11 | "afterEach": false, 12 | "waits": false, 13 | "waitsFor": false, 14 | "runs": false, 15 | "$": false, 16 | "confirm": false 17 | }, 18 | "esnext": true, 19 | "node" : true, 20 | "browser" : true, 21 | "boss" : false, 22 | "curly": false, 23 | "debug": false, 24 | "devel": false, 25 | "eqeqeq": true, 26 | "evil": true, 27 | "forin": false, 28 | "immed": true, 29 | "laxbreak": false, 30 | "newcap": true, 31 | "noarg": true, 32 | "noempty": false, 33 | "nonew": false, 34 | "nomen": false, 35 | "onevar": true, 36 | "plusplus": false, 37 | "regexp": false, 38 | "undef": true, 39 | "sub": true, 40 | "strict": false, 41 | "white": true, 42 | "unused": false 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michael Herman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenFaaS RESTful API w/ Node and Postgres 2 | 3 | Simple example of an [OpenFaaS](https://www.openfaas.com/) RESTful API. 4 | 5 | ## Getting Started 6 | 7 | Build the Docker images for the functions: 8 | 9 | ```sh 10 | $ sh build.sh 11 | ``` 12 | 13 | Initialize Swarm mode: 14 | 15 | ```sh 16 | $ docker swarm init 17 | ``` 18 | 19 | Deploy: 20 | 21 | ```sh 22 | $ docker stack deploy func --compose-file docker-compose.yml --prune 23 | ``` 24 | 25 | Create database and `movie` table: 26 | 27 | ```sh 28 | $ PG_CONTAINER_ID=$(docker ps --filter name=movies-db --format "{{.ID}}") 29 | $ docker exec -ti $PG_CONTAINER_ID psql -U postgres -W 30 | # CREATE DATABASE movies; 31 | # \c movies 32 | # CREATE TABLE movie(id SERIAL, name varchar); 33 | # \q 34 | ``` 35 | 36 | Test: 37 | 38 | ```sh 39 | $ curl -X POST http://localhost:8080/function/func_api-create -d \ 40 | '{"name":"NeverEnding Story"}' 41 | 42 | $ curl http://localhost:8080/function/func_api-read 43 | 44 | $ curl -X POST http://localhost:8080/function/func_api-update -d \ 45 | '{"name":"NeverEnding Story 2", "id": "1"}' 46 | 47 | $ curl -X POST http://localhost:8080/function/func_api-delete -d \ 48 | '{"id":"1"}' 49 | ``` 50 | 51 | ## Deploy to Digital Ocean 52 | 53 | [deploy-swarm.md](deploy-swarm.md) 54 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker build -t mjhea0/faas-api-create ./functions/api-create 4 | docker build -t mjhea0/faas-api-read ./functions/api-read 5 | docker build -t mjhea0/faas-api-update ./functions/api-update 6 | docker build -t mjhea0/faas-api-delete ./functions/api-delete 7 | -------------------------------------------------------------------------------- /deploy-swarm.md: -------------------------------------------------------------------------------- 1 | # Deploy to Digital Ocean via Docker Swarm 2 | 3 | > Get $10 in Digital Ocean credit [here](https://m.do.co/c/d8f211a4b4c2). 4 | 5 | This guide looks at how to deploy a Serverless API to Digital Ocean with OpenFaaS and Docker Swarm mode. 6 | 7 | > Check out the video [here](https://youtu.be/kSZXPH_f-kE)! 8 | 9 | ### Steps 10 | 11 | Create droplet: 12 | 13 | ```sh 14 | $ docker-machine create \ 15 | --driver digitalocean \ 16 | --digitalocean-access-token ADD_YOUR_KEY \ 17 | node; 18 | 19 | $ docker-machine env node 20 | $ eval $(docker-machine env node) 21 | ``` 22 | 23 | Build the Docker images for the functions: 24 | 25 | ```sh 26 | $ sh build.sh 27 | ``` 28 | 29 | Spin up and deploy [OpenFaas](https://www.openfaas.com/): 30 | 31 | ```sh 32 | $ docker-machine ssh node \ 33 | -- docker swarm init \ 34 | --advertise-addr $(docker-machine ip node) 35 | 36 | $ docker stack deploy func --compose-file docker-compose.yml --prune 37 | ``` 38 | 39 | Create database and `movie` table: 40 | 41 | ```sh 42 | $ PG_CONTAINER_ID=$(docker ps --filter name=movies-db --format "{{.ID}}") 43 | $ docker exec -ti $PG_CONTAINER_ID psql -U postgres -W 44 | # CREATE DATABASE movies; 45 | # \c movies 46 | # CREATE TABLE movie(id SERIAL, name varchar); 47 | # \q 48 | ``` 49 | 50 | Test: 51 | 52 | ```sh 53 | $ curl -X POST \ 54 | $(echo http://$(docker-machine ip node):8080)/function/func_api-create -d \ 55 | '{"name":"NeverEnding Story"}' 56 | 57 | $ curl $(echo http://$(docker-machine ip node):8080)/function/func_api-read 58 | 59 | $ curl -X POST \ 60 | $(echo http://$(docker-machine ip node):8080)/function/func_api-update -d \ 61 | '{"name":"NeverEnding Story 2", "id": "1"}' 62 | 63 | $ curl -X POST \ 64 | $(echo http://$(docker-machine ip node):8080)/function/func_api-delete -d \ 65 | '{"id":"1"}' 66 | ``` 67 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | 5 | gateway: 6 | ports: 7 | - 8080:8080 8 | image: functions/gateway:0.7.9 9 | networks: 10 | - functions 11 | environment: 12 | functions_provider_url: 'http://faas-swarm:8080/' 13 | read_timeout: '25s' 14 | write_timeout: '25s' 15 | upstream_timeout: '20s' 16 | dnsrr: 'true' 17 | # faas_nats_address: 'nats' 18 | # faas_nats_port: 4222 19 | direct_functions: 'true' 20 | direct_functions_suffix: '' 21 | deploy: 22 | resources: 23 | reservations: 24 | memory: 100M 25 | restart_policy: 26 | condition: on-failure 27 | delay: 5s 28 | max_attempts: 20 29 | window: 380s 30 | placement: 31 | constraints: 32 | - 'node.platform.os == linux' 33 | 34 | faas-swarm: 35 | volumes: 36 | - '/var/run/docker.sock:/var/run/docker.sock' 37 | image: functions/faas-swarm:0.2.3 38 | networks: 39 | - functions 40 | environment: 41 | read_timeout: '25s' 42 | write_timeout: '25s' 43 | DOCKER_API_VERSION: '1.30' 44 | deploy: 45 | placement: 46 | constraints: 47 | - 'node.role == manager' 48 | - 'node.platform.os == linux' 49 | resources: 50 | reservations: 51 | memory: 100M 52 | restart_policy: 53 | condition: on-failure 54 | delay: 5s 55 | max_attempts: 20 56 | window: 380s 57 | 58 | # nats: 59 | # image: nats-streaming:0.6.0 60 | # command: '--store memory --cluster_id faas-cluster' 61 | # networks: 62 | # - functions 63 | # deploy: 64 | # resources: 65 | # limits: 66 | # memory: 125M 67 | # reservations: 68 | # memory: 50M 69 | # placement: 70 | # constraints: 71 | # - 'node.platform.os == linux' 72 | 73 | # queue-worker: 74 | # image: functions/queue-worker:0.4.3 75 | # networks: 76 | # - functions 77 | # environment: 78 | # max_inflight: '1' 79 | # ack_timeout: '30s' 80 | # deploy: 81 | # resources: 82 | # limits: 83 | # memory: 50M 84 | # reservations: 85 | # memory: 20M 86 | # restart_policy: 87 | # condition: on-failure 88 | # delay: 5s 89 | # max_attempts: 20 90 | # window: 380s 91 | # placement: 92 | # constraints: 93 | # - 'node.platform.os == linux' 94 | 95 | api-create: 96 | image: mjhea0/faas-api-create:latest 97 | labels: 98 | function: 'true' 99 | networks: 100 | - functions 101 | environment: 102 | no_proxy: 'gateway' 103 | https_proxy: $https_proxy 104 | env_file: 105 | - .env 106 | deploy: 107 | placement: 108 | constraints: 109 | - 'node.platform.os == linux' 110 | 111 | api-read: 112 | image: mjhea0/faas-api-read:latest 113 | labels: 114 | function: 'true' 115 | networks: 116 | - functions 117 | environment: 118 | no_proxy: 'gateway' 119 | https_proxy: $https_proxy 120 | env_file: 121 | - .env 122 | deploy: 123 | placement: 124 | constraints: 125 | - 'node.platform.os == linux' 126 | 127 | api-update: 128 | image: mjhea0/faas-api-update:latest 129 | labels: 130 | function: 'true' 131 | networks: 132 | - functions 133 | environment: 134 | no_proxy: 'gateway' 135 | https_proxy: $https_proxy 136 | env_file: 137 | - .env 138 | deploy: 139 | placement: 140 | constraints: 141 | - 'node.platform.os == linux' 142 | 143 | api-delete: 144 | image: mjhea0/faas-api-delete:latest 145 | labels: 146 | function: 'true' 147 | networks: 148 | - functions 149 | environment: 150 | no_proxy: 'gateway' 151 | https_proxy: $https_proxy 152 | env_file: 153 | - .env 154 | deploy: 155 | placement: 156 | constraints: 157 | - 'node.platform.os == linux' 158 | 159 | movies-db: 160 | image: postgres:latest 161 | networks: 162 | - functions 163 | ports: 164 | - 5435:5432 165 | environment: 166 | - POSTGRES_USER=postgres 167 | - POSTGRES_PASSWORD=postgres 168 | 169 | networks: 170 | functions: 171 | driver: overlay 172 | attachable: true 173 | labels: 174 | - 'openfaas=true' 175 | -------------------------------------------------------------------------------- /functions/api-create/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | RUN apk --update add nodejs nodejs-npm 4 | 5 | ADD https://github.com/openfaas/faas/releases/download/0.7.9/fwatchdog /usr/bin 6 | RUN chmod +x /usr/bin/fwatchdog 7 | 8 | WORKDIR /root/ 9 | 10 | COPY package.json . 11 | 12 | RUN npm i 13 | COPY handler.js . 14 | 15 | ENV fprocess="node handler.js" 16 | 17 | HEALTHCHECK --interval=1s CMD [ -e /tmp/.lock ] || exit 1 18 | 19 | CMD ["fwatchdog"] 20 | -------------------------------------------------------------------------------- /functions/api-create/handler.js: -------------------------------------------------------------------------------- 1 | const getStdin = require('get-stdin'); 2 | 3 | const knex = require('knex')({ 4 | client: 'pg', 5 | connection: { 6 | host: process.env.POSTGRES_HOST, 7 | user: process.env.POSTGRES_USER, 8 | password: process.env.POSTGRES_PASS, 9 | database: process.env.POSTGRES_DB 10 | } 11 | }); 12 | 13 | function handle(content, callback) { 14 | const movie = JSON.parse(content); 15 | const returnObject = { 16 | status: 'success', 17 | data: null 18 | }; 19 | return knex('movie') 20 | .insert(movie) 21 | .returning('*') 22 | .then(() => { 23 | knex.destroy(); 24 | returnObject.data = `${movie.name} added!`; 25 | callback(null, JSON.stringify(returnObject)); 26 | }) 27 | .catch((err) => { 28 | knex.destroy(); 29 | returnObject.status = 'error'; 30 | returnObject.data = err; 31 | callback(JSON.stringify(returnObject)); 32 | }); 33 | } 34 | 35 | getStdin() 36 | .then((val) => { 37 | handle(val, (err, res) => { 38 | if (err) { 39 | console.error(err); 40 | } else { 41 | console.log(res); 42 | } 43 | }); 44 | }) 45 | .catch((e) => { 46 | console.error(e.stack); 47 | }); 48 | -------------------------------------------------------------------------------- /functions/api-create/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-create", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "get-stdin": "^6.0.0", 14 | "knex": "^0.14.2", 15 | "pg": "^7.4.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /functions/api-delete/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | RUN apk --update add nodejs nodejs-npm 4 | 5 | ADD https://github.com/openfaas/faas/releases/download/0.7.9/fwatchdog /usr/bin 6 | RUN chmod +x /usr/bin/fwatchdog 7 | 8 | WORKDIR /root/ 9 | 10 | COPY package.json . 11 | 12 | RUN npm i 13 | COPY handler.js . 14 | 15 | ENV fprocess="node handler.js" 16 | 17 | HEALTHCHECK --interval=1s CMD [ -e /tmp/.lock ] || exit 1 18 | 19 | CMD ["fwatchdog"] 20 | -------------------------------------------------------------------------------- /functions/api-delete/handler.js: -------------------------------------------------------------------------------- 1 | const getStdin = require('get-stdin'); 2 | 3 | const knex = require('knex')({ 4 | client: 'pg', 5 | connection: { 6 | host: process.env.POSTGRES_HOST, 7 | user: process.env.POSTGRES_USER, 8 | password: process.env.POSTGRES_PASS, 9 | database: process.env.POSTGRES_DB 10 | } 11 | }); 12 | 13 | function handle(content, callback) { 14 | const movie = JSON.parse(content); 15 | const returnObject = { 16 | status: 'success', 17 | data: 'Movie does not exist!' 18 | }; 19 | return knex('movie') 20 | .del() 21 | .where({ id: parseInt(movie.id) }) 22 | .returning('*') 23 | .then((movies) => { 24 | knex.destroy(); 25 | if (movies.length) returnObject.data = `Movie id ${movie.id} deleted!`; 26 | callback(null, JSON.stringify(returnObject)); 27 | }) 28 | .catch((err) => { 29 | knex.destroy(); 30 | returnObject.status = 'error'; 31 | returnObject.data = err; 32 | callback(JSON.stringify(returnObject)); 33 | }); 34 | } 35 | 36 | getStdin() 37 | .then((val) => { 38 | handle(val, (err, res) => { 39 | if (err) { 40 | console.error(err); 41 | } else { 42 | console.log(res); 43 | } 44 | }); 45 | }) 46 | .catch((e) => { 47 | console.error(e.stack); 48 | }); 49 | -------------------------------------------------------------------------------- /functions/api-delete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-delete", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "get-stdin": "^6.0.0", 14 | "knex": "^0.14.2", 15 | "pg": "^7.4.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /functions/api-read/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | RUN apk --update add nodejs nodejs-npm 4 | 5 | ADD https://github.com/openfaas/faas/releases/download/0.7.9/fwatchdog /usr/bin 6 | RUN chmod +x /usr/bin/fwatchdog 7 | 8 | WORKDIR /root/ 9 | 10 | COPY package.json . 11 | 12 | RUN npm i 13 | COPY handler.js . 14 | 15 | ENV fprocess="node handler.js" 16 | 17 | HEALTHCHECK --interval=1s CMD [ -e /tmp/.lock ] || exit 1 18 | 19 | CMD ["fwatchdog"] 20 | -------------------------------------------------------------------------------- /functions/api-read/handler.js: -------------------------------------------------------------------------------- 1 | const getStdin = require('get-stdin'); 2 | 3 | const knex = require('knex')({ 4 | client: 'pg', 5 | connection: { 6 | host: process.env.POSTGRES_HOST, 7 | user: process.env.POSTGRES_USER, 8 | password: process.env.POSTGRES_PASS, 9 | database: process.env.POSTGRES_DB 10 | } 11 | }); 12 | 13 | function handle(content, callback) { 14 | const returnObject = { 15 | status: 'success', 16 | data: 'No movies!' 17 | }; 18 | return knex('movie') 19 | .select('*') 20 | .then((movies) => { 21 | knex.destroy(); 22 | if (movies.length) returnObject.data = movies; 23 | callback(null, JSON.stringify(returnObject)); 24 | }) 25 | .catch((err) => { 26 | knex.destroy(); 27 | returnObject.status = 'error'; 28 | returnObject.data = err; 29 | callback(JSON.stringify(returnObject)); 30 | }); 31 | } 32 | 33 | getStdin() 34 | .then((val) => { 35 | handle(val, (err, res) => { 36 | if (err) { 37 | console.error(err); 38 | } else { 39 | console.log(res); 40 | } 41 | }); 42 | }) 43 | .catch((e) => { 44 | console.error(e.stack); 45 | }); 46 | -------------------------------------------------------------------------------- /functions/api-read/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-read", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "get-stdin": "^6.0.0", 14 | "knex": "^0.14.2", 15 | "pg": "^7.4.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /functions/api-update/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | RUN apk --update add nodejs nodejs-npm 4 | 5 | ADD https://github.com/openfaas/faas/releases/download/0.7.9/fwatchdog /usr/bin 6 | RUN chmod +x /usr/bin/fwatchdog 7 | 8 | WORKDIR /root/ 9 | 10 | COPY package.json . 11 | 12 | RUN npm i 13 | COPY handler.js . 14 | 15 | ENV fprocess="node handler.js" 16 | 17 | HEALTHCHECK --interval=1s CMD [ -e /tmp/.lock ] || exit 1 18 | 19 | CMD ["fwatchdog"] 20 | -------------------------------------------------------------------------------- /functions/api-update/handler.js: -------------------------------------------------------------------------------- 1 | const getStdin = require('get-stdin'); 2 | 3 | const knex = require('knex')({ 4 | client: 'pg', 5 | connection: { 6 | host: process.env.POSTGRES_HOST, 7 | user: process.env.POSTGRES_USER, 8 | password: process.env.POSTGRES_PASS, 9 | database: process.env.POSTGRES_DB 10 | } 11 | }); 12 | 13 | function handle(content, callback) { 14 | const movie = JSON.parse(content); 15 | const returnObject = { 16 | status: 'success', 17 | data: 'Movie does not exist!' 18 | }; 19 | return knex('movie') 20 | .update({'name': movie.name}) 21 | .where({ id: parseInt(movie.id) }) 22 | .returning('*') 23 | .then((movies) => { 24 | knex.destroy(); 25 | if (movies.length) returnObject.data = `Movie id ${movie.id} updated!`; 26 | callback(null, JSON.stringify(returnObject)); 27 | }) 28 | .catch((err) => { 29 | knex.destroy(); 30 | returnObject.status = 'error'; 31 | returnObject.data = err; 32 | callback(JSON.stringify(returnObject)); 33 | }); 34 | } 35 | 36 | getStdin() 37 | .then((val) => { 38 | handle(val, (err, res) => { 39 | if (err) { 40 | console.error(err); 41 | } else { 42 | console.log(res); 43 | } 44 | }); 45 | }) 46 | .catch((e) => { 47 | console.error(e.stack); 48 | }); 49 | -------------------------------------------------------------------------------- /functions/api-update/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-update", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "get-stdin": "^6.0.0", 14 | "knex": "^0.14.2", 15 | "pg": "^7.4.1" 16 | } 17 | } 18 | --------------------------------------------------------------------------------