Skip to content
This repository has been archived by the owner on May 7, 2024. It is now read-only.

ASP.NET to Azure App Service Migration Workshop

Steve Smith edited this page Nov 17, 2021 · 5 revisions

ASP.NET to Azure App Service Migration Workshop

Workshop Overview

During this workshop, we will take an older .NET Framework web app and migrate it to Azure. This scenario represents a very common set of challenges that customers run into when moving an app to the cloud. We will make key updates to the solution to make it cloud ready.

Please complete the Prerequisites before the lab. Some of the prerequisites can be a bit time and bandwidth intensive to perform the day of the lab.

Prerequisites

Windows and Visual Studio

The lab activities are designed around a .NET Framework web app on Windows. Please ensure that you have a development environment that has Windows 10 and Visual Studio 2022 with the latest updates (Visual Studio 2019 should also work).

If you are working from a Mac or Linux environment, we suggest you create a Virtual Machine with Windows for this lab. If you have never created a Virtual Machine, this Microsoft Learning module will teach you how. Be sure to create the Virtual Machine in your Azure Subscription and not in the lab sandbox.

Azure Subscription

You will need access to an Azure Subscription with permission to create Azure resources, such as an Azure App Service Plan, Azure SQL Database and Azure Redis Cache. Please consult with your Azure Administrator prior to the lab to confirm you have the required permissions. If you do not have access to an Azure Subscription, you may sign up for a free trial.

Azure PowerShell

Now that you have a Windows development environment with a fully updated version of Visual Studio 2022, install Azure PowerShell on Windows.

Verify that your Azure PowerShell installation is working by running the following command from a Windows PowerShell terminal.

Connect-AzAccount

Choose the credentials that correspond to your Azure Subscription and you should be presented with the default Subscriptions that you have access to in the PowerShell terminal.

Clone the Repository

We recommend that you clone the lab repository in advance of the session. To clone it, launch Visual Studio 2022 and choose Clone or check out code. Specify the following URL for the Repository location field

https://github.com/dotnet-architecture/eShopModernizing.git

We recommend a short local path like c:\dev, some systems will have a maximum path character limitation configured. Click clone to download the solution. The default view will show you a list of all items downloaded in the Folder view. Open the Solution eShopLegacyWebForms.sln.

image

If you are using a fresh installation of Visual Studio 2022 you may be prompted to install extra components, if you see this dialog, click Install.

image

Lab Activities

Run the Legacy application

This walkthrough will focus on the eShopLegacyWebForms application. Before continuing, please ensure this opens and runs without error on your local machine. If Visual Studio notifies you that there are missing prerequisites, please install them.

