Deploying a MEAN Stack Application with Cloudsoft AMP

Deploying applications to the cloud can be tricky, especially when there are moving parts.  A common requirement for modern webapp development is to use the MEAN Stack (Mongo, Angular, Express and NodeJS).

With Cloudsoft AMP you can rapidly and consistently deploy and manage applications using a MEAN stack. This article covers deployment with future blogs to cover in-life management such as automation of scaling and restarting.

We’ll start with a simple AngularJS webapp served by Express that we’ll deploy to a cloud simply with Cloudsoft AMP.  We will then modify the application to persist its data to a MongoDB server, which will also be provisioned by AMP.  AMP will take care of the orchestration of the VMs and inject the MongoDB server’s details into the webapp.

Prerequisites

  • Cloudsoft AMP
  • A location set up on AMP

Follow the links to download and install Cloudsoft AMP.

Blueprint

First, let’s start with a basic blueprint to deploy our webapp using Cloudsoft AMP:

services:
- type: org.apache.brooklyn.entity.webapp.nodejs.NodeJsWebAppService
  brooklyn.config:
    nodejs.gitRepo.url: https://github.com/robertgmoss/demo-express-angular-app.git
    nodejs.app.fileName: index.js
    nodejs.app.name: myapp

This tells AMP to launch a VM, clone the node application that is hosted in the github repository, and start it using the index.js file.  The logic for all of this is encapsulated in the NodeJsWebAppService, which is part of the Apache Brooklyn ecosystem.

Express

Next, we’ll drill into the E part of the MEAN stack, the Express server.  The entry point when the blueprint starts is configured using the config key nodejs.app.fileName, in this case index.js.  The index.js file itself is very simple:  

var express = require('express');
var app = express();
var bodyParser = require('body-parser');

app.use(express.static('public'));
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());

var itemsStore = new InMemoryStore();

app.get('/get-items', function (req, res) {
  itemsStore.getItems(function (result) {
    res.json(result);
  });
})

app.post('/add-item', function(req, res){
  itemsStore.add(req.body, function (result) {
    res.json(result);
  });
});
app.listen(3000);

Our Express server provides two routes: one to get the items in the item store, and one to add items.  In both cases, we provide a json response, as these will be used by the AngularJS application to do client-side rendering.  We also expose a public folder for static content, including the AngularJS application, to be served from.  

To assist the server, we model an item store; and to begin with we provide a simple in-memory store, which we will replace with a Mongo store shortly.

function InMemoryStore() {
  var store = this;
  var items = [];

  store.getItems = function (callback) {
    callback(items);
  }
  store.add = function (item, callback) {
    items.push(item);
    callback(item);
  }
}

It simply stores the items in an array and returns that array.

AngularJS

Now for the A part of the MEAN stack.  For this we simply provide two parts, a controller and an html template.

Controllers

The Controller is also very simple; As with best practice, it calls out to get the items from a listService that we provide, and stores them in the controller’s view model.  Whenever an item is added, the item is passed to the listService to save the item, and the view model is updated.

MyController.$inject = ['listService'];
function MyController(listService) {
  var vm = this;

  listService.getItems().then(function(response) {
    vm.items = response.data;
  });

  vm.itemName = "";
  vm.itemDescription = "";

  vm.addItem = function() {
    listService.addItem(vm.itemName, vm.itemDescription).then(function (response) {
      vm.items.push(response.data);
    })
  }
}

The listService simply encapsulates calls to the Angular $http service, which calls out to the routes we defined in the Express server to either get the items or add an item. Each call then returns a promise.

ListServiceController.$inject = ['$http'];
function ListServiceController($http) {
    var service = this;

    service.addItem = function(name, description) {
        return $http.post('/add-item', {name: name, description: description});
    }

    service.getItems = function() {
        return $http.get('/get-items');
    }
}

View

The view has a form consisting of two input fields - one for the item’s name and another for the item’s description

<body ng-app="myApp">
    <div ng-controller="myController as vm" class="container">
      <div class="row">
      <form style="text-align: center">
        <h2>List</h2>
        <input type="text" ng-model="vm.itemName" placeholder="item name">
        <input type="text" ng-model="vm.itemDescription" placeholder="description">
        <button class="btn btn-primary" ng-click="vm.addItem();">Add Item</button>
      </form>
    </div>
      <div class="row">
        <h2>Items</h2>
        <div class="list-group">
          <div class="list-group-item" ng-repeat="item in vm.items">
            <h4 class="list-group-item-heading">{{item.name}}</h4>
            <p class="list-group-item-text">{{item.description}}</p>
          </div>
        </div>
      </div>
    </div>
  </body>

MongoDB

To put the M in MEAN, we need to make a few small adjustments.  First let’s modify the blueprint.  We need to add an additional item in the list of services, giving it an id so that we can reference it in the NodeJsWebAppService definition.

- type: org.apache.brooklyn.entity.nosql.mongodb.MongoDBServer
  id: mongo-server


To wire up the MongoDB server into the NodeJsWebAppService, we need to set an environment variable on that service, which the application will use to establish the connection.

shell.env:
  MONGO_CONNECTION_URL:
    $brooklyn:formatString:
      - "mongodb://%s/items"
      - entity("mongo-server").attributeWhenReady("mongodb.server.endpoint")

Next, we need to modify the Express server to use a Mongo store instead of an in-memory store:

var itemsStore = process.env.MONGO_CONNECTION_URL ?
      new MongoStore(process.env.MONGO_CONNECTION_URL) : new InMemoryStore();

We choose to use the in-memory store only if the application has no environment variable set.  To connect to the MongoDB server, we use the mongoose library, that lets us use a schema for object mapping.  And we implement the same methods as the in-memory store, so this can be used polymorphically.

function MongoStore(connectionUrl) {
  var store = this;
  var mongoose = require('mongoose');
  mongoose.connect(connectionUrl);

  var Schema = mongoose.Schema;
  var ObjectId = Schema.ObjectID;

  var Item = new Schema({
    name: {type: String, required: true, trim: true},
    description: {type: String, required: true, trim: true}
  });

  var ProductCatalog = mongoose.model('Item', Item);
  store.getItems = function (callback) {
    ProductCatalog.find().lean().exec(function(error, data){
      callback(error ? error : data);
    });
  }

  store.add = function (item, callback) {
    new ProductCatalog(item).save(function(error, data){
      callback(error ? error : data);
    });
  }
}

Deploy

With our blueprint and application ready to go, we can use Cloudsoft AMP to deploy the blueprint to a location of our choice. (For more information on setting up locations, see http://docs.cloudsoft.io/locations/index.html)

Once the deployed application is running we can select the NodeJS Application and look for the sensor main.uri to visit our running webapp.

Conclusion

We have built a minimal web application using the MEAN stack and deployed to the cloud using Cloudsoft AMP.  The basic Blueprint was extremely simple: two entities one for the NodeJS and one for the MongoDB server.  The web application itself was also simple (but a more complicated application would be deployed in the same way).  It displays items stored in on the MongoDB server and has a simple form for adding new items, using AngularJS for client-side rendering.  

Beyond deployment, Cloudsoft AMP allows us to monitor and manage both the Application and MongoDB server with its runtime view, which we touched upon when we looked for a particular sensor.  These capabilities are powerful, including effectors to (re)start the server, or if the entity supports it scale out - but that’s a subject for another day.

Help

Contact us for more information

Or join the Slack Channel: https://slack.cloudsoft.io