Monday, January 27, 2014

Adventures with the MEAN Stack and Openshift

I'm rewriting an application. The existing user experience isn't too bad, but it could be better. And the code... But I'm also trying to stay up-to-date, or at least not-too-far-behind. So my aim is to use some modern technologies to make a Good single page application. Technologies of interest include:
  • Node. I'm learning more and more about just how great Javascript can be. 
  • Angular. Before choosing angular, I read a few comparisons with Ember, Knockout, and others. I think Angular was a fairly easy choice, but I wanted to be sure that I would have company.
  • Mongodb. I haven't used a NoSQL for anything more than a tutorial or two. This one seems to be pretty popular. My application will certainly not qualify as Big Data, so I'm sure that just about any SQL/NoSQL system would be fine. But at least I'll learn. Mongoose seems to be the obvious object-database bridge.
  • Bootstrap. I've played with YUI and Google Web Toolkit, but never used Less or any serious CSS framework.
  • Karma and Grunt for rapid testing and development.
  • Openshift: Red Hat's PaaS offering.

MEAN stack running Locally

I decided that the Yeoman angular-fullstack generator would be a good ramp to get me going. There are a few pre-requisites (Node, Yeoman, Bower) which I had already installed before documenting this. If I get time, I'll try on a fresh VM to clarify these. But to start my application, I made myself a nice fresh directory and typed
yo angular-fullstack
I declined the offer to use Sass and Compass for now, but did choose Twitter Bootstrap, all the Angular components (resource, cookies, sanitize, and route), and Mongo & Mongoose. The screen filled with all sorts of downloads. I'm sure I won't even see most of these, let alone learn what they actually do or how to use them in anger. But that's layering (complexity management) for you. After a few minutes, my scaffolded app was ready, and all I had to do was type
grunt serve
and the Node server started, warned me that it couldn't connect to a Mongo on localhost, and then my app page appeared in my browser. It was a little short on detail, but I didn't know that at the time.

I installed mongo in c:\mongodb, and created a config file which simply included a line "dbpath = c:/mongodb/db". I started mongo daemon with the command
bin\mongod --config conf\mongodb.conf
Now I can terminate grunt (ctrl-c) and restart it. A new page appeared, this time with  list of "awesomeThings" retrieved from the database. But that wasn't the most awesome thing: if I edit one of the files, such as app/views/index.html, and save, the browser refreshes. And if I edit server-side javascript and save, the server restarts, and then the browser refreshes. Thank you connect-livereload!

After experimenting with, and learning a little about angular, express, and bootstrap, I wanted to show what I had to a friend. I decided I'd look for a cheap cloud host. Openshift (Paas) was the winner (small app for free), closely followed by DigitalOcean (IaaS). (I've also been playing around with Vagrant and noticed that Packer.io supports DigitalOceans nicely.)

MEAN on Openshift

The question was: how to get the Yeoman app onto Openshift. On Openshift, I created an account, and then started a new app with Node 0.10.0 and Mongo 2.2. It came with a default starter app, which I could clone with git. I could also log in using Putty, which is useful for debugging. It's great that something as simple as "git push" can stop, rebuild, and restart the Openshift app. Similar to Jenkins, but still great.

So after cloning the repository to my local pc, I can download all the dependencies and start the system with 
npm install
node server.js
Then I can point my browser to http://localhost:8080 and there's the app (a single page with some helpful links to Openshift/Node info. And for fun, I can look at the other route installed: http://localhost:8080/asciimo .

The Openshift default app is much smaller than the Yeoman angular-fullstack one. It only has a single npm dependency in the node_packages directory (express), compared to the Yeoman's 45 (including a variety of Grunt support, Karma testing support, and mongoose). So perhaps I should simply copy the Yeoman app into the Openshift directory and commit & push. That way I could have the Karma and Grunt benefits locally.

