Tecnologia

Desmistificando o isolamento de containers

Por: Dextra, abril 27, 2020
Container runtimes/engines (adapted from Nanobox/Richard Robbins)
Container runtimes/engines (adapted from Nanobox/Richard Robbins)

Docker, LXC, rkt, todos esses container runtimes/engines proveem essencialmente a mesma funcionalidade: o isolamento de certas características de ambiente. Basicamente essas características podem ser divididas em dois grupos: o primeiro é o de recursos (cgroups), como CPU, memória, entre outros. O segundo é o de escopos (namespaces), como processos, rede, entre outros.

Containers (DockerCon/Brendan Gregg)
Containers (adapted from DockerCon/Brendan Gregg)

Por exemplo, no artigo anterior, vimos sobre profilers e análise de performance da JVM. Sabemos que, independentemente da escolha do profiler, é necessário que ele consiga estabelecer comunicação com a JVM. Porém, e se o processo da JVM estiver isolado dentro de um container? Neste artigo, de forma prática, vamos entender o que é esse tal de isolamento de processos dos containers.
Pré-requisitos: para realizar os experimentos deste artigo, é necessário utilizar uma distro Linux com os pacotes docker, util-linux e python (qualquer versão) instalados.

Hands-On!

A melhor forma de entender sobre isolamento de processos é colocando a mão na massa! Nos experimentos vamos abordar sobre comunicação de processos, listagem de processos e como entrar no escopo (namespace) de um processo isolado.

Processes isolation (adapted from Intel/Odinot Stanislas)
Processes isolation (adapted from Intel/Odinot Stanislas)

Comandos utilizados

A seguir está a lista dos comandos que serão utilizados e suas funcionalidades:

  • kill: envia um sinal para um processo, sendo necessário especificar o seu PID (process ID). Existem mais de 64 possibilidades de sinais.
  • unshare: isola o processo chamado em um ou mais tipos de namespaces. Namespaces nada mais são que escopos. Existem oito tipos de namespaces: PID (processos), mount, SysV IPC, network, user, cgroup (control group), UTS (hostname) e time.
  • lsns: lista os namespaces e os processos em cada um. O lsns está disponível na versão v2.28 ou superior do util-linux.
  • chroot: o change root muda o diretório raiz de um processo (e seus filhos), isolando-o do resto do sistema, criando assim o chamado chroot jail.
  • mount: faz o attach de um filesystem em um ponto da árvore de diretórios. Inversamente, o comando umount faz o detach desse filesystem da árvore.
  • ps: o processes status mostra os processos que estão sendo executados no sistema, lendo essas informações do diretório /proc.

Observação: os comandos chroot e unshare acima nada mais são que wrappers de syscalls (funções do kernel do sistema operacional), logo, é possível recriar as mesmas funcionalidades de isolamento através de código C, C++, Python, Golang, entre outros.

Experimento 1: List namespaces

Caso tenha a versão v2.28 ou acima do util-linux, execute o lsns e veja como os processos são organizados em diferentes escopos. Caso tenha instalado os navegadores Chrome e Firefox, abra-os, execute o lsns e veja o resultado.

Experimento 2: Process isolation

Para esse experimento vamos utilizar dois terminais. No primeiro digitaremos:

$ python # pode ser python3 também
>>> import os
>>> os.getpid()
<PID_DO_PYTHON>

A saída será o PID do python. Vamos copiá-lo e no segundo terminal digitar:

$ unshare -r --pid --fork /bin/bash
# kill -9 <PID_DO_PYTHON>

A saída será No such process. O motivo é que o unshare isolou o bash em outro namespace de processos, fazendo com que não seja possível o envio de sinais entre ele e o processo do python.

PID namespace (adapted from Toptotal/Mahmud Ridwan)
PID namespace (adapted from Toptotal/Mahmud Ridwan)

Porém, se executarmos agora:

# exit
$ unshare -r --net --fork /bin/bash
# kill -9 <PID_DO_PYTHON>

Veja que agora conseguimos enviar o sinal e matar o processo python. Isso aconteceu porque trocamos o tipo de escopo: desta vez isolamos o bash em outro namespace de rede, e não em outro namespace de processos, sendo possível assim o envio de sinais entre os processos.
Para finalizar, apenas execute um exit no segundo terminal.

Experimento 3: List process

Para esse experimento iremos utilizar dois terminais. No primeiro digitaremos:

$ python # pode ser python3 também
>>> import os
>>> os.getpid()
<PID_DO_PYTHON>

No segundo terminal, execute os seguintes comandos:

$ unshare -r --pid --fork /bin/bash
# ps -eo user,pid,pidns,cmd --sort=pidns

Veja que mesmo isolando o bash em outro namespace de processos, o ps ainda consegue enxergar o processo do python, além de processos de outros namespaces.
Vamos agora criar um root filesystem. Para isso, vamos baixar a imagem do container Linux Alpine:

