Delen via


Playing with Containers in Azure App Service

Talking about containers is getting old by now, but in the light of my last article on deploying to Docker Hub via Azure DevOps I still thought it worthwhile to revisit. (Available here: https://blogs.msdn.microsoft.com/azuredev/2019/01/07/setting-up-a-docker-containers-infrastructure-with-azure-devops/)

You might be able to grasp the beauty of packaging code to Docker images and then deploy a number of containers, but you still need somewhere to actually run these containers. The first choice of this author is usually Azure, but which service in Azure? You have more than one way to skin the proverbial cat in Azure.

I'm not going to cover all these options within this post, but will instead focus on Azure App Service for now. Turns out there are a couple of options inside that offering as well.

Let's cover a basic scenario. We have a setup where we create a frontend and a backend, and want them to communicate with each other. I'm leaving the Docker Hub deployment out of this post - I'll show the Azure parts and assume the images are retrievable.

Start by creating an empty solution in Visual Studio:

Then add a web app:

Add a Dockerfile.CI (if you want to pipe it through Azure DevOps):

 
FROM microsoft/dotnet:2.2-sdk AS build-env
WORKDIR /app

# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

# Build runtime image
FROM microsoft/dotnet:2.2-aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "FooFrontend.dll"]

After testing and building locally you should make sure it ends up as an image you can retrieve. For the purposes of this example I have pushed it to https://hub.docker.com/r/ahelland/hellofoo-frontend.

Create a web app in Azure:

You will get three options - let's start with Single Container:

Startup file should match your ENTRYPOINT in your Dockerfile.

Open the web app to verify it worked:

That was just pure boilerplate code though. Let's add some text like "Hello from Docker". For instance we could edit index.cshtml to contain a div like this:

 
<div class="text-center">
    Hello from Docker!<br />
</div>

That's fairly static though, and we want this to be configurable. Using the standard mechanisms in .NET Core we create a setting in appsettings.json:

 
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "UI_Texts": {
    "Greeting": "Hello from Docker!",
  }
}

And edit index.cshtml accordingly:

 
@page
@using Microsoft.Extensions.Configuration;
@inject IConfiguration configuration;
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    Greeting: @configuration.GetSection("UI_Texts")["Greeting"]<br />
</div>

This works, but it persisted in the Docker image - what if we want to change it? And what if this was just a placeholder value specifically because we don't want to embed secrets in the image?

If we go back to the cloud (after checking in the code and pushing to Docker Hub) we can add an application setting in Azure App Service:

Do note that the hierarchical definition is different for containers than non-containerized app service instances. The format for non-container is parent.child whereas containers require parent__child (double underscore).

This modifies the environment settings in for the host, and Docker picks up on this. A quick refresh of the browser and it should have changed (no redeploy necessary):

That's quite a nice way to handle configuration. No touching of the file system necessary.

I initially said that I wanted to do a frontend vs backend setup, but this just covered a frontend. The next step would be adding a backend:

And we need a Dockerfile.CI for this project too.

 
FROM microsoft/dotnet:2.2-sdk AS build-env
WORKDIR /app

# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

# Build runtime image
FROM microsoft/dotnet:2.2-aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "FooBackend.dll"]

While this API doesn't do much we would still like to print out the result of an API-call. Let's use the same approach as before - add a setting to appsettings.json:

 
"UI_Texts": {
    "Greeting": "Hello from Docker in the cloud!"
  },
"API" {
    "api_address": "https://localhost:1234/api/values"
  }

And add it to index.cshtml:

 
@page
@using Microsoft.Extensions.Configuration;
@using System.Net.Http;
@inject IConfiguration configuration;
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

@{
    using (var client = new HttpClient())
    {
        var apiAddress = configuration.GetSection("API")["api_address"];
        string requestUrl = apiAddress;
        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
        HttpResponseMessage response = client.SendAsync(request).Result;
        var responseString = response.Content.ReadAsStringAsync().Result;
        ViewBag.result = responseString;
    }
}

<div class="text-center">
    Greeting: @configuration.GetSection("UI_Texts")["Greeting"]<br />
    Result of API call: @ViewBag.result;
</div>

You'll notice I needed to add some inline code to execute the http call, and then I pass it back via the ViewBag.

Compile, test, check in and all that jazz. I posted this image to: https://hub.docker.com/r/ahelland/hellofoo-backend

We'll want to move back to the Azure Portal to sort this out, but there are a couple questions to be answered first.

We used "Single container" in the first round, and this doesn't sound like one container?
True, and that's why there is an option for Docker Compose, as the purpose of that is specifically setting up more than one container and in some cases a relationship between containers.

Can we still use the application settings in the portal to set the config for Docker-compose?
No, we can't. But is is possible to do via environment settings in the Docker config. (See below.)

Well, what do I use for the address - clearly "localhost" doesn't sound right?
And here is another beauty about containers; you just refer to it by name and let Docker handle the rest. (Also see below.)

 
version: '3.4'

services:
  foofrontend:
    image: ahelland/hellofoo-frontend:latest
    environment:
      - UI_Texts__Greeting="Hello from Docker-Compose"
      - API__api_address=https://foobackend/api/values
      - ASPNETCORE_URLS=https://+:80
    ports:
      - "59372:80"
  foobackend:
    image: ahelland/hellofoo-backend:latest
    environment:
      - ASPNETCORE_URLS=https://+:80
    ports:
      - "59376:80"

This might actutally take a minute or two to fall in place, but if everything went as planned the web page should look roughly like this:

I have noticed that there are sometimes hangups in the UI where you need to refresh to make a new Docker-Compose "stick", but that's probably a preview thing.

This approach works the way we want it for our little setup, but be aware that there are some limitation to this:
- You can only expose one container on the internet; in this case the frontend (it comes first in the Docker-Compose file). The API is only available inside the container network.
- If you want/need fine-grained control of ports you will notice that Azure App Service does some magic behind the scenes for you which may or may not be ok for you.
- Troubleshooting when it doesn't work isn't all that easy by default, meaning that you might want to make sure you log extra info to the console (which should be visible in the portal)

If you want/need more complicated container setups it might be that Azure App Service isn't right for you. Maybe you need Kubernetes or something. That is outside the scope of this post - this post is just to show some of the thing a web apps can do for you.