Hello World Kubernetes Service on Minikube

This is part 2 of a series on Kubernetes a la minikube. Minikube is (probably) the easiest way of installing a small Kubernetes system including a graphical user interface. In part 1 we have shown how to install such a system on a fresh CentOS Linux system. In this part, we will create our first Hello World Kubernetes Service accessible from the Internet.

Web Browser accessing a Kubernetes Hello World Service

For that, we will create a kubernetes deployment, which automatically creates a POD with a single Docker container. Then, we create a Kubernetes Service of type NodePort, which is accessible from the outside world.

In the appendix, we will show, how to tweak the automatically generated kubernetes dashboard service, so we can reach it from the Internet.

Within this blog post, we will work on minikube installed on a fresh CentOS cloud system we have installed in part 1 of this series.

minikube @ CentOS - Install Minikube on CentOS

Contents [hide]

Step 0: Get Access to a Minishift Installation

One way of achieving this step is to follow steps 1 to 4 of part 1 of this series. However, there are other possibilities to get access to Kubernetes systems as well. For example, you may want to access a Minishift installation as provided by Ben Hall on his Katacoda platform. Check out e.g. the tutorial „Launch A Single Node Cluster„.

Step 1 (optional): Explore the Status

To get acquainted with your minikube installation, you may repeat step 5 of part 1 of this series and run the commands

1
2
3
kubectl version
kubectl cluster-info
kubectl get nodes

Step 2: Create an Application: Deployment vs POD

In Kubernetes, there are, among others, two ways of creating application containers:

  • creating POD directly
  • creating POD via deployments (recommended)

Step 2.1: Creating a POD directly

Even though it is not recommended for most cases (see a discussion of this topic on StackOverflow) we can create a POD directly like follows:

Create a single POD:

1
2
3
4
5
6
7
8
9
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: gitea-pod
spec:
containers:
- name: gitea-container
image: gitea/gitea:latest
1
kubectl apply -f pod.yaml
1
kubectl

This will create a pod as expected:

1
2
3
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 1 24m

Note that it is not possible to create a POD with a kubectl create pod command (crossed out for not confusing the quick reader). Above, we had to create it by reading in a YAML file instead.

kubectl create pod mypod –image=nginx

If the STATUS of your pod is not Running, type following command to show pod status:

1
$ kubectl describe pod gitea

Note also that the POD is restarted, if a container process is killed:

1
2
3
4
5
6
7
$ ps -ef | grep nginx | grep -v grep
root 30466 30450 0 22:13 ? 00:00:00 nginx: master process nginx -g daemon off;
101 31744 30466 0 22:16 ? 00:00:00 nginx: worker process
$ kill -9 31744 30466
$ ps -ef | grep nginx
root 31950 31934 0 22:17 ? 00:00:00 nginx: master process nginx -g daemon off;
101 31965 31950 0 22:17 ? 00:00:00 nginx: worker process

In that sense, the POD we have created directly seems to be durable, even though the discussions below may hint into another direction: they say that only a deployment will restart a POD if it fails. Since we only have a single node cluster, we cannot test, whether a POD is created on another node, if a node fails.

The recommended way of creating a POD is to create a deployment. Why? What is the difference between creating a deployment and creating a pod? What is a deployment, anyway? For an answer to that, let us consult google:

What is a kubernetes deployment?

Google: A Deployment runs multiple replicas of your application and automatically replaces any instances that fail or become unresponsive. In this way, Deployments help ensure that one or more instances of your application are available to serve user requests. Deployments are managed by the Kubernetes Deployment controller.

Okay, when we create a deployment and tell the deployment to create 5 replicas of a POD, this is different from just creating 5 replicas of a POD, because a deployment will restart PODs that have failed. Let us test this.

Most tutorials do that with commands as follows:

1
2
3
4
kubectl run first-deployment --image=katacoda/docker-http-server --port=80
### output:
kubectl run --generator=deployment/apps.v1beta1 is DEPRECATED and will be removed in a future version. Use kubectl create instead.
deployment.apps/first-deployment created

However, as there is a hint that this kind of commands is deprecated, let us create the deployment differently:

If you have done so, and you want to test the recommended way, let us delete the deployment again:

1
$ kubectl delete deployment first-deployment

Now we can create the deployment in the recommended way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
creationTimestamp: 2018-11-19T21:55:59Z
generation: 1
labels:
run: first-deployment
name: first-deployment
namespace: default
resourceVersion: "300379"
selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/first-deployment
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
run: first-deployment
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
run: first-deployment
spec:
containers:
- image: katacoda/docker-http-server
imagePullPolicy: Always
name: first-deployment
ports:
- containerPort: 80
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
EOF

# output:
# deployment.extensions/first-deployment created

To be honest, I have liked the kubectl run command more…

😉

The reason for the depreciation is discussed in this StackOverflow Q&A.

Step 3 (optional): Explore Docker processes, PODs, and Services

This is creating two docker containers: a „pause“ container per POD and the service container in the POD:

1
2
3
4
# docker ps | grep "^CONTAINER\|deployment"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
af733dc6244c katacoda/docker-http-server "/app" 18 hours ago Up 18 hours k8s_first-deployment_first-deployment-59f6bb4956-fqrhd_default_b17c0c0c-e9ee-11e8-bccd-9600001441cb_0
bd1e5b869266 k8s.gcr.io/pause-amd64:3.1 "/pause" 18 hours ago Up 18 hours k8s_POD_first-deployment-59f6bb4956-fqrhd_default_b17c0c0c-e9ee-11e8-bccd-9600001441cb_0

This is quite similar to what is created with the ‚oc new-app‘ command on an OpenShift installation. See e.g. our blog post Getting started with OpenShift. But not quite: here, the service has not yet been created, as can be seen below.

