Criando um Container do Docker (Sem o Docker!)

Criando um Container do Docker (Sem o Docker!)

Aprenda na prática como o Docker gerencia seus containers.

O intuito deste post é mostrar, na prática como o Docker gerencia seus containers. E qual jeito seria melhor de demonstrar o funcionamento de uma ferramenta, se não realizando passo a passo o que ela faz para entregar sua solução, “na unha”!?

Esse será nosso desafio, construir um container do Docker, sem o Docker! Nas minhas pesquisas para esse “projeto”, a única forma que eu encontrei pronta na nossa grande rede mundial de computadores, foi como construir containers usando códigos de programação de diversas linguagens, porém eu queria um conteúdo mais focado nos “SysAdmins” que hoje, tanto quanto os “Devs”, precisam entender e trabalhar com essa maravilhosa ferramenta gerenciadora de, e não só isso, containers.

Para que isso fosse possível, eu precisei encontrar os utilitários no Linux que fazem a gerencia de alguns recursos que um container isola, obviamente não será possível, em um único post, montar um container completo, com todos seus recursos isolados, estáveis e seguros para um ambiente de produção, porém será mais do que suficiente para aprofundar seu entendimento de como esse “engine” funciona.

Entendendo o Gerenciamento do Docker

Antes de colocarmos de fato a “mão na massa”, vamos entender como o Docker gerencia seus ambientes isolados, popularmente conhecidos como “containers”. Veja a imagem abaixo:

Arquitetura Docker
Nós temos 6 principais componentes:

  • Client –> Utilitário usado para se comunicar com o daemon do Docker.
  • Daemon –> Responsável por criar e gerenciar os objetos do Docker (imagens, containers, redes e volumes).
  • DOCKER_HOST –> Ambiente onde o daemon fica hospedado (Na maioria das vezes, mas nem sempre, o mesmo host do Client).
  • Registry –> Servidor que permite o armazenamento e distribuição das imagens.
  • Image –> Um template somente leitura, com dados e instruções para a criação de um container
  • Container –> Um ou mais processos isolados do ambiente padrão do sistema.

Arquitetura de um Container

Como dito anteriormente, um container basicamente é composto de um ou mais processos isolados do ambiente padrão do sistema. Mas o que isso quer dizer na prática?

Para entendermos como os containers são de certa forma “isolados”, precisamos entender alguns recursos do Linux, que já existem há um bom tempo antes do Docker.

Chroot (1982)
Com o intuito de testar a instalação e desenvolvimento do Unix 7, o chroot foi desenvolvido para para alterar o diretório raiz aparente para o processo atual, assim fazendo com que ele enxergue somente arquivos abaixo de onde foi iniciado.

chroot /srv/chroot /bin/bash # O processo do bash irá enxergar somente arquivos abaixo do /srv/chroot

chroot

Kernel Linux (1991)
O Kernel é o coração de todo sistema operacional ele é responsável por gerenciar os recursos no nosso ambiente, incluindo aqueles que permitem o isolamento de processos.

Iptables (1998)
Utilitário que permite realizar a gerência do módulo “Netfilter” no Kernel do Linux, é responsável pelo controle do fluxo de pacotes em nosso ambiente (Firewall), através dele que o Docker consegue realizar o redirecionamento dos pacotes que chegam em nossa interface de rede padrão para a interface de rede do nosso ambiente isolado.

iptables -I PREROUTING  -t nat -p tcp --dport 8080 -j DNAT --to 172.18.0.2:80
#Todo pacote com destino à porta "8080" será redirecionado para o IP 172.18.0.2, na porta "80"

Namespaces (2002)
Implementado na versão 2.4.19 do Kernel Linux, os Namespaces permitem o isolamento da visibilidade dos recursos do Kernel para um grupo de processos, fazendo com que eles só consigam “enxergar” aquilo que foi alocado para eles. Atualmente eles são 7:

  • Mount –> Controla quais pontos de montagem são visíveis para os processos.
  • UTS –> Permite que os processos exerguem um nome e um domínio diferente do padrão.
  • IPC –> Isola a comunicação entre processos (Inter-Process Communications)
  • PID –> Cada PID Namespace tem seu grupo de PIDs para os processos.
  • Network –> Um novo espaço para toda stack de rede.
  • User –> Principal ponto de segurança para os ambientes isolados, já que em cada espaço é possível ter o próprio grupo de UIDs e GIDs
  • Cgroup –> Cria um espaço separado o controle de grupos (Assunto para outro post).