# mkdir -v alpine
# docker pull alpine
# ID=$(docker create alpine)
# docker cp $ID:/ alpine
# docker rm $ID
# cd alpine
# ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var

Em seguida, ainda no segundo terminal, vamos executar:

# chroot $(pwd) ./bin/sh
# ps aux
# kill -9 <PID_DO_PYTHON>

Pronto! Agora conseguimos isolar o shell, impedindo que ele encontre ou envie sinais para nosso processo python!
E como seria se quiséssemos listar todos os processos do sistema, mesmo dentro da “jaula” do chroot? Para isso, basta executarmos os seguintes comandos:

# exit
# exit
$ cd alpine
$ sudo chroot $(pwd) ./bin/sh
# mount -v -t proc none /proc
# ps aux

O que fizemos acima foi montar o diretório /proc do sistema dentro da “jaula” do chroot, permitindo assim que o ps listasse todos os processos.
Para finalizar e limpar tudo:

# umount proc
# exit
$ cd..
$ rm -rfv alpine

Experimento 4: Enter process namespace

E se quisermos iniciar um novo processo dentro do namespace de um já existente? Para isso o kernel do Linux disponibiliza a função setns(). Para utilizá-la, executaremos o comando nsenter, cuja descrição é: “nsenter is a simple wrapper around setns that allows running a new process in the context of an existing process”.

Process Namespace (Ganesh Velrajan)
Process Namespace (by Ganesh Velrajan)

Para esse experimento vamos utilizar dois terminais. No primeiro vamos digitar:

$ unshare -r --pid --mount-proc --fork /bin/bash
# ps -eo user,pid,pidns,cmd

Veja que a saída são apenas os processos do namespace. Agora no segundo terminal:

$ ps -eo user,pid,pidns,cmd --sort=pidns

Copie o PID do bash que executamos no primeiro terminal, e ainda no segundo terminal:

$ nsenter --preserve-credentials --user --target <PID_DO_BASH> --mount --pid /bin/bash
# ps -eo user,pid,pidns,cmd

Conseguimos iniciar o bash em um namespace já existente!
Para finalizar, execute no primeiro terminal:

# ps -eo user,pid,pidns,cmd
# exit

Experimento 5: Enter process namespace… of Docker?

No experimento anterior, iniciamos um bash isolado em outro namespace de processos e depois entramos nesse namespace com outro bash… Mas será possível fazer a mesma coisa com o Docker?
Para esse experimento iremos utilizar dois terminais. Em um vamos digitar:

$ docker run -it --rm alpine /bin/sh
# ps aux

Temos um shell isolado dentro de um Linux Alpine. Agora no segundo terminal digite:

$ docker ps

Copie o ID do container e ainda no segundo terminal:

$ PID=$(docker inspect --format {{.State.Pid}} <CONTAINER_ID>)
$ sudo nsenter --target $PID --mount --pid /bin/sh
# ps aux

Veja que entramos no namespace do shell do Linux Alpine! Apesar de o processo ter sido iniciado pelo Docker, que garante isolamento, conseguimos por meio do host iniciar um outro shell e entrar no mesmo namespace!
Para finalizar, no primeiro terminal digite:

# ps aux
# exit

Conclusão

A partir dos quatro primeiros experimentos pudemos observar que os containers são basicamente uma combinação de várias funcionalidades de isolamento que o kernel do sistema operacional oferece, e ainda no quinto vimos que mesmo um processo isolado pelo Docker pode ter seu escopo acessado.

Fontes e referências

Para conhecer mais sobre os isolamentos do unshare e do chroot, no GitHub existe um projeto didático muito bom feito em shell-script pela Universidade de Aalto (Finlândia):
https://github.com/AaltoScienceIT/isolate-namespace
Ou também:
https://github.com/gustavotemple/isolate-namespace
Para conhecer mais sobre os outros tipos de isolamentos dos containers, existem dois ótimos artigos, um em inglês no Medium, e outro em português do pessoal da 4Linux de São Paulo:
https://medium.com/@saschagrunert/demystifying-containers-part-i-kernel-space-2c53d6979504
https://blog.4linux.com.br/criando-um-container-do-docker-sem-o-docker
Como curiosidade, para saber mais sobre como os arquivos no Linux são usados para praticamente tudo:
https://en.wikipedia.org/wiki/Everything_is_a_file
https://en.wikipedia.org/wiki/Procfs

Documentação

Para saber mais sobre os comandos utilizados, segue as man pages:
http://man7.org/linux/man-pages/man1/ps.1.html
http://man7.org/linux/man-pages/man1/kill.1.html
http://man7.org/linux/man-pages/man1/chroot.1.html
http://man7.org/linux/man-pages/man1/nsenter.1.html
http://man7.org/linux/man-pages/man1/unshare.1.html
http://man7.org/linux/man-pages/man8/lsns.8.html
http://man7.org/linux/man-pages/man8/mount.8.html

  • Receba nosso conteúdo em primeira mão.