Deploy a .Net application on Kubernetes

Deploy a .Net application on Kubernetes

Recently I had to figure out how to deploy an Asp .Net application on Kubernetes. There were a lot of things to learn and hence I decided to document the process. More importantly this guide is specifically for those using WSL and Minikube.

Also, this could be a great project to add to your resume or portfolio if you're looking to get into DevOps.

This article does not focus much on the concepts of Kubernetes instead it guides you through deploying the application. However, it explains almost every command in detail. The aim is that even a beginner should be able to follow along.

There are tons of resources available on Docker and Kubernetes, please check them out if you're an absolute beginner.

You can also check out these blogs on Docker that I had written previously.

https://heloise.hashnode.dev/running-an-app-via-docker-container

https://heloise.hashnode.dev/multi-container-apps-with-docker-network

https://heloise.hashnode.dev/a-brief-guide-to-docker-volume

Platform used: Windows 10 with WSL2 with Ubuntu

Tools: VsCode, Docker, Minikube & Kubectl

NOTE: Working with WSL could be tricky. There could be some errors while trying out this project. However, most of the errors have already been faced and resolved by others. Googling these could be your best bet.

What is Kubernetes?

Docker is used for creating containers for our application be it frontend, backend or database. An application can have multiple containers, and managing each of these would be an arduous task. Here is where Kubernetes comes in.

In the simplest form, Kubernetes or K8s is a tool that creates and manages Docker Containers.

How does Kubernetes create containers?

The Kubernetes cluster will have one or more nodes. On each of these nodes, the containers are deployed.

The image for the Docker container is specified in a Kubernetes deployment file. This image is then used to start up the containers.

The first step is to build the Docker Images required for the containers. In this example, we need three containers Asp. Net, PostgreSQL and Adminer. We will create a custom Dockerfile for the .Net app container and use the official PostgreSQL, Adminer images for the database.

Next, we should test if our application works by connecting it to the Database container. For this, we have used the docker-compose file.

Once the Docker containers are verified and working, we can deploy on Kubernetes. For this, we first need to create a cluster using Minikube.

Next, we create Kubernetes deployment files for the .Net app and database, using the docker images we just created.

What is Minikube?

Well, minikube is used to deploy the Kubernetes cluster on your local machines.

Once we have minikube running, it will create a single-node cluster. On this node, we can deploy our containers.

Building the Dockerfile

  1. Open WSL terminal

  2. Create a folder called Projects (*optional)

     mkdir Projects
    
  3. Switch to the new folder

     cd Projects
    
  4. Clone sample git repository

     git clone https://github.com/ajeetraina/students-database-dotnet-docker
    
  5. Switch to the cloned folder

     cd students-database-dotnet-docker
    
  6. Open the project in VS Code for ease of editing. You can do this manually or by using the following command

     code .
    
  7. You can use the provided Dockerfile or create it yourself to test your Docker skills!

    WORKDIR: sets the current directory where all further commands will be executed

    RUN dotnet build -o /app: is a .Net command to create the build files i.e myWebApp.dll

    -o specifies the path where the files will be stored

    Similarly, the publish command creates files for deployment.

    ENV ASPNETCORE_URLS=http://+:5000: we need to set the application URL manually.

     FROM mcr.microsoft.com/dotnet/sdk:6.0 #specify the base image to be used
     COPY . ./src # copy project contents from current dir to src dir
     WORKDIR /src # sets src dir as the current dir      
     RUN dotnet build -o /app # build app and store output in app dir
     RUN dotnet publish -o /publish #publish app & store output in publish dir
     WORKDIR /app #sets app dir as the current dir
     ENV ASPNETCORE_URLS=http://+:5000 # set the application URL manually
     EXPOSE 5000 # expose the container port
     CMD ["./myWebApp"] # command to start the .Net app using dll file
    
  8. In the WSL terminal build the docker image

     docker build . -t students-database-dotnet-docker-app:latest
    
  9. Open localhost:5000 in the browser, you should see a screen like below

  10. Stop & Delete the running container

    #run each command individually
    docker ps // this will give you the container id
    docker stop container_id
    docker rm container_id
    
  11. Delete the image using rmi command as shown

    docker rmi -f students-database-dotnet-docker-app
    