Implicitamente, nosso ambiente utiliza o que podemos chamar de “Namespace padrão”.

Union File Systems (2003)
UnionFS, é um tipo de file system que trabalha criando “camadas”, é ele que permite que as images sejam leves e portáteis, o engine do Docker trabalha atualmente com algumas implementações, tais como: AUFS, btrfs, vfs, DeviceMapper e atualmente o padrão nas distribuições mais recentes, overlayfs2.

Cgroups (2006)

Desenvolvido pela RHEL os cgroups (Control Groups), permitem limitar o uso de recursos do ambiente, como tempo de CPU, memória do sistema, largura de banda etc… Neste post não iremos utilizar cgroups para limitar os recursos do nosso querido pseudo-container, caso o fizéssemos ficaria mais extenso do que foi planejado, mas nada nos impede de uma segunda parte onde daremos continuidade, implementando-os ao nosso projeto.

Por hora basta sabermos que existem e fazem parte dos componentes básicos de um container.

Em suma…

O que o Docker faz é reunir os recursos que esses componentes disponibilizam, e criar um ambiente “completamente” independente, compartilhando do mesmo Kernel, e adicionando somente utilitários e bibliotecas necessárias para o funcionamento da aplicação.

Para o nosso laboratório ser o mais direto e didático possível, eu decidi isolar somente os recursos que são mais aparentes aos “SysAdmins”, como:

  • Chroot
  • Net Namespace
  • PID Namespace
  • OverlayFS
  • Iptables

Overview do Projeto

A idéia deste projeto é criar um ambiente isolado, quase da mesma forma que o Docker criaria, para “subir” um webserver escutando na porta 8080 do host e redirecionando para a porta 80 do nosso container. E no final deveremos conseguir visualizar um projeto em CSS do Julian Garnier, ótimo projeto, diga-se de passagem.

Criando o laboratório para o container

Sem mais delongas, vamos começar a construção do nosso ambiente onde nosso container irá existir, para isso iremos utilizar dois softwares (irei assumir que estão utilizando um SO Linux):

Mão na massa!

Crie um diretório chamado container e entre nele

mkdir container ; cd container

Inclua no Vagrantfile o script para construção do ambiente:

vim Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.define "container" do |container|
    container.vm.box = "centos/7"
    container.vm.network "private_network", ip: "10.5.25.10"
    container.vm.hostname = "container"
    container.vm.provider "virtualbox" do |vb|
            vb.name = "container"
    end
  end
end

Estamos subindo um ambiente CentOS 7, com a rede “10.5.25.10” e o nome da nossa máquina virtual será “container”.

Saia e salve o arquivo e execute o comando para levantar nosso laboratório.

vagrant up

Após finalizado, acesse via ssh.

vagrant ssh

Montando OverlayFS

Agora, dentro do nosso laboratório podemos começar a montar nosso container, o primeiro passo será organizar as nossas camadas do OverlayFS, de forma que tenhamos algo parecido com o esquema de images do Docker.

Acesse o usuário root:

sudo su -

Instale alguns utilitário que usaremos no decorrer do projeto:

yum install tree wget bridge-utils git -y

Crie um diretório para montarmos a estrutura do nosso OverlayFS

mkdir overlayfs ; cd overlayfs

Agora os diretórios das camadas:

mkdir camada_leitura camada_escrita camada_intermediaria camada_container

Vamos entender melhor como funciona o OverlayFS, ele precisa de no mínimo 4 componentes:

lowerdir (camada_leitura)
Essa é a camada onde ficam os dados somente leitura, não necessáriamente deverá existir somente uma lowerdir, no Docker por exemplo, uma image costuma ser composta de algumas dessas camadas.

upperdir (camada_escrita)
Camada onde os dados serão gravados enquanto nosso container estiver “no ar”, no Docker sempre perdemos os dados que gravamos nessa camada, por que ele simplesmente remove esse diretório na finalização do container, para que consigamos ter dados persistentes em nosso container, precisamos dos chamados volumes.

workdir (camada_intermediaria)
Responsável por preparar os arquivos antes que eles sejam trocados de camada.

overlay (camada_container)
A junção das camadas que permite a visibilidade dos dados como um todo, união das lowerdirs e da upperdir podem ser vistas atráves da overlay

Valide a estrutura de diretórios:

tree -C

Monte o Overlay:

mount -t overlay overlay -o lowerdir=camada_leitura,upperdir=camada_escrita,workdir=camada_intermediaria camada_container

Valide a montagem:

mount | grep overlay

