Author avatar

Stefan Prodan

Scale ASP.NET Core Apps with Docker Swarm Mode

Stefan Prodan

  • Jan 10, 2019
  • 14 Min read
  • 26,051 Views
  • Jan 10, 2019
  • 14 Min read
  • 26,051 Views
Microsoft.NET

Docker Swarm Mode

Starting with Docker 1.12, the Docker Engine comes with a built-in container orchestration module called Docker Swarm mode. The newest Docker Swarm mode is exciting because you no longer need to deal with complex network configurations, load balancers, service registries or certificate generation and management. The new Docker Engine makes multi-host and multi-container orchestration easy, with just a few commands you can setup a production environment for your applications that can surviving hardware failure and handle software updates with zero downtime.

With ASP.NET Core's release, .NET developers will switch to containers as the default deployment model, this article will show you how easily you can build, deploy and scale an ASP.NET Core app using Docker.

Prerequisites

I'm using a Windows 10 machine for development but you can follow this tutorial on a Mac or Linux device using Visual Studio Code and Docker.

Windows 10 prerequisites:

Build the app

We start by creating a very simple ASP.NET Core Web API project. In Visual Studio, go to "File > New Project > .NET Core > ASP.NET Core Web Application". Select Web API from the ASP.NET Core templates. Let's name this app TokenGen.

After the projects is created, go to the Controllers folder and add a new controller named TokenController with the following content:

1namespace TokenGen.Controllers
2{
3    [Route("api/[controller]")]
4    public class TokenController : Controller
5    {
6        [HttpGet]
7        public dynamic Get()
8        {
9            return new
10            {
11                Guid = Guid.NewGuid().ToString(),
12                Expires = DateTime.UtcNow.AddHours(1),
13                Issuer = Environment.MachineName
14            };
15        }
16    }
17}
cs

This code generates a new GUID (Globally Unique Identifier) on every GET call stating the expiration date and the issuer. We use the machine name to identify the issuer. When you run an ASP.NET Core app inside a container, the machine name gets populated with the container-unique identifier. As a result, our code will help us determine in which container our app is running.

Container-ize the app

The next step is to create a docker file, so we can build our app into a container image. Inside the root directory, next to the .sln file, create a file named TokenGen.dockerfile with the following content:

1FROM microsoft/dotnet:latest
2
3# Set environment variables
4ENV ASPNETCORE_URLS="http://*:5000"
5ENV ASPNETCORE_ENVIRONMENT="Development"
6
7# Copy files to app directory
8COPY /src/TokenGen /app
9
10# Set working directory
11WORKDIR /app
12
13# Restore NuGet packages
14RUN ["dotnet", "restore"]
15
16# Build the app
17RUN ["dotnet", "build"]
18
19# Open port
20EXPOSE 5000/tcp
21
22# Run the app
23ENTRYPOINT ["dotnet", "run"]
bash

Now we are ready to build our tokengen-img docker image. Open PowerShell, navigate to your project root directory and execute the build command:

1docker build -t tokengen-img -f TokenGen.dockerfile .
bash

At this point the Docker engine will pull the dotnet:latest image from Docker Hub, copy /src/TokenGen files and run the dotnet CLI commands, you will see in the PS output the build log.

If everything worked out, we are ready to run our app from a container for the first time. Start the TokenGen container using the following bash command:

1docker run --name tokengen -d -p 5000:5000 -t tokengen-img
bash

Let's test the deployment by calling the api/token endpoint from Powershell:

1Invoke-RestMethod http://localhost:5000/api/token
bash

You should get a response like this:

1guid                                 expires                     issuer      
2----                                 -------                     ------      
390c629af-3d78-4b53-81b0-4be563985887 2016-08-09T14:21:11.326422Z 5d0a1f82371c
bash

If you also run docker ps you will notice that the tokengen container ID is the same with the issuer value from the api/token response.

We can now stop and delete the container by using the following commands:

1docker stop tokengen
2docker rm tokengen
bash

Scale the app

