How to deploy a .NET 5 API in a Kubernetes cluster

.NET Jun 28, 2021

In this demo, I will explain how to create and deploy a .NET 5 API in a Kubernetes cluster and expose it via a service object.

Create and deploy a .NET 5 application

First, you need to create a new ASPNET Core application using the webapi project template. This template will already contain an example of a controller for a RESTful HTTP service. You can easily do it via dotnet CLI by using:

dotnet new sln
dotnet new webapi -o Training -f net5.0 --no-https
dotnet sln add Training/Training.csproj

Run your application by executing the following command:

dotnet run -p Training\Training.csproj
Building...
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Users\ivanp\Desktop\Training

Important: dotnet run and dotnet build restore the project dependencies by running the command dotnet restore implicitly.

Now that your application is up and running, perform an HTTP GET request to the WeatherForecast controller by executing the following command in a different command prompt instance.

curl http://localhost:5000/WeatherForecast

StatusCode        : 200
StatusDescription : OK
Content           : [{"date":"2021-01-04T14:33:56.2271578+00:00","temperatureC":-3,"temperatureF":27,"summary":"Sweltering"},{"date":"2021-01-05T14:33:56.2271636+00:00","temperatureC":11,"temperatureF":51,"summary":"Mild...
RawContent        : HTTP/1.1 200 OK
                    Transfer-Encoding: chunked
                    Content-Type: application/json; charset=utf-8
                    Date: Sun, 03 Jan 2021 14:33:55 GMT
                    Server: Kestrel
[{"date":"2021-01-04T14:33:56.2271578+00:00","temper...
Forms             : {}
Headers           : {[Transfer-Encoding, chunked], [Content-Type, application/json; charset=utf-8], [Date, Sun, 03 Jan
                    2021 14:33:55 GMT], [Server, Kestrel]}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 506

Docker

It’s now time to take care of the containerization of your brand-new API using Docker. First, create the following files in the main project folder:

  • dockerfile: A text document that contains all the commands to assemble an image.
  • dockerignore: A list of files and directories to ignore while building an image.

Create a dockerignore file

Whenever you build or run a C# project, the compiler generates a bin and obj folder. To reduce the Docker image size, you should ignore these folders. To do so, copy and paste the following lines into your .dockerignore file.

/bin/
/obj/

Create a docker file

One of the biggest challenges of using Docker is to keep the image size down. For this reason, it is best practice to use a multi-stage Docker image where possible. In this demo, we will first use the official .NET SDK base image to compile and publish the application in a specific path. Then we’ll use the ASPNET base-image as a base image to run the previously published application. The result is the following dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /app
COPY Training/Training.csproj .
RUN dotnet restore Training.csproj
COPY ./Training .
RUN dotnet build Training.csproj -c Release -o /out
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
WORKDIR /app
COPY --from=build /out ./
ENTRYPOINT ["dotnet", "Training.dll"]

Now, build the Docker images from a Dockerfile by executing the following command:

docker build -t training:1.0 .
Sending build context to Docker daemon  6.137MB
Step 1/10 : FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
5.0: Pulling from dotnet/sdk
2b7f697bab3b: Pull complete
5aad8eff7cd0: Pull complete
a8cda85294c0: Pull complete
8030de19a086: Pull complete
9a508d3cf57d: Pull complete
b7df41993415: Pull complete
827aef11a71a: Pull complete
79cbe6ffac82: Pull complete
f9a5febdd0cc: Pull complete
1f3e6da4c5ab: Pull complete
a071c3f1f25c: Pull complete
f372753436ea: Pull complete
516f89826fe7: Pull complete
cc57f44d8884: Pull complete
05ce89b18c43: Pull complete
892785a1b514: Pull complete
3438f78e8199: Pull complete
0db58ad456b3: Pull complete
Digest: sha256:e6df303a21bec0b8086f3c3208bd6a4680287bb3105169f44efc8a7fd5dc73fd
Status: Downloaded newer image for mcr.microsoft.com/dotnet/sdk:5.0
 ---> a7ea5cafba51
Step 2/10 : WORKDIR /app
 ---> Running in 67efb877a5e6
Removing intermediate container 67efb877a5e6
 ---> 3ecb9ae86097
Step 3/10 : COPY Training/Training.csproj .
 ---> 602a96996a07
Step 4/10 : RUN dotnet restore Training.csproj
 ---> Running in d4f4c118c057
  Determining projects to restore...
  Restored C:\app\Training.csproj (in 10.5 sec).
Removing intermediate container d4f4c118c057
 ---> f0e818f60ec3
Step 5/10 : COPY ./Training .
 ---> 7651207dd70f
Step 6/10 : RUN dotnet build Training.csproj -c Release -o /out
 ---> Running in 894fbbd71328
Microsoft (R) Build Engine version 16.8.0+126527ff1 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
  All projects are up-to-date for restore.
  Training -> C:\out\Training.dll
Build succeeded.
    0 Warning(s)
    0 Error(s)
Time Elapsed 00:00:06.79
Removing intermediate container 894fbbd71328
 ---> b7518bbc9f65
Step 7/10 : FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
5.0: Pulling from dotnet/aspnet
2b7f697bab3b: Already exists
5aad8eff7cd0: Already exists
a8cda85294c0: Already exists
8030de19a086: Already exists
9a508d3cf57d: Already exists
b7df41993415: Already exists
827aef11a71a: Already exists
79cbe6ffac82: Already exists
f9a5febdd0cc: Already exists
1f3e6da4c5ab: Already exists
Digest: sha256:fd981768f411e491d1e0e64f82da17755759e703fd55f1130802559bf2adc057
Status: Downloaded newer image for mcr.microsoft.com/dotnet/aspnet:5.0
 ---> d2ed76eaf86f
Step 8/10 : WORKDIR /app
 ---> Running in 7ea5f549e9cd
Removing intermediate container 7ea5f549e9cd
 ---> 11fff6b6ed0d
Step 9/10 : COPY --from=build /out ./
 ---> a3f754716180
Step 10/10 : ENTRYPOINT ["dotnet", "Training.dll"]
 ---> Running in f4ce012dd607
Removing intermediate container f4ce012dd607
 ---> 9e120bd85c97
Successfully built 9e120bd85c97
Successfully tagged training:1.0

Once the build has finished downloading the base images and had copied your application’s files, create and start the container by executing the following command:

docker run -p 8001:80 training:1.0 .
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\app

Then, perform an HTTP GET request to the WeatherForecast controller by executing the following command in a different command prompt instance.

curl http://localhost:8001/WeatherForecast
StatusCode        : 200
StatusDescription : OK
Content           : [{"date":"2021-01-04T18:30:27.2267279+00:00","temperatureC":31,"temperatureF":87,"summary":"Mild"},{"date":"2021-01-05T18:30:27.226738      4+00:00","temperatureC":20,"temperatureF":67,"summary":"Sweltering...
RawContent        : HTTP/1.1 200 OK
                    Transfer-Encoding: chunked
                    Content-Type: application/json; charset=utf-8
                    Date: Sun, 03 Jan 2021 18:30:26 GMT
                    Server: Kestrel
[{"date":"2021-01-04T18:30:27.2267279+00:00","temper...
Forms             : {}
Headers           : {[Transfer-Encoding, chunked], [Content-Type, application/json; charset=utf-8], [Date, Sun, 03 Jan 2021 18:30:26 GMT], [Server,
                    Kestrel]}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 501

Kubernetes

Now that your Docker image is ready and tested, you can move forward with the creation of its deployment on your Kubernetes cluster.

Create a deployment

Create a YAML file and paste the following configuration into it.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: training-deployment
spec:
  selector:
    matchLabels:
      app: training
      version: v1
  replicas: 3
  template:
    metadata:
      labels:
        app: training
        version: v1
    spec:
      containers:
      - name: training-container
        image: training:1.0
        ports:
        - containerPort: 80

Let’s take a minute to discuss the properties in detail. You can split the document into three sections:

  1. General properties
  2. Deployment specification and metadata
  3. Pods/Replicas specification and metadata

In the general section, you can find the following:

  • kind: Specifies the object to create (for example a service, secret, pod, replicationController, etc.)
  • apiVersion: Specifies the API version to use for the specified kind.

In the deployment section, you are going to define all those properties that are deployment-specific:

  • replicas: Specifies the number of replicas of your application that will be created and distributed across the available worker nodes.
  • template: Definition of the objects that are going to be replicated

Last but not least, we have pods and replicas properties:

  • container: Define the container’s characteristics like name, image, and port to be exposed, etc.

To create a deployment from the YAML file, execute the following command:

kubectl create -f deployment.yaml
deployment.apps/training-deployment created

Once completed, you can verify that your deployment is up and running by using the command below:

kubectl get deployment
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
training-deployment   1/1     1            1           18s

If you want to inspect the pods created during the creation of the deployment, you can use the following code:

kubectl get pods -o wide
NAME                                   READY   STATUS    RESTARTS   AGE   IP          NODE             NOMINATED NODE   READINESS GATES
training-deployment-7446bffdc5-7npnf   1/1     Running   0          44s   10.1.0.38   docker-desktop   <none>           <none>
training-deployment-7446bffdc5-9hs52   1/1     Running   0          44s   10.1.0.36   docker-desktop   <none>           <none>
training-deployment-7446bffdc5-k4t84   1/1     Running   0          44s   10.1.0.37   docker-desktop   <none>           <none>

Create a service

As mentioned in the previous article, these pods only have an internal IP address for the cluster and are unreachable from outside. To expose these pods to the outside world, you have to create a service. Create a YAML file and paste the following configuration into it:

apiVersion: v1
kind: Service
apiVersion: v1
metadata:
  name: training-service
spec:
  selector:
    app: training
  type: LoadBalancer
  ports:
    - name: training-port
      port: 8080
      targetPort: 80

The service has a selector that specifies the label training. This means that each pod that has this label will become a member of this service. To create a service, execute the following command:

kubectl create -f service.yaml
service/training-service created

Now verify that your service is working correctly.

kubectl get service -o wide
NAME               TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE   SELECTOR
kubernetes         ClusterIP      10.96.0.1        <none>        443/TCP          68m   <none>
training-service   LoadBalancer   10.110.255.200   localhost     8080:32474/TCP   47m   app=training

As you can see, the service will expose port 8080 and forward the request to the pods whose selector contains the label app=training.

Horizontal Scaling

When your application records an increase in traffic, you can quickly scale horizontally and increase the number of replicas by executing this command:

kubectl scale --replicas=6 deployment/training-deployment
deployment.apps/training-deployment scaled

The results will be:

kubectl get pod
NAME                                   READY   STATUS    RESTARTS   AGE
training-deployment-7446bffdc5-426z4   1/1     Running   0          4s
training-deployment-7446bffdc5-4rswm   1/1     Running   0          4s
training-deployment-7446bffdc5-7npnf   1/1     Running   0          25m
training-deployment-7446bffdc5-9hs52   1/1     Running   0          25m
training-deployment-7446bffdc5-k4t84   1/1     Running   0          25m
training-deployment-7446bffdc5-l82gl   1/1     Running   0          4s

Cleaning up

Enter the following commands to delete the services and deployments:

kubectl delete services training-service
kubectl delete deployment training-deployment

Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.