This Kubernetes (k8s) demo is aimed at beginners which aim at understanding how common k8s tools and services work using a hands-on approach without needing to jump in at the deep end of the massive amount of information.
- .NET 7 SDK & Runtime
- Docker Desktop
You have an application and it needs to be setup in a way where it's able to be deployed to Kubernetes. The application can be found in the apps
folder, it was generated using the dotnet template command.
# apps/
dotnet new webapi -o app1
This command uses a native dotnet template webapi
to create a web API project that generates fake weather data.
To run the application change directory cd apps/app1
, then execute dotnet run
to build and launch the application. Using the port noted in the console logs you can append /swagger
, and the Swagger documentation will be loaded.
Containerization is the act of creating portable, isolated, and reproducible environments for applications. Docker is the most popular tool to do this, however there's also other tools like Containerd. For the purpose of this demo we will use Docker throughout.
Within the context of Docker there is a question that we first need to answer. What is the difference between a Dockerfile
and a docker-compose
file?
- Dockerfile: used to build a Docker Image. a Docker Image contains everything related to the environment that an application needs.
- docker-compose: used to manage one or more images including setting up storage volumes, virtual networks, container resources, and other images that might be a dependency for an application like a database. These qualities make it ideal for local environments.
To containerize our application we first need to create a Dockerfile
in the root of the project.
# apps/app1/
# Use the official Microsoft .NET runtime image as the base image
FROM mcr.microsoft.com/dotnet/aspnet:7.0
# Set the working directory inside the container
WORKDIR /app
# Copy the published output of the application to the container
COPY ./publish /app
# Set the port the application will listen on
ENV ASPNETCORE_URLS=http://+:8080
# Expose the port the application will run on
EXPOSE 8080
# Start the application using the dotnet runtime
ENTRYPOINT ["dotnet", "app1.dll"]
Note that in the ENTRYPOINT
command there is a reference to app1
-- this should be changed to the project name of your application.
In the COPY
command there is a reference to a local directory called publish
. This directory does not exist yet, however we can fix that by publishing our application.
# apps/app1/
dotnet publish --configuration Release --output ./publish
Next we generate our Docker image. The Docker image will be registered in Docker Desktop's local image registry.
# apps/app1/
docker build --tag app1 .
To test our new Docker image we can start a container using the image we just created.
# apps/app1/
docker run --rm -p 5000:8080 --name my_app1 app1
This is a Docker command that will run a container based on the Docker image named "app1" with the name "my_app1", and expose its internal port 8080 to the host's port 5000.
Loading the url http://localhost:5000/WeatherForecast
in a browser or executing a cURL should return a JSON response with a list of fake weather forecasts.
curl -XGET 'http://localhost:5000/WeatherForecast'
Note that the Swagger endpoint we used earlier is not enabled on production environments by default. In order to change the environment of your application you can set the environment variable ASPNETCORE_ENVIRONMENT
to Development
in the Dockerfile, or modify the code within Program.cs
.
If an error comes up that the container is already running it should be stopped. Since we're using the --rm
command to start the container, the container will be removed automatically.
docker stop my_app1
The final step is to publish our image to a remote Docker registry. The app used for this Demo is already available as the image jeanfrg/app1, however to publish your own you'll need to create an account on hub.docker.com, then we'll need to run docker login
in our command line. Once this has been done we need to use these commands to publish a new image. A new repository on your Docker Hub account will be created.
docker tag app1 <your-docker-hub-id>/app1:v1
docker push <your-docker-hub-id>/app1:v1
Kubernetes is an open-source container orchestration platform that automates the deployment, scaling, and management of containerized applications -- we call these tools Orchestrators.
There are multiple ways that one can set up a local kubernetes environment. Since we're using Docker already the easiest way is to use Docker's native integration.
Enable and start Kubernetes on Docker Desktop.
MiniKube is also a good tool that we can use to achieve this.
Test if the installation was successful by getting the Kubernetes version.
kubectl version --short
In the case that kubectl is already set up, the context will need to be switched. Using kubectl config get-contexts
we're able to see which kubernetes is currently selected through noting the one with an asterix. To change the current context we can use kubectl config use-context <context-name>
.
Next we need to create some Kubernetes manifests, which tell Kubernetes how to configure our application.
# kubernetes/apps/app1/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app1
spec:
replicas: 1
selector:
matchLabels:
app: app1
template:
metadata:
labels:
app: app1
spec:
containers:
- name: app1
image: jeanfrg/app1:v1.0.10
ports:
- containerPort: 8080
This configuration file defines a deployment with one replica of our Docker image, and exposes port 8080 on the container.
Note that if you have published your own docker image, spec.template.spec.[].image
should reference your image.
Next we need to create a Kubernetes service which will allow us to expose the service out of Kubernetes so we can hit it locally.
# kubernetes/apps/app1/service.yaml
apiVersion: v1
kind: Service
metadata:
name: app1
spec:
selector:
app: app1
ports:
- protocol: TCP
port: 5000
targetPort: 8080
type: LoadBalancer
This manifest file defines a LoadBalancer service that exposes your application on port 5000 and routes traffic to the target port 8080 on the deployment.
Apply configurations.
# kubernetes/apps/app1/
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
Note we can also change the directory to kubernetes/apps
and use kubectl apply -f app1
to apply all configurations within the folder without specifying each one.
Checking the configuration status in Kubernetes.
kubectl get deployments
kubectl get services
The information printed out from the services command will show us exactly how we're able to access our application from outside Kubernetes.
# kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
app1 LoadBalancer 10.101.183.235 localhost 5000:31785/TCP 1m
In this example we use the value of the external IP column as the host and the first port specified in the port column.
http://localhost:5000/WeatherForecast
Loading this URL should return a JSON response with fake weather forecasts.
Argo CD is an open-source, Kubernetes-native Continuous Delivery (CD) tool that automates the deployment, scaling, and management of applications in Kubernetes clusters. It follows the GitOps methodology, which uses Git as the single source of truth for declarative infrastructure and application configurations.
To install ArgoCD in Kubernetes we first need to create the ArgoCD namespace. Kubernetes namespaces are a way to divide cluster resources among multiple users, teams, or projects.
kubectl create namespace argocd
This new namespace will be listed in the output of this command kubectl get namespaces
.
Next we need to apply the ArgoCD manifests.
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.4.7/manifests/install.yaml
Sources
https://argo-cd.readthedocs.io/en/stable/getting_started/#1-install-argo-cd https://argo-cd.readthedocs.io/en/stable/operator-manual/installation/#non-high-availability
To expose ArgoCD we need to patch its argocd-server
service.
# kubernetes/apps/argocd/
kubectl patch service argocd-server --namespace argocd --type=merge --patch-file argocd-server.service.patch.yaml
With this patch we're now able to access the service outside of Kubernetes using http://localhost:5010/
. The exposed port can be found by executing kubectl get service argocd-server --namespace argocd
. The initial password for the admin user account can be found inside the secret argocd-initial-admin-secret
.
kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}'
The value is base64 encoded, it can be decoded in various ways, however it depends on the shell you're using.
Ubuntu/WSL
kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 --decode
Windows/PowerShell
([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String((kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}'))))
Using the username admin
and the password retreived from either commands, we're now able to login to ArgoCD.
We're now ready to configure ArgoCD to manage the deployment of our application when our Kubernetes manifests for our application change.
To achieve this, we need to create a manifest of kind Application.
# kubernetes/apps/app1/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app1
namespace: argocd
spec:
project: default
source:
repoURL: 'https://github.com/frg/k8-containerization-demo.git'
targetRevision: main
path: kubernetes/apps/app1
destination:
server: 'https://kubernetes.default.svc'
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
At this point we're ready to apply these changes. ArgoCD will attempt to pick up changes from the specified repository in the application.yaml
. This means it's time to commit and push our code to that repository.
# kubernetes/apps/app1/
kubectl apply -f application.yaml
Checking the ArgoCD UI we should now be able to see our application and its sync status. To test if ArgoCD can successfully pick up manifest changes, we can open the deployment.yaml
, change spec.replicas
to 2
, commit and push our changes.
By default ArgoCD polls the configured repository specified in kubernetes/apps/app1/application.yaml
at spec.source.repoURL
every 180 seconds.
Once the changes have been picked up we will be able to see 2 pods that have been deployed instead of 1. For this demo we don't need two so feel free to reduce it back to 1.
Kustomize is a tool for customizing Kubernetes resources through a base and overlay approach. It is designed to make it easy to manage and maintain Kubernetes resource configurations across different environments (e.g., development, staging, production) without requiring manual changes or complex templating systems.
The key components of Kustomize are:
- Base: The base resources that define the core structure and functionality of an application.
- Overlay: A set of modifications applied to the base resources to adapt them for a specific environment or purpose.
- kustomization.yaml: A configuration file that defines how to combine the base resources and overlays, including instructions for merging and patching resources.
Let's say we need this application to be deployed for the development and production environments. We can either take the approach of copying each Kubernetes manifest for every environment or we can use Kustomize to reuse manifests per environment.
First we need to create these directories.
kubernetes/apps/app1/bases
kubernetes/apps/app1/overlays/development
kubernetes/apps/app1/overlays/production
Move the manifests found at kubernetes/apps/app1/
to kubernetes/apps/app1/bases
. As implied, we will use thes manifests as the base of each environment manifest.
Create a kustomization file that will be referenced within each overlay.
# kubernetes/apps/app1/bases/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
We need to create a kustomization file for development.
# kubernetes/apps/app1/overlays/development/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: dev-
resources:
- ../../bases
Two differences that we want the production deployment to have is a different port and an increased amount of replicas. To achieve this we use the concept of patches, which are used to modify resources.
# kubernetes/apps/app1/overlays/production/deployment.patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app1
spec:
replicas: 3
# kubernetes/apps/app1/overlays/production/service.patch.yaml
- op: replace
path: /spec/ports/0/port
value: 5001
Next, we need to create the production kustomization.yaml
so that it references these patch files.
# kubernetes/apps/app1/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: prod-
resources:
- ../../bases
patches:
- path: deployment.patch.yaml
- target:
version: v1
kind: Service
name: app1
path: service.patch.yaml
The ArgoCD application manifest also need to be created for both these deployments.
# kubernetes/apps/app1/overlays/development/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: dev-app1
namespace: argocd
spec:
project: default
source:
repoURL: 'https://github.com/frg/k8-containerization-demo.git'
targetRevision: main
path: kubernetes/apps/app1/overlays/development
destination:
server: 'https://kubernetes.default.svc'
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
# kubernetes/apps/app1/overlays/production/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: prod-app1
namespace: argocd
spec:
project: default
source:
repoURL: 'https://github.com/frg/k8-containerization-demo.git'
targetRevision: main
path: kubernetes/apps/app1/overlays/production
destination:
server: 'https://kubernetes.default.svc'
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
Commit these changes to your repository and finally, we can apply these manifests.
# kubernetes/apps/app1/overlays/production
kubectl apply -f application.yaml
# kubernetes/apps/app1/overlays/development
kubectl apply -f application.yaml