Let Kudu change your API endpoints in Angular when deploying to Azure Webs with Gulp

The problem

Have you ever manually changed the API endpoint in your JavaScript from https://localhost:23434 to https://prodenv.azurewebsites.net before pushing that deploy button? Have you ever forgotten to do this before deploying, resulting in a broken app?

Relying on manual steps in your deployment process is an accident waiting to happen. The example where Knight got a $460 million loss in 45-minutes due to a faulty manual deployment comes to mind. Hopefully a misconfigured API endpoint in an angular app won't be as drastic, but the point is still valid. Having a repeatable process makes things easier.

What I like to do instead of making updates in my JavaScript code by hand is to type git push production and sit back and watch my site deploy with the correct endpoints.

Enter gulp

In a recent project I used Gulp to achieve a workflow that automatically updated the environment variables my angular app needs depending on my environment. Dev variables in dev and prod variables in production.

Gulp is a tool for automating tasks in your build process. It can handle things like minifying your js and css, make your running browser automatically reflect changes in real time or in our case, generate environment variables at deployment time.

One of the really great things about gulp is the fact that it has a tons of different plugins for different problems that you can add.

Gulp-ng-constant is a library for handling the task of setting constants in your JavaScript. When doing normal backend ASP.NET development you could use things like Azure web app settings/web.config and use the CloudConfigurationManager class to read environment variables.

But it's not really optimal to expose the azure app settings via clientside technologies. So what we have to do is make sure that the constants are set at deployment/build time.

gulpfile.js

The first thing we need is a gulpfile to decsribe the tasks that we are going to use to do our transformations. We describe two commands: dev-config that visual studio runs before each build during development and prod-config which runs at deployment time.

var gulp = require('gulp');  
var ngConstant = require('gulp-ng-constant');

gulp.task('dev-config', function () {  
    gulp.src('AppRead/constants.json')
      .pipe(ngConstant({
          name: 'myAngularModule.config',
          deps: [],
          constants: {
              ENV: {
                  apiEndpoint: '../../api',
                  tenant: "tenant.onmicrosoft.com",
                  sharepointUrl: "mydevtenant.sharepoint.com",
                  targetLibraryName: "Dev library",
                  appId: "6ce673fb-4827-40f3-af5d-bcaddac5f76a"
              }
          },
      }))
      .pipe(gulp.dest('dist'));
});

gulp.task('prod-config', function () {  
    gulp.src('AppRead/constants.json')
      .pipe(ngConstant({
          name: 'myAngularModule.config',
          deps: [],
          constants: {
              ENV: {
                  apiEndpoint: 'https://azureweb.azurewebsites.net/api',
                  tenant: "tenant.onmicrosoft.com",
                  sharepointUrl: "myprodtenant.sharepoint.com",
                  targetLibraryName: "Records",
                  appId: "dd952be6-090b-4350-8697-8181a1ff5b1d"
              }
          }
      }))
      .pipe(gulp.dest('dist'));
})

Notice that we are using a constants.json file for other, non-dynamic constants that we use in our app and that we use some simple gulp code to handle the dynamic variables.

constants.json:

{
    "appName": "My Awesome App"
}

In order to be able to run the code we just wrote we need to install gulp and gulp-ng-constant to our project directory.

npm install gulp --save-dev  
npm install gulp-ng-constant --save-dev  

The prod-config and dev-config commands in the gulpfile generates a dist/constants.js which we include when loading our app. It gives us a nice new angular module that contains our environment variables.

angular.module("myAngularModule.config", [])  
.constant("ENV", {
    "apiEndpoint": "../../api",
    "tenant": "tenant.onmicrosoft.com",
    "sharepointUrl": "mydevtenant.sharepoint.com",
    "targetLibraryName": "Dev library",
    "appId": "6ce673fb-4827-40f3-af5d-bcaddac5f76a"
})
;

app.js

In order for our main module to use it we need to declare the module as a dependency of our base app module.

(function(){
  'use strict';
  var myAngularModule = angular.module('myAngularModule ', [
    'ngRoute',
    'AdalAngular',
    'myAngularModule.config'
  ]);
})();

And seeing as adal is more or less a requirement for all dev that we do nowadays I thought it would be nice to show how we use the constants for setting up the required endpoints.

adalconfig.js

I'm not going to cover how adal.js works seeing as it's described in the github repo's readme. For our case all we need to do is include our 'ENV' dedepency.