Create the Docker Compose file

  1. Now that our Dockerfile is correct let us create the Docker Compose file, which includes the database container.

    NOTE: I have updated the file as per my understanding you can use it as given.

    Database images and setup guide.

    Volumes are used for data persistence. Here I have used a named volume.

     services:
      db: # Databse container name
        image: postgres # container image
        restart: always
        environment:
          POSTGRES_PASSWORD: example
        volumes:
          - postgres-data:/var/lib/postgresql/data
    
      adminer: # Databse management container name
        image: adminer
        restart: always
        ports:
          - 8080:8080 #host port:conatiner port
    
      app: # Application container name
        build: ./  #specify path to Dockerfile
        ports:
          - 5000:5000 #host port:conatiner port
        depends_on:
          - db
    
     volumes:
      postgres-data:
    
  2. Create the containers in the compose file using the following command, where -d runs the container in detached mode

     docker compose up -d
    
  3. NOTE: First open localhost:5000 this creates the database my_db then open localhost:8080 and login with the credentials.

    These credentials are specified in the appsettings.json file.

     {
        "Logging": {
            "LogLevel": {
                "Default": "Information",
                "Microsoft": "Warning",
                "Microsoft.Hosting.Lifetime": "Information"
            }
        },
        "AllowedHosts": "*",
        "ConnectionStrings": {
            "SchoolContext": "Host=db;Database=my_db;Username=postgres;Password=example"
        }
     }
    
  4. Open localhost:5000 in the browser, you should see a screen like below

  5. Open localhost:8080

  6. Once you have logged in, this screen will be visible. Here you can add student records, and the name will be displayed here localhost:5000

  7. Stop the containers

     docker compose down
    
  8. Delete images like so

     #run each command individually
     docker rmi -f students-database-dotnet-docker-app
     docker rmi -f adminer
     docker rmi -f postgres
    

Deploy on Kubernetes using Minikube

NOTE: We won't be using any Docker Image Repository here. It is relatively easy and is also common practice. If using an Image from the repository, specify the Image URL in Container Specification in the app deployment file.

Overview when using an Image registry (eg. Docker Hub)

