{{announcement.body}}
{{announcement.title}}

What Is Kubernetes and How Does It Relate to Docker?

DZone 's Guide to

What Is Kubernetes and How Does It Relate to Docker?

Learn more about the ins and outs of container orchestrator Kubernetes and how it fits in with Docker in a microservices application.

· Cloud Zone ·
Free Resource

Kubernetes is most commonly used with Docker managed containers, although it doesn't strictly depend on it. Kubernetes defines a Container Runtime Interface (CRI) that container platforms must implement in order to be compatible. These implementations are colloquially known as "shims". This makes Kubernetes platform agnostic so that instead of Docker you're free to use other platforms with corresponding shims, such as CRI-O or KataContainers.

Automatic scaling and failover are just two of the benefits provided by modern cloud platforms such as Amazon AWS, Microsoft Azure and Google Cloud Platform (GCP). There are still problems with this architecture though, for example:

  1. Stopping and starting instances is slow.
  2. Communicating between nodes in this architecture can be quite complex.

If you're using a monolithic architecture over microservices, you may not need to deal with communication between nodes and so won't experience these issues. You will still need to worry about instance startup time whenever introducing rolling upgrades or performing a failover. This can incur in extra costs since the risk of losing requests becomes higher the longer a restart takes, so you're likely to want to have another instance to minimise that risk.

This is where Kubernetes comes in. Kubernetes is a platform for managing containerised services. This means that it's a tool made to abstract away details such as separation of nodes, while automating things like rolling upgrades, failover, and scaling of services. The idea is that you should be able to deploy in a very similar way whether running locally or in the cloud.

This blog will walk through some of the basics of Kubernetes, and will setup a simple microservice application locally to demonstrate its use. This blog will assume you have the following tools installed and configured already:

  • Maven (v3+)
  • JDK 8
  • Docker

What Do I Need to Install Kubernetes?

Kubernetes is intended to be used with a cloud provider. Despite this, there are several options for running a Kubernetes cluster locally. Each implementation will essentially end up providing the same interface through kubectl. If you're running Windows, Docker-For-Windows has support for Kubernetes. If you're using Linux, two forerunners are MicroK8s and minikube, the latter of which works on Windows, MacOS and Linux. Whereas minikube runs a Kubernetes cluster inside a single VM, MicroK8s is installed using Snap and runs locally with minimal overhead. For the ease of install and use, this blog will be using MicroK8s.

Since MicroK8s was developed by the Kubernetes team at Canonical, it must be installed using the Canonical Linux package manager - a package manager that can be across many Linux operating system and will make sure applications are installed the same way, as well as automatically update them. On Ubuntu, Snap can be installed like so:

Shell
 




x


 
1
sudo apt update 
2
sudo apt install snapd


You may need to either logout/in or restart your system to ensure that the system path is updated correctly.

Once Snap is installed, you can install MicroK8s like so:

Shell
 




xxxxxxxxxx
1


1
snap install microk8s --classic


This will install MicroK8s like a normal system application. All of the MicroK8s utilities are named microk8s.something,  including the main Kubernetes utility as microk8s.kubectl. As this is annoying to type each time, if you don't already have another source of kubectl installed you can alias the main Kubernetes utility with the following command:

Shell
 




xxxxxxxxxx
1


 
1
snap alias microk8s.kubectl kubectl


This will allow you to use kubectl instead of microk8s.kubectl every time. This can be reverted with:

Shell
 




xxxxxxxxxx
1


 
1
snap unalias kubectl


Some key commands that are only provided by MicroK8s are listed below. You'll need to make sure that MicroK8s is started before trying to use kubectl to start services (it needs to be running also for the kubectl get all command mentioned above).

Shell
 




xxxxxxxxxx
1


 
1
# Starts MicroK8s and allows provisioning nodes.
2
microk8s.start 
3
 
          
4
# Stops MicroK8s
5
microk8s.stop 
6
 
          
7
# Clear all resources and revert kubernetes to a 'clean' state
8
microk8s.reset


To check that MicroK8s is installed correctly, list all resources:

Shell
 




xxxxxxxxxx
1


 
1
kubectl get all


Our Microservices Example Application

