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.
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.
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.
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.
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.
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.
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');
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.