Contêineres deveriam ser responsabilidade do sistema operacional

10 de junho de 2025 · 11 min de leitura

Read in english

Não antes de 2018, pouco a pouco, nos dias de trabalho na empresa, meus colegas começaram a falar sobre Docker, uma tecnologia que preparava ambientes para rodar os programas nos servidores. Menos de dois anos depois, Docker tinha se tornado a regra e aplicações novas vinham com Dockerfile e arquivos YAML para Kubernetes.

Apesar de entender a finalidade de uso dos contêineres, parte de mim dizia que eram uma solução desnecessariamente pesada e que os problemas solucionados deveriam ser tratados pelos sistemas operacionais. Minha opinião permanece.

Neste artigo, abordarei os problemas resolvidos pelos contêineres, alternativas e uma proposta para ser implementada por sistemas operacionais.

Começando pelo fim

Afinal de contas, por que usamos contêineres? A maioria absoluta das respostas é: "Porque preciso rodar minha aplicação na nuvem".

O Docker resolveu dois problemas: a preparação do ambiente e a segurança de execução.

A preparação do ambiente se dá porque as imagens dos contêineres vêm com o necessário para a execução do programa, que geralmente são o runtime da aplicação e pacotes relacionados. Isso pode ser complexo principalmente em ambientes Linux, pois há toda uma árvore de dependências que é sensível a mudanças e atualizações. Ter uma árvore de dependências rígida e reproduzível garante estabilidade, evitando que a aplicação pare de funcionar corretamente por incompatibilidade com alguma nova versão de um pacote.

A segurança de execução decorre de o contêiner ser um processo com mapeamento de rede e volume de arquivos isolados, sem risco de invadir e infectar arquivos da máquina principal.

Preparação de ambiente, sem contêineres

Existem várias formas de se preparar um ambiente sem recorrer a contêineres.

Uma é realmente instalar as dependências necessárias na máquina.

Outra é compilação autocontida (self-contained deployment), em que o executável carrega consigo o runtime da linguagem e todas suas dependências. Assim, a máquina não precisa ter ele instalado.

Algumas linguagens oferecem compilação para código de máquina, chamada de AOT (ahead-of-time compilation), que faz com que o runtime não seja necessário e também reduz o número de dependências externas, além de oferecer maior performance e menor consumo de memória aos programas.

A base de uma imagem Docker de sistema operacional é a user space, que é o sistema operacional, menos sua kernel. São programas essenciais, bibliotecas, serviços em background e outros arquivos.

Carregar uma user space de sistema operacional para cada contêiner desperdiça memória RAM e disco rígido. A imagem Docker do Ubuntu, por exemplo, ocupa 188MB, o que é bastante. Existem imagens ultra-leves que deixam apenas o que é estritamente necessário; por exemplo, a imagem Alpine Linux ocupa apenas 5MB. Ainda assim, é um peso extra para cada aplicação.

As soluções acima mostram como mitigar o risco de uma árvore de dependências frágil.

Segurança de execução, sem contêineres

Esse ponto é mais complicado. Aqui, queremos limitar o acesso do programa ao sistema de arquivos e à rede.

Na maioria dos sistemas operacionais, o controle de acesso ao sistema de arquivos é a nível de usuário. Para fazer com que um programa só possa acessar determinados arquivos e diretórios, é preciso criar um usuário (ou grupo de usuários) com essas regras e então fazer com que o programa seja sempre executado a partir desse usuário.

Já a restrição de comunicação de rede é feita via firewall, com controle de acesso a nível de usuários e programas.

Proposta: Manifestos de execução

Uma sugestão para sistemas operacionais seria ter manifestos de execução, que definem claramente como um programa é executado e as permissões dele no sistema.

O manifesto de execução poderia ainda definir com quais dispositivos e periféricos o programa interage. Ou seja, poderia ser útil também para programas desktop, com interfaces gráficas.

Esse arquivo poderia ser assinado com um par de chaves pública-privada para garantia de autenticidade. Um hash verificaria se o executável corresponde ao esperado pelo manifesto.

manifestVersion: v1
author: myCompany
name: myApp
run: ./myapp
hash: <hash_do_executável> # opcional

allowedDirectories:
  - name: appDir
    path: ./**/ # todos os subdiretórios
    permission: read
  - name: sharedFolder
    path: \\EnvGeoServer\Repository\Tools\admin\
    permission: readWrite

allowedNetwork:
  - name: localhost
    direction: inbound
    ip: 127.0.0.1
    protocol: tcp
    port: 8080

allowedDevices:
  - mouse
  - keyboard
  - audioOut
  - display

Fontes e leituras interessantes

A

AlexandreHTRB

Campinas/SP,
Brasil