Deploying to Azure App Service with custom build script
My agency recently started working on a website built with React. Like most modern sites built using frameworks such as React or Angular, it leverages a Node-based build system. While we are not responsible for hosting the site, I needed to set up a test site. Most of our test sites are hosted using Azure App Services which includes convenient continuous deployment options to deploy from Git repositories hosted on Bitbucket and GitHub. The magic that makes this work is Kudu, an open source deployment engine developed by Microsoft.
The Kudu deployment process gets triggered by a webhook from the Git repo when code is pushed up to the configured branch. Kudu then pulls the updated code and runs a deployment script that copies the files over to the web root. For ASP.NET web application projects, it will actually build the code and then deploy it. For static sites, it just copies all the files over to the web root. For Node sites, it copies the files to the web root and then runs npm install. To copy code, it uses KuduSync, which uses manifest files to track which files need to be copied, so deployments of changes to large repos can run quickly.
Fortunately, with Kudu it is possible to customize your own deployment scripts which is what I needed in order to run the build script for the React website. I had a similar situation a couple years ago with an Angular site. With that project I never bothered to figure out how to have the deployment process run the build, so what we did was run the build locally and commit the built code into git and deploy from the build folder. While that worked, it was not ideal -- requiring manually steps and embiggening the git repo with unnecessary files. But it turns out to be fairly easy to modify the deployment script to run the build.
First, install kuduscript and run it to generate a node.js deployment script:
kuduscript -y --node
This generates deploy.cmd and .deployment files for the node template. The .deployment file is simply a config file for indicating to Kudu which script to run. In my case I opted to use an Azure AppService app setting instead. It wasn’t entirely clear to me from the Kudu documentation that this was possible as the example showed this only for the Project setting, but a quick test showed that it does indeed work for the Command property.
The deploy.cmd file is of course the deployment script that we want to customize. Note on a Mac or Linux machine, kuduscript will generate deploy.sh script instead of deploy.cmd. Even though my goal is to deploy a static site, I start with the node template because the build script uses node and the node template already had some node setup such as defining the node.exe path. The part that I needed to modify was the Deployment section. It had three steps:
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :: Deployment :: ---------- :Deployment echo Handling node.js deployment. :: 1. KuduSync IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" ( call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd" IF !ERRORLEVEL! NEQ 0 goto error ) :: 2. Select node version call :SelectNodeVersion :: 3. Install npm packages IF EXIST "%DEPLOYMENT_TARGET%\package.json" ( pushd "%DEPLOYMENT_TARGET%" call :ExecuteCmd !NPM_CMD! install --production IF !ERRORLEVEL! NEQ 0 goto error popd )
The first change I made was to change the destination path from %DEPLOYMENT_TARGET% to %DEPLOYMENT_TEMP% in steps 1 and 3. This allows me to run the build in %DEPLOYMENT_TEMP% and then copy the built code to %DEPLOYMENT_TARGET%. To do this, I add a new step to run my build script by calling npm run build:
:: 4. Build the website IF EXIST "%DEPLOYMENT_TEMP%\scripts\build.js" ( pushd "%DEPLOYMENT_TEMP%" echo "Building web site" call npm run build if !ERRORLEVEL! NEQ 0 goto error popd )
Similar to the code generated by kuduscript, I did this in a conditional check to make sure the build script exists. Next, I create a final step to copy the files from the build folder in %DEPLOYMENT_TEMP% to %DEPLOYMENT_TARGET%:
:: 5. KuduSync to DEPLOYMENT_TARGET echo "Syncing site to Deployment Target" call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_TEMP%\build" -t "%DEPLOYMENT_TARGET%" -x true -i ".git;.hg;.deployment;deploy.cmd" IF !ERRORLEVEL! NEQ 0 goto error
To get this to work I ended up running KuduSync set to ignore the manifest using the -x true option. A couple other adjustments to the original script (which I’m not 100% sure are needed) are that I removed the if conditional from step 1 since I alway want this to run and I also removed the --production flag in the npm install in step 3 based on a suggestion from a blog post on Continuous Deployment, NodeJS and Microsoft Azure.
As I mentioned earlier, I used an Azure AppService app setting to specify the path of my build script but you can accomplish the same using the .deployment file. The end result is whenever we push code to our repo, the deployment runs and builds the site from source. My complete deployment script is published at https://gist.github.com/alindgren/6a5ada02a11b668116b237f0f20193c2.