Kubernetes Intro – Part 9 – Deploying Your Own App

And I’m moving along with the exploration of how to use a managed Kubernetes cluster. In the previous episode, I’ve gone into the details of how to deploy applications into a cluster and hook them up to an Ingress load balancer, so they are reachable from the outside. In this episode, I want to expand on Part 3, in which I explored how to develop a simple node.js based app locally and push it into a local Minikube. The challenge: How can I push my locally developed app into a remote managed Kubernetes cluster?

The two main differences between deploying a self developed app to a local Minikube cluster compared to deploying it to a remote (managed) Kubernetes cluster are:

  • The remote cluster can’t pull the container image of the app from the local notebook. In other words, we need to push the (Docker) container image of the app to a repository, so the Kubernetes cluster can pull it from there.
  • There was no need to use an Ingress/Load balancer in the Minikube deployment example to communicate with the app. When accessing a service in a cluster over the Internet, however, traversing a real ingress component makes sense.

Pushing the Container Image to Docker Hub

Have a look at part 3 again how to develop a simple node.js app on your Linux notebook up to the ‘docker build‘ command. This command will look a bit different, since we intend to push the resulting container image to Docker Hub, so the Kubernetes cluster can later download it from there.

To push a container image to Docker Hub, an account for the site is required. I called my account ‘msx27999‘ and then created a repository that I called ‘first‘. Yes, I’m sure there are better names, but it was my first repository for a container image. So now let’s build the container image that can then be pushed into this repository. The docker build command looks as follows:

docker build -t msx27999/first:v5 -t msx27999/first:latest .

The difference to the build command in part 3 of this article series is that I’m building the container with different tags. These tell docker into which repository the image should be uploaded later. One tag would suffice, but I chose to have two tags, one with the real version number that I want to assign to this particular version of the code, and one tag for ‘latest‘, to indicate that this image is the latest and greatest version until it is superseded by another image that has the ‘latest‘ tag set. This way, I don’t have to modify the Kubernetes deployment yaml files when I want to update the version in the cluster later on.

Once the image has been built with the two tags, it can be pushed to Docker hub with the following two commands:

docker login
docker push msx27999/first --all-tags

And that’s all there is to it, the image is now on Docker Hub and since my repository is public, the image can be used by anyone in any Docker or Kubernetes installation.

Using The Image in the Kubernetes Cluster

Next, let’s deploy the App in the Kubernetes cluster. For this, we need a Service and a Deployment description. As there’s nothing fancy in this app, the yaml file for my helloworld app is refreshingly short:

apiVersion: v1
kind: Service
metadata:
  name: helloworld
spec:
  type: ClusterIP
  ports:
  - port: 8080
    targetPort: 8080
  selector:
    app: helloworld
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld
spec:
  replicas: 2
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
    spec:
      containers:
      - name: helloworld
        image: msx27999/first:latest
        ports:
        - containerPort: 8080

Note that the spec for this app requests a ‘Cluster IP‘, i.e. the service on its own will only be reachable inside the cluster, but not from the outside. For access from the outside, we’ll use the Ingress component of the cluster. Also note the ‘image‘ parameter as part of the deployment, which is set to ‘msx27999/first:latest‘. O.k., so with this helloworld.yaml file in place, the app can now be deployed into the cluster with the following command:

kubectl create -f helloworld.yaml

After the create command returns, use ‘kubectl get all‘ to check that the service and the deployment were created, and that the two helloworld pods are in status ‘running’. Yes, there are two pods, because the number of replicas were set to 2 in the yaml file above. Just for the fun of it!

Connecting to Ingress

Since the app does not have an external IP address, the final step is to create an ingress rule to connect it to the ingress load balancer component of the cluster. I already created ingress in part 8 of this series, so at this point we only need to add the following to the ingress rules yaml file:

  - host: world.example.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: helloworld
            port:
              number: 8080

Note: I chose to connect my helloworld app to the domain ‘world.example.com‘. And since I don’t own ‘example.com‘, I’ve put the domain name, together with the external IP address of the cluster, in my /etc/hosts file. Also note that the app uses TCP port 8080 on the inside of the cluster in all yaml files so far, so the ingress rule needs to reflect that as well. To the outside, ingress uses TCP port 80, i.e. the app will be reachable from the Internet on the standard http port. The updated ingress rules are then pushed into the cluster as follows:

kubectl apply -f my-new-ingress.yaml

To see if the rule is properly connected to the helloworld service, have a look with 'kubectl describe ingress‘.

Pushing a Software Update into the Cluster

And a final thing for this post is how to update the app in the cluster. To do this, I changed the text that is returned in the web browser by the server.js app and then ran the Docker build and push commands again:

docker build -t msx27999/first:v6 -t msx27999/first:latest .

docker push msx27999/first --all-tags

Since the Kubernetes deployment uses the ‘latest‘ tag, changing the version tag to v6 doesn’t really make a difference for the deployment. However, having the extra tag preserves the v5 image on Docker Hub, so it would be possible to fall back to that version. Using the updated image in the cluster can now be triggered with:

kubectl rollout restart deployment/helloworld

If you use ‘kubectl get all‘ during the process, you will see how new pods are created and the old pods are terminated one by one. If a reload of http://world.example.com shows the changed text, the update has gone through.

Summary

Yes, my helloworld app is simple, but even for much more complicated self-developed apps, the steps are exactly same. Larger software projects probably won’t push an image to Docker hub directly from a developer workstation. Here, a CI/CD pipeline from a git repository would make sense, which would push the container image to Docker hub or another image repository.