Konubinix' site

Clk K8s: Accessing the Host From the Pod on Linux and Mac

Fleeting

clk k8s is a tool meant to ease the creation and maintenance of a dev k8s stack.

When I deploy a local k8s stack, I sometimes want the pods to access the host. Because some services need to discuss with others using the public API, hence the ingress, hence the docker published ports.

Note that I wrote another, more generic article that may help understand this one.

One classical example is authorization. The servers should call the authorization server to check the validity of the access token, using some public address. In the local development stack, it makes sense to deploy this authority locally, in another docker container or helm application. And it makes more sense to discuss with it using a “public” interface rather than an internal network.

In more concrete term, you configure your ingresses to bind to custom domain names that redirect to 127.0.0.1, like mystack.localtest.me. Then, you use the command clk k8s add-domain mystack.localtest.me to make this domain actually resolve to the host from the perspective of the pods. That way, every actors of your program, whether it runs on the host or inside the k8s stack, can connect and use the exposed services.

As it happens, this is not as simple as it seems.

in linux

So far, I used the gateway IP of the docker bridge network as IP address to connect to when wanting to access host services.

Let’s simplify the scenario by running a simple nc command on the host.

while true; do nc -l -p 9999; done

Then, you can generally access it using the bridge gateway IP.

docker run alpine sh -c "nc 172.17.0.1 9999 < <(date)"

You should see the date appear on the listening side of the connection.

This also works from a pod running in the local cluster.

kubectl create deployment --image nginx nginx
kubectl exec deployment/nginx -- apt update
kubectl exec deployment/nginx -- apt install netcat-traditional
kubectl exec deployment/nginx -- nc 172.17.0.1 9999 < <(date)

This also shows the date on the listening side.

There is nothing difficult here.

in mac

The listening nc command needs to be slightly different.

while true; do nc -l 0.0.0.0 9999; done

with docker desktop

When people install docker in mac, they generally use the official docker desktop one. So let’s try it first.

docker run alpine sh -c "nc 172.17.0.1 9999 < <(date)"

This does nothing and fails.

They recommend using host.docker.internal.

Let’s try it.

docker run alpine sh -c "nc host.docker.internal 9999 < <(date)"

This one works.

So far so good. Let’s try it in the cluster.

clk k8s create-cluster --flow
kubectl create deployment --image nginx nginx
kubectl exec deployment/nginx -- apt update
kubectl exec deployment/nginx -- apt install netcat-traditional
kubectl exec deployment/nginx -- nc host.docker.internal 9999 < <(date)

It times out.

We can see that the ip address is correctly resolved though.

kubectl exec deployment/nginx -- apt install -y bind9-dnsutils
kubectl exec deployment/nginx -- dig +short host.docker.internal
192.168.65.254

Let’s try it with k3d instead of kind

clk k8s remove
clk k8s --distribution k3d create-cluster --flow
kubectl create deployment --image nginx nginx
kubectl exec deployment/nginx -- apt update
kubectl exec deployment/nginx -- apt install netcat-traditional
kubectl exec deployment/nginx -- nc host.docker.internal 9999 < <(date)

Indeed, host.docker.internal is not even resolved.

Let’s make sure of this.

kubectl exec deployment/nginx -- apt install -y bind9-dnsutils
kubectl exec deployment/nginx -- dig +short host.docker.internal

There is no returned ip address, showing that host.docker.internal does not resolve.

Let’s try using its IP address directly.

kubectl exec deployment/nginx -- nc 192.168.65.254 9999 < <(date)

Again, time out.

In fact, even though it is published on the host, the hosted service is generally run in docker as well.

So let’s try with the server part running inside docker.

docker run -i -t --rm -p 9999:9999 alpine sh -c "while true;do /usr/bin/nc -l -p 9999 -e echo hi;done"
docker run alpine sh -c "nc ${ip} 9999 < /dev/null"

This also times out.

Using the host.docker.internal address

docker run alpine sh -c "nc ${ip} 9999 < /dev/null"
hi

And again in k8s

kubectl create deployment --image nginx nginx
kubectl exec deployment/nginx -- apt update
kubectl exec deployment/nginx -- apt install netcat-traditional
kubectl exec deployment/nginx -- nc host.docker.internal 9999 < /dev/null
hi

So, using host.docker.internal works in k8s, using kind.

With k3d though, it still does not find the host.docker.internal address and times out with 192.168.65.254.

We could dig into the networking stuff, but I don’t think that it is worth it, because they are other more promising docker stacks. Also, the host.docker.internal name is not practical for it makes more complicated the scenario mentioned above with a custom domain like mystack.localtest.me.

Also, I don’t like docker desktop much, because it can only be managed using the graphical user interface, which is not IaC friendly.

trying with colima

/Users/sam/.nix-profile/bin/colima start --network-address
...
> Successfully created context "colima"
> Current context is now "colima"

Let’s first try with the default bridge gateway ID address

docker network inspect bridge | /Users/sam/.nix-profile/bin/jq -r ".[0].IPAM.Config[0].Gateway"
172.17.0.1
docker run alpine sh -c "nc ${ip} 9999 < <(date)"

It fails.

This is expected, because colima runs inside a virtual machine, it can access the host only using the colima network interface created for this.

/Users/sam/.nix-profile/bin/colima list --json | /Users/sam/.nix-profile/bin/jq -r .address
192.168.107.2

Let’s try with this interface, substituting 2 by 1 to get to the host.

docker run alpine sh -c "nc ${ip%*.*}.1 9999 < <(date)"

I can see the date in the listening side, meaning that it works.

But that is not practical, because I have to run a specific colima command to know the ip address.

with the listening part running with docker run -p

As explained earlier, most of my tools are running in docker. So the scenario that I am more interested in is when the server part is also a container.

First, let’s run the nc listening part in docker.

docker run -i -t --rm -p 9999:9999 alpine sh -c "while true;do /usr/bin/nc -l -p 9999 -e echo hi;done"

First, let’s discuss with it from the host.

nc localhost 9999 < /dev/null
hi

That’s ok, now let’s try to discuss with it using the bridge gateway IP from a docker container.

docker run alpine sh -c "nc ${ip} 9999 < /dev/null"
hi

That sounds promising, let’s try with the colima address.

docker run alpine sh -c "nc ${ip} 9999 < /dev/null"
hi

Now, let’s try from another network.

docker network create a
docker run --network a alpine sh -c "nc ${ip} 9999 < /dev/null"
hi

Great. Let’s just try with the colima ip address as well.

docker run --network a alpine sh -c "nc ${ip} 9999 < /dev/null"
hi

Everything works as expected here.

with k8s

Now, let’s try from a k8s deployment.

/Users/sam/.local/bin/clk k8s --distribution kind create-cluster
kubectl create deployment --image nginx nginx
kubectl exec deployment/nginx -- apt update
kubectl exec deployment/nginx -- apt install netcat-traditional
kubectl exec deployment/nginx -- nc "${ip}" 9999 < <(date)
hi

And using the colima address

kubectl exec deployment/nginx -- nc "${ip}" 9999 < <(date)
hi

Sound like everything is working well.

trying with orb

docker network inspect bridge | /Users/sam/.nix-profile/bin/jq -r ".[0].IPAM.Config[0].Gateway"
192.168.215.1

That is not practical as a lot of tools assume this IP address is 172.17.0.1.

Running the listening part in the host

while true; do nc -l 0.0.0.0 9999; done
docker run alpine sh -c "nc ${ip} 9999 < <(date)"

It fails. Like in colima, the virtual machine prevents accessing the host from the container using the gateway ip.

Now, trying from docker to docker.

The listening part now looks like (I took the liberty of putting it already in a separate network to test this case as well):

docker run -i -t --rm --network b -p 9999:9999 alpine sh -c "while true;do /usr/bin/nc -l -p 9999 -e echo hi;done"

Then,

docker run alpine nc "${ip}" 9999 < /dev/null
hi

Great. Now from k8s

kubectl create deployment --image nginx nginx
kubectl exec deployment/nginx -- apt update
kubectl exec deployment/nginx -- apt install netcat-traditional
kubectl exec deployment/nginx -- nc "${ip}" 9999 < /dev/null
hi

So it looks like orb also deals correctly with connecting the pods using the gateway ip.

conclusion

To ease developing in a k8s dev stack, I would definitely avoid docker desktop.

orb and colima work quite well regarding this use case.

orb is fast, but the strange gateway ip address may lead to issues. Also, it is closed source.

colima works well, is open source and has plenty of customization features. I can even enter its virtual machine and install custom stuff in there.

Therefore, I would tend to use colima.

Notes linking here