We can see, that no service is created by deployment:

1
2
3
# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 443/TCP 4h

However, we will find that a POD is created:

1
2
3
# kubectl get pod
NAME READY STATUS RESTARTS AGE
first-deployment-59f6bb4956-fqrhd 1/1 Running 0 17h

To see the services or pods of all namespaces, you also can use the –all-namespaces option:

1
2
3
4
5
6
7
8
9
10
11
12
13
# kubectl get pod --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
default first-deployment-59f6bb4956-fqrhd 1/1 Running 0 18h
kube-system coredns-c4cffd6dc-r8vg4 1/1 Running 0 22h
kube-system etcd-minikube 1/1 Running 0 20h
kube-system kube-addon-manager-minikube 1/1 Running 1 22h
kube-system kube-apiserver-minikube 1/1 Running 0 20h
kube-system kube-controller-manager-minikube 1/1 Running 0 20h
kube-system kube-dns-86f4d74b45-gghfb 3/3 Running 0 22h
kube-system kube-proxy-fhls5 1/1 Running 0 20h
kube-system kube-scheduler-minikube 1/1 Running 0 22h
kube-system kubernetes-dashboard-6f4cfc5d87-tnl6r 1/1 Running 0 22h
kube-system storage-provisioner 1/1 Running 0 22h

and

1
2
3
4
5
# kubectl get svc --all-namespaces
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default kubernetes ClusterIP 10.96.0.1 443/TCP 22h
kube-system kube-dns ClusterIP 10.96.0.10 53/UDP,53/TCP 22h
kube-system kubernetes-dashboard ClusterIP 10.101.34.253 80/TCP 22h

Here, we can see, that minikube has started a kubernetes dashboard and a DNS server in the kube-system namespace.

We can get even more details on the POD by using the describe command:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# kubectl describe pod first-deployment-59f6bb4956-fqrhd
Name: first-deployment-59f6bb4956-fqrhd
Namespace: default
Node: minikube/159.69.221.89
Start Time: Fri, 16 Nov 2018 23:26:42 +0100
Labels: pod-template-hash=1592660512
run=first-deployment
Annotations:
Status: Running
IP: 172.17.0.6
Controlled By: ReplicaSet/first-deployment-59f6bb4956
Containers:
first-deployment:
Container ID: docker://af733dc6244ca05866d664729552a3db4070e8988337687e8a68948b72235a31
Image: katacoda/docker-http-server
Image ID: docker-pullable://katacoda/docker-http-server@sha256:76dc8a47fd019f80f2a3163aba789faf55b41b2fb06397653610c754cb12d3ee
Port: 80/TCP
Host Port: 0/TCP
State: Running
Started: Fri, 16 Nov 2018 23:26:45 +0100
Ready: True
Restart Count: 0
Environment:
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-lj5jx (ro)
Conditions:
Type Status
Initialized True
Ready True
PodScheduled True
Volumes:
default-token-lj5jx:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-lj5jx
Optional: false
QoS Class: BestEffort
Node-Selectors:
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:

Even though the service is not yet exposed, we still can reach it from the Docker host: The Container IP address is 172.17.0.6, as we can read it from the output of the kubectl describe pod command above. The service is running on port 80. So, let us try to access the POD:

1
2
# curl 172.17.0.6:80
<h1>This request was processed by host: first-deployment-59f6bb4956-fqrhd</h1>

Note, however, that the POD IP address 172.17.0.6 is a private address that is not accessible from the outside world. In the next step, we will make sure that the service can be reached from the Internet as well.

Step 4: Expose the Service

Now, we want to expose the service to the outside world. Kubernetes knows three ways of doing this:

  • exposing via ClusterIP:
    reachable internally, only
  • exposing via NodePorts:
    reachable internally and externally
  • exposing via LoadBalancers:
    means of configuring cloud load balancers by way of labels See e.g. https://kubernetes.io/docs/concepts/services-networking/service/ for details on Kubernetes Load Balancer interaction.

The ClusterIP way is the default, but it is reachable from within the kubernetes network only. On the other hand, the LoadBalancer option works only in certain cloud environments. Therefore, we choose to expose the service via NodePort. This way, we can make the service reachable from the Internet and still need no external load balancers.

Let us now demonstrate the NodePorts ways of exposing a service.

Step 4.1: Exposing the Service via NodePorts

If the Node the service is running on has a publicly available IP address (mapped or not), we can access the service by exposing the deployment to a node port as follows:

1
kubectl expose deployment first-deployment --port=80 --type=NodePort

Since we have not added an option like --target-port=<static-port>, the port is allocated dynamically. Therefore, we decide to retrieve the port value from via kubectl as follows:

1
export PORT=$(kubectl get svc first-deployment -o go-template='{{range.spec.ports}}{{if .nodePort}}{{.nodePort}}{{"\n"}}{{end}}{{end}}')

We need that information to access the service via curl:

1
2
# curl 127.0.0.1:$PORT
<h1>This request was processed by host: first-deployment-59f6bb4956-fqrhd</h1>

Since our Docker host is accessible from the Internet, we also can retrieve the same information from any browser on the Internet:

Accessing an exposed service from the Internet

Okay, the random port is not nice. On the other hand, we cannot run all containers on the same well-known ports like port 80 (HTTP) or port 443 (HTTPS) by specifying the same target ports on the expose commands. This issue is something that can only be resolved with the help of a reverse proxy or an HTTP load balancer, which is out of scope for this blog post.

Summary

In this blog post, we have learned how to create, run, and access applications in a Kubernetes environment. For that, we have created Kubernetes PODs, Deployments and Services. We have learned that Deployments help with the management of PODs. E.g. they restart PODs if they fail.