Manage and deploy web applications on compute web hosting nodes

The Azure web hosting batch client API allows you to programmatically control all the components of an Azure web hosting Batch account.

Continuing to enhance your companies console app, you’ll now add all the components needed to convert the videos you uploaded in the last exercise.

By the end of this exercise, you’ll have a working Batch process that can convert MP4 videos to animated GIFs. The app will add a job to the existing pool, and add and start the video conversion tasks on cloud web hosting.

Enhance the code using the batch client

  1. In the Cloud Shell, edit the Program.cs file in the editor:
  2. Add a constant to Program.cs for the JobId we’ll use in our Batch job.
  3. Replace these lines in the Main method:

With a using block for the batchClient

using (BatchClient batchClient = BatchClient.Open(sharedKeyCredentials))
{
    // Create the Batch pool, which contains the compute nodes that execute the tasks.
    await CreateBatchPoolAsync(batchClient, PoolId);

    // Create the job that runs the tasks.
    await CreateJobAsync(batchClient, JobId, PoolId);

    // Create a collection of tasks and add them to the Batch job.
    await AddTasksAsync(batchClient, JobId, inputFiles, outputContainerSasUrl);
}

 

Create a job on cloud web hosting

  1. Add this new method, CreateJobAsync() to Program.cs to create and add a job to the pool.
private static async Task CreateJobAsync(BatchClient batchClient, string jobId, string poolId)
{
        Console.WriteLine("Creating job [{0}]...", jobId);

        CloudJob job = batchClient.JobOperations.CreateJob();
        job.Id = jobId;
        job.PoolInformation = new PoolInformation { PoolId = poolId };

        await job.CommitAsync();
}

The code above uses the batch client to create a job. The method assigns the given job id and information about the pool.

Add a task

  1. With the job created, the last step is to add a task to the job. Add the following method, AddTaskAsync(), to Program.cs.
private static async Task<List<CloudTask>> AddTasksAsync(BatchClient batchClient, string jobId, List<ResourceFile> inputFiles, string outputContainerSasUrl)
{
    Console.WriteLine("Adding {0} tasks to job [{1}]...", inputFiles.Count, jobId);

    // Create a collection to hold the tasks added to the job
    List<CloudTask> tasks = new List<CloudTask>();

    for (int i = 0; i < inputFiles.Count; i++)
    {
        // Assign a task ID for each iteration
        string taskId = String.Format("Task{0}", i);

        // Define task command line to convert the video format from MP4 to animated GIF using ffmpeg.
        // Note that ffmpeg syntax specifies the format as the file extension of the input file
        // and the output file respectively. In this case inputs are MP4.
        string appPath = String.Format("%AZ_BATCH_APP_PACKAGE_{0}#{1}%", appPackageId, appPackageVersion);
        string inputMediaFile = inputFiles[i].FilePath;
        string outputMediaFile = String.Format("{0}{1}",
            System.IO.Path.GetFileNameWithoutExtension(inputMediaFile),
            ".gif");

        // This is the dos command built by using the ffmpeg application package, the paths from the input container
        string taskCommandLine = String.Format("cmd /c {0}\\ffmpeg-3.4-win64-static\\bin\\ffmpeg.exe -i {1} {2}", appPath, inputMediaFile, outputMediaFile);

        // Create a cloud task (with the task ID and command line) and add it to the task list
        CloudTask task = new CloudTask(taskId, taskCommandLine);
        task.ResourceFiles = new List<ResourceFile> { inputFiles[i] };

        // Task output file will be uploaded to the output container in Storage.
        List<OutputFile> outputFileList = new List<OutputFile>();
        OutputFileBlobContainerDestination outputContainer = new OutputFileBlobContainerDestination(outputContainerSasUrl);
        OutputFile outputFile = new OutputFile(outputMediaFile,
                                                new OutputFileDestination(outputContainer),
                                                new OutputFileUploadOptions(OutputFileUploadCondition.TaskSuccess));
        outputFileList.Add(outputFile);
        task.OutputFiles = outputFileList;
        tasks.Add(task);
    }

    // Call BatchClient.JobOperations.AddTask() to add the tasks as a collection rather than making a
    // separate call for each. Bulk task submission helps to ensure efficient underlying API
    // calls to the Batch service.
    await batchClient.JobOperations.AddTaskAsync(jobId, tasks);

    return tasks;
}

This final method does all the complex actions of the web app. A task is added to the job for each file that has been uploaded. The task takes the form of a shell command. The app (ffmpeg) has been installed on each node at a specific location because we used an application package. The Batch service stores that location in an environment variable on the node so that it can be accessed via:

%AZ_BATCH_APP_PACKAGE_ffmpeg#3.4%

Using this approach it’s easy to upload and increment newer versions of the ffmpeg web app. The command looks into the zip folder, and executes:

ffmpeg.exe -i input-filename output-filename

For the best performance, the tasks are added as a list to the batchClient.JobOperations.AddTaskAsync. This is more efficient than making a separate call for each file.

Test the console app

  1. Select the ellipses in the top-right corner of the code editor.
  2. Select Close Editor, and in the dialog select Save.
  3. In the integrated terminal, build and run the app.
  4. The below messages are written to the terminal.
Creating container [input].
Creating container [output].
Uploading file ~\cutifypets\InputFiles.mp4 to container [input]...
Uploading file ~\cutifypets\InputFiles.mp4 to container [input]...
Uploading file ~\cutifypets\InputFiles.mp4 to container [input]...
Uploading file ~\cutifypets\InputFiles.mp4 to container [input]...
Uploading file ~\cutifypets\InputFiles.mp4 to container [input]...
Uploading file ~\cutifypets\InputFiles.mp4 to container [input]...
Creating pool [WinFFmpegPool]...
Creating job [WinFFmpegJob]...
Adding 2 tasks to job [WinFFmpegJob]...
  1. The console web app closes as soon as it has added the tasks. In Azure web hosting, the pool, nodes, job, and tasks are created. There’s nothing monitoring what’s happening within the app as it has exited. To see the current status of the conversion, and check the results, return to the Azure portal.
  2. In the Azure portal, on the Dashboard select the Batch account beginning cutify.result
  3. The health dashboard is shown on the Overview page, from here you can check the status of the current running job and the pool of  hosting nodes.
  4. On the left, select Jobs, select WinFFmpegJob. On this page, you’ll see the current status of the tasks.
  5. When the tasks have completed, on the left select Storage accounts, then select the storage account your created in the first exercise.
  6. On the left select Blobs, then select the output folderoutput
  7. Download a file to check the cutest pet video from cloud.

How to Create a pool of compute nodes to run Azure Web Hosting

To run a web hosting batch job, we need to add a pool to our Batch account. A pool contains compute nodes, which are the engines that run your Batch job. You specify the number, size, and operating system of nodes at creation time. In this exercise, you’ll modify the web hosting console app you made in the previous exercise to add a pool to your web hosting Batch account

Your company wants to control the costs of the app, and have asked you to use a fixed number of nodes.

Add settings for your new pool

In the web hosting Cloud Shell, edit the Program.cs file in the editor:

code Program.cs

Add the following properties to the Program class in Program.cs:

private const string PoolId = "WinFFmpegPool";
private const int DedicatedNodeCount = 0;
private const int LowPriorityNodeCount = 3;
private const string PoolVMSize = "STANDARD_D2_v2";
private const string appPackageId = "ffmpeg";
private const string appPackageVersion = "3.4";

The above settings will be used in the code to create the pool. Looking at each variable we can explain them as follows.

  • PoolId: The name our code will use to reference the pool in other web hosting batch client calls.
  • LowPriorityNodeCount: You are going to create a pool with three low-priority virtual machines (VMs)
  • PoolVMSize: The VMs will be STANDARD_A1_v2, which gives the nodes 1 CPU, 2 GB of RAM, and 10 GB of SSD storage
  • appPackageId: The name of the application package to use on the nodes you create
  • appPackageVersion: The version of the application to use on the nodes you create

Update the Main() method to support asynchronous calls on webhosting.

We’ll be making several asynchronous calls to web hosting cloud services, so the first thing to do is to make Main asynchronous. With C# .NET version 7.1 and onwards, async Main methods in console applications are supported.

  1. Change the web hosting console app to allow async method calls, by first adding System.Threading.Tasks library.
using System.Threading.Tasks;
using System.Collections.Generic; // Also add generics to allow the app to use Lists

Next, update the Main method signature as follows:

static async Task Main(string[] args)

Create a pool

  1. Add the following new method to the Program class to create a Batch pool. The method will:
    • Create an image reference object to store the settings for the nodes to be added to the pool.
    • Use the image reference to create a VirtualMachineConfiguration object on your web hosting.
    • Create an unbound pool using the properties declared above and the VirtualMachineConfiguration.
    • Add an application package reference to the pool.
    • Create the pool on Azure web hosting.
    • Take two parameters, the batchClient and PoolId.
