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

Lift and Shift To Azure

Taylor Southwick edited this page Jul 15, 2019 · 18 revisions

ASP.NET Lift & Shift Workshop

Workshop Overview

In this workshop, we will be taking an older ASP.NET WebForms application and modernizing it so that it can be deployed to Azure AppService. The activities that you will perform during this workshop align with common scenarios encountered while working with customers. The following app modernization activities should be performed sequentially.

The following section is supplementary and may require elevated permissions within your subscription. If you plan to complete these as part of the workshop, please note the requirements in the respective sections prior to beginning the lab.

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 ends up with a yellow screen of death similar to:

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:

Update-Package -reinstall

Initial Setup

In order to walkthrough the process of migrating an ASP.NET application to Azure AppSevice, 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.

For this part of the workshop, we will be using the Azure CLI from the Azure Cloud Shell to provision resources. Log into the Azure Portal and open the Azure Cloud shell and choose Bash to begin.

image

If this is the first time you have used the Azure Cloud Shell, you will be prompted to select your Azure Subscription to allow it to create a storage account for the shell.

The following Azure CLI 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, so you'll use your Microsoft alias to generate unique names.

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

msftalias="yourmsftalias"

Now copy/paste in the rest of the variables you'll use in this workshop:

location="eastus" \
resourcegroupname="$msftalias-workshop-group" \
webappname="$msftalias-workshop-webapp" \
serveradminname="ServerAdmin" \
serveradminpassword="ChangeYourAdminPassword1" \
servername="$msftalias-workshop-server" \
dbname="eShop" \
storageaccountname="${msftalias}workshopstorage" \
storagecontainername="workshopcontainer" \
appinsightsname="$msftalias-workshop-insights" \
rediscachesname="$msftalias-workshop-cache" \
adminuser="anyAdminUserName"

Now that the variables are set, use Azure CLI 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.

az group create --resource-group $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.

az appservice plan create --resource-group $resourcegroupname --name $webappname
az webapp create --resource-group $resourcegroupname --name $webappname --plan $webappname 
az webapp identity assign --resource-group $resourcegroupname --name $webappname 

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

az sql server create --resource-group $resourcegroupname --location $location --name $servername --admin-user $serveradminname --admin-password $serveradminpassword 
az sql server firewall-rule create --resource-group $resourcegroupname --server $servername --name AllowAzureServices --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0 
az sql db create --resource-group $resourcegroupname --server $servername --name $dbname --service-objective S0 

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 KeyVault. Launch Visual Studio 2019 and open the eShopLegacyWebForms solution to begin.

Background

Configuration is central to customizing many applications. This allows setting connection strings or options in a way that does not require recompiling the application. Benefits of this include being able to change behavior in a production environment, or having different settings depending on the runtime environment, such as region deployment or testing slots. In .NET Framework applications, the built in and most common mechanism to customize an application is via the ConfigurationManager type. These are often set in the app.config file for desktop applications or web.config for ASP.NET applications. 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 instances, an application can now bring in settings from whatever makes the most sense. When settings could only be set in app.config or web.config, it was easy to start committing secrets into the versioning system which greatly increases the risk of the 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 that make the most sense, be it environment variables, AppService settings, or a file.

The .NET Framework itself provides a framework from which other ConfigurationBuilders can be developed but doesn’t include any 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 KeyVault, named AzureKeyVault, and the final is for user defined secrets called Secrets.

Make the following modifications to the web.config so that the configuration builders are hooked up correctly:

  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 configuraiton system which builder to use and the order to use.
  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. Finally, add optional="true" to the AzureKeyVault entry.

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="true" /> 
    <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 KeyVault. 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:

vaultName="shopvault$msftalias"
az keyvault create --name $vaultName --resource-group $resourcegroupname --location $location 

Now, create the secret and set the connection string to it:

connectionString="Server=tcp:$servername.database.windows.net,1433;Database=$dbName;User ID=$serveradminname;Password=$serveradminpassword;Encrypt=true;Connection Timeout=30;" 
az keyvault secret set --vault-name $vaultName --name "CatalogDBContext" --value "$connectionString" 

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

appId=$(az ad sp list --display-name $webappname --query '[0].appId' --output tsv)

This will return a table with the AppId which needs to be copied to a variable appId. Once that is done, we can set the policy:

az keyvault set-policy --name $vaultName --spn $appId --secret-permissions 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:

az webapp config appsettings set --resource-group $resourcegroupname --name $webappname --settings KeyVaultName=$vaultName 

Deployment

At this point, the application is ready to be deployed. Since this lab is focused on app changes, we will use the simplest method of deployment - publishing directly from 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

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.

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.

Create Azure Resources

Go back to the Cloud Shell and run the following commands:

  1. Create a blob storage account:
az storage account create --resource-group $resourcegroupname --location $location --name $storageaccountname --sku Standard_LRS  
  1. Create a container on the storage account to hold the log files:
az storage container create --name $storagecontainername --account-name $storageaccountname --public-access blob
  1. Fetch the connection string for this storage account, and record it for later use:
az storage account show-connection-string --resource-group $resourcegroupname --name $storageaccountname
  1. Create an AppInsights instance (AppInsights does not currently have its own AZ CLI create command so we are using the general az resource command here):
