Docker - Como depurar contêineres Distroless e Slim
UPD: Confira iximiuz/cdebug - uma ferramenta de depuração de contêiner que automatiza algumas das técnicas deste post.
Os contêineres finos são mais rápidos (menos material para se mover) e mais seguros (menos lugares para vulnerabilidades entrarem). No entanto, esses benefícios de contêineres finos têm um preço - esses contêineres carecem (às vezes muito necessários) de ferramentas de exploração e depuração. Pode ser bastante desafiador explorar um contêiner que foi construído a partir de uma imagem base sem distração ou fina ou foi minimizado usando DockerSlim ou similar. Ao longo dos anos, aprendi alguns truques para solucionar problemas de contêineres finos, e é hora de compartilhar.
A solução mais óbvia é colocar as ferramentas de depuração de volta quando você precisar delas. Por exemplo, um contêiner construído a partir de carece até mesmo de coisas básicas como:debian:stable-slim
ps
$ docker run -d --rm --name my-slim debian:stable-slim \ sleep 9999
# Nice, the shell is there!
$ docker exec -it my-slim bash
root@6aa917a50213:/$#
# But many tools are missing.
root@6aa917a50213:/$#
ps bash: ps: command not found
Você pode “corrigi-lo” instalando o pacote diretamente no contêiner em execução:procps
root@6aa917a50213:/$# apt-get update; apt-get install -y procps
root@6aa917a50213:/$# ps
PID TTY TIME CMD
7 pts/0 00:00:00 bash
1058 pts/0 00:00:00 ps
A principal vantagem da abordagem é a sua simplicidade. Mas há um monte de desvantagens significativas:
- Você precisa procurar / lembrar os nomes dos pacotes ( não é uma coisa).
apt-get install ps
- As alterações não persistem entre as reinicializações do contêiner (a menos que você as confirme, é claro).
- Os pacotes instalados podem poluir o contêiner e levar a investigação em uma direção errada.
- A abordagem não funciona com imagens sem distro, já que não há shell ou gerenciador de pacotes dentro.
Alternando temporariamente para uma imagem base fat(ter)
A segunda solução mais óbvia é mudar temporariamente para outra imagem base. Em vez de usar o , você pode criar um contêiner a partir da imagem base normal que tenha mais ferramentas pré-instaladas. A abordagem funciona até mesmo para as imagens sem distribuição do GoogleContainerTools, pois elas fornecem variantes de depuração especialmente marcadas.debian:stable-slim
debian:stable
Enquanto a imagem normal não tem nem mesmo o shell:gcr.io/distroless/nodejs
$ docker run -d --rm \
--name my-distroless gcr.io/distroless/nodejs \
-e 'setTimeout(() => console.log("Done"), 99999999)'
$ docker exec -it my-distroless bash
OCI runtime exec failed:
exec failed: unable to start container process:
exec: "bash": executable file not found in $PATH: unknown
$ docker exec -it my-distroless sh
OCI runtime exec failed:
exec failed: unable to start container process:
exec: "sh": executable file not found in $PATH: unknown
… A versão vem não só com o shell, mas também com algumas ferramentas usadas com frequência:gcr.io/distroless/nodejs:debug
$ docker run -d --rm \
--name my-distroless gcr.io/distroless/nodejs:debug \
-e 'setTimeout(() => console.log("Done"), 99999999)'
$ docker exec -it my-distroless sh
/ $# ps
PID USER TIME COMMAND
1 root 0:00 /nodejs/bin/node -e setTimeout(() => console.log("Done"), 99999999)
19 root 0:00 sh
27 root 0:00 ps
/ $# ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=61 time=26.808 ms
64 bytes from 8.8.8.8: seq=1 ttl=61 time=18.175 ms
64 bytes from 8.8.8.8: seq=2 ttl=61 time=40.952 ms
No entanto, se você precisar de uma ferramenta um pouco menos comum (por exemplo, ), você não vai encontrá-lo no contêiner acima nem será capaz de instalá-lo - as imagens base ainda não têm um gerenciador de pacotes adequado:ss
:debug
/ $# apt
sh: apt: not found
/ $# apt-get
sh: apt-get: not found
Então, a única opção para colocar seus próprios pacotes em uma imagem sem distro é reconstruir a base sem distro (leia para aprender bazel 🙈 ).
As imagens do Chainguard (e as ferramentas de satélite) podem simplificar o problema de adicionar coisas em imagens distroles(-like), mas o principal inconveniente permanece - a construção de imagens deve ser um processo fora de banda com a UX que é muito diferente de uma sessão de depuração típica.
Resumindo as desvantagens da abordagem:
- É mais lento e requer ferramentas e conjuntos de habilidades diferentes (do necessário para depuração).
- O contêiner de destino precisa ser reiniciado (ou melhor, completamente substituído usando a nova imagem).
- Nem todas as ferramentas estarão disponíveis prontas para uso, então talvez seja necessário recorrer à abordagem #1.
- A possibilidade de as ferramentas de depuração estragarem o contêiner e enganarem a investigação permanece alta.
Usando e uma montagemdocker exec
Se você não sente vontade de mexer com imagens, mas ainda quer explorar o sistema de arquivos do contêiner de mau comportamento ou executar alguns de seus comandos/ferramentas apesar da falta do shell dentro, o comando pode ser realmente útil.docker exec
Um shell interativo típico iniciado pelo comando residiria nos mesmos namespaces mnt, pid, net, ipc e uts que o contêiner de destino, então “pareceria” estar no próprio contêiner de destino:docker exec -it <target> sh
O problema é que o contêiner de destino pode não ter um shell! Então, aqui está como você pode trazer o seu:
# 1. Prepare the debugger "image" (not quite):
$ docker create --name debugger busybox
$ mkdir debugger
$ docker export debugger | tar -xC debugger
# 2. Start the guinea-pig container (distroless):
$ docker run -d --rm \
-v $(pwd)/debugger:/.debugger \
--name my-distroless gcr.io/distroless/nodejs \
-e 'setTimeout(() => console.log("Done"), 99999999)'
# 3. Start the debugging session:
$ docker exec -it my-distroless /.debugger/bin/sh
O comando acima colocará você diretamente no contêiner de destino (ou seja, todos os seus namespaces serão compartilhados).docker exec
No entanto, o uso das ferramentas de depuração da montagem provavelmente falhará no início. Por exemplo, vamos tentar verificar o sistema de arquivos:
$# ls -l /nodejs/ /.debugger/bin/sh: ls: not found
Felizmente, há uma maneira fácil de corrigi-lo:
/ $# export PATH=${PATH}:/.debugger/bin
Agora, explorar o sistema de arquivos, a árvore de processos e a pilha de rede do contêiner de destino parece absolutamente natural:
/ $# ls -l /nodejs/
total 392
-r-xr-xr-x 1 root root 250180 Jan 1 1970 CHANGELOG.md
-r-xr-xr-x 1 root root 96982 Jan 1 1970 LICENSE
-r-xr-xr-x 1 root root 35345 Jan 1 1970 README.md
drwxr-xr-x 2 root root 4096 Jan 1 1970 bin
drwxr-xr-x 3 root root 4096 Jan 1 1970 include
drwxr-xr-x 5 root root 4096 Jan 1 1970 share
/ $# ps
ps
PID USER TIME COMMAND
1 root 0:00 /nodejs/bin/node -e setTimeout(() => console.log("Done"), 99999999)
23 root 0:00 /.debugger/bin/sh
29 root 0:00 ps
/ $# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
132: eth0@if133: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
E você também pode executar os binários do contêiner de destino como de costume. A melhor parte é que conseguimos tudo isso sem alterações na base das imagens finais!
😎 Dica Pro: Precisa de uma ferramenta que não esteja no busybox? Nixery para o resgate!
O truque de volume + exec acima funciona perfeitamente quando as ferramentas no volume estão vinculadas estaticamente (por exemplo, o kit de ferramentas busybox). No entanto, às vezes, convém usar uma ferramenta vinculada dinamicamente. Aqui está como você pode alcançá-lo com Nixery (obrigado Jérôme Petazzoni pela ideia):
# 1. Prepare the debugger - it'll contain a shell (bash) and tcpdump!
$ docker create --name debugger nixery.dev/shell/tcpdump
$ mkdir debugger
$ docker export debugger | tar -xC debugger
# 2. Start the container that needs to be debugged
# (notice how the volumes are slightly more complex)
$ docker run -d --rm \
-v $(pwd)/debugger/nix:/nix \
-v $(pwd)/debugger/bin:/.debugger/bin \
--name my-distroless gcr.io/distroless/nodejs \
-e 'setTimeout(() => console.log("Done"), 99999999)'
# 3. Start the debugging session:
$ docker exec -it my-distroless /.debugger/bin/bash
bash-5.1$# export PATH=${PATH}:/.debugger/bin
bash-5.1$# tcpdump
O Nixery permite que você construa imagens de contêiner sob demanda simplesmente listando nomes das ferramentas necessárias ao puxar. Tais ferramentas são instaladas usando o gerenciador de pacotes Nix, e a magia de Nix as torna portáteis (mesmo quando estão ligadas dinamicamente). Só precisamos montar dois volumes em vez de um (e o primeiro deve estar sempre em )./nix
Descobri o truque acima há relativamente pouco tempo, então ainda terei que ver se ele se tornará minha nova abordagem favorita para depurar contêineres.
Como sempre, há desvantagens, no entanto:
- Há alguns aros para saltar - preparar o volume do depurador leva tempo.
- Nem todas as imagens podem ser usadas para depuração - as ferramentas vinculadas estaticamente (como busybox) ou distros baseadas em Nix funcionam melhor.
- ~Instalar ferramentas extras sob demanda provavelmente será problemático~ Nixery.dev corrigido para mim.
- A montagem das ferramentas de depuração no contêiner de destino requer uma reinicialização.
A abordagem com uma montagem extra é boa, mas requer uma reinicialização do contêiner. Podemos ter algo semelhante, mas sem interromper o contêiner de destino?docker exec
A necessidade de depurar contêineres finos surge com tanta frequência que o Kubernetes até adicionou um comando especial. Eu já estava me aprofundando nisso há algum tempo, então aqui está uma rápida recapitulação:kubectl debug
Resumindo a história, o comando permite que você inicie um contêiner temporário (chamado efêmero) que compartilha algumas das bordas de isolamento do Pod de mau comportamento. E esse contêiner temporário pode usar sua imagem ️ ️ 🔥 de kit de ferramentas favoritakubectl debug
O acima é possível porque um contêiner pode ser iniciado usando um ou mais namespaces de outro contêiner! Essencialmente, é a mesma técnica que permitiu a criação de Pods em primeiro lugar, apenas estendida para a fase de tempo de execução. Um tempo atrás eu estava mostrando como criar construções semelhantes a Pod usando comandos padrão do Docker. Combinando a ideia de contêineres efêmeros com a técnica desse artigo, podemos reproduzir uma UX semelhante a um Docker:kubectl debug
# Preparing the guinea pig container (distroless/nodejs):
$ docker run -d --rm \
--name my-distroless gcr.io/distroless/nodejs \
-e 'setTimeout(() => console.log("Done"), 99999999)'
# Starting the debugger container (busybox)
$ docker run --rm -it \
--name debugger \
--pid container:my-distroless \
--network container:my-distroless \
busybox \
sh
# Exploring/debugging the guinea pig container using the debugger's tools:
/ $# ps auxf
PID USER TIME COMMAND
1 root 0:00 /nodejs/bin/node -e setTimeout(() => console.log("Done"), 99999999)
25 root 0:00 sh
31 root 0:00 ps auxf
/ $# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
128: eth0@if129: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
Com um simples, você pode iniciar um contêiner de depurador que compartilhará os namespaces pid e net do destino. O contêiner do depurador pode (e provavelmente deve) trazer suas próprias ferramentas, e usar ou nele mostrará exatamente a mesma árvore de processo e a pilha de rede que o contêiner de destino se vê.docker run --pid container:<target> --network container:<target>
ps
ip
Esse truque é tão útil que até construímos um comando PoC durante uma sessão improvisada de Hackathon na KubeCon EU 2022 (o crédito vai para Dan Čermák):docker-slim debug
Minha investigação mostrou que existem três namespaces que podem ser compartilhados dessa maneira: pid, net e ipc. A última, no entanto, só pode ser compartilhada se o próprio alvo tiver sido (re)iniciado com .--ipc 'shareable'
Até agora, essa abordagem tem sido a minha favorita. Ele não tem a maioria das desvantagens das outras abordagens - se você não precisa de um namespace ipc compartilhado, o contêiner de destino nem precisa ser reformulado! Mas há um contra…
O comando (ou ) não permite o compartilhamento do namespace. Tecnicamente, é possível que dois contêineres compartilhem o mesmo sistema de arquivos. No entanto, o Docker (inventado?) promove a UX orientada por imagem - cada contêiner começa a partir de uma imagem e, para evitar colisões, cada (e ) deve iniciar um novo namespace.docker run
docker create
mnt
docker run
docker create
mnt
Isso significa que o sistema de arquivos que você vê enquanto estiver no shell do depurador não é o sistema de arquivos do contêiner de destino!
Com o namespace pid compartilhado, você ainda pode acessar o sistema de arquivos contêiner do destino usando o seguinte truque:
/ $# ls -l /proc/1/root/ # or any other PID that belongs to the target container
Mas pode ser bastante confuso e limitante…
A boa notícia é que um único link simbólico combinado com o processo do depurador pode tornar a sessão de depuração quase indistinguível da boa e velha UX!chroot
docker exec
Eu acho este truque tão útil que eu até mesmo automatizá-lo na minha nova ferramenta de depurador de contêiner iximiuz / cdebug. Com o cdebug, exec-ing em um contêiner torna-se tão simples quanto apenas:
cdebug exec -it <target-container-name-or-id>
O comando acima inicia um contêiner “sidecar” do depurador usando a imagem. Mas se você precisar de algo mais poderoso, você sempre pode usar a bandeira: busybox:latest
--image
cdebug exec --privileged -it --image nixery.dev/shell/ps/vim/tshark <target>
Em vez de conclusão
Bem, é isso por enquanto. E se você souber mais maneiras de depurar contêineres, envie-me uma mensagem ou deixe um comentário. Aprender com o público é um dos principais objetivos por trás deste blog! 🙌
Autor: Ivan Velichko