private static async Task CreateBatchPoolAsync(BatchClient batchClient, string poolId)
    {
        CloudPool pool = null;
        Console.WriteLine("Creating pool [{0}]...", poolId);

        // Create an image reference object to store the settings for the nodes to be added to the pool
        ImageReference imageReference = new ImageReference(
                publisher: "MicrosoftWindowsServer",
                offer: "WindowsServer",
                sku: "2012-R2-Datacenter-smalldisk",
                version: "latest");

        // Use the image reference to create a VirtualMachineConfiguration object
        VirtualMachineConfiguration virtualMachineConfiguration =
        new VirtualMachineConfiguration(
            imageReference: imageReference,
            nodeAgentSkuId: "batch.node.windows amd64");

        try
        {
            // Create an unbound pool. No pool is actually created in the Batch service until we call
            // CloudPool.CommitAsync(). This CloudPool instance is therefore considered "unbound," and we can
            // modify its properties.
            pool = batchClient.PoolOperations.CreatePool(
                poolId: poolId,
                targetDedicatedComputeNodes: DedicatedNodeCount,
                targetLowPriorityComputeNodes: LowPriorityNodeCount,
                virtualMachineSize: PoolVMSize,
                virtualMachineConfiguration: virtualMachineConfiguration);  

            // Specify the application and version to install on the compute nodes
            pool.ApplicationPackageReferences = new List<ApplicationPackageReference>
            {
                new ApplicationPackageReference
                {
                ApplicationId = appPackageId,
                Version = appPackageVersion
                }
            };

            // Create the pool
            await pool.CommitAsync();
        }
        catch (BatchException be)
        {
            // Accept the specific error code PoolExists as that is expected if the pool already exists
            if (be.RequestInformation?.BatchError?.Code == BatchErrorCodeStrings.PoolExists)
            {
                Console.WriteLine("The pool [{0}] already existed when we tried to create it", poolId);
            }
            else
            {
                throw; // Any other exception is unexpected
            }
        }
    }

Call CreateBatchPoolAsync from our Main method. The Main method should now be the following:

static async Task Main(string[] args)
{
    // Read the environment variables to allow the app to connect to the Azure Batch account
    batchAccountUrl = Environment.GetEnvironmentVariable(envVarBatchURI);
    batchAccountName = Environment.GetEnvironmentVariable(envVarBatchName);
    batchAccountKey = Environment.GetEnvironmentVariable(envVarKey);

    // Show the user the batch the app is attaching to
    Console.WriteLine("URL: {0}, Name: {1}, Key: {2}", batchAccountUrl, batchAccountName, batchAccountKey);

    // The batch client requires a BatchSharedKeyCredentials object to open a connection
    var sharedKeyCredentials = new BatchSharedKeyCredentials(batchAccountUrl, batchAccountName, batchAccountKey);
    var batchClient = BatchClient.Open(sharedKeyCredentials);

    // Create the Batch pool, which contains the compute nodes that execute tasks.
    await CreateBatchPoolAsync(batchClient, PoolId);
}

Test the app

  1. Select the ellipses in the top-right corner of the code editor.
  2. Select Close Editor, and in the dialog select Save.
  3. In the webhosting Cloud Shell, compile and run the app with the following command.
dotnet run

The app will take a few minutes to run, and output:

URL: <your batch account url, Name: <your batch name>, Key: <your batch key>
Creating pool [WinFFmpegPool]...

 

Remember that each node is a VM running Windows 2012 server, with only one CPU and 2 GB of ram. It takes time for the Batch to transfer those Windows VM images from the webhosting Azure Virtual Machine Marketplace, create the VM infrastructure and networking, and finally start each node. This is the most time consuming part of most Batch solutions. A typical Batch workflow doesn’t clean up the pool and its nodes.

ASP.NET web app- ConfigurationBuilder in a web app to retrieve access keys

In this tutorial, you configure an existing ASP.NET web app to retrieve sensitive information, such as connection strings, from your Azure key vault. By using Azure Key Vault, you help protect security information that could otherwise be used by a malicious application to attack your system.

The scenario in this module involves an existing .NET Framework web app that runs on-premises. You start by migrating this application to a web app that’s built by using Azure App Service. Next, you store the secrets that are required by this application to connect to resources such as a database in your key vault. Finally, you configure the application by using a ConfigurationBuilder object that retrieves information from the key vault.

Use the ConfigurationBuilder type

Configuring the app is central to building it in a way that allows its dependencies to vary, based on the environment in which it’s deployed. In .NET Framework apps, the most common mechanism to customize app configuration is through the ConfigurationManager type. You can use a configuration manager to read settings that are often stored in the app.config file for desktop apps, or in the web.config file for ASP.NET web apps.

