TL;DR;
Docker-compose allows scaling specific services from the composition. Assume we have a server and worker model. Consider the below compose file for the same
version: '3'
services:
server:
image: alpine
command: sh -c "echo Launching server && exec tail -f /dev/null"
worker:
image: alpine
command: sh -c "echo Launching worker >> /data/worker.log && exec tail -f /dev/null"
volumes:
- ./data:/data
Now if we do docker-compose up
the server will echo Launching server
and worker
will print Launching worker
to the /data/worker.log
file.
Now let us run the same config with the scale option
$ docker-compose up -d --scale worker=5
Above command instructs docker-compose to launch 1 server container and 5 worker container. Now if we print our ./data/worker.log
$ cat data/worker.log
Launching worker
Launching worker
Launching worker
Launching worker
Launching worker
We get logs of all workers mixed into the same log file. The worker container has no idea that it is being scaled and can’t adjust its log filename or folder. Now if check the worker containers using docker ps
$ docker ps | grep worker
0520ea120ee0 alpine "sh -c 'echo Launc..." 32 seconds ago Up 31 seconds scale1_worker_5
5d08bf33d88d alpine "sh -c 'echo Launc..." 32 seconds ago Up 30 seconds scale1_worker_4
34971d89ccdc alpine "sh -c 'echo Launc..." 32 seconds ago Up 31 seconds scale1_worker_3
b789c7806aac alpine "sh -c 'echo Launc..." 32 seconds ago Up 31 seconds scale1_worker_2
1468e77231b0 alpine "sh -c 'echo Launc..." 32 seconds ago Up 31 seconds scale1_worker_1
Let is pick our worker scale1_worker_3
and inspect it.
$ docker inspect scale1_worker_3
....
....
"Labels": {
"com.docker.compose.config-hash": "6b42ee8ad919a3581c7342e91900ac3984fcf5b7c69274e63bc655461e9a0b57",
"com.docker.compose.container-number": "3",
"com.docker.compose.oneoff": "False",
"com.docker.compose.project": "scale1",
"com.docker.compose.service": "worker",
"com.docker.compose.version": "1.15.0"
}
....
....
As we can see that compose will label the container using com.docker.compose.container-number
which indicates a dynamic number for the worker.
If we can get this label inside docker container it would allow to use this number and change our logging or the log filename.
How do we get labels value inside a container? unfortunately docker does not provide anything of this sort.
Cloud providers like AWS provide a metadata url which a instance and hit query details about itself
$ curl http://169.254.169.254/latest/meta-data/local-hostname
ip-10-251-50-12.ec2.internal
So what we need is a docker metadata service which can return details about a container.
Building a Docker metadata service
Docker runs on /var/run/docker.sock
by default and one can query Docker API directly on this socket
Now we can also get this using curl
$ curl -sgG --unix-socket /var/run/docker.sock 'http:::/containers/json' --data-urlencode 'filters={"name":["scale1_worker_3"]}' | jq '.[].Labels."com.docker.compose.container-number"'
"3"
But to do this inside the docker container requires two things. One is to have curl inside, which is ok to have. Second is to share the /var/run/docker.sock
.
Sharing the docker socket is ok if we are doing some testing or don’t care much about security. But if we need to put something in production then this is not a good way.
Building a Lua Nginx based Docker metdata server
Let’s build a service with /metadata/<label>/<labelname>
as the format for the url that a container should hit. Unlike AWS metadata service we can’t determine which container the request generated from. So we need to also get the container id from the container.
So you url should be /metadata/<containerid>/<label>/<labelname>
.
Now the simplest nginx config that can do that would be as below
user root;
events {
worker_connections 8000;
}
http {
server {
listen 80;
location / {
deny all;
}
location ~* "/metadata/(?<cid>[a-zA-Z0-9]{20,})(/(?<command>[^/]+))?(/(?<subcommand>[^/]+))?" {
}
}
}
Now we need a way to query the docker socket but at the same time limit the querys to internal requests only. So we can add
location /containers/json {
internal;
proxy_pass http://unix:/var/run/docker.sock:/containers/json;
}
This exposes only /containers/json
API endpoint to our nginx. Next we need some lua code inside our metadata
location block to fetch the details about container and return the response filtered.
local cjson = require("cjson")
local cid = ngx.var.cid
local command = ngx.var.command
local subcommand = ngx.var.subcommand
ngx.log(ngx.ERR, "cid="..cid.. ", command=".. command .. ", subcommand=" .. subcommand)
local filters = cjson.encode({id = {cid}})
local res = ngx.location.capture("/containers/json", {args = {filters = filters}})
First we get the cid
, command
, subcommand
from our location and then create filter parameter based on the container id. We hit our /containers/json
endpoint which is internal, and get the response object.
Next we need to check if container exist or not, if we don’t get a response back then we just send a 404
back
if not res then
ngx.status = 404
return ngx.exit(ngx.HTTP_NOT_FOUND)
end
If we get a reponse then we want to load that string as a json variable
local data = cjson.decode(res.body);
ngx.header.content_type = "application/json"
Next we create a jsonpath query based on the labels
if command == "labels" then
json_filter = "$..Labels"
if subcommand ~= '' then
json_filter = "$..['Labels']['".. subcommand .. "']"
end
end
Next we apply this json filter and send the response back
local jp = require("jsonpath")
local result = jp.value(data, json_filter)
if type(result) == "string" then
ngx.say(result)
else
ngx.say(cjson.encode(result))
end
The jsonpath
module need to be installed on openresty
image, so we created a new Dockerfile
FROM openresty/openresty:alpine-fat
RUN apk update && apk add git && /usr/local/openresty/luajit/bin/luarocks install jsonpath
To make it easy for any docker container to reach our metadata service we will use a easy to remember IP 172.172.172.172
. Now let us put everything into a compose file
version: '3'
services:
metadata:
build:
context: .
user: root
volumes:
- ./nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
- ./lua:/lua
- /var/run/docker.sock:/var/run/docker.sock
networks:
metadataserver:
ipv4_address: 172.172.172.172
networks:
metadataserver:
driver: bridge
ipam:
config:
-
subnet: 172.172.172.0/24
After doing docker-compose up -d
we can start checking our metadata service
$ docker inspect -f '{{.Id}}' scale1_worker_3
34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
$ curl -s "172.172.172.172/metadata/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754/labels" | jq
{
"com.docker.compose.oneoff": "False",
"com.docker.compose.config-hash": "6b42ee8ad919a3581c7342e91900ac3984fcf5b7c69274e63bc655461e9a0b57",
"com.docker.compose.version": "1.15.0",
"com.docker.compose.container-number": "3",
"com.docker.compose.service": "worker",
"com.docker.compose.project": "scale1"
}
$ curl -s "172.172.172.172/metadata/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754/labels/com.docker.compose.container-number"
3
So our metadata service is working great. Now the only issue is that the service requires the container id. This container id needs to be determined inside the container itself.
Determing container id from inside the container
This is where cgroup becomes our friend. Extracting container id is easy by exploring the cgroup of PID 1
or self
$ cat /proc/1/cgroup
11:freezer:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
10:pids:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
9:perf_event:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
8:net_cls,net_prio:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
7:blkio:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
6:cpu,cpuacct:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
5:hugetlb:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
4:memory:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
3:cpuset:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
2:devices:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
1:name=systemd:/docker/34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
We can easily filter out the container ID using
$ cat /proc/1/cgroup | grep cpuset | awk -F"/" '{print $NF}'
34971d89ccdc2f58e29f4387ef107714682d69abd06c1d6d1cbb5961ac8b4754
Metadata service can be download from below repo
https://github.com/tarunlalwani/docker-container-metdata-service