Infrastructure as Code and Continuous Deployment of a Node.js + Azure DocumentDB Solution

This post covers the following DevOps practices:

  • Infrastructure as Code
  • Continuous Deployment

I created an Azure Resource Manager template, and modified the sample Node.js solution from the Azure team to include the instructions from the Visual Studio Team Services team for creating builds with Node.js for Azure.

The changes are in my sample on GitHub

Infrastructure as Code - Web App and Document DB in an Azure Resource Manager Template

You can use Azure Resource Manager (ARM) templates to declaratively describe the infrastructure behind your solution. Azure DocumentDB has recently been added to the list of resources supported by ARM, so now you can use ARM to define your DocumentDB account as well.

My sample DocumentDB + Web App ARM template is on GitHub. Here's how I declared the DocumentDB account:

{
      "apiVersion": "2015-04-08",
      "type": "Microsoft.DocumentDb/databaseAccounts",
      "name": "[variables('databaseAccountName')]", 
      "location": "[resourceGroup().location]", 
      "properties": {
          "name": "[variables('databaseAccountName')]",
          "databaseAccountOfferType":  "Standard"
      }
}

I also reference the DocumentDB account endpoint and key info from the Web App resource, linking back to the DocumentDB account endpoint and key: 

"appSettings":  [
              {
                "Name":  "DOCUMENTDB_ENDPOINT",
                "Value":  "[reference(concat('Microsoft.DocumentDb/databaseAccounts/', variables('databaseAccountName'))).documentEndpoint]"
              },
              {
                  "Name":  "DOCUMENTDB_PRIMARY_KEY",
                  "Value":  "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', variables('databaseAccountName')), '2015-04-08').primaryMasterKey]"
              }
          ]

With these details my ARM template defines:

  • A DocumentDB account
  • An App Hosting Plan
  • A Web App in the Hosting Plan, with app settings pointing to the DocumentDB account
  • Three parameters (only projectName is required)
  • Three variables (they use projectName to create names for the site, hosting plan, and DocumentDB account)

Here's the outline of the json file:

You can deploy this template using the Azure CLI or Powershell, but I wanted to include it in my continuous deployment workflow, so read on!

Continuous Deployment - Visual Studio Team services Build Definition

My Build Definition in Visual Studio Team Services first has an Azure Resource Group Deployment task. Note that Template and Template Parameters are set to the ARM template and parameters files described above:

The next task is npm install. This looks for package.json files in your code and installs all the npm modules defined there:

Here's the package.json from my sample, note gulp and gulp-zip as part of devDependencies: 

  "dependencies": {
    "express": "^4.13.3",
    "async": "^0.9.0",
    "body-parser": "~1.10.2",
    "cookie-parser": "~1.3.3",
    "documentdb": "^0.9.3",
    "jade": "~1.9.1"
  },
  "devDependencies": {
    "gulp": "^3.9.0",
    "gulp-zip": "^3.0.2"
  }

Next I have a Gulp step and I point it at the gulpfile.js file that's at the root of my code:

Screen Shot 2015-11-12 at 4.48.05 PM.png

This gulpfile is important as it creates a zip file with the contents our node site. Note in the code how I'm creating a file called Package.zip and use process.env.BUILD_STAGINGDIRECTORY to make sure the zip file is put on the Build agent's staging directory:

var gulp = require('gulp');
var path = require('path');
var zip = require('gulp-zip');
var fs = require('fs');
var packagename = "Package.zip";

gulp.task('default', function () {

    var packagePaths = ['**', 
                    '!**/_package/**', 
                    '!**/typings/**',
                    '!typings', 
                    '!_package', 
                    '!gulpfile.js']
    
    //add exclusion patterns for all dev dependencies
    var packageJSON = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
    var devDeps = packageJSON.devDependencies;

    for(var propName in devDeps)
    {
        var excludePattern1 = "!**/node_modules/" + propName + "/**";
        var excludePattern2 = "!**/node_modules/" + propName;
        packagePaths.push(excludePattern1);
        packagePaths.push(excludePattern2);
    }

    var stagingpath = process.env.BUILD_STAGINGDIRECTORY;
    
    console.log("Creating file " + stagingpath + "\\" + packagename);
    
        return gulp.src(packagePaths)
            .pipe(zip(packagename))
            .pipe(gulp.dest(stagingpath));
});

And finally I have the Azure Web App Deployment step. Note how I configured it to look for $(Build.StagingDirectory)/package.zip as that's the zip file created by the previous task:

After this I can kick off builds manually or via continuous integration that will make sure my infrastructure is as I defined, restores npm packages, creates the zip file, and deploys the changes to the Azure Web App created by the first step:

The first time your build runs it will take longer as it is running the full ARM deployment and creating the DocumentDB account, Hosting Plan and Web App. Currently around 10 minute for me on the hosted controller provided by the Build system. Unless you change the ARM template or make changes to your resource group on Azure the build should go much faster, around 3 minutes for me currently.

This is what the resource group looks like after it is all deployed:

Screen Shot 2015-11-12 at 5.11.46 PM.png

And finally, browsing to the web app will show the ToDo List sample:

The first time the site is run it will also create the DocumentDB database, and subsequently it will use that database. 

Closing Notes

This post described a simple example of infrastructure as code, continuous integration, and continuous deployment of a Node.js and DocumentDB sample using Visual Studio Team Services. Hopefully helps you and your team automate your solution as well.

The full source code is on GitHub.