11.1 Exposing pods via services
If an application running in one pod needs to connect to another application running in a different pod, it needs to know the address of the other pod. This is easier said than done for the following reasons:
- Pods are ephemeral. A pod can be removed and replaced with a new one at any time. This happens when the pod is evicted from a node to make room for other pods, when the node fails, when the pod is no longer needed because a smaller number of pod replicas can handle the load, and for many other reasons.
- A pod gets its IP address when it’s assigned to a node. You don’t know the IP address of the pod in advance, so you can’t provide it to the pods that will connect to it.
- In horizontal scaling, multiple pod replicas provide the same service. Each of these replicas has its own IP address. If another pod needs to connect to these replicas, it should be able to do so using a single IP or DNS name that points to a load balancer that distributes the load across all replicas.
Also, some pods need to be exposed to clients outside the cluster. Until now, whenever you wanted to connect to an application running in a pod, you used port forwarding, which is for development only. The right way to make a group of pods externally accessible is to use a Kubernetes Service.
11.1.1 Introducing services
A Kubernetes Service is an object you create to provide a single, stable access point to a set of pods that provide the same service. Each service has a stable IP address that doesn’t change for as long as the service exists. Clients open connections to that IP address on one of the exposed network ports, and those connections are then forwarded to one of the pods that back that service. In this way, clients don’t need to know the addresses of the individual pods providing the service, so those pods can be scaled out or in and moved from one cluster node to the other at will. A service acts as a load balancer in front of those pods.
Understanding why you need services
The Kiada suite is an excellent example to explain services. It contains three sets of pods that provide three different services. The Kiada service calls the Quote service to retrieve a quote from the book, and the Quiz service to retrieve a quiz question.
I’ve made the necessary changes to the Kiada application in version 0.5. You can find the updated source code in the Chapter11/
directory in the book’s code repository. You’ll use this new version throughout this chapter. You’ll learn how to configure the Kiada application to connect to the other two services, and you’ll make it visible to the outside world. Since both the number of pods in each service and their IP addresses can change, you’ll expose them via Service objects, as shown in the following figure.
Figure 11.3 Exposing pods with Service objects
By creating a service for the Kiada pods and configuring it to be reachable from outside the cluster, you create a single, constant IP address through which external clients can connect to the pods. Each connection is forwarded to one of the kiada pods.
By creating a service for the Quote pods, you create a stable IP address through which the Kiada pods can reach the Quote pods, regardless of the number of pod instances behind the service and their location at any given time.
Although there’s only one instance of the Quiz pod, it too must be exposed through a service, since the pod’s IP address changes every time the pod is deleted and recreated. Without a service, you’d have to reconfigure the Kiada pods each time or make the pods get the Quiz pod’s IP from the Kubernetes API. If you use a service, you don’t have to do that because its IP address never changes.
Understanding how pods become part of a service
A service can be backed by more than one pod. When you connect to a service, the connection is passed to one of the backing pods. But how do you define which pods are part of the service and which aren’t?
In the previous chapter, you learned about labels and label selectors and how they’re used to organize a set of objects into subsets. Services use the same mechanism. As shown in the next figure, you add labels to Pod objects and specify the label selector in the Service object. The pods whose labels match the selector are part of the service.
Figure 11.4 Label selectors determine which pods are part of the Service.
The label selector defined in the quote
service is app=quote
, which means that it selects all quote
pods, both stable and canary instances, since they all contain the label key app
with the value quote
. Other labels on the pods don’t matter.
11.1.2 Creating and updating services
Kubernetes supports several types of services: ClusterIP
, NodePort
, LoadBalancer
, and ExternalName
. The ClusterIP
type, which you’ll learn about first, is only used internally, within the cluster. If you create a Service object without specifying its type, that’s the type of service you get. The services for the Quiz and Quote pods are of this type because they’re used by the Kiada pods within the cluster. The service for the Kiada pods, on the other hand, must also be accessible to the outside world, so the ClusterIP
type isn’t sufficient.
Creating a service YAML manifest
The following listing shows the minimal YAML manifest for the quote
Service object.
Listing 11.1 YAML manifest for the quote service
apiVersion: v1
kind: Service
metadata:
name: quote
spec:
type: ClusterIP
selector:
app: quote
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
NOTE
Since the quote
Service object is one of the objects that make up the Quote application, you could also add the app: quote
label to this object. However, because this label isn’t required for the service to function, it’s omitted in this example.
NOTE
If you create a service with multiple ports, you must specify a name for each port. It’s best to do the same for services with a single port.
NOTE
Instead of specifying the port number in the targetPort
field, you can also specify the name of the port as defined in the container’s port list in the pod definition. This allows the service to use the correct target port number even if the pods behind the service use different port numbers.
The manifest defines a ClusterIP
Service named quote
. The service accepts connections on port 80
and forwards each connection to port 80
of a randomly selected pod matching the app=quote
label selector, as shown in the following figure.
Figure 11.5 The quote service and the pods that it forwards traffic to
To create the service, apply the manifest file to the Kubernetes API using kubectl apply
.
Creating a service with kubectl expose
Normally, you create services like you create other objects, by applying an object manifest using kubectl apply
. However, you can also create services using the kubectl expose
command, as you did in chapter 3 of this book.
Create the service for the Quiz pod as follows:
$ kubectl expose pod quiz --name quiz
service/quiz exposed
This command creates a service named quiz
that exposes the quiz
pod. To do this, it checks the pod’s labels and creates a Service object with a label selector that matches all the pod’s labels.
NOTE
In chapter 3, you used the kubectl expose
command to expose a Deployment object. In this case, the command took the selector from the Deployment and used it in the Service object to expose all its pods. You’ll learn about Deployments in chapter 13.
You’ve now created two services. You’ll learn how to connect to them in section 11.1.3, but first let’s see if they’re configured correctly.
Listing services
When you create a service, it’s assigned an internal IP address that any workload running in the cluster can use to connect to the pods that are part of that service. This is the cluster IP address of the service. You can see it by listing services with the kubectl get services
command. If you want to see the label selector of each service, use the -o wide
option as follows:
$ kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
quiz ClusterIP 10.96.136.190 <none> 8080/TCP 15s app=quiz,rel=stable
quote ClusterIP 10.96.74.151 <none> 80/TCP 23s app=quote
NOTE
The shorthand for services
is svc
.
The output of the command shows the two services you created. For each service, the type, IP addresses, exposed ports, and label selector are printed.
NOTE
You can also view the details of each service with the kubectl describe svc
command.
You’ll notice that the quiz
service uses a label selector that selects pods with the labels app: quiz
and rel: stable
. This is because these are the labels of the quiz
pod from which the service was created using the kubectl expose
command.
Let’s think about this. Do you want the quiz
service to include only the stable pods? Probably not. Maybe later you decide to deploy a canary release of the quiz service in parallel with the stable version. In that case, you want traffic to be directed to both pods.
Another thing I don’t like about the quiz
service is the port number. Since the service uses HTTP, I’d prefer it to use port 80 instead of 8080. Fortunately, you can change the service after you create it.
Changing the service’s label selector
To change the label selector of a service, you can use the kubectl set selector
command. To fix the selector of the quiz
service, run the following command:
$ kubectl set selector service quiz app=quiz
service/quiz selector updated
List the services again with the -o wide
option to confirm the selector change. This method of changing the selector is useful if you’re deploying multiple versions of an application and want to redirect clients from one version to another.
Changing the ports exposed by the service
To change the ports that the service forwards to pods, you can edit the Service object with the kubectl edit
command or update the manifest file and then apply it to the cluster.
Before continuing, run kubectl edit svc quiz
and change the port from 8080
to 80
, making sure to only change the port
field and leaving the targetPort
set to 8080
, as this is the port that the quiz
pod listens on.
Configuring basic service properties
The following table lists the basic fields you can set in the Service object.
Table 11.1 Fields in the Service object’s spec for configuring the service’s basic properties
Field | Field type | Description |
---|---|---|
type | string | Specifies the type of this Service object. Allowed values are ClusterIP , NodePort , LoadBalancer , and ExternalName . The default value is ClusterIP . The differences between these types are explained in the following sections of this chapter. |
clusterIP | string | The internal IP address within the cluster where the service is available. Normally, you leave this field blank and let Kubernetes assign the IP. If you set it to None , the service is a headless service. These are explained in section 11.4. |
selector | map[string]string | Specifies the label keys and values that the pod must have in order for this service to forward traffic to it. If you you don’t set this field, you are responsible for managing the service endpoints. This is explained in section 11.3. |
ports | []Object | List of ports exposed by this service. Each entry can specify the name , protocol , appProtocol , port , nodePort , and targetPort . |
Other fields are explained throughout the remainder of this chapter.
IPV4/IPV6 DUAL-STACK SUPPORT
Kubernetes supports both IPv4 and IPv6. Whether dual-stack networking is supported in your cluster depends on whether the IPv6DualStack
feature gate is enabled for the cluster components to which it applies.
When you create a Service object, you can specify whether you want the service to be a single- or dual-stack service through the ipFamilyPolicy
field. The default value is SingleStack
, which means that only a single IP family is assigned to the service, regardless of whether the cluster is configured for single-stack or dual-stack networking. Set the value to PreferDualStack
if you want the service to receive both IP families when the cluster supports dual-stack, and one IP family when it supports single-stack networking. If your service requires both an IPv4 and an IPv6 address, set the value to RequireDualStack
. The creation of the service will be successful only on dual-stack clusters.
After you create the Service object, its spec.ipFamilies
array indicates which IP families have been assigned to it. The two valid values are IPv4
and IPv6
. You can also set this field yourself to specify which IP family to assign to the service in clusters that provide dual-stack networking. The ipFamilyPolicy
must be set accordingly or the creation will fail.
For dual-stack services, the spec.clusterIP
field contains only one of the IP addresses, but the spec.clusterIPs
field contains both the IPv4 and IPv6 addresses. The order of the IPs in the clusterIPs
field corresponds to the order in the ipFamilies
field.
11.1.3 Accessing cluster-internal services
The ClusterIP
services you created in the previous section are accessible only within the cluster, from other pods and from the cluster nodes. You can’t access them from your own machine. To see if a service is actually working, you must either log in to one of the nodes with ssh
and connect to the service from there, or use the kubectl exec
command to run a command like curl
in an existing pod and get it to connect to the service.
NOTE
You can also use the kubectl port-forward svc/my-service
command to connect to one of the pods backing the service. However, this command doesn’t connect to the service. It only uses the Service object to find a pod to connect to. The connection is then made directly to the pod, bypassing the service.
Connecting to services from pods
To use the service from a pod, run a shell in the quote-001
pod as follows:
$ kubectl exec -it quote-001 -c nginx -- sh
/ #
Now check if you can access the two services. Use the cluster IP addresses of the services that kubectl get services
displays. In my case, the quiz
service uses cluster IP 10.96.136.190
, whereas the quote
service uses IP 10.96.74.151
. From the quote-001
pod, I can connect to the two services as follows:
/ # curl http://10.96.136.190
This is the quiz service running in pod quiz
/ # curl http://10.96.74.151
This is the quote service running in pod quote-canary
NOTE
You don’t need to specify the port in the curl command, because you set the service port to 80, which is the default for HTTP.
If you repeat the last command several times, you’ll see that the service forwards the request to a different pod each time:
/ # while true; do curl http://10.96.74.151; done
This is the quote service running in pod quote-canary
This is the quote service running in pod quote-003
This is the quote service running in pod quote-001
...
The service acts as a load balancer. It distributes requests to all the pods that are behind it.
CONFIGURING SESSION AFFINITY ON SERVICES
You can configure whether the service should forward each connection to a different pod, or whether it should forward all connections from the same client to the same pod. You do this via the spec.sessionAffinity
field in the Service object. Only two types of service session affinity are supported: None
and ClientIP
.
The default type is None
, which means there’s no guarantee to which pod each connection will be forwarded. However, if you set the value to ClientIP
, all connections originating from the same IP will be forwarded to the same pod. In the spec.sessionAffinityConfig.clientIP.timeoutSeconds
field, you can specify how long the session will persist. The default value is 3 hours.
It may surprise you to learn that Kubernetes doesn’t provide cookie-based session affinity. However, considering that Kubernetes services operate at the transport layer of the OSI network model (UDP and TCP) not at the application layer (HTTP), they don’t understand HTTP cookies at all.
Resolving services via DNS
Kubernetes clusters typically run an internal DNS server that all pods in the cluster are configured to use. In most clusters, this internal DNS service is provided by CoreDNS, whereas some clusters use kube-dns. You can see which one is deployed in your cluster by listing the pods in the kube-system
namespace.
No matter which implementation runs in your cluster, it allows pods to resolve the cluster IP address of a service by name. Using the cluster DNS, pods can therefore connect to the quiz
service like so:
/ # curl http://quiz
This is the quiz service running in pod quiz
A pod can resolve any service defined in the same namespace as the pod by simply pointing to the name of the service in the URL. If a pod needs to connect to a service in a different namespace, it must append the namespace of the Service object to the URL. For example, to connect to the quiz
service in the kiada
namespace, a pod can use the URL http://quiz.kiada/
regardless of which namespace it’s in.
From the quote-001
pod where you ran the shell command, you can also connect to the service as follows:
/ # curl http://quiz.kiada
This is the quiz service running in pod quiz
A service is resolvable under the following DNS names:
<service-name>
, if the service is in the same namespace as the pod performing the DNS lookup,<service-name>.<service-namespace>
from any namespace, but also under<service-name>.<service-namespace>.svc
, and<service-name>.<service-namespace>.svc.cluster.local
.
NOTE
The default domain suffix is cluster.local
but can be changed at the cluster level.
The reason you don’t need to specify the fully qualified domain name (FQDN) when resolving the service through DNS is because of the search
line in the pod’s /etc/resolv.conf
file. For the quote-001 pod
, the file looks like this:
/ # cat /etc/resolv.conf
search kiada.svc.cluster.local svc.cluster.local cluster.local localdomain
nameserver 10.96.0.10
options ndots:5
When you try to resolve a service, the domain names specified in the search
field are appended to the name until a match is found. If you’re wondering what the IP address is in the nameserver
line, you can list all the services in your cluster to find out:
$ kubectl get svc -A
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
default kubernetes ClusterIP 10.96.0.1 <none> 443/TCP
kiada quiz ClusterIP 10.96.136.190 <none> 80/TCP
kiada quote ClusterIP 10.96.74.151 <none> 80/TCP
kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP...
The nameserver in the pod’s resolv.conf
file points to the kube-dns
service in the kube-system
namespace. This is the cluster DNS service that the pods use. As an exercise, try to figure out which pod(s) this service forwards traffic to.
CONFIGURING THE POD’S DNS POLICY
Whether or not a pod uses the internal DNS server can be configured using the dnsPolicy
field in the pod’s spec
. The default value is ClusterFirst
, which means that the pod uses the internal DNS first and then the DNS configured for the cluster node. Other valid values are Default
(uses the DNS configured for the node), None
(no DNS configuration is provided by Kubernetes; you must configure the pod’s DNS settings using the dnsConfig
field explained in the next paragraph), and ClusterFirstWithHostNet
(for special pods that use the host’s network instead of their own - this is explained later in the book).
Setting the dnsPolicy
field affects how Kubernetes configures the pod’s resolv.conf
file. You can further customize this file through the pod’s dnsConfig
field. The pod-with-dns-options.yaml
file in the book’s code repository demonstrates the use of this field.
Discovering services through environment variables
Nowadays, virtually every Kubernetes cluster offers the cluster DNS service. In the early days, this wasn’t the case. Back then, the pods found the IP addresses of the services using environment variables. These variables still exist today.
When a container is started, Kubernetes initializes a set of environment variables for each service that exists in the pod’s namespace. Let’s see what these environment variables look like by looking at the environment of one of your running pods.
Since you created your pods before the services, you won’t see any environment variables related to the services except those for the kubernetes
service, which exists in the default
namespace.
NOTE
The kubernetes
service forwards traffic to the API server. You’ll use it in chapter 16.
To see the environment variables for the two services that you created, you must restart the container as follows:
$ kubectl exec quote-001 -c nginx -- kill 1
When the container is restarted, its environment variables contain the entries for the quiz
and quote
services. Display them with the following command:
$ kubectl exec -it quote-001 -c nginx -- env | sort
...
QUIZ_PORT_80_TCP_ADDR=10.96.136.190
QUIZ_PORT_80_TCP_PORT=80
QUIZ_PORT_80_TCP_PROTO=tcp
QUIZ_PORT_80_TCP=tcp://10.96.136.190:80
QUIZ_PORT=tcp://10.96.136.190:80
QUIZ_SERVICE_HOST=10.96.136.190
QUIZ_SERVICE_PORT=80
QUOTE_PORT_80_TCP_ADDR=10.96.74.151
QUOTE_PORT_80_TCP_PORT=80
QUOTE_PORT_80_TCP_PROTO=tcp
QUOTE_PORT_80_TCP=tcp://10.96.74.151:80
QUOTE_PORT=tcp://10.96.74.151:80
QUOTE_SERVICE_HOST=10.96.74.151
QUOTE_SERVICE_PORT=80
Quite a handful of environment variables, wouldn’t you say? For services with multiple ports, the number of variables is even larger. An application running in a container can use these variables to find the IP address and port(s) of a particular service.
NOTE
In the environment variable names, the hyphens in the service name are converted to underscores and all letters are uppercased.
Nowadays, applications usually get this information through DNS, so these environment variables aren’t as useful as in the early days. They can even cause problems. If the number of services in a namespace is too large, any pod you create in that namespace will fail to start. The container exits with exit code 1 and you see the following error message in the container’s log:
standard_init_linux.go:228: exec user process caused: argument list too long
To prevent this, you can disable the injection of service information into the environment by setting the enableServiceLinks
field in the pod’s spec
to false
.
Understanding why you can’t ping a service IP
You’ve learned how to verify that a service is forwarding traffic to your pods. But what if it doesn’t? In that case, you might want to try pinging the service’s IP. Why don’t you try that right now? Ping the quiz
service from the quote-001
pod as follows:
$ kubectl exec -it quote-001 -c nginx -- ping quiz
PING quiz (10.96.136.190): 56 data bytes
^C
--- quiz ping statistics ---
15 packets transmitted, 0 packets received, 100% packet loss
command terminated with exit code 1
Wait a few seconds and then interrupt the process by pressing Control-C. As you can see, the IP address was resolved correctly, but none of the packets got through. This is because the IP address of the service is virtual and has meaning only in conjunction with one of the ports defined in the service. This is explained in chapter 18, which explains the internal workings of services. For now, remember that you can’t ping services.
Using services in a pod
Now that you know that the Quiz and Quote services are accessible from pods, you can deploy the Kiada pods and configure them to use the two services. The application expects the URLs of these services in the environment variables QUIZ_URL
and QUOTE_URL
. These aren’t environment variables that Kubernetes adds on its own, but variables that you set manually so that the application knows where to find the two services. Therefore, the env
field of the kiada
container must be configured as in the following listing.
...
env:
- name: QUOTE_URL
value: http://quote/quote
- name: QUIZ_URL
value: http://quiz
- name: POD_NAME
....
The environment variable QUOTE_URL
is set to http://quote/quote
. The hostname is the same as the name of the service you created in the previous section. Similarly, QUIZ_URL
is set to http://quiz
, where quiz
is the name of the other service you created.
Deploy the Kiada pods by applying the manifest file kiada-stable-and-canary.yaml
to your cluster using kubectl apply
. Then run the following command to open a tunnel to one of the pods you just created:
$ kubectl port-forward kiada-001 8080 8443
You can now test the application at http://localhost:8080 or https://localhost:8443. If you use curl
, you should see a response like the following:
$ curl http://localhost:8080
==== TIP OF THE MINUTE
Kubectl options that take a value can be specified with an equal sign or with a space. Instead of -tail=10, you can also type --tail 10.
==== POP QUIZ
First question
0) First answer
1) Second answer
2) Third answer
Submit your answer to /question/1/answers/<index of answer> using the POST method.
==== REQUEST INFO
Request processed by Kubia 1.0 running in pod "kiada-001" on node "kind-worker2".
Pod hostname: kiada-001; Pod IP: 10.244.1.90; Node IP: 172.18.0.2; Client IP: ::ffff:127.0.0.1
HTML version of this content is available at /html
If you open the URL in your web browser, you get the web page shown in the following figure.
Figure 11.6 The Kiada application when accessed with a web browser
If you can see the quote and quiz question, it means that the kiada-001
pod is able to communicate with the quote
and quiz
services. If you check the logs of the pods that back these services, you’ll see that they are receiving requests. In the case of the quote
service, which is backed by multiple pods, you’ll see that each request is sent to a different pod.