1. Build image
2. Create Docker Hub Account
3. Login to docker hub from cli
4. Push image to docker hub
5. Run docker container using image from docker hub (to test if all works fine)
6. In Kubernetes deployment file specify this same image in the container specification section
  1. Start Minikube Cluster

     minikube start
    
  2. Point the local Docker daemon to the minikube internal Docker registry. This is so that we can use a locally built Docker image in the deployment file. As mentioned above you can opt for using some docker image registry instead.

    (This step will be omitted in case you use an image registry.)

     eval $(minikube -p minikube docker-env)
    
  3. Build the docker image. This image will be used in the app deployment file.

    (This step will also be omitted in case you use an image registry.)

     docker build . -t students-database-dotnet-docker-app:latest
    
  4. Create the app-deploy.yml file as shown.

    Refer here for Kubernetes deployment syntax & Service syntax. Generally, we refer to K8s documentation for the format and modify images, container names, ports, replicas or any other relevant details in the sample YAML file.

    Also, another good practice is to have the deployment and service in a single YAML file.

     apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: app-deploy # name of deployment
       namespace: default
     spec:
       replicas: 1 # specify number of replicas
       selector:
         matchLabels:
           app: app # this selector label should be the same accross the         deployment and service files.
       template:
         metadata:
           labels:
             app: app  # same selector label as deployment
         spec: # container specifications
           containers:
             - name: students-database-dotnet-docker-app 
               image: students-database-dotnet-docker-app:latest
               imagePullPolicy: IfNotPresent 
               ports:
                 - containerPort: 5000
    
     ---   # separates deployment and service
     apiVersion: v1
     kind: Service
     metadata:
       name: app-service # name of service
     spec:
       type: NodePort # since this service has to be accessible externally
       selector:
         app: app # same selector label as deployment
       ports:
         - nodePort: 30001 # any port in the range 30000-32768
           port: 5000 #container port
           targetPort: 5000 
     # A Service can map any incoming port to a targetPort. By default and for convenience, the targetPort is set to the same value as the port field.
    

    Observe how we have used the locally built image for the container specification. Also, specify the imagePullPolicy as IfNotPresent (pulls the image from the repository only if it's not available locally)

    NodePort: used when a service needs to be accessed externally we specify the type as NodePort

  5. Similarly, create the other deployment files.

    db-deploy.yaml

     apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: db-deploy
       namespace: default
     spec:
       replicas: 1
       selector:
         matchLabels:
           app: pgdb
       template:
         metadata:
           labels:
             app: pgdb 
         spec:
           containers:
             - name: postgres
               image: postgres
               env:
                 - name: POSTGRES_PASSWORD
                   value: example
               ports:
                 - containerPort: 5432
    
     ---
     apiVersion: v1
     kind: Service
     metadata:
       name: db-service 
     spec:
     # default type ClusterIP is used since database should not be accessible externally
       selector:
         app: pgdb
       ports:
         - port: 5432
           targetPort: 5432
    
  6. admin-db-deploy.yaml

     apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: admin-deploy
       namespace: default
     spec:
       replicas: 1
       selector:
         matchLabels:
           app: admin
       template:
         metadata:
           labels:
             app: admin
         spec:
           containers:
             - name: admin
               image: adminer
               ports:
                 - containerPort: 8080
    
     ---
     apiVersion: v1
     kind: Service
     metadata:
       name: admin-service
     spec:
       type: NodePort
       selector:
         app: admin
       ports:
         - protocol: TCP
           nodePort: 30002
           port: 8080
           targetPort: 8080
    
  7. Now the Database node is exposed via the service, hence this service name db-service should be used wherever we need to connect to the Database like in the appsettings.json file and while logging in to Adminer portal.

    Updated appsettings.json file code

     {
        "Logging": {
            "LogLevel": {
                "Default": "Information",
                "Microsoft": "Warning",
                "Microsoft.Hosting.Lifetime": "Information"
            }
        },
        "AllowedHosts": "*",
        "ConnectionStrings": {
            "SchoolContext": "Host=db-service;Database=my_db;Username=postgres;Password=example"
        }
     }
    
  8. In the terminal run, the kubectl apply command to create the pods and services

     #run each command individually 
     minikube kubectl -- apply -f db-deploy.yaml
     minikube kubectl -- apply -f admin-db-deploy.yaml
     minikube kubectl -- apply -f app-deploy.yaml
    

  9. Check if pods are created

     minikube kubectl -- get pods
    

    The status should be running. This may take some time.

  10. Check if services are created

    minikube kubectl -- get svc
    

  11. Now to access the application on the browser we need to fetch Minikube IP and service NodePort

    minikube service app-service --url
    

    *Do not cancel this operation as long as you need to access it in the browser.

    Access the application using the IP address(http://127.0.0.1:33291). Once you do this, the database "my_db" is created.

  12. Now let us access the database on the browser. In a new terminal run the following

    minikube service admin-service --url
    

  13. Open the browser to this IP and log in as shown.

    *Server will be the database service name used in the appsettings.json file i.e db-service

  14. You can now add records to the table, that will be seen on the web app.

  15. Refresh the application at http://127.0.0.1:33291. You should see the name you added to the table.

    Congratulations!!! You've deployed an ASP. Net app on Kubernetes.

  16. Make sure to delete the services and pods once you have finished

    #run each command individually 
    minikube kubectl -- delete -f db-deploy.yaml
    minikube kubectl -- delete -f admin-db-deploy.yaml
    minikube kubectl -- delete -f app-deploy.yaml
    

Ideally, we would add an Ingress service and controller. I have created the Ingress resource which is available in the repository mentioned below. If you are using Minikube on windows or mac this won't be an issue. You can set up ingress as given here.

However, accessing ingress via Minikube and WSL is a hurdle. There are a couple of workarounds which I've tried. But they seemed beyond the scope of this article and it's still a work in progress.

Adding ingress is optional in this case. So you could try out a similar approach to other projects and applications.

Happy learning!!

Git repository

How to run locally built images in minikube

How to access applications running within minikube

Did you find this article valuable?

Support WeMakeDevs by becoming a sponsor. Any amount is appreciated!