Azure Web Sites Git Deploy For Multiple Project Solutions

If you are trying to Git deploy a solution with multiple web projects to multiple Windows Azure websites, you will find that it does not work out of the box. This post describes a simple way to workaround this omission by creating a custom deployment script. Don't worry though - it is dead simple.

_ UPDATE: Kudu has been updated since the publication of this post. It is not longer necessary to modify the deployment script to cope with solutions with multiple projects. See the docs for more information. _

With the introduction of ASP.NET Web API, it is now a very common requirement to want to add an API to your website. Most examples add Web API code into the same Visual Studio Project as your main website but if you are creating an API for public consumption, you probably do not want to take this approach. Instead, you might want a solution containing at least three projects: The Website, the Web API and a common library that is used by the other two projects. This is all very simple to set up within Visual Studio but if you want to Git deploy your projects to Windows Azure Websites (WAWS), things get a little bit more complicated.

Git deployment to Azure is fantastic, particularly if you are integrating your source control repo into the mix. Commit your code to GitHub, Bitbucket (or many others) and have azure automatically deploy the latest version. All very nice and with a single web application in your solution, this works seamlessly. Once we add a second web project to the solution for the API, things get interesting. To put it simply, out of the box, you will not be able to deploy both projects to Azure successfully. The root of the problem is that is is not possible (via the Azure portal) to tell an Azure website which project within the solution to deploy. Attempting to configure multiple websites to the same Git repository will deploy the first web project in the solution to both websites.

Fortunately, as is often the case, it is relatively easy to workaround these limitations.

One simple approach is to utilise the fact that you can tell azure which project to use via a .deployment file in the root of your repo.

[config]
project = MyProject.Api/MyProject.Api.csproj

If you use different branches for each website, you can change this file in each branch to specify which project to use.

Whilst this works, it is not an ideal solution and I do not want to create separate branches for something as simple as this. Therefore, we are going to take another approach which involves using a custom deployment script.

You can get a lot more information from a great blog series by Amit Apple but for the sake of clarity, we will shortcut the process and just detail the files necessary to get it working.

In essence, we are just extending the above approach using the .deployment file. This time, instead of just specifying a project to use, we change the .deployment file to indicate that we want to customise the deploy process and use our own deployment script:

[config]
command = deploy.cmd

The batch file that gets called needs to contain conditional logic to use the correct project for each azure website. In order to do this, we can utilise the fact that any app settings created via the azure portal will be available as environment variables within our script. So, let's go into the Azure portal and create a new key:

Windows Azure Web Sites App Settings

Now, it would be nice if we could just pass the appropriate project name to the default deployment script but I haven't found any way of dynamically doing this. Instead we are providing our own deployment script in place of the default:

@echo off

:: ----------------------
:: KUDU Deployment Script
:: ----------------------

:: 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_COMMAND (
  :: 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_COMMAND=node "%appdata%\npm\node_modules\kuduSync\bin\kuduSync"
)
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 NOT DEFINED MSBUILD_PATH (
  SET MSBUILD_PATH=%WINDIR%\Microsoft.NET\Framework\v4.0.30319\msbuild.exe
)

IF NOT DEFINED PROJECT_PATH (
  echo Missing PROJECT_PATH app setting. Please configure in Azure portal and redeploy.
  goto error
)

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

echo Handling .NET Web Application deployment.

:: 1. Build to the temporary path
%MSBUILD_PATH% "%DEPLOYMENT_SOURCE%\%PROJECT_PATH%" /nologo /verbosity:m /t:pipelinePreDeployCopyAllFilesToOneFolder /p:_PackageTempDir="%DEPLOYMENT_TEMP%";AutoParameterizationWebConfigConnectionStrings=false;Configuration=Release
IF !ERRORLEVEL! NEQ 0 goto error

:: 2. KuduSync
echo Kudu Sync from "%DEPLOYMENT_TEMP%" to "%DEPLOYMENT_TARGET%"
call %KUDU_SYNC_COMMAND% -q -f "%DEPLOYMENT_TEMP%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.deployment;deploy.cmd" 2>nul
IF !ERRORLEVEL! NEQ 0 goto error

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

goto end

:error
echo An error has occurred during web site deployment.
exit /b 1

:end
echo Finished successfully.

Yes, this is quite a lot of code but rest assured that I did not just make it up. This is the default deployment script with a couple of tweaks. If you want to confirm this, you can generate it yourself using the Deployment Script Generator that is part of azure-cli (NB Node.js must be installed to use this tool). You can find out more about this tool and the deployment process in general on the Project Kudu GitHub Wiki.

In terms of changes to the default script, all we are doing is making sure that the environment variable is available and if so, using it in the MSBuild call. It would be nice to allow just the folder name to work rather than the full relative path of the csproj file but I do not know enough about MSBuild and deployments in general to be able to do this. It might also be useful to revert to the default deploy behaviour (picking the first web project in the solution) if the environment variable is not found but again, do not know how to do this.

Once you have created these two files (.deployment and deploy.cmd) and checked them in to your repo (at the root), provided that you have added the correct appsettings, you should find that both your websites are updated correctly. All subsequent commits will deploy both sites.

Comments

Avatar for Dave Dave wrote on 01 May 2013

Nice post. You can also integrate unit tests into deploy script. see http://blog.maartenballiauw.be/post/2013/03/26/Running-unit-tests-when-deploying-ASPNET-to-Windows-Azure-Web-Sites.aspx