In order to scale our app we first need to enable Docker Swarm mode; do this by running docker swarm init in PowerShell.

Now that Docker Swarm mode is enabled, we will create a task for the swarm and start our app as a service. The service command is similar to the docker run command. You should name your service the same way we named our container earlier, so it's easy to target the service using scale.

Create and start our new tokengen service on Docker Swarm:

1docker service create --publish 5000:5000 --name tokengen tokengen-img
bash

Now if we run docker service ls, we can check if our service is running:

1ID            NAME      REPLICAS  IMAGE         COMMAND
296lqd9bdvwhs  tokengen  1/1       tokengen-img  
bash

And if we call the api/token endpoint from PowerShell with Invoke-RestMethod http://localhost:5000/api/token we get the same result:

1guid                                 expires                  issuer      
2----                                 -------                  ------      
340afe497-ce14-4f65-936e-910c1490165a 2016-08-09T14:46:15.506Z df5197d48d32
bash

It's time to scale our app. The scale command lets us run replicas of our application.

1docker service scale tokengen=3

Running docker ps will show 3 tokengen containers:

1CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                    NAMES
2a87acd74a274        tokengen-img:latest   "dotnet run"             41 seconds ago      Up 35 seconds       5000/tcp                 tokengen.1.ayuux8p9ztip5uyappics17i7
3debcdb4c7e9d        tokengen-img:latest   "dotnet run"             41 seconds ago      Up 35 seconds       5000/tcp                 tokengen.2.7gh3immv1tu5as0w4ps4709ib
4df5197d48d32        tokengen-img:latest   "dotnet run"             8 minutes ago       Up 8 minutes        5000/tcp                 tokengen.3.2g61t441iyuyehoafoelvkbik
bash

Now we can see the Docker Swarm built-in load balancer at work. Open 3 PowerShell windows and run Invoke-RestMethod http://localhost:5000/api/token:

1guid                                 expires                     issuer      
2----                                 -------                     ------      
30b36bb43-20f0-4183-a364-0c83894e79ac 2016-08-09T14:55:02.673038Z a87acd74a274
4b74b4808-237e-406a-8a8b-f580dcc78225 2016-08-09T14:55:02.68909Z  debcdb4c7e9d
592dd2f7b-e47d-4b1d-8192-141d307d276d 2016-08-09T14:55:02.691565Z df5197d48d32
bash

As you can see the load balancer distributes our calls to all 3 replicas. Docker Swarm uses a round-robin system to load balance between the containers.

We can also scale down our service or even stop it by scaling to 0:

1docker service scale tokengen=0
bash

We can remove the service in the same way we deleted the container (using rm):

1docker service rm tokengen
bash

Update the app

With Docker Swarm you can apply rolling updates to a service without disruption. In order to publish updates we first need to version our app.

Open project.json and add the version element "version": "1.0.0-*". Now we need to use the version number to tag the Docker image.

In the project root, next the .sln file, create a PowerShell script swarm-ver-deploy.ps1 with the following content:

1$serviceName = "tokengen"
2
3# parse project.json and extract app version
4$rootPath = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
5$projectPath = "$rootPath\src\TokenGen\project.json"
6$json = Get-Content -Raw -Path $projectPath | ConvertFrom-Json
7$version = $json.version.Split("-")[0]
8
9# tag docker image with app version
10$imageName = "$serviceName-img:$version"
11
12# build image
13if(docker images -q $imageName){
14    "Image $imageName exists!"
15    return 
16}else{
17    docker build -t $imageName -f "$rootPath\TokenGen.dockerfile" $rootPath
18}
19
20# create service
21docker service create --publish 5000:5000 --name $serviceName --replicas 3 --update-delay 5s $imageName
bash

This script reads the version from project.json, builds the docker image tokengen-img:version, and creates the tokengen service.

Note that we are using update-delay parameter to configure the swarm with a 5 second update delay, so the new service instances have time to start up.

If we run swarm-ver-deploy.ps1 then docker ps we get the following:

1CONTAINER ID        IMAGE                COMMAND                  CREATED              STATUS              PORTS                    NAMES
2223a898ee96b        tokengen-img:1.0.0   "dotnet run"             About a minute ago   Up About a minute   5000/tcp                 tokengen.3.8ugt7nogpqczkorm7ehdvw2nb
30b2d5e3b7288        tokengen-img:1.0.0   "dotnet run"             About a minute ago   Up About a minute   5000/tcp                 tokengen.2.creqhebrccauxmiwm2iaz6fxt
49edb15886b9a        tokengen-img:1.0.0   "dotnet run"             About a minute ago   Up About a minute   5000/tcp                 tokengen.1.a3ufm7s7iul5nouidrjt38tkg

We can now create another script that will publish new versions. Name this script swarm-ver-update.ps1:

1$serviceName = "tokengen"
2
3# parse project.json and extract app version
4$rootPath = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
5$projectPath = $rootPath + "\src\TokenGen\project.json"
6$json = Get-Content -Raw -Path $projectPath | ConvertFrom-Json
7$version = $json.version.Split("-")[0]
8
9# tag docker image with app version
10$imageName = "$serviceName-img:$version"
11
12# build image
13if(docker images -q $imageName){
14    "Image $imageName exists!"
15    return 
16}else{
17    docker build -t $imageName -f "$rootPath\TokenGen.dockerfile" $rootPath
18}
19
20# apply update
21docker service update --image $imageName $serviceName
bash

Like the deploy script, swarm-ver-update.psl reads the version from project.json and builds the new image. But instead of creating a service, we use service update command to specify the new image.

If you change the project version to 1.0.1-* and run swarm-ver-update.ps1 then docker ps output will be:

1CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                    NAMES
2728cb9538f16        tokengen-img:1.0.1   "dotnet run"             8 seconds ago       Up 3 seconds        5000/tcp                 tokengen.2.e9cyto0nuwmtdfo02tij3z0m9
331dfc6b8d626        tokengen-img:1.0.1   "dotnet run"             17 seconds ago      Up 13 seconds       5000/tcp                 tokengen.3.e1qxbp7c2496pmuefjq4dhrok
4e88d06710e20        tokengen-img:1.0.1   "dotnet run"             27 seconds ago      Up 22 seconds       5000/tcp                 tokengen.1.7p4gsxocbfa141uwuwedpfvut
bash

As you can see all service replicas have been updated to v1.0.1. If you would run docker ps while the update was running you would see that the v1.0.1 was gradually applied.

Set up a reverse proxy

Diagram

The Kestrel web server is not meant to be an internet-facing server. For this task, we can use IIS on Windows or NGNIX/HAProxy on Linux.

Using a reverse proxy in front of the Docker Swarm load balancer offers a lot of advantages. I will enumerate those I find most useful:

Let's assume you want to test on your local machine the TokenGen app with SSL.

In IIS, create a new website named tokengen-rp and bind port 80 and 443 to tokengen.local. You can use the IIS Express Development certificate or generate a new one from IIS. Edit the windows hosts file and map tokengen.local to ::1.

Download and install IIS application request routing module.

In order to configure reverse proxy for TokenGen app, execute the following steps:

  • In the tokengen-rp site settings click on "URL Rewrite" icon
  • Right-click on the "inbound rules list"
  • Select "Add Rule(s)"
  • Choose "Reverse proxy" from the rule templates
  • Enter 10.0.75.2:5000 as the server name where HTTP requests will be forwarded
  • Check "Enable SSL Offloading"

Note that 10.0.75.2 is the default IP for the Docker MobyLinuxVM that's running on your local machine.

Now we can test our TokenGen app from PowerShell over SSL:

1[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
2Invoke-RestMethod https://tokengen.local/api/token
bash

Conclusion

I hope I've proven how easy and straightforward scaling an application can be with Docker Swarm mode. You can find the TokenGen project and all PowerShell script on GitHub in this public repository.

In the articles to come, I plan to use a NoSQL database to store and retrieve tokens, thereby showcasing Docker networks and container linking.

You can also follow me on twitter @stefanprodan