Faça o download da nossa “image” personalizada que eu construí para o lab, nela há somente o necessário para o funcionamento do nosso webserver:

wget https://storage.googleapis.com/4525-repositorio/Materiais/rootfs.tar.gz

Exploda o tarball dentro do diretório da camada de leitura

tar -vzxf rootfs.tar.gz -C camada_leitura/

Entendendo o chroot

Como dito na introdução do post no chroot o processo iniciado exergará somente arquivos abaixo da nova raiz, então para que nosso ambiente funcione corretamente, dentro do tarball rootfs.tar.gz, existe uma estrutura raiz com os utilitários e bibliotecas necessárias para isso.

Entre no novo ambiente com o chroot, iniciando-o com o processo bash (Perceba que os dados foram jogados dentro da camada de leitura, e o ambiente será montado na camada do container, e graças ao Overlay temos acesso aos dados das camadas abaixo dele).

chroot camada_container/ /bin/bash

Trabalhando com Namespaces

PID

Agora que já mudamos a visibilidade da raiz do nosso ambiente padrão para o container, vamos começar a isolar quais processos nosso container irá enxergar:

Primeiro, para ter visibilidade dos processos, precisamos montar o diretório /proc:

mount -t proc proc /proc

Se executarmos o comando top agora, veremos que ainda exergamos os processos do ambiente padrão:

top

Para mudar isso, vamos sair do nosso container e agora iniciar nosso processo em um PID Namespace diferente:

exit

Para isso iremos usar o utilitário unshare para montar o chroot que por sua vez iniciará o processo do bash:

unshare -p  -f --mount-proc=$PWD/camada_container/proc chroot camada_container /bin/bash

Agora tente visualizar novamente os processos, dessa vez só devemos enxergar o que foi iniciado abaixo do chroot:

top

Isso quer dizer que o processo do bash, está em um PID Namespace diferente do ambiente padrão, então ele não irá conseguir visualizar nenhum processo dele.

Net

Com a visibilidade dos processos isolada, agora precisamos dar vida ao nosso Namespace de rede.

Para isso saia do container:

exit

Adicione um novo Namespace de rede com o comando ip netns

ip netns add rede_isolada

Perceba que agora, para isolarmos simultâneamente, PID e NET, precisamos criar uma estrutura onde o utilitario que isola os processos, irá chamar o utilitário que isola a rede, que por sua vez chamará o chroot e iniciará o bash dentro de ambos Namespaces:

unshare -p  -f --mount-proc=$PWD/camada_container/proc \
nsenter --net=/var/run/netns/rede_isolada \
chroot camada_container /bin/bash

Dentro do container, liste as redes disponíveis:

ip -c a

Somente a lo existe, agora saia para começarmos a construção da stack de rede do nosso container:

exit

Vamos criar um par de interfaces virtuais:

ip link add isolate_net0 type veth peer name isolate_net1

Valide se foram criadas de fato:

ip -c link list

Insira o par dentro do Namespace criado para o container:

ip link set isolate_net1 netns rede_isolada

Entre no container:

unshare -p  -f --mount-proc=$PWD/camada_container/proc \
nsenter --net=/var/run/netns/rede_isolada \
chroot camada_container /bin/bash

Valide:

iptables -t nat -nL
iptables -nL
ip -c a
ip route show

Perceba que toda stack de rede desse novo NET Namespace, tem configurações isoladas da padrão.

Configurando Rede (bridge)

O Docker basicamente trabalha com dois tipos de rede:
Host –> Utiliza o mesmo Namespace do ambiente padrão.
Bridge –> Rede padrão, cria um Namespace separado.

Para este Lab iremos utilizar a bridge, por questões didáticas e ser a mais vista nos ambientes Docker.

Vamos configurar nossa interface virtual dentro do container:

ip add add 172.18.0.2/24 dev isolate_net1 ; ip link set up dev isolate_net1

Agora seu par fora do container:

exit
ip add add 172.18.0.1/24 dev isolate_net0 ; ip link set up dev isolate_net0

Como citado o Docker usar iptables para o redirecionamento de pacotes, então vamos habilitar o encaminhamento:

iptables -P FORWARD ACCEPT

Agora vamos garantir que nosso container consiga acesso a rede externa:

iptables -t nat -I POSTROUTING -s 172.18.0.0/24 -d 0/0 -j MASQUERADE
iptables -t nat -I POSTROUTING -s 10.5.25.0/24 -d 0/0 -j MASQUERADE

Para que os pacotes sejam redirecionados da 8080do host para 80 do container, vamos inserir a seguinte regra:

