Run a Node Express Web App on Heroku Using AWS RDS Postgres DB

Posted on Tuesday, May 5, 2015



I have already set up a basic webapp with a free "Hobby" Postgres database see http://www.whiteboardcoder.com/2015/04/your-second-heroku-node-app.html  The "Hobby" Database can only contain 10K rows.   You can pay Heroku $9/mo to up that to 10M rows, which is a very good way to go.   They also offer some paid Postgres servers that do not have these limits.

But!  What if you want to use your own Postgres Database?   What if you want to use your own Amazon RDS Postgres Database?  How do you do that?  How do you hook up your app to use a remote Database?




I am going to try and cover that as best I can in this write up.


The code


Create the app locally.


  > mkdir myapp
  > cd myapp
  > npm init




Edit the package.json file


  > vi package.json


Add the following


{
  "name": "myapp",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "private":"true",
  "scripts": {
    "test": "mocha --recursive test"
  },
  "author": "Patrick Bailey",
  "license": "ISC"
}


I am going to make this a private module (So it can't accidentally get published to npm). 





Install npm modules


For testing install


  > npm install --save-dev mocha
  > npm install --save-dev chai
  > npm install --save-dev chai2-json-schema
  > npm install --save-dev superagent


The rest, Express, config, and Postgres.


  > npm install -S express
  > npm install -S config
  > npm install -S pg





Create app.js and server.js



  > vi app.js


And place the following in it.


var express = require('express');
var config = require('config');
var pg = require('pg');

var app = express();
var server;
var conString = "postgres://" + config.get('db.user') + ":"
   
+ config.get('db.pass') + "@" + config.get('db.host')
    +
"/" + config.get('db.name');


var start = exports.start = function start(port, callback) {
   
server = app.listen(port, callback);
};

var stop = exports.stop = function stop(callback) {
   
server.close(callback);
};

app.get('/', function sendResponse(req,res) {
    res.
status(200).send('Hello World!');
});

app.get('/db', function sendResponse(req,res) {
    getMessage(
1, function(err, msg) {
       
if(err){
            res.
status(404).send("404: Error talking to database " + err);
        }
       
else{
            res.
status(200).send(msg);
        }
    });
});

function getMessage(id, next) {
   
pg.connect(conString, function(err, client, done){
       
if(err) {
            next(err,
undefined);
        }
       
else{
            client.
query("select * from message where id='" + id + "'",
                                            function(err, result) {
               
if(err) {
                    next(err,
undefined);
                }
               
else {
                    next(
undefined, result.rows[0].msg);
                }
            });
        }
    });
};





Edit server.js


  > vi server.js


And place the following in it.


var app = require('./app');

app.start(process.env.PORT || 3000);








Install Postgres and create the DB


Install Postgres, I am assuming you are on Ubuntu 14.04.


  > sudo apt-get update
  > sudo apt-get install postgresql


Switch to the Postgres user (the only user who can login at this point)


  > sudo su postgres


Create a script called script.sql


  > cd
  > vi script.sql


And place the following in it


-- Add Users
CREATE USER app_ro WITH PASSWORD 'myPassword';
CREATE USER app_rw WITH PASSWORD 'myPassword';

-- Create DB
CREATE DATABASE myapp;

-- login to the new DB
\c myapp

-- Revoke all Privileges
REVOKE ALL ON DATABASE myapp FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM PUBLIC;

-- Set up privileges for app_ro
GRANT CONNECT ON DATABASE myapp to app_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_ro;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO app_ro;
GRANT USAGE ON SCHEMA public to app_ro;

-- Set up privileges for app_rw
GRANT CONNECT ON DATABASE myapp to app_rw;
GRANT SELECT, UPDATE, INSERT, DELETE ON ALL TABLES IN SCHEMA public TO app_rw;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO app_rw;
GRANT USAGE ON SCHEMA public to app_rw;


GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO app_ro;
GRANT USAGE ON SCHEMA public to app_ro;

-- Set up privileges for app_ro (for new tables)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
   GRANT SELECT ON TABLES TO app_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
   GRANT SELECT ON SEQUENCES TO app_ro;

-- Set up privileges for app_rw (for new tables)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
   GRANT SELECT, UPDATE, INSERT, DELETE ON TABLES TO app_rw;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
   GRANT SELECT, UPDATE ON SEQUENCES TO app_rw;

--Create tables
CREATE TABLE message (id serial, msg varchar(1024));

--Insert some test data
INSERT INTO message (msg) VALUES ('First Test Message');
INSERT INTO message (msg) VALUES ('Second Test Message');


This script create a database called myapp with a table called message and populates that table with two messages.  It also creates two users for that database.  app_ro is a read only user and app_rw  is a read write user.  Each user has a password of "myPassword".


Run the following command to run the script.


  > psql -U postgres -d postgres -a -f script.sql


Run the following command to confirm the database has been created, the table is populated, and the app_ro can read from the database.



  > psql -h localhost -d myapp -U app_ro -c "select * from message;"


Enter your password "myPassword"



OK local database is working!


Exit out of the Postgres user.


 > exit





Set up tests


Create a few TDD tests.  I already created these before in http://www.whiteboardcoder.com/2015/04/your-second-heroku-node-app.html.
Now I am just going to show you what I put in it.

First the database test db.test.js


  > mkdir test
  > vi test/db.test.js


And place the following in it.


var chai = require('chai')
var assert = chai.assert;
var request = require('superagent');
var pg = require('pg');
var config = require('config');


describe('DB Test', function() {
   
var conString = "postgres://" + config.get('db.user') + ":"
       
+ config.get('db.pass') + "@" + config.get('db.host')
        +
"/" + config.get('db.name');

   
describe('Test the connection to the DB', function () {
       
it('should connect to the DB', function (done) {
           
pg.connect(conString, function(err, client, db_done) {
               
if(err) {
                   
assert.equal("can-connect", "did-not-connect");
                }
               
else {
                   
assert.equal("can-connect","can-connect");
                }
                db_done(done());

            });
        });
    });
});




Then create the app.test.js


> vi test/app.test.js


And place the following in it.


var chai = require('chai')
var assert = chai.assert;
var request = require('superagent');
var pg = require('pg');
var config = require('config');

describe('My App', function() {
   
var myApp = require('../app.js');
   
var port = 3000;
   
var baseUrl = 'http://localhost:' + port;
   
var conString = "postgres://" + config.get('db.user') + ":"
       
+ config.get('db.pass') + "@" + config.get('db.host')
        +
"/" + config.get('db.name');


    before(
function(done) {
       
myApp.start(port, done);
    });

    after(
function(done) {
       
myApp.stop(done);
    });

   
describe('When requested at /db', function () {
       
it('should return 200 code', function (done) {
           
request.get(baseUrl + "/db").end(function(err, res) {
               
assert.equal(res.status, 200);
                done();
            });
        });
    });


   
describe('When requested at /db', function () {
       
it('should return message from db where id = 1', function (done) {
           
request.get(baseUrl + "/db").end(function(err, res) {
                getMessage(
1, function(msg) {
                   
assert.equal(res.text, msg);
                    done();
                });
            });
        });
    });

   
function getMessage(id, next){
       
pg.connect(conString, function(err, client, done){
           
if(err) {
                next(
undefined);
            }
           
else {
                client.
query("select * from message where id = '" + id + "'",
                                        function(err, result) {
                   
if(err) {
                        next(
undefined);
                    }
                   
else{
                        next(result.
rows[0].msg);
                    }
                });
            }
            done();
        });
    };
});



Create the config files


Create the default config file


  > mkdir config
  > vi config/default.json


And place the following in it


{
    "db": {
        "host": "localhost",
        "port": 5432,
        "name": "myapp",
        "user": "app_ro",
        "pass": "myPassword"
    }
}







Run the test via npm


  > npm test








Test the app


Run the app locally


> npm start






Looks good!  The message is from the database.





Git


Run git init


  > git init




Edit the .gitignore


  > vi .gitignore


Here is my .gitignore file (Based on these .gitignore files I found https://github.com/github/gitignore/blob/master/Node.gitignore [1]  https://github.com/github/gitignore/tree/master/Global [2]




#Intellij
*.iml
.idea/

# Linux
.directory
*~

# OS X
.DS_Store*
Icon?
._*

# Windows
Thumbs.db
ehthumbs.db
Desktop.ini

# npm
node_modules
logs
coverage
*.log
*.gz
.grunt




I think it's always a good idea to run git status before adding anything to your git repo to see what is going to be added (sometimes I screw up our .gitignore file or some odd file sneaks in there)


  > git status




Looks good, it's not adding the node_modules folder, but it is the test folder.




Do an initial commit.


  > git add --all
  > git status
  > git commit  -m "Initial commit"








Create the Heroku App


I am using the free version of Heroku so I can only have one app going at a time.  I am going to list my current app and delete it.



  > heroku apps




My current app is called glacial-anchorage-5446.



Remove this app, 'destroy' it muhahaha


  > heroku apps:destroy --app glacial-anchorage-5446






Run the following to create an app


  > heroku create






I get



A placeholder app


When you run heroku create it adds in the heroku remotes to the git repo for you.  Run the following command to list the remotes.


  > git remote -v








Push it up


Just push it up!


  > git push heroku master




open



It's not working because I need a database.

Run the following to check your addons


  > heroku addons





Run this command to provision a Postgres hobby database.


  > heroku addons:add heroku-postgresql:hobby-dev







Getting Data into the Database


The DB is empty, I need to fill it up with data.

Head over to https://postgres.heroku.com/databases and login you should be able to see your database.



Click on your Database





Grab your database information. And to make life simpler place it in ~/.pgpass file.



For example for me I can run the following command.


  > echo "ec2-107-22-161-155.compute-1.amazonaws.com:5432:d2s7i164kc4lq0:vnzccunmgkafqb:WThddGe7JIDrWfeEUwYESmoABC" >> ~/.pgpass


It's in the form "hostname:port:database_name:username:password"


You then must change access to this file to 600


  > chmod 600 ~/.pgpass


Login (no need to use password)


  > psql -U vnzccunmgkafqb -h ec2-107-22-161-155.compute-1.amazonaws.com d2s7i164kc4lq0









Move Database


To move the database run the following commands.  (There are better ways to do this with larger databases see https://devcenter.heroku.com/articles/heroku-postgres-import-export [8])

First dump the current database to an outfile


  > pg_dump -h localhost -U app_rw myapp  --no-acl --no-owner  -f dump_myapp.sql


Now create and populate the Heroku database with the following command.  (still making use of my ~/.pgpass file)


  > psql -U vnzccunmgkafqb -h ec2-107-22-161-155.compute-1.amazonaws.com d2s7i164kc4lq0 -f dump_myapp.sql





Don't forget to remove the dump file.


  > rm dump_myapp.sql


Login and check it


  > psql -U vnzccunmgkafqb -h ec2-107-22-161-155.compute-1.amazonaws.com d2s7i164kc4lq0


And run the following command.


  > select * from message;








Fix the config


The Database at Heroku is no correct, but the config file, that contains the db connection info is not.

Create the production config file


> vi config/production.json


And place the following in it.



{
    "db": {
        "host": "ec2-107-22-161-155.compute-1.amazonaws.com",
        "name": "d2s7i164kc4lq0",
        "user": "vnzccunmgkafqb",
        "pass": "WThddGe7JIDrWfeEUwYESmoABC"
    }
}




Commit the changes and push them up


  > git add --all
  > git status
  > git commit  -m "Added config"
  > git push heroku master






On the Heroku dyno set the $NODE_ENV to production (config uses this)


  > heroku config:set NODE_ENV=production


Then to check it


  > heroku config










It's working


Why did I do all that?   Well, I think now that I have an app working in Heroku and talking to it's database I think it may only be a matter of changing the config file config/application.json to point the RDS database I am about to make.





Before RDS


I have a Postgres database running locally at home…  What if I just open up a port and route it to this box?  Can I get Heroku to use my database running at home?  I am guessing so, let me try it out.


You need to expose your server to the internet. In my case route port 5432 from my router to this virtual machine.

After I got this set up I tried to login to Postgres remotely.  (I changed the IP address here to protect the innocent, in fact I just found out 203.0.113.0 - 203.0.113.253 are example IP addresses and don't exist J much like example.com)


  > psql -h 203.0.113.220 -d myapp -U app_ro -c "select * from message;"


Ooops it got rejected… I need to tweak my /etc/postgresql/9.3/main/postgresql.conf file to allow access from outside the machine.





  > sudo vi /etc/postgresql/9.3/main/postgresql.conf


Update listen_addresses to the following


listen_addresses = '*'





I also need to edit /etc/postgresql/9.3/main/ pg_hba.conf file to allow access from outside the machine.


  > sudo vi /etc/postgresql/9.3/main/pg_hba.conf


Append the following line to the end of this document (this allows connections from anywhere)


host    all             all             0.0.0.0/0           md5







Restart the Postgres server.


  > sudo service postgresql stop
  > sudo service postgresql start


OK now I should be able to log in.



  > psql -h 203.0.113.220 -d myapp -U app_ro -c "select * from message;"




Finally worked from outside my system.







Heroku talking to my local DB


Now can I get my app on Heroku talking to my database at home?
Well first I should update the message where id=1 so it's easy to tell.


  > psql -h 203.0.113.220 -d myapp -U app_rw -c "update message set msg='from remote database' where id=1; "


Edit config/production.json


  > vi config/production.json


To


{
    "db": {
        "host": "203.0.113.220",
        "name": "myapp",
        "user": "app_ro",
        "pass": "myPassword"
    }
}




Push it up to Heroku


  > git add --all
  > git commit -m "Talking to Home Postgres server"
  > git push heroku master





Nice, it worked.  OK if that worked once I get the RDS up and running this should be a no brainer.

Before I move on though,  I am closing access to my local Postgres server,  putting pg_hba and postgresql.conf back to the way they were and stop port forwarding to this machine.







Create the RDS


I am not going to go into how to set up an AWS account in this post.  I am assuming you already have one set up.



Login to your amazon console and click on RDS


Select the region you want to create the RDS in. 




Click on the Pull down in the upper right and select
"US East (N. Virginia).

Why?  Because all the amazon Heroku servers are run out of there.




Click on Instances







Click Launch DB Instance







Choose PostgreSQL and click Select.





Select No for Multizone and click Next Step.  (You can always make it a MZ later.






Select your Options



I chose…
·         DB Engine 9.3.6
·         db.t2.micro  (The smallest cheapest server $0.018/hr ~ $14/mo)
·         Mutli-Zone No
·         Storage Type Magnetic

Settings
·         Named it testing
·         Master user = postgres
·         Master password = myPassword**   (don't worry I am wiping this RDS after I write this up)


Click Next Step






The defaults on this one are good.  I have no VPC security groups so I may have to create one after this?







I am leaving the Database Name blank (I don't want to manually create it later).

All the rest looks good.  Click Launch DB Instance








It's being created.  Click On View Your DB Instance.








It's still 'creating' give it a few minutes.








OK now it's available.










There is my endpoint

testing.chkcoozrprm6.us-east-1.rds.amazonaws.com:5432

But what about my permissions?









Hover over i and this pop up come up.

Looks like it gave inbound access to my current IP address (Which I covered up).  And access to no one else.

Nice, not bad for not setting up a security group.  Let me see if it works. 




From my local box let me try to login to this database.


  > psql -h testing.chkcoozrprm6.us-east-1.rds.amazonaws.com -U postgres




That worked I am connected!

\q to get out


  > \q





Create a script called script.sql


  > cd
  > vi script.sql


And place the following in it


-- Add Users
CREATE USER app_ro WITH PASSWORD 'myPassword';
CREATE USER app_rw WITH PASSWORD 'myPassword';

-- Create DB
CREATE DATABASE myapp;

-- login to the new DB
\c myapp

-- Revoke all Privileges
REVOKE ALL ON DATABASE myapp FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM PUBLIC;

-- Set up privileges for app_ro
GRANT CONNECT ON DATABASE myapp to app_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_ro;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO app_ro;
GRANT USAGE ON SCHEMA public to app_ro;

-- Set up privileges for app_rw
GRANT CONNECT ON DATABASE myapp to app_rw;
GRANT SELECT, UPDATE, INSERT, DELETE ON ALL TABLES IN SCHEMA public TO app_rw;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO app_rw;
GRANT USAGE ON SCHEMA public to app_rw;


GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO app_ro;
GRANT USAGE ON SCHEMA public to app_ro;

-- Set up privileges for app_ro (for new tables)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
   GRANT SELECT ON TABLES TO app_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
   GRANT SELECT ON SEQUENCES TO app_ro;

-- Set up privileges for app_rw (for new tables)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
   GRANT SELECT, UPDATE, INSERT, DELETE ON TABLES TO app_rw;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
   GRANT SELECT, UPDATE ON SEQUENCES TO app_rw;

--Create tables
CREATE TABLE message (id serial, msg varchar(1024));

--Insert some test data
INSERT INTO message (msg) VALUES ('First Test Message FROM Amazon RDS');
INSERT INTO message (msg) VALUES ('Second Test Message FROM Amazon RDS');



Run the following command to run the script.


  > psql -h testing.chkcoozrprm6.us-east-1.rds.amazonaws.com -U postgres -d postgres -a -f script.sql





See if I can log in as the app_ro user.


  > psql -h testing.chkcoozrprm6.us-east-1.rds.amazonaws.com -U app_ro -d myapp




OK its working!

Remove the sql file


  > rm script.sql









Update config


Update the config/production file.


  > vi config/production.json


Update it to.


{
    "db": {
        "host": "testing.chkcoozrprm6.us-east-1.rds.amazonaws.com",
        "name": "myapp",
        "user": "app_ro",
        "pass": "myPassword"
    }
}




Test it locally by setting the Environment variable NODE_ENV to production


  > export NODE_ENV=production





Now run the program


  > npm start






Wahoo it's working J







Heroku push


Commit the changes in git and push it up to Heroku


  > git status
  > git add --all
  > git commit -m "Updated to talk to RDS"
  > git push heroku master







I should get an error, as I am pretty sure Heroku cannot access my RDS.



Yep.

OK I need to update my Security group for my Database.






Security Group







Click on Security Groups








Click on VPC page.  (The database was put in a VPC network)










Click on Security Groups and then on Create Security Group










Name it.   Then select the VPC the RDS is a part of.   (In my case its easy I have no other VPCs so I just choose the one, if you are unsure you can check the RDS instance and it will list the VPC it is in)

Click Yes, Create.









Select Inbound Rules and click Edit.








Select Custom TCP Rule.  Set the port Range to 5432 and the Source to 0.0.0.0/0

This effectively says I accept connections from anyone.   (They still need a username and password though)

Click Save













The security group is made and now needs to be applied to the Database Instance.









Click on Instances.  Select the Database, then from the pull down menu Select Modify.








Select the New Security Group.











Checkbox Apply Immediately  (If you don't the changes will take effect during the next scheduled maintenance window)

Click Continue.









Click Modify DB Instance







Wahoo!  It's working!!


Heroku is now talking to an AWS RDS server that I can easily convert to a Multizone server (Giving me a HA postgres database server).







References
 [1]       How do you install “development only” NPM modules for Node.js (package.json)?
                Accessed 4/2015
[2]        Running scripts with npm

                Accessed 4/2015 

No comments:

Post a Comment