(function () {
  'use strict';

  var myAngularModule = angular.module('myAngularModule');

  myAngularModule.config(['$httpProvider', 'ENV', 'adalAuthenticationServiceProvider', adalConfigurator]);

  function adalConfigurator($httpProvider, ENV, adalProvider) {
      var adalConfig = {
          tenant: ENV.tenant,
          clientId: ENV.appId,
          endpoints: {
             'https://outlook.office365.com':'https://outlook.office365.com'
          },
          cacheLocation: 'localStorage'
      };

      adalConfig["endpoints"]["https://" + ENV.sharepointUrl + "/_api/"] = "https://" + ENV.sharepointUrl;

      adalProvider.init(adalConfig, $httpProvider);
  }
})();

data-service.js

Now you probably have your code that actually uses the endpoint we configured earlier in angular services. Notice that all we need to do is include the 'ENV' dep to get the ENV object which contains our constants in the same way as we did with our service above.

(function () {
    'use strict';

    angular.module('myAngularModule')
           .service('dataService', ['$q', '$http', 'ENV', dataService]);

    function dataService($q, $http, ENV) {

        return {
            getSomething: getSomething
        };

        function getSomething(dataBlob) {
            var deferred = $q.defer();

            $http({
                method: 'POST',
                url: ENV.apiEndpoint + "/Controller/Action",
                data: dataBlob
            }).then(function (r) {
                deferred.resolve(r);
            }, function (err) {
                deferred.reject(err);
            });

            return deferred.promise;
        }
    }
})();

Kudu

In order for Kudu to use our gulp actions we first need to pull down and modify the kudu deployment file.

We do this by going to https://azurewebsitename.scm.azurewebsites.net and downloading the script.

This will give us a deploy.cmd and a .deployment file. You need to put these in the base directory of your git repository. Kudu will then see these when you deploy and use them instead of the ones already present/generated by Kudu.

Updating deploy.cmd and .deployment

First let's tell Kudu about our deploy.cmd file by editing the .deployment we just downloaded.

[config]
command = deploy.cmd  

Now let's move on to the depoy.cmd. I decided to break the file up in a few different paragraphs here so that you can have the complete file as a reference.
The first part of the file we don't have to change, it only sets things up that are required for us to keep doing magic later on.

@if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off

:: ----------------------
:: KUDU Deployment Script
:: Version: 1.0.4
:: ----------------------

:: Prerequisites
:: -------------

:: Verify node.js installed
where node 2>nul >nul  
IF %ERRORLEVEL% NEQ 0 (  
  echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment.
  goto error
)

:: Setup
:: -----

setlocal enabledelayedexpansion

SET ARTIFACTS=%~dp0%..\artifacts

IF NOT DEFINED DEPLOYMENT_SOURCE (  
  SET DEPLOYMENT_SOURCE=%~dp0%.
)

IF NOT DEFINED DEPLOYMENT_TARGET (  
  SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot
)

IF NOT DEFINED NEXT_MANIFEST_PATH (  
  SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest

  IF NOT DEFINED PREVIOUS_MANIFEST_PATH (
    SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest
  )
)

IF NOT DEFINED KUDU_SYNC_CMD (  
  :: Install kudu sync
  echo Installing Kudu Sync
  call npm install kudusync -g --silent
  IF !ERRORLEVEL! NEQ 0 goto error

  :: Locally just running "kuduSync" would also work
  SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd
)
IF NOT DEFINED DEPLOYMENT_TEMP (  
  SET DEPLOYMENT_TEMP=%temp%\___deployTemp%random%
  SET CLEAN_LOCAL_DEPLOYMENT_TEMP=true
)

IF DEFINED CLEAN_LOCAL_DEPLOYMENT_TEMP (  
  IF EXIST "%DEPLOYMENT_TEMP%" rd /s /q "%DEPLOYMENT_TEMP%"
  mkdir "%DEPLOYMENT_TEMP%"
)

IF DEFINED MSBUILD_PATH goto MsbuildPathDefined  
SET MSBUILD_PATH=%ProgramFiles(x86)%\MSBuild\14.0\Bin\MSBuild.exe  
:MsbuildPathDefined

Now we must update the deployment part of the script to actually perform our transformations. In order to do this we utilize the fact that the repo is cloned to one location and then later synced to the wwwroot. If we apply our transformations before the filesync happens we can do all sorts of nice things, like running gulp commands.

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Deployment
:: ----------

echo Handling .NET Web Application deployment.

echo Restore NuGet packages  
IF /I "MyProject.sln" NEQ "" (  
  call :ExecuteCmd nuget restore "%DEPLOYMENT_SOURCE%\MyProject.sln"
  IF !ERRORLEVEL! NEQ 0 goto error
)

echo Install npm packages  
IF EXIST "%DEPLOYMENT_SOURCE%\MyProjectWeb\package.json" (  
  pushd "%DEPLOYMENT_SOURCE%\MyProjectWeb"
  call npm install
  IF !ERRORLEVEL! NEQ 0 goto error
  popd
)