iptables -I PREROUTING  -t nat -p tcp --dport 8080 -j DNAT --to 172.18.0.2:80
iptables -A OUTPUT -t nat -p tcp --dport 8080 -j DNAT --to 172.18.0.2:80

Com o comando brctl criaremos uma bridge que permita a comunicação entre o par de interfaces virtuais:

brctl addbr br0

Configure a bridge e valide:

ip addr add dev br0 172.18.0.3/24 ; ip link set br0 up
brctl show br0

“Pendure” nossa interface virtual na bridge:

brctl addif br0 isolate_net0

Exclua uma rota que é criada por padrão quando adicionamos os pares de interfaces virtuais:

ip route del 172.18.0.0/24 dev isolate_net0 proto kernel scope link src 172.18.0.1

Habilite o forward para o encaminhamento de pacotes (sem isso o kernel não permitirá o redirecionamento):

sysctl -w net.ipv4.ip_forward=1

Entre no container:

unshare -p  -f --mount-proc=$PWD/camada_container/proc \
nsenter --net=/var/run/netns/rede_isolada \
chroot camada_container /bin/bash

Inicie o Nginx

nginx

Acesse pelo seu navegador o IP:
10.5.25.10:8080

Volumes e Bind Mounts

Para que seja possível persistir os dados dentro dos containers, o Docker utiliza os volumes e bind mounts, eles nada mais são do que pontos de montagem independentes da camada_container. O intuito de ambos é o mesmo, a diferença é que um fica no controle do Docker (Volume), e outro depende do da estrutura de diretórios do host (Bind Mount):

Volume
Volume
Bind Mount
Bind Mount

Em nosso Lab iremos utilizar o Bind Mount, para popular nosso webserver de forma persistente.

Saia do container:

exit

Crie o bind que será montado:

sudo mkdir bind

Clone o projeto em CSS do git:

git clone https://github.com/juliangarnier/3D-CSS-Solar-System.git bind/

Com o comando mount, vamos conseguimos criar uma montagem do tipo bind para dentro do diretório no nosso container:

mount --bind -o rw bind camada_container/usr/share/nginx/html/

Entre e inicie o nginx:

unshare -p  -f --mount-proc=$PWD/camada_container/proc \
nsenter --net=/var/run/netns/rede_isolada \
chroot camada_container /bin/bash
nginx

Acesse no navegador o URL:

10.5.25.10:8080

E com isso, finalizamos nosso pseudo-container do Docker, essa seria nossa vida, se não houvessem os gerenciadores de container para nos auxiliar no isolamento de processos, no final de tudo, todo esse trabalho poderia ter sido resumido em um simples comando:

docker run -dit -p 80:8080 -v /root/overlayfs/bind:/usr/share/nginx/html/ nginx:alpine

Esse conteúdo tem intuito único, e exclusivamente didático, e espero que tenha atendido seu propósito e que tenham entendido um pouco melhor o funcionamento do Docker em relação a criação dos containers.

 

 

Líder em Treinamento e serviços de Consultoria, Suporte e Implantação para o mundo open source. Conheça nossas soluções:

CURSOSCONSULTORIA

Anterior Melhore seus processos com a consultoria de TI especializada da 4Linux
Próxima Aprenda a criar módulos com Terraform na prática

About author

Rafael Foschiani
Rafael Foschiani 1 posts

Instrutor e consultor de Linux/DevOps, na 4Linux! Desde os 15 anos trabalhando com Linux. Certificado em Docker (DCA), GSTI (ITIL) e Linux (LPIC Essentials, LPIC-1, e LFCS ).

View all posts by this author →

Você pode gostar também

DevOps

Tutorial: Configurando o Traefik no Docker Swarm com arquivos YAML

Neste tutorial, você aprenderá como configurar o Traefik no Docker Swarm usando arquivos YAML, uma forma fácil e eficiente de gerenciar seus aplicativos de contêineres em larga escala. Ao seguir

Cloud

Autenticando terraform na Google Cloud sem compartilhamento de chaves

O terraform atualmente é, de longe, a ferramenta de infraestrutura como código mais popular no mundo de TI e de Cloud. Ele tem suporte aos principais provedores de cloud e

DevOps

Kubernetes – Configurando um Cluster Multi-Master

Neste post vamos configurar um cluster Kuberentes Multi-Master apenas com a sua máquina. Mas antes de falarmos sobre um cluster em Kubernetes trabalhando em modo Multi-Master… Uma palavra sobre containers…