In this lab, you will create two .NET Core applications -- an event processor that receives messages from an Azure Event Hub, and a reporting front-end website to display any data received. After these simple applications have been created, you will containerize them by adding the appropriate Dockerfile. To complete the lab, you should be able to show that the two applications are running as containers hosted on your local development system.
This lab expects the following prerequisites have been installed or configured:
- Access to either an Azure Subscription or Resource Group for which you are either in the Owner or Contributor role
- .NET Core Software Development Kit (SDK) version 2.1 or greater
- Installs the
dotnet
command-line interface tools and .NET Core libraries
- Installs the
- Visual Studio Code version 1.25 or greater
- VS Code C# extension
- Provides full support for C# IntelliSense and debugging
- Azure Command-Line Interface (CLI) version 2.0 or greater
- Docker for Windows
- Setup to build Linux Containers
- Ensure daemon is running
- (Optional) VS Code Docker extension
- Provides Dockerfile and docker-compose intellisense
- Docker Explorer to show Images, Containers, and Registries
-
Open a terminal and run:
az login
-
Set the current subscription context. Replace
MyAzureSub
with the name of the Azure subscription you want to use:az account set --subscription MyAzureSub
The following commands will provision the required Event Hubs resources. You will create a new resource group to hold all provisioned resources for easy cleanup. You'll need a storage account to enable the Capture feature on our Event Hub. Finally, you have to create both the namespace and event hub.
Any value below prefixed with my
indicates a value that should be replaced with
an appropriate value specific to you since it may need to be globally unique. Any
values may be replaced as long as the resources are successfully provisioned in
your resource group. Note the AZURE_STORAGE_CONNECTION_STRING
value should be
replaced with the response from the previous command returning the connection
string for the newly created storage account.
Replace myResourceGroup
, myStorage
, myProcessorContainer
,
myNamespace
, and myEventHub
with the appropriate values:
# Create a resource group
az group create --name myResourceGroup --location westus
# Create a general purpose standard storage account
az storage account create --name myStorage --resource-group myResourceGroup --location westus --sku Standard_RAGRS --encryption blob
# Get and save the connection string for container creation
az storage account show-connection-string --name myStorage --resource-group myResourceGroup
# Create the container for event processing (holds lease and checkpoint data)
az storage container create --name myProcessorContainer --connection-string AZURE_STORAGE_CONNECTION_STRING
# Create an Event Hubs namespace
az eventhubs namespace create --name myNamespace --resource-group myResourceGroup -l westus
# Create an event hub
az eventhubs eventhub create --name myEventHub --resource-group myResourceGroup --namespace-name myNamespace
# Get namespace connection string
az eventhubs namespace authorization-rule keys list --resource-group myResourceGroup --namespace-name myNamespace --name RootManageSharedAccessKey
Important: Take note of the namespace
primaryConnectionString
as we will use this value to connect to the event hub to send and receive events. In a non-lab or development environment, you should create a more specific AuthorizationRule rather than use theRootManageSharedAccessKey
.
- Navigate to the tester Event Generator.
- Enter the saved
primaryConnectionString
and yourmyEventHub
name used during provisioning in the Event Hub Settings section. - Leave the last three options blank. This will generate 60 events about every second for about one minute.
- Click the Submit Job button to start the event generation.
- Browse to https://portal.azure.com/. Find your Event Hub by searching for the name of your Event Hub namespace in the search box, selecting the resource.
- Explore the metrics to see that messages have hit your event hub.
Navigate to the directory in which you would like to create your event processor application. Run the following commands to create and run the console application:
dotnet new console -o SongRequest.MessageConsumer
cd SongRequest.MessageConsumer
dotnet run
Our message consumer will be responsible for connecting to our event hub instance and consuming messages from the stream. The required libraries are available in the Microsoft.Azure.EventHubs and Microsoft.Azure.EventHubs.Processor NuGet packages. Run the following commands to add them to your project:
dotnet add package Microsoft.Azure.EventHubs
dotnet add package Microsoft.Azure.EventHubs.Processor
dotnet restore
-
In Visual Studio Code, open the
SongRequest.MessageConsumer
parent directory (since we'll have more than one service) as your workspace:cd .. code .
-
Add another file, SongRequestProcessor.cs to the SongRequest.MessageConsumer directory (where the Program.cs can be found). This file will implement the IEventProcessor to handle consuming events flowing over the event hub.
-
Add the following
using
statements:using Microsoft.Azure.EventHubs; using Microsoft.Azure.EventHubs.Processor; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks;
-
Implement the
IEventProcessor
interface. Add the following implementation:namespace SongRequest.MessageConsumer { public class SongRequestProcessor : IEventProcessor { public Task CloseAsync(PartitionContext context, CloseReason reason) { Console.WriteLine($"SongRequestProcessor shutting down. Partition '{context.PartitionId}', Reason: '{reason}'."); return Task.CompletedTask; } public Task OpenAsync(PartitionContext context) { Console.WriteLine($"SongRequestProcessor initialized. Partition '{context.PartitionId}'."); return Task.CompletedTask; } public Task ProcessErrorAsync(PartitionContext context, Exception error) { Console.WriteLine($"Error on Partition '{context.PartitionId}', Error: {error.Message}"); return Task.CompletedTask; } public Task ProcessEventsAsync(PartitionContext context, IEnumerable<EventData> messages) { foreach (var eventData in messages) { var data = Encoding.UTF8.GetString(eventData.Body.Array, eventData.Body.Offset, eventData.Body.Count); Console.WriteLine($"Message received. Partition: '{context.PartitionId}', Data: '{data}'"); } return context.CheckpointAsync(); } } }
-
Add the following
using
statements to the top of the Program.cs file:using Microsoft.Azure.EventHubs; using Microsoft.Azure.EventHubs.Processor; using System; using System.Threading; using System.Threading.Tasks;
-
Add constants to the
Program
class for the event hub connection string and event hub name:private const string EventHubConnectionString = "REPLACE_WITH_EVENT_HUB_CONNECTION_STRING"; private const string EventHubName = "REPLACE_WITH_MY_EVENT_HUB_NAME"; private const string StorageContainerName = "REPLACE_WITH_MY_PROCESSOR_CONTAINER_NAME"; private const string StorageConnectionString = "REPLACE_WITH_STORAGE_CONNECTION_STRING";
-
Add a new method,
MainAsync
to theProgram
class:private static async Task MainAsync(string[] args) { Console.WriteLine("Registering SongRequestProcessor..."); var eventProcessorHost = new EventProcessorHost( EventHubName, PartitionReceiver.DefaultConsumerGroupName, EventHubConnectionString, StorageConnectionString, StorageContainerName); try { // Register the Event Processor Host and start receiving messages await eventProcessorHost.RegisterEventProcessorAsync<SongRequestProcessor>(); Console.WriteLine("SongRequestProcessor registered."); Console.WriteLine("Receiving. Press CTRL+C to stop worker."); // Prevents this host process from terminating so services keep running Thread.Sleep(Timeout.Infinite); } catch (Exception ex) { Console.WriteLine($"Unexpected exception: {ex.Message}. Stopping processor."); } // Dispose the Event Processor Host await eventProcessorHost.UnregisterEventProcessorAsync(); }
-
Replace the
Main
method implementation with the following line of code:MainAsync(args).GetAwaiter().GetResult();
-
Navigate to the SongRequest.MessageConsumer project root and execute:
dotnet build dotnet run
-
Verify the application is running as expected.
-
Resend messages to the event hub instance from the Event Generator and watch your event processor output.
-
Create a new
Dockerfile
file at the root of the SongRequest.MessageConsumer project. You'll use the base imagemicrosoft/dotnet
with two different tags -- one for building and one for executing. Use the following content for the Dockerfile:FROM microsoft/dotnet:sdk AS build-env WORKDIR /app # Copy only the project file and restore as a distict layer of image COPY *.csproj ./ RUN dotnet restore # Copy all other files and build as a distinct layer of image COPY . ./ RUN dotnet publish -c Release -o out # Build the runtime image FROM microsoft/dotnet:runtime WORKDIR /app COPY --from=build-env /app/out . ENTRYPOINT ["dotnet", "SongRequest.MessageConsumer.dll"]
-
Add a
.dockerignore
file to the project root, and include the following lines to keep your build context as small as possible (so the files in these directories are not copied to the image layers):bin\ obj\
-
Build and run the Docker image by executing the following from the project root directory:
docker build -t songrequest.messageconsumer:dev . docker run -it --name messageconsumer songrequest.messageconsumer:dev
-
Exit the container by hitting the
CTRL+C
key combination.
-
Download or Clone the latest from the GitHub repository awkwardindustries/aks-labs-webfrontendservice to your application root directory. This project contains an ASP.NET Core website using Razor pages.
Note: Although not required to build, this project uses the new Microsoft Library Manager (LibMan) to manage the client-side libraries. A cross-platform LibMan CLI is available. To install, execute:
dotnet tool install --global Microsoft.Web.LibraryManager.Cli --version 1.0.163
Client-side libraries can then be added with commands like
libman install jquery
or restored based on the libman.json file withlibman restore
. -
Examine the
Dockerfile
to see it is almost the same as that used by our .NET Core console application project, but it uses theaspnetcore-runtime
image versus theruntime
image since the former includes the required ASP.NET Core libraries. -
Because this container will host a web server, we'll need to expose a port from the container to the host. In this example, we'll expose port 80 and also map the host's port 80 to that exposed port. You could also use
-p 8080:80
to map the host's port 8080 to the container's port 80, but we'll just use the default. Build and run the Docker image by executing the following from the project root directory:docker build -t songrequest.webfrontend:dev . docker run -d -p 80:80 --name webfrontend songrequest.webfrontend:dev
-
Navigate to http://localhost/ to see the website running from within the container. Although the website should run, there is no backend yet to support it, so any attempt to request songs or display song requests will result in an expected error. Once we have hosted the backend API, we will need to update this container's environment variables by setting the
SONGREQUEST_APISERVICE_BASE_URL
variable to the correct path. This can be done with the docker run command, i.e.,docker run -d -p 80:80 -e SONGREQUEST_APISERVICE_BASE_URL='http://localhost:8000/' songrequest.webfrontend:dev
By completing this lab, you've started with two key components of our system -- an event processor to handle song requests streaming across an Event Hub, and a front end that allows a single request to be created and also shows summary data of most recent requests and top song requests. You should have both of these running successfully in their own containers hosted on your local machine.