View on GitHub

await.sh

Self-contained POSIX shell scripts to await the availability of resources and services. Can be used as a workaround for Docker Swarm's missing depends_on support.

await.sh

Build Build Status License Contributor Covenant

Feedback and high-quality pull requests are highly welcome!

  1. What is it?
  2. Usage
    1. await-cmd.sh
    2. await-http.sh
    3. await-tcp.sh
    4. Docker Swarm Example
    5. Kubernetes Example
  3. License

What is it?

This repository contains POSIX shell scripts that wait for a given time until resources (TCP ports, HTTP services) become available and then execute predefined commands.

These scripts where created to workaround the fact that Docker Swarm does not support the depends_on constraint of Docker Compose, which means there is no control over the startup order of multiple containers possible. For the interested reader, a lengthy discussion about this issue can be found here https://github.com/moby/moby/issues/31333 with a lot of pros & cons.

In contrast to other third-party solutions, such as:

the scripts provided here have minimal system requirements (a POSIX shell, timeout, nc, curl or wget), work with BusyBox, Alpine Linux images and can be used without modifying existing images.

The scripts are automatically tested using Bats and executed under ash, bash, busybox, dash, ksh and zsh.

Usage

await-cmd.sh

Usage: await-cmd.sh [OPTION]... TIMEOUT TEST_COMMAND [ARG...] [-- COMMAND [ARG...]]

Executes TEST_COMMAND repeatedly until it's exit code is 0. Then executes COMMAND.

Parameters:
  TIMEOUT       - Duration in seconds within TEST_COMMAND must return exit code 0.
  TEST_COMMAND  - Command that will be executed to test if the waiting condition is met.
  COMMAND       - Command to be executed once the TEST_COMMAND succeeded (optional).

Options:
  -f       - Force execution of COMMAND even if timeout occurred.
  -t SECS  - Duration in seconds after which a TEST_COMMAND process is terminated (optional, default: 10 seconds).
  -w SECS  - Waiting period in seconds between each execution of TEST_COMMAND (optional, default: 5 seconds).

Examples:
  await-cmd.sh 30 /opt/scripts/check_remote_services.sh -- /opt/server/start.sh --port 8080
  await-cmd.sh -w 10 30 /opt/scripts/check_remote_services.sh -- /opt/server/start.sh --port 8080

await-http.sh

Usage: await-http.sh [OPTION]... TIMEOUT URL... [-- COMMAND [ARG...]]

Repeatedly performs HTTP GET requests until the URL returns a HTTP status code <= 399. Then executes COMMAND.

Parameters:
  TIMEOUT  - Number of seconds within the URL must be reachable.
  URL      - URL(s) to be checked using HTTP GET.
  COMMAND  - Command to be executed once the wait condition is satisfied.

Options:
  -f       - Force execution of COMMAND even if timeout occurred.
  -t SECS  - Duration in seconds after which a connection attempt is aborted (optional, default: 10 seconds).
  -w SECS  - Duration in seconds to wait between retries (optional, default: 5 seconds).

Examples:
  await-http.sh 30 http://service1.local -- /opt/server/start.sh --port 8080
  await-http.sh 30 http://service1.local https://service2.local -- /opt/server/start.sh --port 8080
  await-http.sh -w 10 30 https://service1.local -- /opt/server/start.sh --port 8080

await-tcp.sh

Usage: await-tcp.sh [OPTION]... TIMEOUT HOSTNAME:PORT... [-- COMMAND [ARG...]]

Repeatedly attempts to connect to the given address until the TCP port is available. Then executes COMMAND.

Parameters:
  TIMEOUT        - Duration in seconds within the TCP port of the given host must be reachable.
  HOSTNAME:PORT  - Target TCP address(es) to connect to.
  COMMAND        - Command to be executed once a connection could be established (optional).

Options:
  -f       - Force execution of COMMAND even if timeout occurred.
  -t SECS  - Duration in seconds after which a connection attempt is aborted (optional, default: 10 seconds).
  -w SECS  - Duration in seconds to wait between retries (optional, default: 5 seconds).

