TL/DR - Use Azure Devops (or your preferred build platform) to include the Node executable it uses to build your app inside the zip package used to deploy your Azure Functions application.

Azure Functions (AF) presents a compelling sales pitch. They've built a platform where you can write code that they'll run. They'll handle the scaling for you, and on their consumption plan, you only pay for the actual server time you use. This can drastically reduce costs for running your applications in the cloud. As long as you are able to work within the confines of the platform, everything is awesome. If you're a .NET/C# developer, AF presents a compelling platform.

Unfortunately, the picture isn't currently (April 2019) quite as rosy for developers who wish to use other programming languages/platforms. The primary Azure Functions product/platform is built on top of the App Service platform, which presents some issues for non .NET developers. In particular, you are tied to the exact version of your programming platform's runtime that the AF team ships. I ran in to this issue recently while building a NodeJS-based Azure Functions application for a conference talk. Let's look at how I solved my particular issue.

NodeJS iterates quickly. The App Services team tries to keep up, but they have to lock to supporting certain versions of Node for a while. As I write this, the App Service environment my AF application runs in uses Node v8.11.1. It's important for my issue to note that it uses the 32-bit (x86) build of Node. This is a problem for me as my demo has a dependency on a package that only support 64-bit (x64) builds of Node. Also, I'd prefer to be on the latest LTS release, which is 10.x as of today. The package I'm using that requires 64-bit Node is called Sharp. This is an image manipulation library that uses native libraries to do the actual image manipulation. This makes resizing images (my use case) MUCH faster than using a JavaScript based package to do this.

Unfortunately, I developed my application locally never realizing there was going to be an issue, as the local development environment for Azure Functions uses your locally installed copy of NodeJS. I run on a 64-bit Windows 10 (and I would guess that most Windows developers in 2019 run on 64-bit builds of Windows), and thus I have the 64-bit NodeJS build installed. When I finished developing my demo I tried to push it to Azure Functions using the simple Github based deployment model, where the AF platform handles running npm install for me. I was flummoxed when npm install kept failing saying a dependency required a 64-bit version of Node. After some research, I realized that Sharp was the dependency that was the issue.

I then tried to figure out how to switch to a 64 bit Node build. The Azure Functions UI on the Azure Portal provides a "Platform" switch to switch between 32-bit and 64-bit. I tried switching that to 64-bit, but it didn't change the bitness of Node. At this point, I decided to file an issue on the Azure Functions team Github repo. This was in December. I also decided to re-write my demo to use a JavaScript based package called JIMP to handle resizing images for my demo. This caused serious performance issues for me, but I didn't have much choice.

Fast-forward to this spring. Marie Hoeger from the AF team had worked with me to come up with some solutions on how to bring my own Node executable, but I didn't really have much success with these tips until she and I worked side-by-side on the issue at the MVP Summit in March. This was in part due to the instructions not being 100% clear for me, and also due to an unrelated issue causing me to go on a side-track with my app for a while. The eventual solution we used was what she proposed in the GitHub issue thread on February 19.

Specifically, these are the steps:

  • In Kudu, which will be at [sitename], go to Debug Console.
  • Drop the node.exe you want to use in D:\home (can be in root, can be nested)
  • Take note of the path (ex: D:\home\node)
  • Add the AppSetting languageWorkers:node:defaultExecutablePath and set it to the path (from before) to your node.exe (ex: D:\home\node)

Note that you have to remove the .exe from executable name when writing it to the Appsetting. So D:\home\node.exe become D:\home\node.

So you can use that, but that means you have to deploy your node.exe file by hand. So there has to be a better way. While we were trying to figure out the unrelated issue (which turned out to be an errant app_offline.html file), Marie suggested I copy node.exe to the folder with my function app and push that to GitHub. We could then point to that executable. At the time, this didn't work due to the app_offline.html file, so we gave up on it. When we got everything working, we were using the exact strategy she had suggested in February, so the suggestion was forgotten for a few weeks.

But yesterday, I was playing with Azure Devops trying to get my custom Devops build (YAML-based, of course!) for my application working. The first thing I do in my build is to install Node 10.x

- task: NodeTool@0
  displayName: 'Use Node 10.x'
    versionSpec: 10.x

In AzureDevops, this will pull down the newest x64 build of Node for the versionSpec specified. More information is located here.

At some point, the thought struck me... I often use the which command in Bash to determine where the executable is coming from. Why couldn't I use that command to find the Node executable and copy it from wherever it is installed on the build agent to the folder with my application? This would allow me to always use the exact same executable for installing dependencies and running my application. This is important, as I've found that sometimes Sharp can be temperamental about being installed using a different major version of Node from what it is running. It would also remove the manual process of deploying the Node executable in Kudu.

I updated the languageWorkers:node:defaultExecutablePath Appsetting to D:\home\site\wwwroot\node as my Node executable would now be located at D:\home\site\wwwroot\node.exe.

I then added a Bash step to my build definition, making sure this command is before the Archive Files step:

- bash: 'cp "$(which node.exe)" .'
  workingDirectory: 'function-app'
  displayName: 'Copy node.exe for deployment'

In my case, my function app is not located at the root of my Git repo, but in a folder called function-app, this is why the working directory for the command must be set.

I then ran my build and deployed it using Azure Devops. I'm now able to use a 64-bit version of Node with my application, and I ensure that it is the exact same version of Node that was used to install my dependencies. I believe this pattern will work for any platform where you want to use a specific version of your platform's executable, or at least override the default version that the Azure Functions platform uses. You would need to change the languageWorkers:[YOUR_LANGUAGE_HERE]:defaultExecutablePath appsetting to match whatever language worker you are using, though.

I hope this can help others with similar situations.