For the past few years containerisation has become one of the best ways to increase business speed and agility by organisations. Then, combined with mature container platforms and orchestration options, like Kubernetes, managing microservices has become the new normal. This shows in the recent statistics, where 90% of enterprises globally, have adopted or are planning to adopt microservices soon, as a strategy to become more competitive, providing more products and better services faster.
There are a lot of benefits around managing microservices using Kubernetes. However, before we run we must learn to walk. In this blog, I am going to try to set a baseline to anyone relatively new to containers and Kubernetes, but interested in understanding the technical aspects of packaging and running microservices.
The expectation is that at the end of this blog, you will understand how to containerise microservices using Docker and you will know how to deploy them into Kubernetes.
Specifically, I am going to take you through the following steps:
- Git clone an existing repo containing a “Hello World” microservice built in NodeJS. It will come with its own Dockerfile and Vagrant file to be easily imaged and containerised.
- I’m going to be using an Ubuntu 20.04 Vagrant box VM to build and test our microservices locally. Then, inside this VM, we will Docker build our app and push the image into Docker Hub, which is a Container Image repository.
- Finally, I’ll ask you to go into your Kubernetes cluster and apply a Kubernetes Deployment, pointing to the Docker Image pushed in step 2.
Easy huh? Ok then, let’s have fun!
In this blog, I assume the following:
· You have already provisioned a Kubernetes cluster environment somewhere. I am going to be using a simple local minikube, but if using any other Managed Kubernetes such as EKS, AKS, GKE, the steps are mostly identical. If you need help to install a minikube, have a look at this reference.
· Also I assume a certain familiarity with Docker and containers in general. If you need a quick refresher, have a look at this reference.
Build your Hello World App as a Docker Image
I am going to use a Vagrant Box as the dev environment to git clone, containerise my “Hello World” application and then push it into Docker Hub. This is for simplicity reasons, so that you don’t have to install Docker Engine on your laptop (I don’t personally like installing software on my laptop/host, I prefer to do so using disposable VMs). For this, I made a Vagrant Box publicly available in a Git repo, to be used as a dev env.
The Hello World Application that I am going to use is a simple NodeJS express application that returns a simple, “Hello World” when invoked. Nothing fancy, just enough to prove the concept.
· Clone my hello-microservices repository:
· Move into the nodejs-ms directory:
· Now, start your vagrant box:
Note: Give it some time the first time. It will download the Ubuntu Box and install all dependencies. Subsequent times will be much faster.
· Once it finishes, as per the bootstrap process, your Vagrant VM is going to come with all necessary components installed, like Docker Engine, Node JS, etc, so that you can build your Hello World app as a Docker image.
· Vagrant ssh into it.
· Move into your host auto-mounted working directory.
· Feel free to explore the Dockerfile. It is doing the following:
o Line 1: Starting from existing Docker Hub node image version 9.11.1 (the latest stable when writing this blog)
o Line 2: Setting the Working directory within the new Docker node image (creating and changing current directory)
o Line 3: Adding all the local directory (i.e. the “nodeJS-ms” App) content/files into the Working directory
o Line 4: It will run “npm install” to retrieve all the NodeJS App dependencies. In this case only the “express” module (see package.json for more information).
o Line 5: Defines the intended port on which the App is configured to run on. In this case 3000.
o Line 6: Setting the command to run when “this” image is run. In this case, running the NodeJS App (as indicated in package.json).
· As for the NodeJS app, I tried to keep it extremely simple. The actual NodeJS app is app.js
o Lines 1 to 5: Require the “express” module and getting the config file that contains the port on which this Application will listen on (i.e. 3000)
o Lines 8 to 12: Define a GET API URI (i.e. “/”) and return “Hello World from a NodeJS Application” – Feel free to change the message if you like.
o Lines 15 to 18: Defines the actual Listening service running on the configured port (i.e. 3000) and giving a welcome message.
· The other file that you might want to have a look is the actual NodeJS descriptor package.json
It is quite self-explanatory, but just pay close attention to:
o dependencies -> express – This is what is executed at “docker build” time, as defined in the Dockerfile (RUN npm install).
o scripts -> Start: node app.js – This is what will be executed at “docker run” time as defined in the Dockerfile (CMD npm start).
· Lastly, have a look at config.js – It defines the port on which the NodeJS app is going to run by default if not otherwise set as a system variable.
Also notice that this aligns with the Dockerfile EXPOSE 3000 directive.
· Ok, now that everything is clear, let’s build our Docker image. Since we already added the vagrant user to the docker group during the bootstrap of this Vagrant Box instance. Let’s simply build the docker image:
|docker build .|
Note: Notice the last dot “.”
· Give it some time the first time, as it has to pull the node image from Docker Hub first (~200MB).
· As the Docker build process moves across the 6 steps, you will be able to see the progress in the console.
At the end it will show you the id of your final Docker image. Make a note of it, as you will need it later when tagging your image.
· Let’s quickly test that our new Docker image works well. For this, let’s run the image using its id as a reference. The command goes like this:
|docker run -p [HostPort]:[ContainerPORT] -it [DockerImageId]|
Note: -i is to run it in interactive mode, which means that you can stop it later on by ctrl+c.
For example, given this output:
|docker run -p 3000:3000 -it f1851266d4fc|
This will run a Docker container from our Docker image and start the NodeJS App. It will map the internal 3000 port from the container into also port 3000 in our Host.
· The provided Vagrant box is configured by default with NAT and Port-Forwarding on port 3000:3000, so you can open a browser on your host machine and go to localhost:3000 – You should be able to see the “Hello World from a NodeJS Application” message.
· Confirm that you get the “Hello World” message that you configured in the NodeJS application.
· Now that we know that our Docker image works as intended, let’s move on to the next section to push it into Docker Hub
Push your NodeJS App Docker Image to Docker Hub
Now that we have created our Docker image and that we have briefly tested it locally, let’s proceed to push it into a Docker Hub repository. For this, I assume that you already have a Docker Hub account (or equivalent) and that you have created a repository.
For example, I created one called: mulethunder/hello-microservices-k8s
Notice that Docker Hub repositories are always prefixed with your Docker Hub username, so you might want to choose the same repo name if you like, just be cautious to reference yours in the coming steps if you want to test against your own docker image. Having said that, if you don’t want to push your own image and want to deploy mine, feel free to do so too.
· Go back to your terminal window (vagrant ssh) and cd to /vagrant if not there already
· In case you are still running the container from last section, Cbreak it with a Ctrl + C
· In the terminal, first we need to set the Docker Hub login details.
Then enter your own username and password when requested. Make sure to get a successful login attempt.
· Tag the Docker image:
|docker tag [Image_ID] [DockerHubUsername]/[DockerHubRepoName]:[version]|
NB: If you need to get again your docker image id, do a simple: docker images
Yours, will be the one without a TAG already:
|docker tag f1851266d4fc mulethunder/hello-microservices-k8s:1.0|
Note: You could’ve tagged your Docker image at the moment of “docker building” by using -t [user/repoName]
Also, always make sure to add a version. Otherwise it will get a default “latest” version that will be hard to manage later on when running Deployments in Kubernetes.
· To see these changes, run another: docker images
· Then finally, Docker push the image – Including the version!
|docker push [DockerHubUsername]/[DockerHubRepoName]:[version]|
|docker push mulethunder/hello-microservices-k8s:1.0|
· Give it some time, as it uploads your compressed docker image into your specified Docker Hub repository.
· After a few minutes, your docker image will appear in your Docker Hub specified repo.
Run your Hello World App Docker Image in Kubernetes
Once your docker image resides in a Docker repository (or equivalent), like Docker Hub, we can easily pull it and run it on Kubernetes.
Applications in Kubernetes run within the concept of “pods”, that are logical runtime grouping of Containers that make up a whole Application. In our case, for the “NodeJS App” there will be just one Docker Container. Pods are defined as YAML files.
· SSH into the environment where you installed kubectl pointing to your Kubernetes cluster. In my case, it is a local 1 node minikube running on my laptop, but it could be anywhere else, like a Master node of a Managed Kubernetes cluster running on EKS/AKS/GKE cluster.
· Make sure your Kubernetes cluster is up and running and that kubectl is pointing to it
|kubectl get nodes|
You should see at least 1 worker node ready.
· If you are running kubectl in a different machine where you had previously git cloned the “hello-microservices” repository, you will need to retrieve it again from GitHub, so that you have the sample Deployment YAML file already configured to easily pull our docker image and deploy it in Kubernetes.
· Move into the kubernetes directory:
· Inside, there is a file called “hello-nodejs-dpl.yaml_sample”, that is the Deployment definition for our Hello World NodeJS App demo.
Note: The reason why it is a template, is because I want to give you the opportunity to modify it and enter your own container image pushed in the previous step.
· Most of the directives are self-explanatory, but let’s review the most important ones:
o Line 2: We are defining a Deployment, so we use this as the type. Deployments facilitate a history of applied versions, allowing us to easily roll out / rollback specific revisions.
o Line 4: The name of our Deployment
o Line 6 – 7: Labels selectors for filtering and matching purposes.
o Line 8: The namespace where we want to allocate our Deployment to.
o Line 10: The number of replicas that we want to run for this application. That is, in this case I am asking Kubernetes to always maintain 2 replicas of this Application running in the cluster.
o Line 11: The number of allowed revision history to track in time.
o Lines 12 – 20: Applying the same labels selector as defined before
o Lines 23-24: The name and location of the docker image to be pulled out and run as part of this deployment. Note, unless specified differently, this defaults to Docker Hub
o Lines 25-31: This is some resource limit controls, in order to ensure that our application we do not vertically scale and by mistake starve resources from other processes.
o Line 32-34: Environment variable that in this case specifies the port in which we want the NodeJS application to run on.
o Line 36: Container Port where our application will be running on.
· Then, copy this file and remove the trailing “_template”
|mv hello-nodejs-dpl.yaml_sample hello-nodejs-dpl.yaml|
· Update line 23 and replace ENTER_IMAGE_TAG_NAME_HERE with your own container image name. This is the one that you previously pushed into Docker Hub. Make sure to include the version number.
For example, in my case:
· Before we apply the Deployment definition, let’s create the namespace where we want our Deployment to run.
|kubectl create namespace hello-microservices|
· Now, use kubectl to apply the Deployment definition
|kubectl apply -f hello-nodejs-dpl.yaml|
· You should see a message saying that your Deployment was created. However, give it some time, as your image needs to be downloaded.
· Validate the status of your new pod:
|kubectl get pods -n hello-microservices -w|
· After a minute or two, the pods should be up and running
· Once this is the case, break the wait with a Ctrl + C
· Also, make sure that the whole deployment is up and available
|kubectl get deployment -n hello-microservices|
· If you want more details about your pod, you can describe them:
|kubectl describe pod [YOUR_POD_NAME] -n [NS]|
|kubectl describe pod hello-nodejs-deployment-5bf65f8f7-gmmtn -n hello-microservices|
· Now let’s test our Hello World NodeJS Application running on Kubernetes. For this, there are 2 simple ways we can do it:
o 1) Simply port-forward to the host machine, so that we can access it. This is good for simple tests, as this will keep the prompt engaged, so that a simple ctrl + c will break the port being exposed.
|kubectl port-forward [POD_NAME] 8081:3000 -n hello-microservices -n [NS]|
|kubectl port-forward hello-nodejs-deployment-5bf65f8f7-gmmtn 8081:3000 -n hello-microservices|
Similarly, as you did before, open terminal window/ssh and do a simple curl command, e.g.
You will get the generic “Hello World” HTML message that you configured in the NodeJS application.
Additionally, if running from your laptop, like in my case as I am running minikube, you can point to your own browser:
Feel free to Ctrl + C your port forward command.
o 2) The second option is by creating a service definition. A service definition is valuable if you wish to expose your Application as a long-running process. This way internally the Service maintains a real-time status of the pods running, so that regardless of whether pods get terminated or initiated, we can always reach them all seamlessly, without any manual effort.
|kubectl expose deployment [DEPLOYMENT_NAME] –type=NodePort –name=[SERVICE_NAME] -n [NS]|
Pay special attention to the type NodePort. This is a type of Service that assigns an external port to each of the Worker Nodes, where the deployment is working, so that you can access your deployment externally.
|kubectl expose deployment hello-nodejs-deployment –type=NodePort –name=hello-nodejs-svc -n hello-microservices|
You can gather information about your services:
|kubectl get service -n hello-microservices|
|kubectl describe services [SERVICE_NAME] -n hello-microservices|
- Note: Typically, your service definitions are also described as yaml files, so that you can version control them easily.
- If you prefer, instead of creating the service in command line, but using an actual YAML file definition, you can instead apply the Service file provided:
- If you want to test this, run the following commands:
- First delete the service:
|kubectl delete service [SERVICE_NAME] -n [NS]|
|kubectl delete service hello-nodejs-svc -n hello-microservices|
- Within the same folder for the Deployment YAML definition, there is another file called: hello-nodejs-svc.yaml
Review it and make sure it is equivalent to the command used before to expose the Deployment.
- Finally apply the file:
|kubectl apply -f hello-nodejs-svc.yaml|
· Regardless of whether you created the NodePort Service via command or via a proper file definition, the outcome is the same, although as mentioned, I personally prefer the file YAML definition, that way I can easily version control it.
· if you are running your Kubernetes cluster with minikube, you can get the address of the service with:
|minikube service [SERVICE_NAME] -n [NS] –url|
|minikube service hello-nodejs-svc -n hello-microservices –url|
· Otherwise, a simple way to test your service is by looking at the NodePort:
· Once again, runa simple curl command or open in the browser to confirm it is accessible.
Congratulations!!! Your Hello World Application is up and running on Kubernetes.
I will keep publishing more advanced topics on Kubernetes and Cloud Native in general, so stay tuned.
I hope you found this blog useful. If you have any question or comment, feel free to contact me directly at https://www.linkedin.com/in/citurria/
Thanks for your time.