5.3 Interacting with the application and the pod
Your container is now running. In this section, you’ll learn how to communicate with the application, inspect its logs, and execute commands in the container to explore the application’s environment. Let’s confirm that the application running in the container responds to your requests.
Sending requests to the application in the pod
In chapter 2, you used the kubectl
expose
command to create a service that provisioned a load balancer so you could talk to the application running in your pod(s). You’ll now take a different approach. For development, testing and debugging purposes, you may want to communicate directly with a specific pod, rather than using a service that forwards connections to randomly selected pods.
You’ve learned that each pod is assigned its own IP address where it can be accessed by every other pod in the cluster. This IP address is typically internal to the cluster. You can’t access it from your local computer, except when Kubernetes is deployed in a specific way – for example, when using kind or Minikube without a VM to create the cluster.
In general, to access pods, you must use one of the methods described in the following sections. First, let’s determine the pod’s IP address.
Getting the pod’s IP address
You can get the pod’s IP address by retrieving the pod’s full YAML and searching for the podIP
field in the status
section. Alternatively, you can display the IP with kubectl describe
, but the easiest way is to use kubectl get
with the wide
output option:
$ kubectl get pod kubia -o wide
NAME READY STATUS RESTARTS AGE IP NODE ...
kubia 1/1 Running 0 35m 10.244.2.4 worker2 ...
As indicated in the IP column, my pod’s IP is 10.244.2.4
. Now I need to determine the port number the application is listening on.
Getting the port the application is bound to
If I wasn’t the author of the application, it would be difficult for me to find out which port the application listens on. I could inspect its source code or the Dockerfile of the container image, as the port is usually specified there, but I might not have access to either. If someone else had created the pod, how would I know which port it was listening on?
Fortunately, you can specify a list of ports in the pod definition itself. It isn’t necessary to specify any ports, but it is a good idea to always do so. See sidebar for details.
Why specify container ports in pod definitions
Specifying ports in the pod definition is purely informative. Their omission has no effect on whether clients can connect to the pod’s port. If the container accepts connections through a port bound to its IP address, anyone can connect to it, even if the port isn’t explicitly specified in the pod spec or if you specify an incorrect port number.
Despite this, it’s a good idea to always specify the ports so that anyone who has access to your cluster can see which ports each pod exposes. By explicitly defining ports, you can also assign a name to each port, which is very useful when you expose pods via services.
The pod manifest says that the container uses port 8080, so you now have everything you need to talk to the application.
Connecting to the pod from the worker nodes
The Kubernetes network model dictates that each pod is accessible from any other pod and that each node can reach any pod on any node in the cluster.
Because of this, one way to communicate with your pod is to log into one of your worker nodes and talk to the pod from there. You’ve already learned that the way you log on to a node depends on what you used to deploy your cluster. In Minikube, you can run minikube ssh
to log in to your single node. On GKE use the gcloud compute ssh
command. For other clusters refer to their documentation.
Once you have logged into the node, use the curl
command with the pod’s IP and port to access your application. My pod’s IP is 10.244.2.4 and the port is 8080, so I run the following command:
$ curl 10.244.2.4:8080
Hey there, this is kubia. Your IP is ::ffff:10.244.2.1.
Normally you don’t use this method to talk to your pods, but you may need to use it if there are communication issues and you want to find the cause by first trying the shortest possible communication route. In this case, it’s best to log into the node where the pod is located and run curl
from there. The communication between it and the pod takes place locally, so this method always has the highest chances of success.
Connecting from a one-off client pod
The second way to test the connectivity of your application is to run curl
in another pod that you create specifically for this task. Use this method to test if other pods will be able to access your pod. Even if the network works perfectly, this may not be the case. In chapter 24, you’ll learn how to lock down the network by isolating pods from each other. In such a system, a pod can only talk to the pods it’s allowed to.
To run curl
in a one-off pod, use the following command:
$ kubectl run --image=tutum/curl -it --restart=Never --rm client-pod curl 10.244.2.4:8080
Hey there, this is kubia. Your IP is ::ffff:10.244.2.5.
pod "client-pod" deleted
This command runs a pod with a single container created from the tutum/curl
image. You can also use any other image that provides the curl
binary executable. The -it
option attaches your console to the container’s standard input and output, the --restart=Never
option ensures that the pod is considered Completed when the curl
command and its container terminate, and the --rm
options removes the pod at the end. The name of the pod is client-pod
and the command executed in its container is curl 10.244.2.4:8080
.
NOTE
You can also modify the command to run the bash
shell in the client pod and then run curl
from the shell.
Creating a pod just to see if it can access another pod is useful when you’re specifically testing pod-to-pod connectivity. If you only want to know if your pod is responding to requests, you can also use the method explained in the next section.
Connecting to pods via kubectl port forwarding
During development, the easiest way to talk to applications running in your pods is to use the kubectl port-forward
command, which allows you to communicate with a specific pod through a proxy bound to a network port on your local computer, as shown in the next figure.
Figure 5.8 Connecting to a pod through the kubectl port-forward proxy
To open a communication path with a pod, you don’t even need to look up the pod’s IP, as you only need to specify its name and the port. The following command starts a proxy that forwards your computer’s local port 8080
to the kubia
pod’s port 8080
:
$ kubectl port-forward kubia 8080
... Forwarding from 127.0.0.1:8080 -> 8080
... Forwarding from [::1]:8080 -> 8080
The proxy now waits for incoming connections. Run the following curl
command in another terminal:
$ curl localhost:8080
Hey there, this is kubia. Your IP is ::ffff:127.0.0.1.
As you can see, curl
has connected to the local proxy and received the response from the pod. While the port-forward
command is the easiest method for communicating with a specific pod during development and troubleshooting, it’s also the most complex method in terms of what happens underneath. Communication passes through several components, so if anything is broken in the communication path, you won’t be able to talk to the pod, even if the pod itself is accessible via regular communication channels.
NOTE
The kubectl port-forward
command can also forward connections to services instead of pods and has several other useful features. Run kubectl port-forward --help
to learn more.
Figure 5.9 shows how the network packets flow from the curl
process to your application and back.
Figure 5.9 The long communication path between curl and the container when using port forwarding
As shown in the figure, the curl
process connects to the proxy, which connects to the API server, which then connects to the Kubelet on the node that hosts the pod, and the Kubelet then connects to the container through the pod’s loopback device (in other words, through the localhost address). I’m sure you’ll agree that the communication path is exceptionally long.
NOTE
The application in the container must be bound to a port on the loopback device for the Kubelet to reach it. If it listens only on the pod’s eth0
network interface, you won’t be able to reach it with the kubectl port-forward
command.
Viewing application logs
Your Node.js application writes its log to the standard output stream. Instead of writing the log to a file, containerized applications usually log to the standard output (stdout) and standard error streams (stderr). This allows the container runtime to intercept the output, store it in a consistent location (usually /var/log/containers
) and provide access to the log without having to know where each application stores its log files.
When you run an application in a container using Docker, you can display its log with docker logs <container-id>
. When you run your application in Kubernetes, you could log into the node that hosts the pod and display its log using docker
logs
, but Kubernetes provides an easier way to do this with the kubectl logs
command.
Retrieving a pod’s log with kubectl logs
To view the log of your pod (more specifically, the container’s log), run the command shown in the following listing on your local computer:
Listing 5.4 Displaying a pod’s log
$ kubectl logs kubia
Kubia server starting...
Local hostname is kubia
Listening on port 8080
Received request for / from ::ffff:10.244.2.1
Received request for / from ::ffff:10.244.2.5
Received request for / from ::ffff:127.0.0.1
Streaming logs using kubectl logs -f
If you want to stream the application log in real-time to see each request as it comes in, you can run the command with the --follow
option (or the shorter version -f
):
$ kubectl logs kubia -f
Now send some additional requests to the application and have a look at the log. Press ctrl-C to stop streaming the log when you’re done.
Displaying the timestamp of each logged line
You may have noticed that we forgot to include the timestamp in the log statement. Logs without timestamps have limited usability. Fortunately, the container runtime attaches the current timestamp to every line produced by the application. You can display these timestamps by using the --timestamps=true
option, as shown in the next listing.
Listing 5.5 Displaying the timestamp of each log line
$ kubectl logs kubia –-timestamps=true
2020-02-01T09:44:40.954641934Z Kubia server starting...
2020-02-01T09:44:40.955123432Z Local hostname is kubia
2020-02-01T09:44:40.956435431Z Listening on port 8080
2020-02-01T09:50:04.978043089Z Received request for / from ...
2020-02-01T09:50:33.640897378Z Received request for / from ...
2020-02-01T09:50:44.781473256Z Received request for / from ...
TIP
You can display timestamps by only typing --timestamps
without the value. For boolean options, merely specifying the option name sets the option to true
. This applies to all kubectl options that take a Boolean value and default to false
.
Displaying recent logs
The previous feature is great if you run third-party applications that don’t include the timestamp in their log output, but the fact that each line is timestamped brings us another benefit: filtering log lines by time. Kubectl provides two ways of filtering the logs by time.
The first option is when you want to only display logs from the past several seconds, minutes or hours. For example, to see the logs produced in the last two minutes, run:
$ kubectl logs kubia --since=2m
The other option is to display logs produced after a specific date and time using the --since-time
option. The time format to be used is RFC3339. For example, the following command is used to print logs produced after February 1st, 2020 at 9:50 a.m.:
$ kubectl logs kubia –-since-time=2020-02-01T09:50:00Z
Displaying the last several lines of the log
Instead of using time to constrain the output, you can also specify how many lines from the end of the log you want to display. To display the last ten lines, try:
$ kubectl logs kubia –-tail=10
NOTE
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
.
Understanding the availability of the pod’s logs
Kubernetes keeps a separate log file for each container. They are usually stored in /var/log/containers
on the node that runs the container. A separate file is created for each container. If the container is restarted, its logs are written to a new file. Because of this, if the container is restarted while you’re following its log with kubectl logs -f
, the command will terminate, and you’ll need to run it again to stream the new container’s logs.
The kubectl logs
command displays only the logs of the current container. To view the logs from the previous container, use the --previous
(or -p
) option.
NOTE
Depending on your cluster configuration, the log files may also be rotated when they reach a certain size. In this case, kubectl logs
will only display the new log file. When streaming the logs, you must restart the command to switch to the new file.
When you delete a pod, all its log files are also deleted. To make pods’ logs available permanently, you need to set up a central, cluster-wide logging system. Chapter 23 explains how.
What about applications that write their logs to files?
If your application writes its logs to a file instead of stdout, you may be wondering how to access that file. Ideally, you’d configure the centralized logging system to collect the logs so you can view them in a central location, but sometimes you just want to keep things simple and don’t mind accessing the logs manually. In the next two sections, you’ll learn how to copy log and other files from the container to your computer and in the opposite direction, and how to run commands in running containers. You can use either method to display the log files or any other file inside the container.
Copying files to and from containers
Sometimes you may want to add a file to a running container or retrieve a file from it. Modifying files in running containers isn’t something you normally do - at least not in production - but it can be useful during development.
Kubectl offers the cp
command to copy files or directories from your local computer to a container of any pod or from the container to your computer. For example, to copy the /etc/hosts
file from the container of the kubia
pod to the /tmp
directory on your local file system, run the following command:
$ kubectl cp kubia:/etc/hosts /tmp/kubia-hosts
To copy a file from your local file system to the container, run the following command:
$ kubectl cp /path/to/local/file kubia:path/in/container
NOTE
The kubectl cp
command requires the tar
binary to be present in your container, but this requirement may change in the future.
Executing commands in running containers
When debugging an application running in a container, it may be necessary to examine the container and its environment from the inside. Kubectl provides this functionality, too. You can execute any binary file present in the container’s file system using the kubectl exec
command.
Invoking a single command in the container
For example, you can list the processes running in the container in the kubia
pod by running the following command:
Listing 5.6 Processes running inside a pod container
$ kubectl exec kubia -- ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 1.3 812860 27356 ? Ssl 11:54 0:00 node app.js
root 120 0.0 0.1 17500 2128 ? Rs 12:22 0:00 ps aux
This is the Kubernetes equivalent of the Docker command you used to explore the processes in a running container in chapter 2. It allows you to remotely run a command in any pod without having to log in to the node that hosts the pod. If you’ve used ssh to execute commands on a remote system, you’ll see that kubectl
exec
is not much different.
In section 5.3.1 you executed the curl
command in a one-off client pod to send a request to your application, but you can also run the command inside the kubia
pod itself:
$ kubectl exec kubia -- curl -s localhost:8080
Hey there, this is kubia. Your IP is ::1.
Why use a double dash in the kubectl exec command?
The double dash (--
) in the command delimits kubectl arguments from the command to be executed in the container. The use of the double dash isn’t necessary if the command has no arguments that begin with a dash. If you omit the double dash in the previous example, the -s
option is interpreted as an option for kubectl
exec
and results in the following misleading error:
$ kubectl exec kubia curl -s localhost:8080
The connection to the server localhost:8080 was refused – did you specify the right host or port?
This may look like the Node.js server is refusing to accept the connection, but the issue lies elsewhere. The curl command is never executed. The error is reported by kubectl
itself when it tries to talk to the Kubernetes API server at localhost:8080
, which isn’t where the server is. If you run the kubectl options
command, you’ll see that the -s
option can be used to specify the address and port of the Kubernetes API server. Instead of passing that option to curl, kubectl adopted it as its own. Adding the double dash prevents this.
Fortunately, to prevent scenarios like this, newer versions of kubectl are set to return an error if you forget the double dash.
Running an interactive shell in the container
The two previous examples showed how a single command can be executed in the container. When the command completes, you are returned to your shell. If you want to run several commands in the container, you can run a shell in the container as follows:
$ kubectl exec -it kubia -- bash
root@kubia:/#
The -it
is short for two options: -i
and -t
, which indicate that you want to execute the bash
command interactively by passing the standard input to the container and marking it as a terminal (TTY).
You can now explore the inside of the container by executing commands in the shell. For example, you can view the files in the container by running ls -la
, view its network interfaces with ip link
, or test its connectivity with ping
. You can run any tool available in the container.
Not all containers allow you to run shells
The container image of your application contains many important debugging tools, but this isn’t the case with every container image. To keep images small and improve security in the container, most containers used in production don’t contain any binary files other than those required for the container’s primary process. This significantly reduces the attack surface, but also means that you can’t run shells or other tools in production containers. Fortunately, a new Kubernetes feature called ephemeral containers allows you to debug running containers by attaching a debug container to them.
NOTE TO MEAP READERS
Ephemeral containers are currently an alpha feature, which means they may change or even be removed at any time. This is also why they are currently not explained in this book. If they graduate to beta before the book goes into production, a section explaining them will be added.
Attaching to a running container
The kubectl attach
command is another way to interact with a running container. It attaches itself to the standard input, output and error streams of the main process running in the container. Normally, you only use it to interact with applications that read from the standard input.
Using kubectl attach to see what the application prints to standard output
If the application doesn’t read from standard input, the kubectl attach
command is only an alternative way to stream the application logs, as these are typically written to the standard output and error streams, and the attach
command streams them just like the kubectl logs -f
command does.
Attach to your kubia
pod by running the following command:
$ kubectl attach kubia
Defaulting container name to kubia.
Use 'kubectl describe pod/kubia -n default' to see all of the containers in this pod.
If you don't see a command prompt, try pressing enter.
Now, when you send new HTTP requests to the application using curl
in another terminal, you’ll see the lines that the application logs to standard output also printed in the terminal where the kubectl
attach
command is executed.
Using kubectl attach to write to the application’s standard input
The kubia application doesn’t read from the standard input stream, but you’ll find another version of the application that does this in the book’s code archive. You can change the greeting with which the application responds to HTTP requests by writing it to the standard input stream of the application. Let’s deploy this version of the application in a new pod and use the kubectl
attach
command to change the greeting.
You can find the artifacts required to build the image in the kubia-stdin-image/
directory, or you can use the pre-built image docker.io/luksa/kubia:1.0-stdin
. The pod manifest is in the file kubia-stdin.yaml
. It is only slightly different from the pod manifest in kubia.yaml
that you deployed previously. The differences are highlighted in the following listing.
Listing 5.7 Enabling standard input for a container
apiVersion: v1
kind: Pod
metadata:
name: kubia-stdin
spec:
containers:
- name: kubia
image: luksa/kubia:1.0-stdin
stdin: true
ports:
- containerPort: 8080
As you can see in the listing, if the application running in a pod wants to read from standard input, you must indicate this in the pod manifest by setting the stdin
field in the container definition to true
. This tells Kubernetes to allocate a buffer for the standard input stream, otherwise the application will always receive an EOF
when it tries to read from it.
Create the pod from this manifest file with the kubectl apply
command:
$ kubectl apply -f kubia-stdin.yaml
pod/kubia-stdin created
To enable communication with the application, use the kubectl port-forward
command again, but because the local port 8080
is still being used by the previously executed port-forward
command, you must either terminate it or choose a different local port to forward to the new pod. You can do this as follows:
$ kubectl port-forward kubia-stdin 8888:8080
Forwarding from 127.0.0.1:8888 -> 8080
Forwarding from [::1]:8888 -> 8080
The command-line argument 8888:8080
instructs the command to forward local port 8888
to the pod’s port 8080
.
You can now reach the application at http://localhost:8888:
$ curl localhost:8888
Hey there, this is kubia-stdin. Your IP is ::ffff:127.0.0.1.
Let’s change the greeting from “Hey there” to “Howdy” by using kubectl attach
to write to the standard input stream of the application. Run the following command:
$ kubectl attach -i kubia-stdin
Note the use of the additional option -i
in the command. It instructs kubectl
to pass its standard input to the container.
NOTE
Like the kubectl
exec
command, kubectl
attach
also supports the --tty
or -t
option, which indicates that the standard input is a terminal (TTY), but the container must be configured to allocate a terminal through the tty
field in the container definition.
You can now enter the new greeting into the terminal and press the ENTER key. The application should then respond with the new greeting:
Howdy
Greeting set to: Howdy
To see if the application now responds to HTTP requests with the new greeting, re-execute the curl
command or refresh the page in your web browser:
$ curl localhost:8888
Howdy, this is kubia-stdin. Your IP is ::ffff:127.0.0.1.
There’s the new greeting. You can change it again by typing another line in the terminal with the kubectl attach
command. To exit the attach
command, press Control-C or the equivalent key.
NOTE
An additional field in the container definition, stdinOnce
, determines whether the standard input channel is closed when the attach session ends. It’s set to false
by default, which allows you to use the standard input in every kubectl attach
session. If you set it to true
, standard input remains open only during the first session.