Examples:
  await-tcp.sh 30 service1.local:389 -- /opt/server/start.sh --port 8080
  await-tcp.sh 30 service1.local:389 service2.local:5672 -- /opt/server/start.sh --port 8080
  await-tcp.sh -w 10 30 service1.local:389 -- /opt/server/start.sh --port 8080

Docker Swarm Example

Here is an example stack (compose file) where two nginx servers are started in an ordered fashion.

The idea is to upload the await script into the config store, then mount into a service and override the default command. This way no additional Dockerfile/image needs to be created containing the script to support horizontal scaling.

version: '3.7'

configs:
  # stores the script into the swarm config service, which automatically distributes
  # it to the nodes running the containers.
  await_http_script:
    file: /opt/await/await-http.sh # existing path on the swarm master node

services:

  backend_service:
    image: karthequian/helloworld
    deploy:
      mode: replicated
      replicas: 1
      restart_policy:
        condition: on-failure
        delay: 5s

  frontend_service:
    image: karthequian/helloworld
    configs:
      # mount the script from config service into the container
      - source: await_http_script
        target: /await-http.sh
        mode: 0555
    command:
      # wait up to 30 seconds for backend_service to become available, then start nginx
      /await-http.sh 30 http://backend_service -- nginx
    ports:
      - 80:80
    deploy:
      mode: replicated
      replicas: 1
      restart_policy:
        condition: on-failure
        delay: 5s

Kubernetes Example

One way to use the scripts with Kubernetes is creating a configmap and mount it to the container. So you needn’t change your containers.

Create configmap from scripts

kubectl create configmap await-config --from-file=await-cmd.sh --from-file=await-http.sh --from-file=await-tcp.sh

Kubernetes multi-container pods have the same resources, so we can check the MySQL port on localhost within our helloworld container.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  labels:
    app: deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: deployment
  template:
    metadata:
      labels:
        app: deployment
    spec:
      containers:
      - image: mysql:5.6
        name: mysql
        env:
          # Use secret in real usage
        - name: MYSQL_ROOT_PASSWORD
          value: password
        ports:
        - containerPort: 3306
          name: mysql
      - name: frontendservice
        image: karthequian/helloworld
        imagePullPolicy: Always
        command: ["/bin/sh","-c", "echo 'start await' && cp /opt/scripts/*.sh /tmp && sh /tmp/await-tcp.sh 30 localhost:3306 -- nginx"]
        ports:
        - containerPort: 80
        volumeMounts:
        - name: await-volume
          mountPath: /opt/scripts
      volumes:
      - name: await-volume
        configMap:
          name: await-config

Example output:

➜ kubectl logs myapp-deployment-67d6946f86-8qxwc frontendservice
start await
Waiting up to 30 seconds for [localhost:3306] to get ready...
=> executing [perl -e 'use IO::Socket;
my $socket=IO::Socket::INET->new(PeerAddr => "localhost", PeerPort => 3306, Timeout => 10);
if (defined $socket) {sleep 1; (defined $socket->connected?exit(0):exit(1))} else {exit(1)}']...ERROR
=> executing [perl -e 'use IO::Socket;
my $socket=IO::Socket::INET->new(PeerAddr => "localhost", PeerPort => 3306, Timeout => 10);
if (defined $socket) {sleep 1; (defined $socket->connected?exit(0):exit(1))} else {exit(1)}']...ERROR
=> executing [perl -e 'use IO::Socket;
my $socket=IO::Socket::INET->new(PeerAddr => "localhost", PeerPort => 3306, Timeout => 10);
if (defined $socket) {sleep 1; (defined $socket->connected?exit(0):exit(1))} else {exit(1)}']...OK
SUCCESS: Waiting condition is met.
Executing [nginx]...

License

All files are released under the Apache License 2.0.

Individual files contain the following tag instead of the full license text:

SPDX-License-Identifier: Apache-2.0

This enables machine processing of license information based on the SPDX License Identifiers that are available here: https://spdx.org/licenses/.