Splitting the Pod and establishing communication through Services
Let's take a look at a ReplicaSet definition for a Pod with only the database:
cat svc/go-demo-2-db-rs.yml
The output is as follows:
apiVersion: apps/v1beta2 kind: ReplicaSet metadata: name: go-demo-2-db spec: selector: matchLabels: type: db service: go-demo-2 template: metadata: labels: type: db service: go-demo-2 vendor: MongoLabs spec: containers: - name: db image: mongo:3.3 ports: - containerPort: 28017
We'll comment only on the things that changed.
Since this ReplicaSet defines only the database, we reduced the number of replicas to 1. Truth be told, MongoDB should be scaled as well, but that's out of the scope of this chapter (and probably the book as well). For now, we'll pretend that one replica of a database is enough.
Since selector labels need to be unique, we changed them slightly. The service is still go-demo-2, but the type was changed to db.
The rest of the definition is the same except that the containers now contain only mongo. We'll define the API in a separate ReplicaSet.
Let's create the ReplicaSet before we move to the Service that will reference its Pod.
kubectl create \ -f svc/go-demo-2-db-rs.yml
One object was created, three are left to go.
The next one is the Service for the Pod we just created through the ReplicaSet.
cat svc/go-demo-2-db-svc.yml
The output is as follows:
apiVersion: v1 kind: Service metadata: name: go-demo-2-db spec: ports: - port: 27017 selector: type: db service: go-demo-2
This Service definition does not contain anything new. There is no type, so it'll default to ClusterIP. Since there is no reason for anyone outside the cluster to communicate with the database, there's no need to expose it using the NodePort type. We also skipped specifying the nodePort, since only internal communication within the cluster is allowed. The same is true for the protocol. TCP is all we need, and it happens to be the default one. Finally, the selector labels are the same as the labels that define the Pod.
Let's create the Service:
kubectl create \ -f svc/go-demo-2-db-svc.yml
We are finished with the database. The ReplicaSet will make sure that the Pod is (almost) always up-and-running and the Service will allow other Pods to communicate with it through a fixed DNS.
Moving to the backend API...
cat svc/go-demo-2-api-rs.yml
The output is as follows:
apiVersion: apps/v1beta2 kind: ReplicaSet metadata: name: go-demo-2-api spec: replicas: 3 selector: matchLabels: type: api service: go-demo-2 template: metadata: labels: type: api service: go-demo-2 language: go spec: containers: - name: api image: vfarcic/go-demo-2 env: - name: DB value: go-demo-2-db readinessProbe: httpGet: path: /demo/hello port: 8080 periodSeconds: 1 livenessProbe: httpGet: path: /demo/hello port: 8080
Just as with the database, this ReplicaSet should be familiar since it's very similar to the one we used before. We'll comment only on the differences.
The number of replicas is set to 3. That solves one of the main problems we had with the previous ReplicaSets that defined Pods with both containers. Now the number of replicas can differ, and we have one Pod for the database, and three for the backend API.
The type label is set to api so that both the ReplicaSet and the (soon to come) Service can distinguish the Pods from those created for the database.
We have the environment variable DB set to go-demo-2-db. The code behind the vfarcic/go-demo-2 image is written in a way that the connection to the database is established by reading that variable. In this case, we can say that it will try to connect to the database running on the DNS go-demo-2-db. If you go back to the database Service definition, you'll notice that its name is go-demo-2-db as well. If everything works correctly, we should expect that the DNS was created with the Service and that it'll forward requests to the database.
In earlier Kubernetes versions it used userspace proxy mode. Its advantage is that the proxy would retry failed requests to another Pod. With the shift to the iptables mode, that feature is lost. However, iptables are much faster and more reliable, so the loss of the retry mechanism is well compensated. That does not mean that the requests are sent to Pods "blindly". The lack of the retry mechanism is mitigated with readinessProbe, which we added to the ReplicaSet.
The readinessProbe has the same fields as the livenessProbe. We used the same values for both, except for the periodSeconds, where instead of relying on the default value of 10, we set it to 1. While livenessProbe is used to determine whether a Pod is alive or it should be replaced by a new one, the readinessProbe is used by the iptables. A Pod that does not pass the readinessProbe will be excluded and will not receive requests. In theory, Requests might be still sent to a faulty Pod, between two iterations. Still, such requests will be small in number since the iptables will change as soon as the next probe responds with HTTP code less than 200, or equal or greater than 400.
Ideally, an application would have different end-points for the readinessProbe and the livenessProbe. This one doesn't so the same one should do. You can blame it on me being too lazy to add them.
Let's create the ReplicaSet.
kubectl create \ -f svc/go-demo-2-api-rs.yml
Only one object is missing, that is service:
cat svc/go-demo-2-api-svc.yml
The output is as follows:
apiVersion: v1 kind: Service metadata: name: go-demo-2-api spec: type: NodePort ports: - port: 8080 selector: type: api service: go-demo-2
There's nothing truly new in this definition. The type is set to NodePort since the API should be accessible from outside the cluster. The selector label type is set to api so that it matches the labels defined for the Pods.
That is the last object we'll create (in this section), so let's move on and do it:
kubectl create \ -f svc/go-demo-2-api-svc.yml
We'll take a look at what we have in the cluster:
kubectl get all
The output is as follows:
NAME DESIRED CURRENT READY AGE rs/go-demo-2-api 3 3 3 18m rs/go-demo-2-db 1 1 1 48m rs/go-demo-2-api 3 3 3 18m rs/go-demo-2-db 1 1 1 48m NAME READY STATUS RESTARTS AGE po/go-demo-2-api-6brtz 1/1 Running 0 18m po/go-demo-2-api-fj9mg 1/1 Running 0 18m po/go-demo-2-api-vrcxh 1/1 Running 0 18m po/go-demo-2-db-qcftz 1/1 Running 0 48m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
AGE svc/go-demo-2-api NodePort 10.0.0.162 <none> 8080:31256/TCP
2m svc/go-demo-2-db ClusterIP 10.0.0.19 <none> 27017/TCP
48m svc/kubernetes ClusterIP 10.0.0.1 <none> 443/TCP
1h
Both ReplicaSets for db and api are there, followed by the three replicas of the go-demo-2-api Pods and one replica of the go-demo-2-db Pod. Finally, the two Services are running as well, together with the one created by Kubernetes itself.
Before we proceed, it might be worth mentioning that the code behind the vfarcic/go-demo-2 image is designed to fail if it cannot connect to the database. The fact that the three replicas of the go-demo-2-api Pod are running means that the communication is established. The only verification left is to check whether we can access the API from outside the cluster. Let's try that out.
PORT=$(kubectl get svc go-demo-2-api \ -o jsonpath="{.spec.ports[0].nodePort}") curl -i "http://$IP:$PORT/demo/hello"
We retrieved the port of the service (we still have the Minikube node IP from before) and used it to send a request. The output of the last command is as follows:
HTTP/1.1 200 OK Date: Tue, 12 Dec 2017 21:27:51 GMT Content-Length: 14 Content-Type: text/plain; charset=utf-8 hello, world!
We got the response 200 and a friendly hello, world! message indicating that the API is indeed accessible from outside the cluster.
At this point, you might be wondering whether it is overkill to have four YAML files for a single application. Can't we simplify the definitions? Not really. Can we define everything in a single file? Read on.
Before we move further, we'll delete the objects we created. By now, you probably noticed that I like destroying things and starting over. Bear with me. There is a good reason for the imminent destruction:
kubectl delete -f svc/go-demo-2-db-rs.yml kubectl delete -f svc/go-demo-2-db-svc.yml kubectl delete -f svc/go-demo-2-api-rs.yml kubectl delete -f svc/go-demo-2-api-svc.yml
Everything we created is gone, and we can start over.