Using ASP.NET Core with Azure Cloud Services
Overview
Cloud Services may be the old-timer of Azure’s offerings, but there are still some cases where it is useful. For example, today, it is the only available PaaS way to run a Windows Server 2016 workload in Azure. Sure, you can run a Windows Container with Azure Container Services, but that’s not really PaaS to me. You still have to be fully aware of Kubernetes, DC/OS, or Swarm, and, as with any container, you are responsible for patching the underlying OS image with security updates.
In developing my Code Signing Service, I stumbled upon a hard dependency on Server 2016. The API I needed to Authenticode sign a file using Azure Key Vault’s signing methods only exists in that version of Windows. That meant that using Azure App Services was out, as it uses Server 2012 (based on the version numbers from its command line). That left Cloud Service Web Roles as the sole remaining option if I wanted PaaS. I could have also used a B-Series VM, that’s perfect for this type of workload, but I really don’t want to maintain a VM.
If you have tried to use ASP.NET Core with a Cloud Service Web Role, you’ll probably have come away disappointed as Visual Studio doesn’t let you do this…. until now. Never one to accept no for an answer, I found a way to make this work, and with a few workarounds, you can too.
The solution presented here handles deployment of an MVC & API application that along with config settings and deployment of the ASP.NET Core Windows Hosting Module. VS Cloud Service tooling works for making changes to config and publishing to cloud services (though please use CI/CD in VSTS!)
Many thanks to Scott Hunter‘s team, Jaques Eloff and Catherine Wang in particular, on figuring out a workaround for some gotcha’s when installing the Windows Hosting Module.
Pieces to the puzzle
You can see the sample solution here, and it may be helpful to clone and follow along in VS.
There are a few pieces to making this work:
- TheWebsite The ASP.NET Core MVC site. Nothing significantly special here, just an ordinary site.
- TheCloudService The Cloud Service project. Contains the configuration files and service definition.
- TheWebRole ASP.NET 4.6 project that contains the Web Role startup scripts and “references” the
TheWebsite
site. This is where the tricks are.
At a high level, the Cloud Service “sees” TheWebRole
as the configured website. The cloud service doesn’t know anything about ASP.NET Core. The trick is to get the ASP.NET Core site published and running “in” an ASP.NET site.
Doing this yourself
The Projects
In a new solution, create a new ASP.NET Core 2 project. Doesn’t really matter what template you use. For the descriptions here, I’ll call it TheWebsite
. Build and run the site, it should debug and run normally in IISExpress.
Next, create a new Cloud Service (File -> Add -> New Project -> Cloud -> Azure Cloud Service
). I’ll call the cloud service TheCloudService
, and on the next dialog, add a single Web Site. I called mine TheWebRole
.
Finally, on the ASP.NET Template selection, choose “Empty” and continue.
Right now, we have an ASP.NET Core Website and an Azure Cloud Service with a single ASP.NET 4.6 WebRole. Next up is to clear out almost everything from TheWebRole
since it won’t actually contain any ASP.NET Code. Delete the packages.config
and Web.config
files.
Save the project, then select “Unload” from the project’s context menu. Right-click again and select “Edit TheWebRole.csproj”. We need to delete the packages brought in by NuGet along with the imported props and target. There are three areas to delete as noted in the screen shots: Props at the top, all Reference
elements with a HintPath
pointing to ..\packages\
and the Target
at the bottom.
At this point, your project file should look similar to this here. You can also view the complete diff.
Magic
Now comes the special sauce — we need a way to have TheWebRole
build TheWebsite
and include TheWebsite
‘s publish output as Content
. Doing this ensures that TheCloudService
Package contains the correct folder layout. Add the following snippet to the bottom of TheWebRole
‘s project file to call Publish
on our website before the main build step.
<Target Name="BeforeBuild">
<MSBuild Projects="..\TheWebsite\TheWebsite.csproj" Targets="Publish" Properties="Configuration=$(Configuration)" />
</Target>
Then, add the following ItemGroup
to include TheWebsite
‘s publish output as Content
in the TheWebRole
project:
<ItemGroup>
<Content Include="..\TheWebsite\bin\$(Configuration)\netcoreapp2.0\publish\**\*.*" Link="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
Save the csproj file, then right-click the TheWebRole
and click Reload. You can test that the cloud service package is created correctly by right-clicking TheCloudService
and selecting Package
. After choosing a build configuration and hitting “Package,” the project should build and the output directory pop up.
The .cspkg
is really a zip file, so extract it and you’ll see the guts of cloud service packages. Look for the .cssx
file and extract that (again, just a zip file)
Inside there, open the approot
folder and that is the root of your website. If the previous steps were done correctly, you should see something like the following
You should see TheWebsite.dll
, TheWebsite.PrecompiledViews.dll
, wwwroot
, and the rest of your files from TheWebsite
.
Congratulations, you’ve now created a cloud service that packages up and deploys an ASP.NET Core website! This alone won’t let the site run though since the Cloud Service images don’t include the Windows Hosting Module.
Installing .NET Core 2 onto the Web Role
Installing additional components onto a Web Role typically involves a startup script, and .NET Core 2 is no different. There is one complication though: the installer downloads files into the TEMP
folder, and Cloud Services has a 100MB hard limit on that folder. We need to specify an alternate folder to use as TEMP
with a higher quota (this is what Jaques and Catherine figured out).
In TheCloudService
, expand Roles
, right click TheWebRole
and hit properties. Go to Local Storage
and add a new location called CustomTempPath
with a 500MB limit (or whatever else your app might need).
Next, we need the startup script. Go to TheWebRole
, add a new folder called Startup
and add the following files to it. Ensure that the Build Action
is set to Content
and that Copy to Output Directory
is set to Copy if newer
. Finally, we need to configure the cloud service to invoke our startup task. Open the ServiceDefinition.csdef
file and add the following xml in the WebRole
node to define the startup task:
<Startup>
<Task commandLine="Startup\startup.cmd" executionContext="elevated" taskType="simple">
<Environment>
<Variable name="IsEmulated">
<RoleInstanceValue xpath="/RoleEnvironment/Deployment/@emulated" />
</Variable>
</Environment>
</Task>
</Startup>
Now we finally have a cloud service that can be deployed, install .NET Core, and run the website. The first time you publish, it will take a few minutes for the role instance to become available since it
has to install the hosting module and restart IIS.
Note: I leave creating a cloud service instance in the Azure Portal as an exercise to the reader
Configuration
There are many ways of getting configuration into an ASP.NET Core application. If you know you’ll only be running in Cloud Services, you may consider taking a direct dependency on the Cloud Services libraries and using the RoleEnvironment
types to get populate your configuration. Alternatively, you can likely write a configuration provider that funnels in the RoleEnvironment
configuration into the ASP.NET Core configuration system.
In my original case, I didn’t want my ASP.NET Core website to have any awareness of Cloud Services, so I came up with another way—in the startup script, I copy the values from the RoleEnvironment
into environment variables that the default configuration settings pick up. The key here to making this transparent is knowing that the double-underscore, __
, translates into the :
when read from an environment variable. This means you can define a setting like CustomSetting__Setting1
, and then you can access it with Configuration["CustomSetting:Setting1"]
, or similar mechanisms.
To bridge this gap, we can add this to the startup script (complete script):
$keys = @(
"CustomSetting__Setting1",
"CustomSetting__Setting2"
)
foreach($key in $keys){
[Environment]::SetEnvironmentVariable($key, [Microsoft.WindowsAzure.ServiceRuntime.RoleEnvironment]::GetConfigurationSettingValue($key), "Machine")
}
This copies the settings from the Cloud Service Role Environment into environment variables on the host, and from there, the default ASP.NET Core configuration adds them into configuration.
Considerations
- Session affinity If you need session affinity for session state, you’ll need to configure that.
- Data Protection API Unlike Azure App Services, Cloud Services doesn’t have any default synchronization for the keys. You’ll need a solution for this. If anyone comes up with a reusable solution, I’ll happily mention it here. More info on configuring DPAPI is here.
- Local Debugging Due to the way local debugging of cloud services works (it directly uses
TheWebRole
as a startup project in IIS Express), directly debugging the cloud service does not work with the current patterns. Instead, you can setTheWebsite
as a startup project and debug that normally. The underlying issue is thatTheWebRole
includesTheWebsite
asContent
and does not copy the published files toTheWebRole
‘s directory. It may be possible to achieve this, though you’d likely want additional.gitignore
rules to prevent those files from being committed. In my case, I did not want my service to have any direct dependency on Cloud Services, so this wasn’t an issue—I simply needed a Server 2016 web host.
CI / CD with VSTS
It is possible to automate build/deploy of these cloud service web role projects using VSTS. My next blog post will show how to set that up.
Update October 18: The post is live
Thanks Oren, I followed your guide and it works really well (I’m a bit worried though, that a future update to Visual Studio/Azure Toolset will break things .. )
Regarding the configuration, I wanted to use my existing appsettings.production.json file and managed to get it working by adding the following lines to Startup.ps1:
Do you have any idea if it’s possible to run a Azure Web Job in a Cloud Service?
Regarding web jobs, I guess that is not possible. However, the IHostedService stuff in .Net Core 2.0 works great and is enough for my needs. I used the this example from David Fowler as a base:
https://gist.github.com/davidfowl/a7dd5064d9dcf35b6eae1a7953d615e3
Hey Johan , i want to add my environment variables to appsettings also , would you provide me the script you’ve used ?
Hello Oren.
I have a Core WebApi solution which consists of 4 projects. It’s a and two Console projects start when I run the solution in Visual Studio, IISExpress. However, when I deploy the application on Azure only one project starts. I wonder if it’s possible to use your solution to make my application with 2 starting projects run on Azure?
Thanks in advance, Gunnar Siréus
Hello Oren.
I have a Core WebApi solution which consists of 4 projects. It’s a “Multiple Startup Projects” Solution and two Console projects start when I run the solution in Visual Studio, IISExpress. However, when I deploy the application on Azure only one project starts. I wonder if it’s possible to use your solution to make my application with 2 starting projects run on Azure?
Thanks in advance, Gunnar Siréus
Hi Oren,
This is really useful, thanks!
Just one doubt: how does IIS and/or the Cloud Service runtime knows that the entry point is “TheWebsite.dll” and not “TheWebRole.dll”?
Regards,
Andrea
Many thanks Oren, your article saved my lots of hours that I would have spent otherwise to find out how to make it work. Appreciate your effort!