dot net

One of the challenges with storing configuration settings in this manner is that values to be kept secret often end up being stored as plain text somewhere. This practice creates a security vulnerability. You use the ConfigurationBuilder class to remove this vulnerability.

What is the ConfigurationBuilder class?

The ConfigurationManager approach that’s used by many traditional .NET Framework and ASP.NET web apps allows an administrator to store configuration information as a series of keys and values in a config file. ConfigurationManager has been the primary mechanism to avoid hard-wiring information into an app, and it’s well understood by most ASP.NET developers.

The simplicity of using ConfigurationManager can also be a significant shortcoming. It’s not easy to store and retrieve configuration information in sources other than the config file that’s associated with the app. Large-scale enterprise systems, especially those running in the cloud, frequently need to adapt their configuration dynamically, and they require other sources of configuration information.

Additionally, the config file that’s used by ConfigurationManager is typically held as plain text, although some encryption options are available. Encryption can be cumbersome, and it still requires an encryption key to be stored somewhere. Hard-coding the encryption key in app code isn’t a secure solution, even after the app is compiled. A determined attacker could disassemble your code and read the key. These issues can make using ConfigurationManager an unsuitable mechanism for storing sensitive configuration information, such as passwords and connection strings, unless a great deal of care is taken.

A ConfigurationBuilder object is designed to enable you to retrieve configuration information from a variety of sources.

The ConfigurationBuilder mechanism is an extension of the concepts implemented by the ConfigurationManager class. Rather than restricting configuration information to a strict XML grammar stored in a text file, you can use a variety of configuration builders, based on different configuration sources. A configuration builder provides its own specific means to access data. Currently available configuration builders include:

  • Microsoft.Configuration.ConfigurationBuilders.Environment: Adds settings from the environment variables of the current process
  • Microsoft.Configuration.ConfigurationBuilders.UserSecrets: Adds 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

You can also create your own custom ConfigurationBuilder class if you need to access configuration information that’s held in store and for which no builder is currently available.

The choice as to which configuration builder to use depends on the scenarios and the requirements of an individual app.

How ConfigurationBuilder objects work

An app reads information from a configuration builder object in exactly the same way as it would have previously, by using a ConfigurationManager object. You can continue to use the ConfigurationManager.AppSettings[“settingName”] idiom, and you don’t need to change your app code.

Instead, you provide the details of the configuration builders to use in the app config file on cloud or hosting. You specify which builders to apply to retrieve app settings and connection strings by using the configBuilders attribute of the appSettings and connectionStrings sections in the app config file.

You add ConfigurationBuilder types to the <configBuilders> section in the config. Each builder you add is assigned a name (a string that’s used to reference the builder from elsewhere in the config), and the full type name. Many builders take additional, builder-specific parameters. You also need to add the assembly that processes the <configBuilders> section.

The following example adds the builders for using environment variables and user secrets. The userSecretsId parameter indicates the identity of the user secret that contains the data. At runtime, the builder looks in the secrets.xml file, which is stored in a protected location (%APPDATA%\Microsoft\UserSecrets<userSecretsId>\secrets.xml in Windows), for the value to use:

<configuration>
  <configSections>
    <section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />
    ...
  </configSections>
  <configBuilders>
    <builders>
      <add name="Environment" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
      <add name="Secrets" userSecretsId="c96e0578-6490-4a2d-b6c5-cb2b0baaeae8" type="Microsoft.Configuration.ConfigurationBuilders.UserSecretsConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.UserSecrets, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </builders>
  </configBuilders>
  <appSettings configBuilders="Environment,Secrets">
    <add key="MySecretData" value="" />
    <add key="MyEnvironmentData" value="" />
  </appSettings>
  ...
<configuration>

In this example, when an app retrieves the value of the MySecretData key or the MyEnvironmentData key, the configuration entry is composed of values from the specified sources. The values are listed in the order in which they are stated. Here, entries from Environment are added first, followed by those from Secrets. This means that the last source that has a specified value is surfaced to the user. You can change the search order by switching the sequence in the configBuilders attribute.

Configuration is central to building an app in a way that allows its dependencies to vary, based on the environment in which it’s deployed.

In this post, you learned:

The purpose of the ConfigurationBuilder mechanism, which is to help avoid storing sensitive configuration information in easily accessible locations.
How to use ConfigurationBuilder in a web app to retrieve access keys from your Azure key vault at runtime.

When you’re working in your own subscription, it’s a good idea at the end of a project to identify whether you still need the resources you created. Resources left running can cost you money. You can delete resources individually or delete the resource group to delete the entire set of resources.