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:
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
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 8080
do 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
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:
About author
Você pode gostar também
Entenda os Microsserviços: A Revolução no Desenvolvimento de Softwares
Vivemos em tempos onde a evolução das tecnologias e dos tipos de serviços com os quais estamos acostumados se transformam e mudam em uma velocidade impressionante. E isso tem sido
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
Descubra as Oportunidades no Mercado DevOps em Ascensão
Oportunidades no Mercado DevOps O mercado de trabalho para quem conhece DevOps está em acensão, e atualmente é uma das áreas que mais contrata funcionários tanto no Brasil quanto no