By default, the application uses mock data and needs to be switched to using the local database (which we'll migrate to the cloud during the course of the walkthrough).

Open web.config and change the UseMockData="true" to UseMockData="false".

If running the application shows a standard ASP.NET error screen such as:

Could not find a part of the path '[path]\eShopLegacyWebForms\bin\roslyn\csc.exe'

Open the NuGet Package Manager Console (Tools->NuGet Package Manager->Package Manager Console) and run the following command.

If you see a prompt to replace a file, ensure you do NOT replace those files; it will break the site if you do.

Update-Package -reinstall

Initial Setup - Creating Azure Resources

In order to walkthrough the process of migrating an ASP.NET application to Azure AppService, we will be using the eShopModernizing project available on GitHub. This project has a sample of a WCF, ASP.NET WebForms, and ASP.NET MVC project with a before (marked as legacy) and after (marked as modernized). We will focus on modernizing the WebForms in the walkthrough, but the sample has both of the ASP.NET applications updated to use the more cloud ready patterns discussed in the walkthrough.

Getting started with Azure PowerShell

For this part of the workshop, we will be using Azure PowerShell to provision resources. Launch a Windows PowerShell window and log into your Azure Subscription.

Connect-AzAccount

The following Azure PowerShell commands will set up the initial AppService and SQL server that will be used to deploy to Azure. Many Azure resource names are required to be globally unique. Choose a term that will make your resources unique. We will call this your alias throughout the lab.

Start by creating a variable with your alias - make sure it is lower case and that you do not use any dashes or underlines in your alias name. Replace "youruniquealias" below.

$useralias = "youruniquealias"

Set the following variables that will be used throughout the workshop (choose your own admin password for the server admin password variable)

$location = "eastus"
$resourcegroupname = (-join($useralias,"-workshop-group"))
$webappname = (-join($useralias,"-workshop-webapp"))
$serveradminname = "ServerAdmin"
$serveradminpassword = "ChangeYourAdminPassword1"
$servername = (-join($useralias, "-workshop-server"))
$dbname = "eShop"
$storageaccountname = (-join($useralias, "workshopstorage"))
$storagecontainername = "workshopcontainer"
$appinsightsname = (-join($useralias, "-workshop-insights"))
$rediscachename = (-join($useralias, "-workshop-cache"))
$adminuser = "anyAdminUserName"
$vaultname = (-join("shopvault", $useralias))

You can verify that all variables are set by copying the following block of commands and running them in the PowerShell window:

Write-Host $useralias
Write-Host $location 
Write-Host $resourcegroupname 
Write-Host $webappname 
Write-Host $serveradminname 
Write-Host $serveradminpassword
Write-Host $servername 
Write-Host $dbname 
Write-Host $storageaccountname 
Write-Host $storagecontainername 
Write-Host $appinsightsname 
Write-Host $rediscachename 
Write-Host $adminuser
Write-Host $vaultname

Now that the variables are set, use Azure PowerShell to create a resource group. Resource groups are a logical container into which Azure resources like web apps, databases, and storage accounts are deployed and managed.

You can verify the commands have completed successfully by browsing to the resource group in the Azure Portal after they complete.

New-AzResourceGroup -Name $resourcegroupname -Location $location

Next create an App Service Plan and a Web App. An App Service plan specifies the location, size, and features of the web server farm that hosts your app. We will then assign a Managed Identity to the web app for later use.

New-AzAppServicePlan -Name $webappname -ResourceGroup $resourcegroupname -Location $location
New-AzWebApp -Name $webappname -AppServicePlan $webappname -ResourceGroup $resourcegroupname
Set-AzWebApp -AssignIdentity $true -Name $webappname -ResourceGroupName $resourcegroupname 

Using the following commands, create a SQL Database server, add a firewall rule to allow access from other Azure services, and create a SQL database.

Some commands may take a few minutes to complete once you start them

New-AzSqlServer -ServerName $servername -ResourceGroupName $resourcegroupname -Location $location -SqlAdministratorCredentials $(New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $serveradminname, $(ConvertTo-SecureString -String $serveradminpassword -AsPlainText -Force))
New-AzSqlServerFirewallRule -ResourceGroupName $resourcegroupname -ServerName $servername -FirewallRuleName "AllowedIPs" -StartIpAddress "0.0.0.0" -EndIpAddress "0.0.0.0"
New-AzSqlDatabase  -ResourceGroupName $resourcegroupname -ServerName $servername -DatabaseName $dbName -RequestedServiceObjectiveName "S0"

Now create an Azure Cache for Redis

New-AzRedisCache -ResourceGroupName $resourcegroupname -Name $rediscachename -Location $location -Sku Basic -Size c1

Checkpoint! Review the resources you have created in the Azure Portal.

Configuration

In this walkthrough, we will look at updating the .NET configuration system to take advantage of ConfigurationBuilders, why it matters, and how to use them to easily pull secrets from Azure Key Vault. Launch Visual Studio 2022 and open the eShopLegacyWebForms solution to begin.

Background

Configuration is central to building an app in a way that allows its dependencies to vary based on the environment it is deployed to. In .NET Framework applications, the built-in and most common mechanism to customize app configuration is via the ConfigurationManager type. These are often set in the app.config file for desktop apps or web.config for ASP.NET web apps. Historically, this was the only source that the ConfigurationManager knew about; however, with the release of .NET 4.7.1, it was updated to know about an extensible mechanism called ConfigurationBuilders that allow for composition of configuration sources.

By expanding the available sources for ConfigurationManager, an app can now bring in settings from other sources. When settings could only be set in app.config or web.config, it was easy to start committing secrets into the code repository which greatly increases the risk of secrets leaking. With ConfigurationBuilders, the secrets that need to be kept confidential can be stored in a secure location (such as Azure Key Vault), while the other settings can be pulled from sources like environment variables, AppService settings, or a file.

The .NET Framework itself provides a framework from which other ConfigurationBuilders can be developed, but does not include any providers out of the box. In order to make use of this system, custom builders must be built, or can be chosen from those maintained here: https://github.com/aspnet/MicrosoftConfigurationBuilders.

Supported configuration builders include:

  • Microsoft.Configuration.ConfigurationBuilders.Environment – Adds settings from the environment variables of the current process
  • Microsoft.Configuration.ConfigurationBuilders.UserSecrets - User secrets contained in an XML file external to the code base
  • Microsoft.Configuration.ConfigurationBuilders.Azure - Pulls items from key vault
  • Microsoft.Configuration.ConfigurationBuilders.KeyPerFile - File based where the name of the file is the key and the contents are the value
  • Microsoft.Configuration.ConfigurationBuilders.Json - Pulls key/value pairs from JSON files

The choice as to which one should be used depends on the scenarios and data stores of an individual application. At the time of writing, v2 of these ConfigurationBuilders is still in pre-release, but brings enough features that simplify deployment to the cloud that this walkthrough will be using those. They are planned to RTM for v2 soon.

Add ConfigurationBuilders to the project

The first thing that needs to be done in order to enable ConfigurationBuilders is to install the builder via NuGet. Since we will be using a prerelease version, if you use the GUI in Visual Studio, you’ll need to check the “Allow Prerelease”. The following needs to be installed (the Package Manager commands will be shown, but these packages can be added via the GUI as well):

Install-Package Microsoft.Configuration.ConfigurationBuilders.Environment -Version 2.0.0-beta 

Install-Package Microsoft.Configuration.ConfigurationBuilders.Azure -Version 2.0.0-beta 

Install-Package Microsoft.Configuration.ConfigurationBuilders.UserSecrets -Version 2.0.0-beta 

This will update the web.config file to add a ConfigSection for configBuilders, wherein three builders are registered. One is for the environment variables, named Environment, another is for Key Vault, named AzureKeyVault, and the final is for user defined secrets called Secrets.

Make the following modifications to the web.config to configure the added ConfigurationBuilders:

  1. Change the [vaultName] in the AzureKeyVault entry to ${KeyVaultName}. This allows for substitution for the name of the vault. This way, we can configure a different vault for local, dev, or production use.

  2. Add an attribute configBuilders with the value of Secrets,Environment,AzureKeyVault to the appSettings and the connectionStrings nodes. This tells the configuration system which builder to use and the order to use.

    image

  3. Add an empty key in the appSettings for KeyVaultName. By default, configuration builders will only replace values that already exist in the appsettings, so we need to add that so our vault name replacement in step 1 works.

  4. Replace the type for the AzureKeyVault with eShopLegacyWebForms.OptionalKeyVaultConfigurationBuilder, eShopLegacyWebForms. This is because there is currently a bug that has been fixed but not released allowing optional usage of key vault.

  5. Add the following class to your project:

    using Microsoft.Configuration.ConfigurationBuilders;
    using System;
    using System.Collections.Specialized;
    
    namespace eShopLegacyWebForms 
    { 
        /// <summary> 
        /// A version of <see cref="AzureKeyVaultConfigBuilder"/> that correctly handles optional. See https://github.com/aspnet/MicrosoftConfigurationBuilders/pull/56 
        /// for general fix. 
        /// </summary> 
        public class OptionalKeyVaultConfigurationBuilder : AzureKeyVaultConfigBuilder 
        { 
            protected override void LazyInitialize(string name, NameValueCollection config) 
            { 
                try 
                { 
                    base.LazyInitialize(name, config); 
                } 
                catch (Exception) when (Optional) 
                { 
                } 
            } 
        } 
    } 
  6. Add optional="true" to the AzureKeyVault entry.

  7. In order to seed the database, find all the *.sql files under Models/Infrastructure and comment out (with --) the USE ... lines. Azure SQL does not support this statement, and is unnecessary as we select the database by name in the connection string.

Your web.config should now look like the one below:

  <configBuilders> 
    <builders> 
      <add name="Environment" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment, Version=2.0.0.0, Culture=neutral" /> 
      <add name="AzureKeyVault" vaultName="${KeyVaultName}" optional="true" type="eShopLegacyWebForms.OptionalKeyVaultConfigurationBuilder, eShopLegacyWebForms" /> 
      <add name="Secrets" userSecretsId="5702632e-73c6-478a-a875-2714c09d921b" type="Microsoft.Configuration.ConfigurationBuilders.UserSecretsConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.UserSecrets, Version=2.0.0.0, Culture=neutral" /> 
    </builders> 
  </configBuilders> 
  <connectionStrings configBuilders="Secrets,Environment,AzureKeyVault"> 
    <add name="CatalogDBContext" connectionString="Server=tcp:127.0.0.1,5433;Initial Catalog=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=Pass@word" providerName="System.Data.SqlClient" /> 
  </connectionStrings> 
  <appSettings configBuilders="Secrets,Environment,AzureKeyVault"> 
    <add key="KeyVaultName" value="" /> 
    <add key="UseMockData" value="false" /> 
    <add key="UseCustomizationData" value="false" />
  </appSettings> 

By adding the configBuilders, the app can then access any settings as it normally would, namely via ConfigurationManager.AppSettings[“settingName”]. This works as well for connection strings and is extensible to any custom configuration section.

Create KeyVault and hook it up to ConfigurationManager

One of the challenges with configuration is that often values that need to be kept secret end up as configuration values in plain text somewhere. This is a security vulnerability and by using ConfigurationBuilders, these sensitive values can be saved somewhere secure and pulled in when needed. Azure provides a system that allows for secure storage of values called Key Vault. The next step will be to add a KeyVault instance to our subscription and connect it to the application.

In order to create the key vault instance run the following commands from the cloud shell

New-AzKeyVault -Name $vaultname -ResourceGroupName $resourcegroupname -location $location

We will now create a connection string for our database and store it in the Azure Key Vault. Begin by generating the connection string.

$connectionstringplaintext = (-join("Server=tcp:", $servername, ".database.windows.net,1433;Database=", $dbname, ";User ID=", $serveradminname, ";Password=", $serveradminpassword, ";Encrypt=true;Connection Timeout=30;"))

Now convert the connection string to a Secure String

$connectionstring = ConvertTo-SecureString $connectionstringplaintext -AsPlainText -Force

Now we will push the secret to our Key Vault

Set-AzKeyVaultSecret -VaultName $vaultname -Name "CatalogDBContext" -SecretValue $connectionstring 

Now we must enable the webapp to access secrets with the get permission. In order to do this, we need the appId

$appId = (Get-AzADServicePrincipal -DisplayName $webappname).Id

This will assign the app Id to a variable that we can use to assign our Key Vault access policy

Set-AzKeyVaultAccessPolicy -VaultName $vaultname -ResourceGroupName $resourcegroupname -ObjectId $appId -PermissionsToSecrets Get

The final thing is to set the vault name as an AppSetting on the web service so that the application knows what vault to retrieve values from:

Set-AzWebApp -Name $webappname -ResourceGroupName $resourcegroupname -AppSettings @{KeyVaultName = $vaultname}

Deployment

At this point, the application is ready to be deployed. We will deploy the first version of our site from within Visual Studio. Right-click on the eShopLegacyWebForms project and choose Publish. From the Publish dialog, choose App Service, Select Existing and click "Create Profile".

image

Next, ensure that you are logged in with the correct account on the top right of the next dialog, choose your Azure Subscription, highlight the web app and click OK and then Publish the web app to your App Service.

image

Once your application deployment is completed, a browser window will open up and navigate to the deployed App Service. The window that will open will need to be refreshed once the deployment has succeeded. In the future, you can deploy the application by going back to the Publish dialog and choose the Publishing profile you have just created and click Publish.

image

Section Completed

The presenter will provide a guided recap of this section at the end of the allotted time, at which time we will move on as a group to Logging.

Logging

There are many different logging frameworks available for .NET developers to use – including custom designed solutions. For this walkthrough, we have chosen to use the popular Log4Net framework. Log4Net is designed to use log levels and appenders. The legacy applications in this walkthrough are configured to use a filesystem-based appender for all log levels, which is representative of the approach we commonly encounter for this type of web app.

You can mix and match which appenders are active for which log levels. If you are unfamiliar with this approach to logging in .NET apps please take a look at the Log4Net documentation before proceeding.

We are going to update the logger to write to Azure Blob Storage rather than the local filesystem. We will also configure the logger to write to Application Insights. This will help us consolidate our logs into a common location in Azure, and demonstrate how simple it is to push logs into Application Insights to capture custom telemetry.

Create Azure Resources

From your PowerShell terminal, run the following commands

  1. Create a blob storage account:
$storageaccount = New-AzStorageAccount -ResourceGroupName $resourcegroupname -Location $location -AccountName $storageaccountname -SkuName Standard_LRS
  1. Create a container on the storage account to hold the log files:
New-AzStorageContainer -Name $storagecontainername -Permission Blob -Context $storageaccount.Context
  1. Fetch the connection string for this storage account, and record it for later use:
$storageaccountkey = (Get-AzStorageAccountKey -ResourceGroupName $resourcegroupname -Name $storageaccountname)[0].Value

$storageconnectionstring = ((-join('DefaultEndpointsProtocol=https;AccountName=', $storageaccountname, ';AccountKey=', $storageaccountkey,';EndpointSuffix=core.windows.net' )))
  1. Create an AppInsights instance. Neither the Azure PowerShell nor the Azure CLI have a direct command for creating this resource type at this time, so we will use the general purpose Azure Resource creation command.
$appinsights = New-AzResource -ResourceName $appinsightsname -ResourceGroupName $resourcegroupname -Location $location -ResourceType "Microsoft.Insights/components" -Properties (-join('{"ApplicationId":"', $appinsightsname, '","Application_Type":"other"}'))
  1. Fetch your instrumentation key and record it for later use:
$appinsights.Properties.InstrumentationKey 

Update eShopLegacyWebForms Solution logging configuration

Open the web app using Visual Studio 2022, run it and navigate through the site. Open the folder that the solution is running from, find the logs directory and review the output of the local log file appender. These files will be located under the WebForms project file in the logFiles directory.

image

Notice the messages in the log file (DEBUG/INFO, etc. are log levels):

image

Logging to Blob Storage & AppInsights

It is fairly common to find legacy applications using popular logging frameworks to write to a local filesystem or a UNC path. Because these approaches do not guarantee log durability when deployed to Azure App Service, we will now update the application to make use of Azure Blob Storage to hold our log files and send them to Application Insights.

For our sample application, we will be updating an application that was configured to use Log4Net. Begin by updating the Log4Net configuration file log4Net.xml – replace the contents of the file with these 2 new appender configurations. For this example, replace YourStorageAccountConnectionString with the value from step 3 in the setup steps for Logging.

<log4net> 
  <root> 
    <level value="ALL" /> 
    <appender-ref ref="aiAppender" /> 
    <appender-ref ref="azureAppendBlobAppender" /> 
  </root> 
  <!-- https://blog.ehn.nu/2014/11/using-log4net-for-application-insights/ --> 
  <appender name="aiAppender" type="Microsoft.ApplicationInsights.Log4NetAppender.ApplicationInsightsAppender, Microsoft.ApplicationInsights.Log4NetAppender"> 
    <layout type="log4net.Layout.PatternLayout"> 
      <conversionPattern value="%date [%thread] %property{activity} %level %logger - %property{requestinfo}%newline%message%newline%newline" /> 
    </layout> 
  </appender> 
  <!-- http://stemarie.github.io/log4net.Azure/ --> 
  <appender name="azureAppendBlobAppender" type="log4net.Appender.AzureAppendBlobAppender, log4net.Appender.Azure"> 
    <param name="ContainerName" value="workshopcontainer"/> 
    <param name="DirectoryName" value="eShopModernizedLogs"/> 
    <!--You can either specify a connection string or use the ConnectionStringName property instead--> 
    <param name="ConnectionString" value="YourStorageAccountConnectionString"/> 
    <bufferSize value="1" /> 
  </appender> 
</log4net> 

Don't forget to update YourStorageAccountConnectionString!

Fetch your connection string for the storage account from the variable you stored it in.

Write-Host $storageconnectionstring 

Use this value to replace YourStorageAccountConnectionString in the log4net configuration.

Next, update the instrumentation key for AppInsights by opening ApplicationInsights.config and adding the InstrumentationKey element in the file with the value from step 5 in the setup steps for logging.

image

With the configuration completed, we now need to install the packages required for these new appenders. Right-click on the project in your Solution Explorer and choose Manage NuGet Packages.

image

Search for and install Microsoft.ApplicationInsights.Log4NetAppender and log4net.Appender.Azure.

image

image

Open your web.config file and make sure there are not any log4net configuration elements - installing NuGet packages may re-generate a configuration block. If one is created it would override your log4net.xml config file.

Publish the website again and navigate through it to generate new log file entries. Then, navigate to the Azure Portal and open the Blob Storage account and view your log file entries. Once done, head over to your AppInsights instance and review your data under the Performance blade. Note that aggregated details will take several minutes before they’re available in the portal.

image

image

image

image

Section Completed

The presenter will provide a guided recap of this section at the end of the allotted time, at which time we will move on as a group to Session.

Session

It was a common for developers to rely heavily upon Session when developing older applications. The default configuration for Session is to store it in memory on the server that runs the app. The default configuration, InProc, does not permit horizontal scaling if your application is using Session. This was often worked around by using sticky session / session affinity. That solution does not support multi-region AppService deployments.

There are several easy to implement providers that allow you to offload Session to a backing store that allows for scaling – including SQL Databases and Redis Cache. For this walkthrough we will configure an Azure Redis Cache and update the application to offload Session persistence to it.

We created the Azure Cache for Redis instance at the beginning of the lab. Navigate to the resource in the Azure portal. The Status flag in the Overview pane for your cache will show “Creating” until it is done, at which point it will show “Running”.

image

Next, fetch and record your access keys for the cache:

Get-AzRedisCacheKey -Name $rediscachename -ResourceGroup $resourcegroupname

You may move on to configuring the application to use the Azure Cache for Redis Session Provider at this time, but be sure to check back and make sure the Status is “Running” before you launch the website to test the changes.

Configuring the Session Provider

With the cache provisioned, we will now follow the basic steps outlined in our public documentation on the ASP.NET Session State Provider for Azure Cache for Redis.

Begin by opening the eShopLegacyWebForms solution in Visual Studio 2022, right clicking on the WebForms project and choosing Manage NuGet Packages. Click on Browse, search for and install Microsoft.Web.RedisSessionStateProvider by Microsoft.

Along with the packages that are required to interact with the cache, the configuration block for the Session state provider will be added to your project’s web.config file:

image

Make the following changes to your configuration, replacing the values for host and accessKey with those configured in the previous step.

Make sure you remember to replace [youralias] and the value of accessKey with one of the two keys you just fetched!

<sessionState mode="Custom" customProvider="MySessionStateStore">
  <providers>
    <add name="MySessionStateStore"
         type="Microsoft.Web.Redis.RedisSessionStateProvider"
         host="[youralias]-workshop-cache.redis.cache.windows.net"
         accessKey="OneOfYourRecordedCacheAccessKeys"
         ssl="true" />
  </providers>
</sessionState>

Finally, we need to remove the following element from your web.config file:

<sessionState mode="InProc"/>

Launch the website and your Session state is now offloaded to Azure Cache for Redis.

Checkpoint Reached

Congratulations! You have reached the end of the self-paced lab activities. Please sync up with your cohort and help ensure everyone has completed their changes. The remaining lab activities will be instructor led.

Cleanup

When you are finished with the lab, you can clean up all the resources we created today by running the following command. Please note that this will delete all resources within the specified Resource Group. Prior to running this command, review the contents of the Resource Group. Run the following command to view the name of your resource group variable

Write-Host $resourcegroupname

Once you have reviewed the contents of the Resource Group and have confirmed you are ready to delete, run this command to permanently delete the Resource Group and its contents.

Remove-AzResourceGroup -ResourceGroupName $resourcegroupname
Clone this wiki locally