Skip to content

Instantly share code, notes, and snippets.

@mvalipour
Last active March 9, 2020 16:42
Show Gist options
  • Save mvalipour/75136095a164b57fc94fafebc34e0906 to your computer and use it in GitHub Desktop.
Save mvalipour/75136095a164b57fc94fafebc34e0906 to your computer and use it in GitHub Desktop.
Transpile and Deploy ES6 node app to Azure app service using TeamCity and Octopus

Background

Web have a node application written in ES6/babel which cannot (yet) be run by the latest stable node engine. Locally we use babel-node to run the application using babel's on-the-fly transpiler; However this is strongly discourages on the production environment (due to memory and performance footprints).

So we would need to transpile our application to the stable ECMA and deploy the artefacts to Azure -- instead of the original app.

Why not use kudu?

Although kudu provides the option for custom build script, kudu is a deployment tool and is not designed around the build pipeline, doing precompilations, etc. For example, you would immediately start realizing that your dev dependencies are unavailable in the kudu custom script. I spent hours trying to do this (wrong) thing with kudu and a very trivial task getting harder and harder.

TeamCity and Octopus

Luckily we already have a TeamCity build server and an Octopus deployment server so I decided to utilize those instead of using the "Deployment from source control" feature of Azure.

NOTE: You can use any other build server and deployment server of your choice -- as long as it is capable of deploying to Azure App Service or you are willing to script it yourself.

Build

The first step is to build the solution:

build/build.cmd
@echo off

echo installing npm modules
call npm install

echo cleaning artefacts
call npm run clean

echo running tests
call npm run test

echo compiling server and app
call npm run compile

echo build script finished.

In the above script, we first install all dependencies (including dev), clean the artefacts (to avoid cross-deployment file conflict), run tests and finally compile the app.

These npm commands are defined in the package.json:

{
  "devDependencies": {
    "babel-cli": "^6.18.0",
    "babel-core": "^6.18.2",
    "babel-preset-es2015": "^6.18.0",
    "babel-preset-react": "^6.16.0",
    "babel-preset-stage-0": "^6.16.0",
    "rimraf": "^2.5.4"
  },
  "scripts": {
    "server:start": "./node_modules/.bin/babel-node ./server",
    "server:watch": "nodemon --exec babel-node -- ./server",
    "test": "./node_modules/.bin/jest --coverage --verbose",
    "clean": "./node_modules/.bin/rimraf ./compiled ./coverage ./build/dist",
    "start": "node ./compiled/server",
    "compile": "./node_modules/.bin/babel ./app -d ./compiled"
  }
}

note that server:start and server:watch are used to run the application locally.

At this point we will have the application transpiled into the ./compiled directory.

Pack

Next step is to package up the compiled solution for the use on octopus.

You can skip this section if you would be deploying using FTP or through other means. But whatever you do the aim is to deploy the content (as proposed in this section) to the /wwwroot directory of the app service in Azure.

The package to be deployed will include everything that is needed to run the application. Here we create a .nuspec file to define our package structure:

app.nuspec
<?xml version="1.0"?>
<package >
  <metadata>
    <id>My.App</id>
    <version>0.0.1</version>
    <authors>Me!</authors>
    <description>My App!</description>
  </metadata>
  <files>
    <file src="compiled\**" target="compiled" />
    <file src="node_modules\**" target="node_modules" />
    <file src="package.json" target="\" />
    <file src="azure\app.js" target="\" />
    <file src="azure\web.config" target="\" />
  </files>
</package>

Then we will use the nuget cli to create a package and at the same time create a release using the package on octopus:

build/pack.ps1
Param(
  [string]$buildNumber = "0",
  [string]$octoServer,
  [string]$octoSecret,
  [string]$octoProject
)
$version = "$(Get-Content .\.version).$buildNumber"
$packagePath = ".\dist\My.App.$version.nupkg"
Write-Host "Version is: $version"
Write-Host "Package is: $packagePath"

Write-Host "Building..."
cd .\build
& .\build.cmd

Write-Host "Packing..."
if(-not (Test-Path .\dist)) {
  mkdir .\dist | Out-Null
}
& .\nuget.exe pack ..\app.nuspec -BasePath ..\ -OutputDirectory .\dist -Version $version

Write-Host "Uploading package..."
& .\nuget.exe push $packagePath -Source $octoServer/nuget/packages -ApiKey $octoSecret

Write-Host "Creating release..."
& .\octo.exe  create-release --server $octoServer --apikey $octoSecret --project $octoProject --enableservicemessages --version $version --packageversion $version

A few points here:

  • We use a .version file to keep the current semantic version of the app (e.g. 1.2.0). Then append a build number at the end for uniqueness.
  • We use the octopus built-in nuget repository.

Deploy

In your octopus server: add a project with a single step of type "Deploy as Azure Web App" with just the name of the package and your Azure app service details. -- This took me 1 minute to setup.

Azure Node

All azure app services come bundled with IIS that has iisnode installed. This makes it super easy to run node apps. All you need, is to include a web.config in the root of your app to instruct IIS to serve up the incoming traffic using the node engine.

azure/web.confg
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="iisnode" path="app.js" verb="*" modules="iisnode"/>
    </handlers>
    <rewrite>
      <rules>
          <rule name="DynamicContent">
               <match url="/*" />
               <action type="Rewrite" url="app.js"/>
          </rule>
     </rules>
    </rewrite>
  </system.webServer>
</configuration>

Another gotcha (which took me a few hours to figure out) is that some inner parts of my node were not functioning correctly (with relative paths) when the node app was not started from the root directory. For this reason and better conciseness, I've included an app.js as the entry point of the node application which just simply hooks into the actual app entry point:

azure/app.js
require('./compiled/server');

Conclusion

Although all of this seem too much just to run a node app on Azure, it certainly is a step in the right direction if your application is planned to grow big in size and complexity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment