"wait-for", a tiny utility to wait for TCP or UDP resources to be ready

Use it in your Kubernetes clusters to wait for resources to be ready, like SQL Databases

7 minutes read, 1307 words
Posted on January 23, 2022, last updated on December 20, 2024

The screen of my Macbook Pro when I was writing some Go code in 2018

I know I’m not the first one to create this and, in fact, there are a plethora of options out there to use as of right now. Still, I wrote my own version of wait-for, and there are a few differences that make it to be a little more useful than the alternatives.

For those of you who have never heard about this, wait-for is a very simple and tiny application with a unique purpose: it allows you to define several endpoints – that is, endpoints like a MySQL Database (or MariaDB if you’re in line with the new waves), or even NoSQL ones, like Redis or MongoDB – and wait for them to be ready.

Now ready is a strong word, so to put it in clear terms, wait-for will wait until a connection can be established against a endpoint, and that’s it, it will allow you to configure some basic automation, needed in environments like docker-compose or Kubernetes. Specific protocols can define their own “ready” definition: see How it works? for details.

Running it as a CLI

Simply download a release from the releases page for your platform then run it like this:

wait-for \
  --host="example.com:443" \
  --host="mysql.example.com:3306" \
  --host="udp://8.8.8.8:53" \
  --verbose

For TCP endpoints, you can use the protocol-style prefix like tcp:// but it’s not necessary (for backwards compatibility). For UDP endpoints, you must use the udp:// prefix.

The check above connects to the MySQL endpoint via TCP. If you require additional verification, consider using the mysql:// protocol.

Other protocols are also supported, see Supported targets for more information.

The benefit of this CLI though lies on being useful in Kubernetes workloads. You can use it as an initContainer to preemptively check and make your main container wait until the resources are ready.

How does it work?

wait-for uses a protocol mechanism to match a protocol to a “pinger” function. The protocol is something like tcp:// or udp:// (or hey, even http://) and the pinger is a function that will try to connect to the endpoint and return a boolean value if it was successful or not.

The definition of “successful” depends on the pinger feature themselves. In the case of a TCP connection, for example, it will be considered successful if you can connect to it. UDP on the other hand uses a more “fire-and-forget” approach, where it will send a packet and not wait for a response. If the packet is sent, it’s considered successful.

Other “pinger” protocols can define their own logic. For example, the http:// protocol defines that it will make a GET request to the endpoint and consider it successful if it gets a HTTP response in the range of 200-299.

Supported targets

As of right now, wait-for supports the following protocols:

  • tcp:// - for TCP connections (it is, in fact, the default if you don’t specify a protocol) (docs)
  • udp:// - for UDP connections with a successful packet sent (docs)
  • http:// - for HTTP connections with a HTTP response in the 200-299 range (docs)
  • https:// - for HTTPS connections with a HTTP response in the 200-299 range with certificate verification (docs)
  • mysql:// - for MySQL connections with a successful ping to the server (docs)
  • postgres:// for PostgreSQL connections with a successful ping to the server (docs)

How to check in a Kubernetes cluster if your database is ready?

Databases in Kubernetes and how to wait for them to be ready

The best example is often used in Kubernetes environments where you want to wait for a MariaDB database to be available to connect to it or fail fast if it’s not. The easiest way to do this is by putting this application in an init container and then to probe every second if the database has finally come online.

More Kubernetes-savvy people might work this issue out in different ways, for example:

  • They could build their application so that the startup process checks for the Database to be ready and crash the container if it’s not. Kubernetes will reschedule the app until it either gives up restarting it or until the database has come online.
  • They could also configure the health checks to validate if the database is active, by just requesting it. This works well, too, since you can mark your application as “unhealthy”, which is especially useful when monitoring is active, and you want to be warned of this situation.

While those are great arguments against wait-for, this app is meant to be a stop-gap and not a one-size-fits-all solution. Often teams migrating their apps to Kubernetes environments might not have the time nor the ability to “patch” the application before it makes it to Kubernetes just to implement this feature.

Example YAML manifest for Kubernetes

The following YAML, taken as-is from the wait-for documentation page, outlines a very basic use case scenario:

 pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: init-container-demo
spec:
  initContainers:
  - name: wait-for
    image: ghcr.io/patrickdappollonio/wait-for:latest
    env:
    - name: POSTGRES_HOST
      value: "postgres.default.svc.cluster.local:5432"
    command:
      - /wait-for
    args:
      - --host="google.com:443"
      - --host="mysql.example.com:3306"
      - --host="$(POSTGRES_HOST)"
      - --verbose
  containers:
  - name: nginx-container
    image: nginx

The highlighted lines above show how to use an init container before the original application starts. In this case, we’re hoping nginx comes up after. Right before we boot it, we create an init container that pings google.com on port 443 (for https), mysql.example.com on port 3306, and postgres.default.svc.cluster.local on port 5432 – note for this last one, the setting is coming as an environment variable fed to the init container.

What do you get out of the box?

Out of the box, you can expect:

  • Proper exit codes for when connecting to those resources succeeded vs failed. It’s amazing how many of the solutions do not account for this, which is critical to ensure the next container, your app container, either runs or gets cancelled. That, or in CI/CD environments where a non-zero exit code allows to mark the pipeline as “failed”.
  • The ability to validate multiple hosts at once. You can call the --host flag multiple times, and for each, configure the appropriate port.
  • A configurable time between connections. This is a simple one, and the long-story-short is that you might not want to be making requests every second to a service that’s taking its precious time to boot. Perhaps you want to try every 5 seconds instead? That’s configurable using the --every flag.

If there are other features you would like to see, please open an issue with your request! Happy to discuss those further!

Some things you should know…

There are a couple of caveats that I want to wait to see addressed in the open and gauge the interest to know what people would expect here:

  • The Docker image is a scratch image, and that means there’s no terminal or shell interpreter like bash or sh. It’s often, personally, not needed, and you can get environment variable injection from Kubernetes itself as shown above. Still, if there’s a need to have either sh or bash, I’m down to hear the recommendations.
    • Due to this, by the way, I’ve already received feedback to allow the option to “pass” a command to execute when all resources are available. I might implement this either as a wait-for -h "example.com:443" -- echo "Success!" or something along those lines. It gets challenging if you also want a command to execute on failures.
  • If your database or third party resource requires authentication, wait-for might not work for your use case. It will all depend on the protocol support whether they can use authentication or not. For example, MySQL and PostgreSQL support authentication, but HTTP and HTTPS do not.

Other than that, if you encounter any issues or there’s a feature you would like to see added, please don’t hesitate and open an issue! It also works for additional features to preexistent protocols, so if there’s a feature you would see implemented, let me know!

Link: wait-for on Github

Share this: