# Scaling .NET 8 Applications in Kubernetes

This article is the second in the series "Use Kubernetes to host .NET 8 applications":

* [Containerize and Host .NET 8 applications in Kubernetes](https://iharmaiseyeu.net/containerize-and-host-net-8-application-in-kubernetes)
    
* Scaling .NET 8 Applications in Kubernetes (this article)
    
* Hosting .NET 8 Applications in Azure Kubernetes Service (AKS)
    

The [previous article](https://iharmaiseyeu.net/containerize-and-host-net-8-application-in-kubernetes) focused on containerizing and hosting a simple .NET 8 console application in Kubernetes. We started by creating a test application and crafting a Dockerfile to build a Docker image of the application. We then explored the basics of Kubernetes, setting up a local Kubernetes cluster using Docker Desktop, and deploying our application container within a Kubernetes pod. Finally, we addressed state persistence challenges by configuring a `hostPath` volume, ensuring our application's progress is saved across pod restarts.

In this article, I will reveal a few vital components of Kubernetes that help deliver and scale applications.

## Test application

The test application from the previous article emulates some work and saves the process in a text file. You can follow the process in two ways: observing application logs using `kubectl logs` or reading the configuration file.

This requires access to Kubernetes and its components. In typical scenarios, users don't have this access. Imagine if, when uploading a video to YouTube, you had to log into the servers to check the status. First, Google wouldn't allow it. Second, it's a poor user experience.

To eliminate this problem we will create an API with an endpoint that shows the current operation state ([the source code](https://github.com/MasDenBy/blog-samples/tree/main/k8s-net8-part2)). Here's a snippet of the code.

```csharp
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

const string configFileName = "/configuration/step.config";

app.MapGet("/api/state", () =>
{
    if(File.Exists(configFileName))
    {
        return Results.Ok(File.ReadAllText(configFileName));
    }

    return Results.BadRequest();
});

app.MapGet("/api/health", () =>
{
    return Results.Ok(Environment.MachineName);
});

app.Run();
```

The API has two GET endpoints:

* `/api/state` shows the current operation state, performed by the background process
    
* `/api/health` indicates if the API works as expected and returns the machine name.
    

The Dockerfile and pod configuration files are similar to the ones used for the background process, so they are not included in this article.

## Deployment

The way a pod was created in [the previous article](https://iharmaiseyeu.net/containerize-and-host-net-8-application-in-kubernetes) has a few drawbacks. The main ones are difficulty in scaling and recreating the pod when it dies.

A Deployment in Kubernetes is like a blueprint for your application. It defines the desired state for your pods and ensures your cluster matches this state. Whether you need to roll out a new version of your app, scale up to handle increased traffic or roll back to a previous version in case of issues, Deployments have you covered.

Deployments make updating your application seamless. You update the Deployment configuration, and Kubernetes takes care of the rest—creating new pods, managing old ones, and ensuring zero downtime. This powerful feature helps you maintain high availability and resilience for your .NET 8 applications.

Next, we will create the deployment for our API service.

```plaintext
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-kube-api-deploy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-kube-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    metadata:
      labels:
        app: hello-kube-api
    spec:
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: hello-pvc
      containers:
      - name: hello-kube-api
        image: hello-kube-api:1.0
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: data
          mountPath: /configuration
```

The deployment file looks bulky, but if you look closely, you’ll notice that some parts are similar to pod configuration and only half is new. It's better to start with the new parts. All configuration elements are under the `spec` element, so I will omit it for clarity.

* `replicas: 2` ensures that two instances (pods) of our application are running at all times, providing redundancy and load balancing.
    
* `selector` with `matchLabels` tells Kubernetes which pods are managed by this Deployment. Here, it selects pods with the label `app: hello-kube-api`.
    

The `strategy` configuration defines the update strategy: `RollingUpdate` or `Recreate`. The `RollingUpdate` strategy ensures zero downtime by incrementally updating pods and maintaining application availability throughout the process. You can control the update pace with parameters like `maxUnavailable` and `maxSurge`, ensuring a seamless user experience. On the other hand, the `Recreate` strategy shuts down all existing pods before starting new ones, resulting in downtime. This straightforward approach is suitable for non-critical applications or when updates require significant changes incompatible with running pods.

To ensure zero downtime by gradually replacing old pods with new ones, the `type: RollingUpdate` suits us best.

* The `rollingUpdate` element allows adjusting the strategy more thoroughly.
    
    * `maxUnavailable: 1` allows only one pod to be unavailable during the update process.
        
    * `maxSurge: 1` specifies that one additional pod can be created during the update, ensuring there’s always an extra pod available during the rollout.
        

The `template` describes the pod that will be created with this deployment. It specifies the container and the volume. This configuration is similar to the one used for creating the pod, with the only difference being that we use the API image and instead of mounting `hostPath`, we use `persistentVolumeClaim`.

> Note that `template.metadata` sets app label as `hello-kube-api` which matches the `matchLabels` selector.

In Kubernetes, PersistentVolume (PV) and PersistentVolumeClaim (PVC) are essential for managing storage. A PersistentVolume is a piece of storage provisioned by an administrator or dynamically created using Storage Classes. It's independent of any individual pod, ensuring your data persists beyond the lifecycle of pods. On the other hand, a PersistentVolumeClaim is a request for storage by a user. When you create a PVC, Kubernetes finds a PV that matches the claim’s requirements, binding them together. This separation allows for dynamic and flexible storage management, ensuring your applications have the persistent storage they need.

The configurations for PV and PVC needed for our applications are below.

```plaintext
apiVersion: v1
kind: PersistentVolume
metadata:
  name: hello-pv
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  hostPath:
    path: "/mnt/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: hello-pvc
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
```

By defining these PV and PVC we create a persistent storage solution that all our Kubernetes pods can use, ensuring data is retained even if the pods are terminated or rescheduled.

We can apply deployment and storage configuration in any order, but we have to understand that our deployment cannot be created until the storage is provided. Thus I recommend provisioning the storage first and only after that apply deployment configuration.

```bash
kubectl apply -f storage.yml
kubectl apply -f api-deploy.yml
```

As usual, we can use `kubectl get` to check the status of our deployment.

```plaintext
> kubectl get deploy

NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
hello-kube-api-deploy   2/2     2            2           10m
```

The interesting part is that we asked to create two replicas of our pods instead of one pod as it was with our background process. We can see more details using the `describe` command. If you use it, you can receive a more detailed response about the deployment, pod, volume, and events.

```plaintext
> kubectl describe deployments hello-kube-api-deploy

Name:                   hello-kube-api-deploy
Namespace:              default
CreationTimestamp:      Sun, 23 Jun 2024 11:44:58 +0200
Labels:                 <none>
Annotations:            deployment.kubernetes.io/revision: 1
Selector:               app=hello-kube-api
Replicas:               2 desired | 2 updated | 2 total | 2 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  1 max unavailable, 1 max surge
Pod Template:
  Labels:  app=hello-kube-api
  Containers:
   hello-kube-api:
    Image:        hello-kube-api:1.0
    Port:         8080/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:
      /configuration from data (rw)
  Volumes:
   data:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  hello-pvc
    ReadOnly:   false
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   hello-kube-api-deploy-b6686ccc6 (2/2 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  34s   deployment-controller  Scaled up replica set hello-kube-api-deploy-b6686ccc6 to 2
```

Right now we have two applications running in Kubernetes: the background process and the API.

```plaintext
NAME                                    READY   STATUS    RESTARTS      AGE
app-pod                                 1/1     Running   2 (62m ago)   33d
hello-kube-api-deploy-b6686ccc6-kxc9j   1/1     Running   0             59m
hello-kube-api-deploy-b6686ccc6-sc7lj   1/1     Running   0             59m
```

Because we created the replica set with two pods in it K8S needs to assign unique names for each pod. For that, it uses the following format:

`[the name of deployment]-[unique identifier of deployment]-[unique identifier of pod]`

## Load Balancing and Service Configuration

Our application works in Kubernetes, but we still haven't seen the results. Creating a frontend with a UI is outside the scope of this article, so we will try to call our API to see the result.

Kubernetes assigns a private cluster IP for each pod in a cluster so that pods can communicate. This private IP is not accessible from the outside world. Each pod has a unique name and IP, which we can use to communicate with them. However, relying on pod names and IPs is impractical because pods can be deleted and recreated with different names and IP addresses at any time. To solve this issue, Kubernetes uses an abstraction called Services.

Services are essential for enabling communication between different components of an application. A Service defines a logical set of pods and a policy to access them. Kubernetes offers different types of services:

* ClusterIP for internal communication within the cluster.
    
* NodePort for exposing services on each node's IP at a static port.
    
* LoadBalancer for exposing services externally using a cloud provider's load balancer.
    

We want to access our pods from the external world, so we will use a LoadBalancer service.

```yaml
apiVersion: v1
kind: Service
metadata:
  name: hello-kube-lb-svc
  labels:
    app: hello-kube-api
spec:
  type: LoadBalancer
  ports:
  - port: 8080
    protocol: TCP
  selector:
    app: hello-kube-api
```

This configuration creates a **LoadBalancer** service named `hello-kube-lb-svc` that listens on port `8080` and directs traffic to our API pods labeled `app: hello-kube-api`.

Apply the configuration with:

```bash
kubectl apply -f api-lb.yml
```

Check the status of the service:

```bash
kubectl get svc
```

And use `describe` to see more details

```bash
kubectl describe svc hello-kube-lb-svc
```

## Testing the applications

We are finally ready to test our applications. Start by checking the health endpoint by navigating to `http://localhost:8080/api/health` in your browser.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1719141864378/164c0525-09ad-4ade-8f63-6b451e30bb24.png align="center")

You should see a `200 OK` response with the text `hello-kube-api-deploy-[unique identifier]`, which matches the pod name. Refresh the page a few times, and you will notice different responses as the load balancer directs traffic to different pods.

If you delete a pod:

```bash
kubectl delete pod [pod-name]
```

Refresh the page again, you should still get a `200 OK` response, but from a different pod, indicating that Kubernetes has recreated the deleted pod and the service is working as expected.

Let's scale our application to have five replicas of the API. Modify the `spec.replicas` parameter in the deployment configuration and apply the changes:

```bash
kubectl apply -f api-deploy.yml
```

Verify the number of running pods:

```bash
kubectl get pods
```

You should see five pods running:

```plaintext
NAME                                    READY   STATUS    RESTARTS       AGE
app-pod                                 1/1     Running   0              33d
hello-kube-api-deploy-b6686ccc6-8kkql   1/1     Running   0              19s
hello-kube-api-deploy-b6686ccc6-8vmzs   1/1     Running   0              9m16s
hello-kube-api-deploy-b6686ccc6-hh9hd   1/1     Running   0              19s
hello-kube-api-deploy-b6686ccc6-kxc9j   1/1     Running   0              126m
hello-kube-api-deploy-b6686ccc6-prbmc   1/1     Running   0              19s
```

Finally, let's inspect the state endpoint to check the status of the background operation. This endpoint shows the result of the work done by two different pods, with the result persisting in the volume.

Navigate to `http://localhost:8080/api/state` in your browser. You should see a response like `1293`. Recheck it after a few minutes, and the number should increment, indicating the background process is working correctly.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1719142192804/e960749a-8031-4e80-a8f2-44157cd2afd5.png align="center")

## Summary

In this article, we build on our previous work containerizing a .NET 8 application by focusing on scaling and managing the application within Kubernetes. We introduce a new API service for monitoring the application's state and health, and we create a Kubernetes Deployment to ensure the application runs smoothly with zero downtime. Persistent storage is managed using PersistentVolumes (PV) and PersistentVolumeClaims (PVC), ensuring data persistence across pod restarts. Finally, we configure a LoadBalancer service to expose the API to external traffic, enabling us to test and verify the application's scalability and resilience.

## Links

[The source code](https://github.com/MasDenBy/blog-samples/tree/main/k8s-net8-part2)

[Deployments | Kub](http://localhost:8080/api/health)[ernetes](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/)

[Persistent Volumes | Kubernetes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)

[ReplicaSet | Kubernetes](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/)

[Service | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/service/)

Image credits: [Cargo Container Lot](https://www.pexels.com/photo/cargo-container-lot-906494/) by [Chanaka](https://www.pexels.com/@chanaka-318741/)