One significant challenge with this approach is that Openshift installs all those dependencies - even the ones which are for development only. It's true that Openshift has Jenkins support, so running tests is quite feasible. But it means that the post-git-push build step takes fifteen minutes (in stark contrast to the 2s Grunt connect-livereload!), and it uses 75MB of storage and more than 12,000 files! Not ideal for a free system, even if there's 1GB quota per gear, with the first 3 gears free. I'd like to be able to tell my Openshift npm to only install the production npms. I decided (at least for this test) not to commit the node_packages directory. The advice seems to be that node_packages should be committed, but only for long term stability.

So I commit and push, and wait the 15 minutes for all the npm activity (which shows up as part of the git push) to finish. It finishes with 
remote: npm info ok
remote: Preparing build for deployment
remote: Deployment id is a6d28b2f
remote: Activating deployment
remote: Starting MongoDB cartridge
remote: Starting NodeJS cartridge
remote: Result: success
remote: Activation status: success
remote: Deployment completed with status: success
To ssh://52e60ad95004463c5b000384@test2-yesberg.rhcloud.com/~/git/test2.git/
   809f542..5897269  master -> master  

The words "Status: success" sound good. But when I go to my app page, I get a 503 Service Temporarily Unavailable. To locate the problem, I need to login with Putty. (Or I could use the rhc app: rhc ssh). To see the log files, I need to use the tail_all command. (Note that this only works from the home directory.) Every couple of seconds, it seems there's a new copy of the following error:
Error: listen EACCES    at errnoException (net.js:884:11)    at Server._listen2 (net.js:1003:19)    at listen (net.js:1044:10)    at Server.listen (net.js:1110:5)    at Function.app.listen (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/lib/application.js:533:24)    at Object. (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/server.js:39:5)    at Module._compile (module.js:456:26)    at Object.Module._extensions..js (module.js:474:10)    at Module.load (module.js:356:32)    at Function.Module._load (module.js:312:12)DEBUG: Program node server.js exited with code 8
DEBUG: Starting child process with 'node server.js'
It looks like the listen() call on line 39 of server.js is failing. The default with Yeoman had been 9000 (although the server.js code has 3000 as another possibility), whereas the Openshift port was 8080. And if I type "export" at the Openshift shell, there is an environment variable 

 OPENSHIFT_NODEJS_PORT="8080"

