Configurando namespaces e cgroups manualmente para entender containers

1. Introdução aos fundamentos de containers Linux

Containers Linux não são uma tecnologia mágica — eles são construídos sobre dois mecanismos fundamentais do kernel: namespaces e cgroups (control groups). Enquanto namespaces fornecem isolamento de recursos (cada processo vê apenas seu próprio mundo), cgroups impõem limites sobre quanto desses recursos um processo pode consumir.

A diferença entre um container completo (Docker, Podman) e a criação manual é que ferramentas modernas automatizam dezenas de configurações complexas. Ao fazer manualmente, você ganha compreensão profunda do que realmente acontece quando executa docker run.

Pré-requisitos: Kernel Linux 4.15+ com suporte a namespaces e cgroups v2. Verifique com:

uname -r
mount | grep cgroup

2. Isolando processos com namespaces

Namespaces isolam processos em "visões" separadas do sistema. Vamos começar com o namespace de montagem:

# Criar um processo filho em novo namespace de montagem
unshare --mount --propagation private bash

Para isolar PID, rede e UTS simultaneamente:

# Isolar PID, rede e hostname
unshare --pid --net --uts --fork bash

Exemplo prático: executar um shell em namespace de rede vazio:

# No terminal 1
unshare --net bash
ip link show  # Mostra apenas loopback
ip addr       # Sem interfaces reais

No namespace de rede, você não tem acesso à internet do host. Para verificar:

ping 8.8.8.8  # Falha - rede isolada

3. Montando um sistema de arquivos isolado

Agora vamos criar uma raiz isolada com pivot_root:

# Baixar Alpine Linux minimalista
cd /tmp
wget https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-minirootfs-3.19.1-x86_64.tar.gz
mkdir container_root
tar -xzf alpine-minirootfs-3.19.1-x86_64.tar.gz -C container_root

# Criar diretório para novo root
mkdir -p container_root/old_root

# Montar sistemas de arquivos essenciais
mount --bind /proc container_root/proc
mount --bind /sys container_root/sys
mount --bind /dev container_root/dev

# Usar pivot_root para mudar a raiz
pivot_root container_root container_root/old_root

Após pivot_root, o sistema vê apenas os arquivos dentro de container_root. Para verificar:

ls /  # Mostra apenas o conteúdo do Alpine

4. Limitando recursos com cgroups v2

Cgroups v2 usa uma hierarquia unificada de diretórios. Vamos criar limites:

# Criar subgrupo
mkdir /sys/fs/cgroup/my_container

# Limitar CPU a 50% de um core (50000 microssegundos de 100000)
echo "50000 100000" > /sys/fs/cgroup/my_container/cpu.max

# Limitar memória a 64MB
echo 67108864 > /sys/fs/cgroup/my_container/memory.max

# Verificar configurações
cat /sys/fs/cgroup/my_container/cpu.max
cat /sys/fs/cgroup/my_container/memory.max

Depuração: Para verificar se os limites estão ativos:

stat /sys/fs/cgroup/my_container/
cat /sys/fs/cgroup/my_container/cpu.stat

5. Combinando namespaces e cgroups em um container funcional

Script completo para criar um container funcional:

#!/bin/bash
set -e

# Configurar cgroups
CGROUP_DIR="/sys/fs/cgroup/meu_container"
mkdir -p $CGROUP_DIR
echo "50000 100000" > $CGROUP_DIR/cpu.max
echo 67108864 > $CGROUP_DIR/memory.max

# Criar diretório raiz do container
mkdir -p /tmp/container_root
cd /tmp/container_root

# Baixar e extrair Alpine
wget -q https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-minirootfs-3.19.1-x86_64.tar.gz
tar -xzf alpine-minirootfs-3.19.1-x86_64.tar.gz
rm alpine-minirootfs-3.19.1-x86_64.tar.gz

# Criar par veth para rede
ip link add veth0 type veth peer name veth1
ip link set veth0 up
ip addr add 10.0.1.1/24 dev veth0

# Iniciar processo em namespaces isolados
unshare --mount --pid --net --uts --fork /bin/bash << 'INNER'
  # Configurar hostname
  hostname meu-container

  # Mover veth1 para o namespace
  ip link set veth1 netns $$
  ip addr add 10.0.1.2/24 dev veth1
  ip link set veth1 up

  # Montar sistemas de arquivos
  mount -t proc proc /proc
  mount -t sysfs sys /sys

  # Aplicar cgroups
  echo $$ > /sys/fs/cgroup/meu_container/cgroup.procs

  # Executar comando
  exec busybox sh
INNER

Para executar um comando simples dentro do ambiente:

# Depois de entrar no shell do container
echo "Container funcionando!"
ps aux  # Mostra apenas processos do container

6. Gerenciamento de processos e limpeza

Para encerrar processos dentro do namespace:

# Do host, encontrar PID real
ps aux | grep busybox
kill -9 <PID>  # Mata o processo no host

# Ou dentro do namespace
kill -9 1  # Mata o processo init do container

Para limpar completamente:

# Remover cgroups
rmdir /sys/fs/cgroup/meu_container

# Desmontar pontos de montagem
umount /tmp/container_root/proc
umount /tmp/container_root/sys
umount /tmp/container_root/dev

# Remover par veth
ip link delete veth0

# Remover diretório raiz
rm -rf /tmp/container_root

Boas práticas:
- Sempre verifique processos órfãos com ps aux | grep defunct
- Use namespace sandboxing para evitar vazamentos
- Implemente timeouts para processos que não respondem

7. Comparação com ferramentas modernas e próximos passos

Docker e Podman abstraem todo esse processo em comandos simples:

docker run -it alpine sh

Esse comando executa automaticamente:
1. Criação de múltiplos namespaces (mount, pid, net, uts, ipc, user)
2. Configuração de cgroups com limites padrão
3. Montagem de sistemas de arquivos
4. Configuração de rede com bridge
5. Gerenciamento de ciclo de vida

Limitações da abordagem manual:
- Rede complexa (múltiplos veth pairs, bridges, NAT)
- Armazenamento persistente (volumes, camadas)
- Isolamento de usuário (user namespace)
- Limites de I/O (blkio)

Próximos experimentos sugeridos:
- Adicionar isolamento de usuário com unshare --user
- Configurar limites de I/O com io.max em cgroups v2
- Implementar montagens bind para volumes
- Criar uma bridge de rede manual com ip link add br0 type bridge

Referências