echo Install bower packages  
IF EXIST "%DEPLOYMENT_SOURCE%\MyProjectWeb\bower.json" (  
  pushd "%DEPLOYMENT_SOURCE%\MyProjectWeb"
  call :ExecuteCmd .\node_modules\.bin\bower install
  IF !ERRORLEVEL! NEQ 0 goto error
  popd
)

echo Run gulp transformations  
IF EXIST "%DEPLOYMENT_SOURCE%\MyProjectWeb\gulpfile.js" (  
  pushd "%DEPLOYMENT_SOURCE%\MyProjectWeb"
  call :ExecuteCmd .\node_modules\.bin\gulp prod-config
  IF !ERRORLEVEL! NEQ 0 goto error
  popd
)

In the code above we make sure all packages are installed in our %DEPLOYMENT_SOURCE% and then the magic happens in the line where we call gulp prod-config.

The rest of the deployment file is listed below as a reference.

echo Build to the temporary path  
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (  
  call :ExecuteCmd "%MSBUILD_PATH%" "%DEPLOYMENT_SOURCE%\MyProjectWeb\MyProjectWeb.csproj" /nologo /verbosity:m /t:Build /t:pipelinePreDeployCopyAllFilesToOneFolder /p:_PackageTempDir="%DEPLOYMENT_TEMP%";AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release;UseSharedCompilation=false /p:SolutionDir="%DEPLOYMENT_SOURCE%\.\\" %SCM_BUILD_ARGS%
) ELSE (
  call :ExecuteCmd "%MSBUILD_PATH%" "%DEPLOYMENT_SOURCE%\MyProjectWeb\MyProjectWeb.csproj" /nologo /verbosity:m /t:Build /p:AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release;UseSharedCompilation=false /p:SolutionDir="%DEPLOYMENT_SOURCE%\.\\" %SCM_BUILD_ARGS%
)

IF !ERRORLEVEL! NEQ 0 goto error

echo KuduSync  
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (  
  call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_TEMP%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd"
  IF !ERRORLEVEL! NEQ 0 goto error
)



::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

:: Post deployment stub
IF DEFINED POST_DEPLOYMENT_ACTION call "%POST_DEPLOYMENT_ACTION%"  
IF !ERRORLEVEL! NEQ 0 goto error

goto end

:: Execute command routine that will echo out when error
:ExecuteCmd
setlocal  
set _CMD_=%*  
call %_CMD_%  
if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_%  
exit /b %ERRORLEVEL%

:error
endlocal  
echo An error has occurred during web site deployment.  
call :exitSetErrorLevel  
call :exitFromFunction 2>nul

:exitSetErrorLevel
exit /b 1

:exitFromFunction
()

:end
endlocal  
echo Finished successfully.  

Now when we git push production everything is handled for us without ever having to do the dreaded manual replacement of the API endpoints. Notice how it copies the dist/constants.js at the end.

Counting objects: 4, done.  
Delta compression using up to 8 threads.  
Compressing objects: 100% (2/2), done.  
Writing objects: 100% (3/3), 314 bytes | 0 bytes/s, done.  
Total 3 (delta 1), reused 0 (delta 0)  
remote: Updating branch 'master'.  
remote: Updating submodules.  
remote: Preparing deployment for commit id 'e35be64bcd'.  
remote: Running custom deployment command...  
remote: Running deployment command...  
remote: Handling .NET Web Application deployment.  
remote: Restore NuGet packages  
remote: All packages listed in packages.config are already installed.  
remote: Install npm packages  
remote: ...............................  
remote: Install bower packages  
remote: ...................................  
remote: Run gulp transformations  
remote: .................  
remote: [15:24:36] Using gulpfile D:\home\site\repository\MyProject\gulpfile.js  
remote: [15:24:36] Starting 'prod-config'...  
remote: [15:24:36] Finished 'prod-config' after 47 ms  
remote: Build to the temporary path  
remote: .......  
remote:   MyProjectWeb -> D:\home\site\repository\MyProject\bin\MyProject.dll  
remote:   Transformed Web.config using D:\home\site\repository\MyProject\Web.Release.config into obj\Release\TransformWebConfig\transformed\Web.config.  
remote:   Copying all files to temporary location below for package/publish:  
remote:   D:\local\Temp\8d303d14772064c.  
remote: KuduSync  
remote: KuduSync.NET from: 'D:\local\Temp\8d303d14772064c' to: 'D:\home\site\wwwroot'  
remote: Copying file: 'bin\MyProject.dll'  
remote: Copying file: 'dist\constants.js'  
remote: Finished successfully.  
remote: Deployment successful.  
To https://user@mysite.scm.azurewebsites.net:443/MyProject.git  
   d2150b0..e35be64  master -> master

Hope this helps and reduces future failed deployments in your future.

As usual, let me know in the comment section if you have any comments or ideas.