On systems I'm used to, listening on the wrong port wouldn't give an EACCES error. But I notice that there are SELINUX environment variables - perhaps this is all part of multitenant hosting (assuming that's what Red Hat does with Openshift). I decided to try to adjust my local app to use 8080. And then I need to ensure that the livereload facility doesn't cause problems - it uses websockets on a high numbered port to ask the browser to reload.


I don't really understand exactly how it's all working under the hood at this stage. But I notice the following snippet in server.js
// Start server
var port = process.env.PORT || 3000;
app.listen(port, function () {
  console.log('Express server listening on port %d in %s mode', port, app.get('env'));
});
It looks like something must be setting the PORT environment variable to 9000 - otherwise we'd be using 3000. So if I grep for 9000, I find the Gruntfile.js includes
    express: {
      options: {
        port: process.env.PORT || 9000
      },
      dev: {
        options: {
          script: 'server.js',
          debug: true
        }
      },
      prod: {
        options: {
          script: 'server.js',
          node_env: 'production'
        }
      }
    },
So I could just change the 9000 to 8080 in the Gruntfile, but then Openshift isn't using Grunt. So I decided to use a Console.log just before the listen command (instead of just after it) to display the value of port. And given the length of time it takes to deploy (only 5 minutes this time), I thought I'd add in another "or":
var port = process.env.OPENSHIFT_NODEJS_PORT || process.env.PORT || 3000;
console.log("Attempting to listen on port %d",port);
The push, deploy and activation succeeds again.but still gives a 503 error. The log shows the following every 2-3s.


connect.multipart() will be removed in connect 3.0
visit https://github.com/senchalabs/connect/wiki/Connect-3.0 for alternatives
connect.limit() will be removed in connect 3.0
Attempting to listen on port 8080
events.js:72
        throw er; // Unhandled 'error' event
              ^
Error: listen EACCES
    at errnoException (net.js:884:11)
    at Server._listen2 (net.js:1003:19)
    at listen (net.js:1044:10)
    at Server.listen (net.js:1110:5)
    at Function.app.listen (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/lib/application.js:533:24)
    at Object. (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/server.js:40:5)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
DEBUG: Program node server.js exited with code 8
DEBUG: Starting child process with 'node server.js'
So it is now attempting to use 8080, but still throwing the EACCES error. An answer on Stackoverflow suggests that it might be the hostname, rather than the port. I guess I need to add another argument to the listen().
var port = process.env.OPENSHIFT_NODEJS_PORT || process.env.PORT || 3000;var ip = process.env.OPENSHIFT_NODEJS_IP || "127.0.0.1";console.log("Attempting to listen on port %d on IP %s",port,ip);app.listen(port, ip, function () {  console.log('Express server listening on port %d in %s mode', port, app.get('env'));});
While I'm there, it seems that I should adjust the database connection details. Rather than hard coding, I can use an environment variable. This is an extract from lib/db/mongo.js
var uristring =
    process.env.OPENSHIFT_MONGODB_DB_URL ||
  process.env.MONGOLAB_URI ||
  process.env.MONGOHQ_URL ||
  'mongodb://localhost/test';
The 503 error has now disappeared, but is replaced by:
Error: Failed to lookup view "index" in views directory "/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/app/views"    at Function.app.render (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/lib/application.js:493:17)    at ServerResponse.res.render (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/lib/response.js:798:7)    at exports.index (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/lib/controllers/index.js:18:7)    at callbacks (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/lib/router/index.js:164:37)    at param (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/lib/router/index.js:138:11)    at pass (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/lib/router/index.js:145:5)    at Router._dispatch (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/lib/router/index.js:173:5)    at Object.router (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/lib/router/index.js:33:10)    at next (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/node_modules/connect/lib/proto.js:193:15)    at Object.methodOverride [as handle] (/var/lib/openshift/52e60ad95004463c5b000384/app-root/runtime/repo/node_modules/express/node_modules/connect/lib/middleware/methodOverride.js:48:5) 
GET / 500 46ms - 1.52kB
Using Putty, it seems that there's no "app/views" directory. I am more comfortable with hg than git, but could I have forgotten to commit the views somehow? Then I noticed that the .gitignore file provided by Yeoman included a line "views". I don't understand why that would be appropriate, so I deleted the line, committed and pushed.

This time, when I refresh my Openshift app page, the result is a blank white page. View Source shows that the index.html file is there. The problem is that all the css and javascript files refer to the bower_components folder, which is absent.
       

When Grunt starts up the system locally, it runs Bower, which downloads the appropriate js & css components according to bower.json. But Grunt isn't running on Openshift.  I decided to check to see whether the browser was receiving a 404 error for those files. But it wasn't. In fact, it was receiving a copy of index.html for each one of the files. The server.js file was routing any default GET to the index.index function:

// Angular Routes
app.get('/partials/*', index.partials);
app.get('/*', index.index);

So I need to choose one of the following approaches
  • run bower on Openshift
  • install all the framework files (.js and .css) into the repository,
  • or point to the CDN instead of bower_components
(To be continued...)










5 comments:

TH said...

Your tracking of this process has certainly been useful to me thus far. Please continue.

Bala said...

So u succeeded in deploying the MEAN stack in Open shift ?..Even i am trying to deploy the MEAN in Open Shift..But having some depedency issues..

Chris G said...
This comment has been removed by a blog administrator.
Chris G said...

You can run the 'grunt' command to create a /dist with all the files ready for deployment. See http://yeoman.io/deployment.html for more info.

John said...

Thanks Chris,

I agree that the Yeoman scaffolding can create a nice /dist. But then the OpenShift model of deplying by git-push is complex. I would need to have one repo with my source, and a separate one for the /dist. It may be the right way - interested in your thoughts.

John.