az resource create --resource-group $resourcegroupname --location $location --name $appinsightsname --resource-type "Microsoft.Insights/components" --properties '{"ApplicationId":"$appinsightsname","Application_Type":"other"}'
  1. Fetch your instrumentation key and record it for later use:
az resource show --resource-group $resourcegroupname --name $appinsightsname --resource-type "Microsoft.Insights/components" --query properties.InstrumentationKey

Update eShopLegacyWebForms Solution logging configuration

Open the web app using Visual Studio 2019, 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

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> 

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

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

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 configure, 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.

Create an Azure Cache for Redis

We will begin by creating the Azure Cache for Redis using the Azure CLI, note that the name must be globally unique. We will assume you are appending your alias to the name to make it unique:

az redis create --location $location --name $rediscachesname --resource-group $resourcegroupname --sku Basic --vm-size c1

Once the command has provisioned the Azure Cache for Redis, there is a delay while the database is initialized. 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:

az redis list-keys --name $rediscachesname --resource-group $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” if you run into any issues when you test it after completing the next session – it can take some time for this operation to complete.

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 2019, 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.

<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 tag from your web.config file:

<sessionState mode="InProc"/>

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

Managed Identity

This sample is intended to showcase a possible use of Managed Identity. There are plans to incorporate Managed Identity into SqlClient which will simplify some of these steps. A couple caveats to keep in mind:

  • This sets the application up to be an admin on the SQL server. In order to follow security best practices see the discussion here. These directions require collaboration with an AD admin to set up the security groups, so it is out of scope for this walkthrough.
  • In order to set the access token we must manage the SqlConnection object manually for each DbContext instance. This may not be desired in production where SQL connections may be pooled.
  • Retrieving the access token is an async operation, which must be resolved in a synchronous way due to DbContext design. The token is cached so most of the time it will not cause issues, but there is the chance of a deadlock due to the mixture of async and syncronous code.

Managed identity is a feature of many services in Azure, including AppService. This feature is designed to provide a strong identity to a resource that can be used to authorize it against specific resources. The benefit of using this rather than a custom service principal is that the system manages the lifetime of this identity and ties it directly to the resource it identifies. An application running in AppService that has Managed Identity turned on can then take advantage of this to authorize access to resources such as storage accounts, SQL server, and events (for a full list of available resources, see here). For this part of the workshop, we will update the application to use Managed Identity to connect to SQL server so connection strings do not need to be stored anywhere.

Application changes

In order to use managed identity in the application for SQL access, we must use access tokens instead of connection strings. This is only available in .NET Framework 4.6+. In order to use an access token for SQL access instead of a connection string, we need to change the constructor of the context to accept a token provider and then construct a SQL connection from that. First, let's add the factory methods to get an access token:

using Microsoft.Azure.Services.AppAuthentication;
using System.Configuration;
using System.Data.SqlClient;

namespace eShopLegacyWebForms
{
    public interface ISqlConnectionFactory
    {
        SqlConnection CreateConnection();
    }

    public class ManagedIdentitySqlConnectionFactory : ISqlConnectionFactory
    {
        private readonly AzureServiceTokenProvider _provider;

        public ManagedIdentitySqlConnectionFactory()
        {
            _provider = new AzureServiceTokenProvider();
        }

        public SqlConnection CreateConnection()
        {
            return new SqlConnection
            {
                AccessToken = AccessToken,
                ConnectionString = ConfigurationManager.ConnectionStrings["CatalogDBContext"].ConnectionString
            };
        }

        private string AccessToken
            => _provider.GetAccessTokenAsync("https://database.windows.net/").ConfigureAwait(false).GetAwaiter().GetResult();
    }

    public class AppSettingsSqlConnectionFactory : ISqlConnectionFactory
    {
        public SqlConnection CreateConnection()
        {
            return new SqlConnection
            {
                ConnectionString = ConfigurationManager.ConnectionStrings["CatalogDBContext"].ConnectionString
            };
        }
    }
}

Now, we need to update the database context. Find CatalogDBContext and replace the constructor with the following:

public CatalogDBContext(ISqlConnectionFactory provider)
    : base(provider.CreateConnection(), true)
{
}

One final application change is to update the dependency injection in ApplicationModule to register the managed identity SQL connection factory instead of using AppSettings:

builder.RegisterType<ManagedIdentitySqlConnectionFactory>()
    .As<ISqlConnectionFactory>()
    .SingleInstance();

Now publish these changes to your web app.

Azure infrastructure

In the setup, we already turned on managed identity for the application and stored the service principal ID as $appId. Now, we need to do two things:

  1. Add the application to SQL server:
az sql server ad-admin create --resource-group $resourcegroupname --server-name $servername --display-name $adminuser --object-id $appId
  1. Update the connection string. It doesn't have any secrets now, so it doesn't need to be in KeyVault:
az webapp config connection-string set --resource-group $resourcegroupname --name $webappname --settings CatalogDBContext="Server=tcp:${servername}.database.windows.net,1433;Database=${dbname};" --connection-string-type SQLAzure

At this point, you can test the app again and use the Azure Portal to confirm that the connectionString was successfully updated.

Clone this wiki locally