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-slimps

$ 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-slimdebian: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

Comando docker exec visualizado.

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

Kubernetes Ephemeral Container - explicação visual.

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>psip

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

comando docker-slim debug visualizado.

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 rundocker createmntdocker rundocker createmnt

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!chrootdocker exec

comando docker-slim debug visualizado.

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

Artigo Original