Socket activation of containers using Traefik's ForwardAuth

Tags: docker, traefik, webhook
Published: 2020-07-25 17:25:09

This is just an idea of mixing & matching a few services to get something like socket activation for docker containers.

Let's say there is a web hook which isn't triggered very often and it is created as a docker container. There is no point of running the container all the time just for an occasional calls to a web hook. But there is a problem how to start a container when needed, when a request is sent for that hook. Of course I could have something like OpenFaaS but that seems like a overkill for an intermittent process. In addition, OpenFaaS doesn't support mounting volumes by design so if I want to receive a file, confirm that it is received, process it in the background and store it somewhere on a filesystem, it requires an additional effort.

I'm already using [Traefik](https://traefik.io) as a reverse proxy and [webhook](https://github.com/adnanh/webhook) for some scenarios and I wanted to see if I could use that to achieve something similar to socket activation.

Traefik doesn't have "execute this shell script" middleware, but it does have [ForwardAuth](https://docs.traefik.io/middlewares/forwardauth/) middleware. It sends the request to the AuthServer and if the response code is 2XX access is granted ad original request is performed.

So my idea was: *If I use webhook as an "AuthServer" with a hook being a shell script that starts a container, could this work?*

As it turns out, **it does work**.

## The setup

Instead of an actual container doing some work, this example will just be starting simple nginx container, and stopping it after 20 seconds.

In a proper setup, instead of a nginx there would be a real container doing real processing and after processing is complete it would exit gracefully. Instead of blindly running a container, and stopping it after 20 seconds, there would be check in place whether the container is already running etc. Pretty much everything that OpenFaaS Gateway module does.  But this is just a general idea.

First I have a docker container which has webhook and docker-cli installed, built as:

```dockerfile
FROM        golang:alpine3.11 AS build
WORKDIR     /go/src/github.com/adnanh/webhook
ENV         WEBHOOK_VERSION 2.7.0
RUN         apk add --update -t build-deps curl libc-dev gcc libgcc \
            && curl -sSL https://github.com/adnanh/webhook/archive/${WEBHOOK_VERSION}.tar.gz \
              | tar -xz -C /go/src/github.com/adnanh/webhook --strip 1 \
            && go get -d \
            && go build -o /usr/local/bin/webhook

FROM        alpine
COPY        --from=build /usr/local/bin/webhook /usr/local/bin/
RUN         apk update \
            && apk add --no-cache docker-cli
WORKDIR     /etc/webhook
ENTRYPOINT  ["/usr/local/bin/webhook"]
```

That container has configuration mounted as `/etc/webhook/hooks.json`:

```json
[
  {
    "id": "start-nginx1",
    "execute-command": "/etc/webhook/scripts/nginx1",
  }
]
```

A shell script that will be executed, which starts a container (mounted as `/etc/webhook/scripts/nginx1`)

```bash
#!/bin/sh
set -e
docker run -d --name nginx1 --network skynet nginx:alpine
sleep 5
nohup /etc/webhook/scripts/nginx1-remove >& /dev/null &
```

And a shell script that will stop the container after 20 seconds
(mounted as `/etc/webhook/scripts/nginx1-remove`)

```bash
#!/bin/sh
set -e
sleep 20 && docker stop nginx1 && docker rm nginx1
```

With that in place, let's call this container `activator` and start it like:

```bash
docker run -d --name=activator \
-v /srv/activator:/etc/webhook \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--network skynet \
test/activator -hooks=/etc/webhook/hooks.json -verbose -hotreload 
```

For Traefik, configuration (dynamic part) is:

```toml
[http.routers]
  [http.routers.nginx1-http]
    entryPoints = ["web"]
    rule = "Host(\"nginx1.myservers.local\")"
    middlewares = ["auth-run"]
    service = "nginx1"

[http.middlewares]
  [http.middlewares.auth-run.forwardAuth]
    address = "http://activator:9000/hooks/nginx1"
[[http.services.nginx1.loadBalancer.servers]]
  url = "http://nginx1/" 
```



## The execution

With all of the above set, and given that dns entries are correct and all containers are on the same network once the http://nginx1.myservers.local is opened in a browser, the following will happen:

1. Browser will send a request to the Traefik
2. Traefik will, as a part of the authorization, call the nginx1 hook
3. Webhook will run a shell script
4. Shell script will start docker container and start remove script detached
5. If starting a shell script was successful, webhook will return code 200 to Traefik
6. Traefik will accept that as a authorization confirmation and pass the request to the, now running, nginx1 container
7. Nginx will respond with a welcome page
8. Welcome page (from nginx) will be displayed in a browser
9. After 20 seconds, nginx1 container will be removed
10. It works!



In a scenario where something wrong happens with a script (for example, a subsequent request within the 20sec period will fail to start a container with the same name while it is already running), webhook will not return 200, Traefik's authentication sequence will fail and  browser will get unauthorized error.



In a way this is exploiting ForwardAuth middleware for something totally different, but it does the trick.



## The application

Where I see this idea fit is for intermittent web hooks that do some processing in the background. For example, when a web hook is triggered, response is sent right away (confirmation that data is received), while the processing continues in the background, and once complete, container can shut down.