In order to demonstrate a microservices application in action, we will first need an application to work with. The application we will be using will randomly generate Payara Micro instance names. It can be found in the Payara Examples repositoryI recommend cloning this repository to follow along with the steps to run the application.

The application contains 3 components:

  1. Frontend - A Vue.js application that uses the backend API to produce a message 
  2. Backend - A backend API that handles requests from the frontend
  3. Name Generator - A name generation service that produces random instance names 

The first thing we'll be doing is running the application locally without Kubernetes to see what it looks like.

Starting the Frontend

The frontend code can be found in the frontend/ folder. If you already have NodeJS (v9+) and NPM installed, you can start the project like so:

Shell
 




xxxxxxxxxx
1


 
1
# Install dependency packages to node_modules/
2
npm install 
3
 
          
4
# Start the server
5
npm run serve


If you instead only have Maven, you can run the project with:

Shell
 




xxxxxxxxxx
1


1
# Download a local NodeJS installation and use it to start the server
2
mvn test -Pserve


Any changes made to the code will appear immediately using the hot reloading feature of vue-loader.

You can now navigate to the link in the terminal (by default it's http://localhost:8080/). You'll see the following page:

Instance name generator

When you click on the GENERATE NAME button you'll see an error message. From the developer console you'll see the full error that explains what went wrong. In Google Chrome this can be found by pressing the F12 key and navigating to the Console tab.

Console

The button is calling the backend API on port 8081 but can't reach it! That's because we haven't started the backend server to serve these requests.

Starting the Backend

From another terminal, visit the backend/ folder and run the following command:

Shell
 




xxxxxxxxxx
1


 
1
# Build the application, bundle it with Payara Micro and then run it
2
mvn verify -Pserve


This will start the server on port 8081 to begin serving requests. If you now click the button on the website you'll receive a different error in the console:

Shell
 




xxxxxxxxxx
1


 
1
 GET http://localhost:8081/api/greeting 500 (Internal Server Error)


This means that the backend has been reached, but it is returning a 500. The reason for this is explained in the main bulk of the code for that service:

Java
 




xxxxxxxxxx
1
15


 
1
@Path("/greeting")
2
@RequestScoped
3
public class BackendService {
4
 
          
5
    private NameService nameService;
6
 
          
7
    @Inject    public void initNameService(@ConfigProperty(name = "name.service.url") URI nameServiceUrl) {       
8
 
          
9
 nameService = RestClientBuilder                
10
            .newBuilder()                
11
            .baseUri(nameServiceUrl)                
12
            .build(NameService.class);    }
13
    @GET    public String getGreeting() {        return "Hello " + nameService.getRandomName();    }
14
 
          
15
}


This code uses the MicroProfile Config and REST Client APIs to fetch the random name from the name service, and returns the "Hello" string with the random name appended. Since the third service isn't available, an HTTP 500 error will be propagated to the frontend. See our blogs to read more about MP Rest Client  and MP Config API, or our documentation for more information on both.

The server URL for the name service is fetched from the name.service.url Config property, which defaults to http://localhost:8082 as specified in src/main/resources/META-INF/microprofile-config.properties. The MicroProfile Config specification allows overwriting this by configuring the NAME_SERVICE_URL environment variable before running the application.

Starting the Name Generator Service

The final part of the application is the name service. Visit the name-generator folder and run the following command:

Shell
 




xxxxxxxxxx
1


 
1
# Build the application, bundle it with Payara Micro and then run it
2
mvn verify -Pserve


This will start the server on port 8082 to begin serving requests. If you click the button on the website you'll see random names being printed out!

Name generator

This means that all 3 services are functioning correctly! Now that we've seen what the application looks like, we can begin to port this application across to containers, which can then be used with Kubernetes.

Porting the Services to Containers

Each of the services has a Dockerfile in their respective folders. To build them, run the following command from the root of the kubernetes example folder:

Shell
 




xxxxxxxxxx
1


1
mvn install -Pdocker


Assuming you have Docker installed locally and can run Docker without sudo, this command will produce three Docker containers, one for each of the services. These are named:

  • payara/kubernetes-frontend
  • payara/kubernetes-backend
  • payara/kubernetes-name-generator

You can alternatively build the images manually using the Dockerfiles in each subfolder.

Creating a Docker Network

Linking containers by name used to be possible using the --link parameter.  This is now deprecated in favour of creating user defined bridges. This basically allows containers to reference each other by their container name (the one assigned with --name).

To create a Docker network, run the following command:

Shell
 




xxxxxxxxxx
1


1
docker network create kubernetes-network


You can add containers to this network with the --network parameter when creating containers. When you're done with the network, you can remove it with: 

Shell
 




xxxxxxxxxx
1


 
1
docker network rm kubernetes-network
2
 
          


Running the Name Service Container

The first container to start will be the name generator service. The Dockerfile simply starts the Payara Micro instance as we did before, but without modifying the port (meaning it will run on the default port 8080). This is because Docker separates out each container as it's own virtual host so there is no need to pick a different port for each container.

You can run the container like so:

Shell
 




xxxxxxxxxx
1


 
1
docker run -d --rm --name name-generator --network kubernetes-network payara/kubernetes-name-generator 
2
 
          


This will start the container in the background. This command makes sure that the container has an appropriate name with --name, and connects it to the new network with --network. This is required since the container will be referenced later by the backend container. You can verify that the container has been started by checking the container logs with:

Shell
 




xxxxxxxxxx
1


 
1
docker container logs name-generator


Alternatively, you can get the container IP by running:

Shell
 




xxxxxxxxxx
1


 
1
docker inspect name-generator


You can then test the endpoint by fetching a random instance name like so:

Shell
 




xxxxxxxxxx
1


 
1
curl <container-ip>/name/random


Running the Backend Container

The second container is exactly the same as the previous container, but for the backend service JAR. You can run the container like so:

Shell
 




xxxxxxxxxx
1


 
1
docker run -d --rm --name backend -p 8081:8080 --network kubernetes-network --env NAME_SERVICE_URL=http://name-generator:8080 payara/kubernetes-backend


There are two additional arguments here that you'll notice.

The first new bit is '-p 8081:8080'. This is the Docker argument to map a container port to a localhost port. In this case, it's mapping port 8080 of the container (the one used by Payara Micro) to port 8081 on the host machine. The reason for this is that JS on the web page will need to be able to access the backend service, and the easiest way to do this is to host the backend on localhost. This will also be solved when we start using Kubernetes.

The second new bit is '--env NAME_SERVICE_URL=http://name-generator:8080'. You'll recall from before that this service allows the name service URL to be overriden by this environment variable. This is the final bit that allows the backend to find the name service and communicate with it.

Running the Frontend Container

The last container is the frontend container. This one runs slightly differently to the other containers in that it serves compiled JS code to the client via Nginx, so must be compiled beforehand. As part of the Maven build, the command npm run build will be run which will generate the static files to be served to the user. Amongst other things this will disable the dynamic reloading, disable the verbose logging and generally make the code ready for production. It's at this time that all environment variables should be inserted into the compiled code. Because of this, where the Payara Micro service can override the required environment variables, the JS code requires that it be specified for the maven build. You'll need to edit the variable found in the file 'frontend/.env.production'. This will determine the URL of the backend service to be called by the user. Usually you'd replace this with the domain name for your website API, but in this case we'll just change it to 'http://localhost:8081', since that's where the Docker container will be hosted.

Once you've done this, you can rebuild the frontend with 'mvn clean install',  then start the container like so:

Shell
 




xxxxxxxxxx
1


 
1
docker run -d --rm --name frontend -p 8080:80 --network kubernetes-network payara/kubernetes-frontend


This will start the container and bind it to port 8080 on the host. Visiting http://localhost:8080/ will show the webpage just as before, except it's now running in a container!

When you're done with the containers, you can stop them with the following Docker command:

Shell
 




xxxxxxxxxx
1


1
docker stop frontend backend name-generator


You can then delete the network as explained previously. 

Porting the Services to Kubernetes

Now that we've proven that the Docker containers work, we can begin to port these components to Kubernetes. If we were to deploy now with the current setup, we would run into issues when we wanted to scale containers, since we'd have to either create a load balancing node for each container type, or manually link each container to another one, which would be difficult to manage at scale. We would also run into problems if we wanted to perform a rolling upgrade since the load balancing wouldn't take this into account, so we'd need to first configure the load balancer, then upgrade a container, then reconfigure the load balancer, and so on. Kubernetes makes each of these processes a lot easier with its services.

We'll assume that you installed MicroK8s as per the instructions at the start of this blog. If not, see there to make sure you have it installed correctly before continuing. Before we start running through any Kubernetes steps, it'll be useful to have a quick crash course on Kubernetes concepts:

  • Container: An executable image that contains a single piece of software and all it's dependencies. In our case it's a Docker container.
  • Node: A virtual or physical machine that is used as a worker for Kubernetes. Kubernetes will abstract its management away in practice. A Kubernetes cluster contains a master node that is used to manage the state of the cluster in real time and allow user interaction.
  • Cluster: A set of nodes that run containers managed by Kubernetes. This is analogous to a Domain in Payara Server.
  • Pod: The smallest Kubernetes object. A pod runs a set of containers. Usually, pods have  one container each, but in some cases they may also include additional container services such as one to add logging capabilities, for example.
  • Deployment: An object that manages a set of replicated pods. Scaling a microservice involves adding more pods to a deployment.
  • Service: An object that describes how to access deployments or groups of pods via a common endpoint. May create things like load balancers to access the resource.
  • Label: Labels are added to Kubernetes objects (pods, deployments, services, etc.) in order to be selected together.
  • Selector: A selector references a label in order to allow the interaction with a set of objects, like multiple containers for the purposes of load balancing, for example.
  • Kubectl: The command line utility used to interact with the Kubernetes master node.

Kubernetes can use YAML manifests to describe any resources (e.g. services, deployments or pods) deployed to it. Those, who are familiar with the Docker Compose tool, will find the YAML configuration familiar. For example, the pod.yaml file in the frontend gives an example of a pod descriptor:

YAML
 




xxxxxxxxxx
1
12


 
1
apiVersion: v1 
2
kind: Pod 
3
metadata:  
4
name: frontend  
5
labels:    
6
app: frontend 
7
spec:  
8
   containers:    
9
        - image: payara/kubernetes-frontend      
10
          name: frontend      
11
          ports:        
12
            - containerPort: 80


This descriptor references a pod to be deployed to Kubernetes. It is given the name 'frontend', and assigned the label 'app: frontend'. This can be used by selectors, and shared between pods. The Docker image is then referenced, and the descriptor dictates the port used by the container. Note that pods should not be worked with directly; this is only shown to give a basic descriptor for the smallest Kubernetes object.

Trying to use this will cause a problem however: Kubernetes will be unable to find the named image, since it has no access to the local Docker registry. If the image were pushed to the Docker Hub container registry, Kubernetes would be able to find it. You can also connect your Kubernetes cluster to private registries. You can find more information on using private registries from the Kubernetes website. But MicroK8s includes a feature that helps us in local development: it's own local registry. This allows you to push from Docker to this registry, and then use the image in MicroK8s.

Using the MicroK8s Local Registry

To initialize the MicroK8s local registry, run the following command:

Shell
 






This will initialize the local repository so that you can begin to push images to it. You'll remember we have 3 Docker images to be used:

  1. payara/kubernetes-frontend
  2. payara/kubernetes-backend
  3. payara/kubernetes-name-generator

To push these into the local repository, run the following commands:

Shell
 




xxxxxxxxxx
1


 
1
docker tag payara/kubernetes-frontend 127.0.0.1:32000/kubernetes-frontend 
2
docker tag payara/kubernetes-backend 127.0.0.1:32000/kubernetes-backend 
3
docker tag payara/kubernetes-name-generator 127.0.0.1:32000/kubernetes-name-generator
4
 
5
docker push 127.0.0.1:32000/kubernetes-frontend 
6
docker push 127.0.0.1:32000/kubernetes-backend 
7
docker push 127.0.0.1:32000/kubernetes-name-generator


The first 3 commands will tag the image with the link to the registry. This tells Docker where to push the image to.

The second 3 commands will push the image to the registry.

Since this registry is intended for local development, it is insecure by default. This may mean that Docker will throw an error when pushing to it. In order to fix this, you might need to create a configuration file located at /etc/docker/daemon.json with the following content:

JSON
 




xxxxxxxxxx
1


 
1
{    "insecure-registries": ["127.0.0.1:32000"] }
2
 
          


This configuration will tell the Docker daemon that insecure connections are allowed to the MicroK8s registry. You must restart the Docker daemon for these changes to take effect:

Shell
 




xxxxxxxxxx
1


 
1
sudo systemctl restart docker


Once you've done this, the images will be pushed correctly to the MicroK8s registry. MicroK8s contains a reference to this registry called 'local.insecure-registry.io'. You must make sure when using any Kubernetes descriptors that instead of the old repository name (payara/<image-name>), the new registry will be used (local.insecure-registry.io/<image-name>).

After editing the pod descriptor, you can instruct Kubernetes to create it with the following command:

Shell
 




xxxxxxxxxx
1


1
kubectl create -f pod.yaml


This will create the pod following the instructions in the manifest. You can see that it's started using the describe command:

Shell
 




xxxxxxxxxx
1


1
kubectl describe pods


You'll notice however, that we have no way of accessing this pod! To do this we'll first create a deployment. As we mentioned before interfacing with pods directly is generally bad, so the deployment will be our base object to work from. The next section will describe what deployments are and how to use them. First you'll need to delete the pod though:

Shell
 




xxxxxxxxxx
1


 
1
kubectl delete pod/frontend


Creating Our First Kubernetes Deployment

As described in the terminology section, a deployment is an object that will be used to encapsulate our pods and help us scale the microservices. Our first deployment  can be found in the frontend folder named deployment.yaml. The contents are shown below:

YAML
 




xxxxxxxxxx
1
31


 
1
apiVersion: apps/v1 
2
kind: Deployment 
3
metadata:  
4
  name: frontend 
5
spec:  
6
  replicas: 2  
7
  strategy:    
8
    type: RollingUpdate    
9
    rollingUpdate:      
10
      maxUnavailable: 1      
11
      maxSurge: 1  
12
  selector:    
13
    matchLabels:      
14
       app: frontend  
15
  template:    
16
    metadata:      
17
     labels:        
18
       app: frontend    
19
    spec:      
20
      containers:        
21
        - image: local.insecure-registry.io/kubernetes-frontend          
22
          name: frontend          
23
          ports:            
24
            - containerPort: 80          
25
          readinessProbe:              
26
              httpGet:                  
27
                  path: /                  
28
                  port: 80              
29
              initialDelaySeconds: 3              
30
              periodSeconds: 3              
31
              failureThreshold: 2


This deployment descriptor supersedes the pod descriptor, and can be used without it. You'll notice that the contents are broadly similar to the pod descriptor, with a few differences:

  1. The replicas tag dictates how many pods will be created initially for that deployment. In this case, 2 pods are to be created.
  2. The strategy block. This block states the restrictions for updating or rolling back the deployment. The max unavailable value defines how many pods can be unavailable at a time. The max surge value defines how may pods that can be created over the replica count while updating. In this case that means that the replica count could be a maximum of 3.
  3. The template block. This defines how pods created under this deployment should be named.
  4. The selector block. This defines the criteria for a pod to be managed by this deployment. In this case it's the same as the labels specified by the template.
  5. The readiness probe. This dictates how Kubernetes should check that the instance is considered 'ready'. This will be utilised by the rollback strategy, as well as any services utilising these pods.
    • Type: In this case we simply ping the context root and check for a 200. A full list of possible readiness checks is available in the Kubernetes documentation.
    • Initial Delay: this means that after starting Kubernetes should wait at least 3 seconds before considering the container 'ready'.
    • Period: every 3 seconds a readiness check should be made.
    • Failure Threshold: If 2 consecutive readiness checks fail, the container should be considered not ready and restarted.

You can apply this deployment with the following command:  

Shell
 




xxxxxxxxxx
1


1
kubectl apply -f deployment.yaml


This utilises the Kubernetes declarative management API. Whereas in the previous section we used the create command, apply can also be used to scale and make most other changes to containers. If you need to make changes to a descriptor, you simply change the file and run the apply command again. For the rest of this guide we'll be using this command for ease.

Wait a few seconds for the containers to start, and you'll see two pods created under the frontend deployment when listing Kubernetes resources.

Shell
 




xxxxxxxxxx
1


1
kubectl get all


Plain Text
 




xxxxxxxxxx
1
12


 
1
NAME                            READY   STATUS    RESTARTS   AGE 
2
pod/frontend-65756df4c8-fkrjd   1/1     Running   0          12s 
3
pod/frontend-65756df4c8-jcq5x   1/1     Running   0          12s
4
 
5
NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE 
6
service/kubernetes   ClusterIP   10.152.183.1   <none>        443/TCP   170m
7
 
8
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE 
9
deployment.apps/frontend   2/2     2            2           12s
10
 
11
NAME                                  DESIRED   CURRENT   READY   AGE 
12
replicaset.apps/frontend-65756df4c8   2         2         2       12s


One key thing to notice is that there is no IP address assigned to the deployment. Addresses are assigned to services, and this deployment doesn't have a service exposing it yet. We'll create that now.

Creating Our First Kubernetes Service

The frontend folder contains one final YAML descriptor: service.yaml.

YAML
 




xxxxxxxxxx
1
12


 
1
apiVersion: v1 
2
kind: Service 
3
metadata:  
4
  name: frontend-lb 
5
spec:  
6
  type: LoadBalancer  
7
  ports:  
8
  - port: 80    
9
    protocol: TCP    
10
    targetPort: 80  
11
  selector:    
12
    app: frontend


This is our first service. It will be used to access the frontend deployment created before. You'll notice that the deployment itself isn't referenced here at all. Instead a selector is used, allowing us to select all pods matching a label. This is one of the reasons Kubernetes is so flexible.

The only new thing in this descriptor is the port configuration. This service will route TCP connections from port 80 on it's assigned IP to target port 80 on the referenced pods.

You can apply this service with the following command:  

Shell
 




xxxxxxxxxx
1


1
kubectl apply -f service.yaml


You'll now see a new addition to the 'kubectl get all' result:

Plain Text
 




xxxxxxxxxx
1
13


 
1
NAME                            READY   STATUS    RESTARTS   AGE 
2
pod/frontend-65756df4c8-fkrjd   1/1     Running   0          86m 
3
pod/frontend-65756df4c8-jcq5x   1/1     Running   0          86m 
4
 
          
5
NAME                  TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE 
6
service/frontend-lb   LoadBalancer   10.152.183.109   <pending>     80:31415/TCP   5s 
7
service/kubernetes    ClusterIP      10.152.183.1     <none>        443/TCP        4h16m 
8
 
          
9
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE 
10
deployment.apps/frontend   2/2     2            2           86m 
11
 
          
12
NAME                                  DESIRED   CURRENT   READY   AGE 
13
replicaset.apps/frontend-65756df4c8   2         2         2       86m


From here you can see the new load balancer service. You'll notice that the external IP is set to "Pending". In a cloud environment, this IP will be assigned automatically by the provider and synchronized with its corresponding DNS. In MicroK8s, we can just visit the cluster IP from our browser to see the endpoint. Visiting the cluster IP of frontend-lb will take you to the frontend of the site through one of the frontend pods.

Creating The Backend Kubernetes Service

The backend folder contains two YAML descriptors: deployment.yaml and service.yaml.

YAML
 




xxxxxxxxxx
1
34


 
1
apiVersion: apps/v1 
2
kind: Deployment 
3
metadata:  
4
  name: backend 
5
spec:  
6
  replicas: 2  
7
  strategy:    
8
    type: RollingUpdate    
9
    rollingUpdate:       
10
      maxUnavailable: 1      
11
      maxSurge: 1  
12
  selector:    
13
    matchLabels:      
14
      app: backend  
15
  template:    
16
    metadata:      
17
      labels:        
18
        app: backend    
19
    spec:      
20
      containers:        
21
        - image: local.insecure-registry.io/kubernetes-backend          
22
          name: backend          
23
          ports:            
24
            - containerPort: 8080          
25
          env:            
26
            - name: NAME_SERVICE_URL              
27
              value: "http://name-generator"          
28
          readinessProbe:              
29
              httpGet:                  
30
                  path: /health                  
31
                  port: 8080              
32
              initialDelaySeconds: 20              
33
              periodSeconds: 3              
34
              failureThreshold: 3


The deployment descriptor is formatted in a similar way to the frontend service with a few changes.

Firstly each image is started with the environment variable required for the docker image. Since Kubernetes services can be accessed by name, the name-generator server name is passed as the corresponding value. The other change is that the initial delay for the readiness probe is increased. The reason for this is that Payara Micro takes longer to startup than the nginx server used in the frontend, so Kubernetes needs to wait longer before polling to see if it's ready.

YAML
 




xxxxxxxxxx
1
12


 
1
apiVersion: v1 
2
kind: Service 
3
metadata:  
4
  name: backend 
5
spec:  
6
  type: LoadBalancer  
7
  ports:    
8
    - port: 80      
9
      protocol: TCP      
10
      targetPort: 8080  
11
  selector:    
12
    app: backend


The service.yaml manifest is very similar to the one for the frontend. The only difference is that the target port for the container is 8080. Despite this, the service for the deployment still uses port 80. This adds some consistency between services.

You can apply both of these configuration files, and you'll see the Kubernetes cluster looks like this:

Plain Text
 




xxxxxxxxxx
1
18


 
1
NAME                            READY   STATUS    RESTARTS   AGE 
2
pod/backend-5b5bcf5dcd-gn4kk    1/1     Running   0          27s 
3
pod/backend-5b5bcf5dcd-vdmxt    1/1     Running   0          27s 
4
pod/frontend-65756df4c8-fkrjd   1/1     Running   0          115m 
5
pod/frontend-65756df4c8-jcq5x   1/1     Running   0          115m 
6
 
          
7
NAME                  TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE 
8
service/backend-lb    LoadBalancer   10.152.183.141   <pending>     80:31931/TCP   24s 
9
service/frontend-lb   LoadBalancer   10.152.183.109   <pending>     80:31415/TCP   29m 
10
service/kubernetes    ClusterIP      10.152.183.1     <none>        443/TCP        4h45m 
11
 
          
12
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE 
13
deployment.apps/backend    2/2     2            2           28s 
14
deployment.apps/frontend   2/2     2            2           115m 
15
 
          
16
NAME                                  DESIRED   CURRENT   READY   AGE 
17
replicaset.apps/backend-5b5bcf5dcd    2         2         2       28s 
18
replicaset.apps/frontend-65756df4c8   2         2         2       115m


You can see that the backend pods have been created and they're being load balanced by the backend-lb service. All we need to do now is create the name generator service.

Creating The Name Generation Kubernetes Service

The name generation service deployment.yaml is exactly the same as the one for the backend service, but with the name and image names changes. You can apply the deployment in exactly the same way. The only difference between these descriptors and those of the backend service is in the service.yaml:

YAML
 




xxxxxxxxxx
1
11


 
1
apiVersion: v1 
2
kind: Service 
3
metadata:  
4
  name: name-generator 
5
spec:  
6
  ports:    
7
    - port: 80      
8
      protocol: TCP      
9
      targetPort: 8080  
10
  selector:    
11
    app: name-generator


While this is mostly similar to the previous service descriptor, it has one line missing:

YAML
 




xxxxxxxxxx
1


 
1
type: LoadBalancer


This line assigns the service an IP to be used as an external load balancer. When this descriptor is applied without that line, the service is still accessible by name by other pods, but won't be assigned an external IP. Since this is an internal IP, it has no need to be externally accessible (and in fact would be a security weakness if it were).

Once you've applied the deployment.yaml and service.yaml from this folder, you'll be almost done!

The Result

Once you've deployed all 3 deployments and all 3 services, you should see the following Kubernetes cluster:

Plain Text
 




xxxxxxxxxx
1
23


 
1
NAME                                  READY   STATUS    RESTARTS   AGE 
2
pod/backend-5b5bcf5dcd-gn4kk          1/1     Running   0          131m 
3
pod/backend-5b5bcf5dcd-vdmxt          1/1     Running   0          131m 
4
pod/frontend-65756df4c8-fkrjd         1/1     Running   0          4h6m 
5
pod/frontend-65756df4c8-jcq5x         1/1     Running   0          4h6m 
6
pod/name-generator-855f8f9c59-dckd4   1/1     Running   0          30s 
7
pod/name-generator-855f8f9c59-vllns   1/1     Running   0          30s 
8
 
          
9
NAME                     TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE 
10
service/backend-lb       LoadBalancer   10.152.183.141   <pending>     80:31931/TCP   130m 
11
service/frontend-lb      LoadBalancer   10.152.183.109   <pending>     80:31415/TCP   160m 
12
service/kubernetes       ClusterIP      10.152.183.1     <none>        443/TCP        6h57m 
13
service/name-generator   ClusterIP      10.152.183.115   <none>        80/TCP         26s 
14
 
          
15
NAME                             READY   UP-TO-DATE   AVAILABLE   AGE 
16
deployment.apps/backend          2/2     2            2           131m 
17
deployment.apps/frontend         2/2     2            2           4h6m 
18
deployment.apps/name-generator   2/2     2            2           30s 
19
 
          
20
NAME                                        DESIRED   CURRENT   READY   AGE 
21
replicaset.apps/backend-5b5bcf5dcd          2         2         2       131m 
22
replicaset.apps/frontend-65756df4c8         2         2         2       4h6m 
23
replicaset.apps/name-generator-855f8f9c59   2         2         2       30s



Visiting the IP of the frontend load balancer (purple) will take you to the website. There are still a couple of problems though. Remember when we recreated this with Docker and we had to change the frontend code to point to the backend? This would be solved in production with a domain name, but since it's local we've got to workaround it! Luckily, we set the backend URL to localhost:8081 and Kubernetes provides a useful utility to help:

Shell
 




xxxxxxxxxx
1


 
1
kubectl port-forward service/backend-lb 8081:80


This temporarily binds port 8081 on the host to the backend load balancer service port 80. This will allow the AJAX calls to correctly be directed to the backend service.

If you don't want to keep a terminal window open, you can instead change the frontend image environment variable we changed earlier back from 'http://localhost:8081' to 'http://backend' and add the following line to your /etc/hosts:

Shell
 




xxxxxxxxxx
1


 
1
<backend-url> backend


The backend URL can be found in green above the purple frontend URL. This will have the same effect, but without a terminal window!

The final problem is that by default MicroK8s doesn't have a DNS service running (the service that allows other services to be referenced by name rather than IP). You can enable this with the following command:

Shell
 




xxxxxxxxxx
1


 
1
microk8s.enable dns


Once this is enabled you can visit the frontend URL again and the app should work! You can see from the cluster configuration that we have 6 instances in this configuration, and this setup can be easily scaled up to meet large application demand.

Some Kubernetes Coolness

Suppose your backend developers decide to release a new version. They're not happy that the app keeps saying 'Hello', and think it would be greatly improved if it said 'Hey' instead. They push a new Docker image and ask you to deploy it. Without Kubernetes you'd likely be going to each instance and deploying the new application, which is not a viable solution at all. Kubernetes allows rolling updates to current running containers.

Rolling out an update with Kubernetes is as easy as updating the version of the image of the corresponding containers in the descriptor and running the omnipotent command:

Shell
 




xxxxxxxxxx
1


1
kubectl apply -f deployment.yaml


Then you can just wait as Kubernetes performs the rolling restart. You can also change the number of replicas in each deployment by either editing the value in the descriptor and applying it in this way.

But wait! The backend developers pushed a broken image! No worries, you can simply rollback the deployment. List the previous deployments with the following command:

Shell
 




xxxxxxxxxx
1


 
1
kubectl rollout history deployment/backend


Select a deployment number from this list. If you don't want to see change information here, you should add the --record flag when applying new deployments. To inspect a single revision run:

Shell
 




xxxxxxxxxx
1


1
kubectl rollout history deployment/backend --revision=2


Once you've selected a deployment, roll back to it with:

Shell
 




xxxxxxxxxx
1


1
kubectl rollout undo deployment/backend --to-revision=2


Omitting the --to-revision will roll back to the previous version of the deployment.

Conclusion

Hopefully this blog has helped shed some light on what Kubernetes is as well as some of the benefits of using it in your production environment. Each cloud provider provides a Kubernetes deployment option, as well as its own method of auto-scaling clusters, each provider have its own benefits but all of them share the same  basic concepts and follow the same architecture, so the knowledge you've learned here will be applicable across all of them. With Kubernetes you can make your deployments almost entirely agnostic to the underlying infrastructure, and only concern yourself with selecting the image tags that are running at any time.

Topics:
cloud, container, docker, java, kubernetes, microservice, microservice architecture

Published at DZone with permission of Steve Millidge , DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}