BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Construindo um container com menos de 100 linhas em Go

Construindo um container com menos de 100 linhas em Go

Favoritos

A versão em código aberto do Docker lançada em março de 2013 provocou uma grande mudança na forma como a indústria de desenvolvimento de software realizava o empacotamento e implantação de sistemas modernos. A criação de muitos concorrentes, ao mesmo tempo complementares e de apoio às tecnologias de container, seguiu o despertar do Docker, e isso vem gerando muita euforia e concomitantemente algumas desilusões em torno deste assunto. Este artigo visa esclarecer as confusões existentes e explica como os containers vem sendo utilizado nas empresas.

Este artigo começa com um olhar para a tecnologia por trás dos containers e como esta tecnologia está sendo utilizado atualmente pelos desenvolvedores; em seguida examina os principais desafios com containers implantados em empresas, tais como: a integração da contentorização na integração contínua e nos pipelines de entrega contínua; e explica como aprimorar o monitoramento para suportar as mudanças de carga de trabalho e potencial de uma transição. A série finaliza com um olhar sobre o futuro do uso dos containers , e discute o papel que os unikernels estão atualmente exercendo dentro das organizações que se destacam no uso deste tipo de tecnologia.

Este artigo da InfoQ é parte de uma série chamada "containers no Mundo Real - pisando fora da curva do modismo". Se preferir, realize sua inscrição para receber notificações via RSS.

O problema com analogias é que elas tendem a desligar o seu cérebro quando se ouve falar delas. Alguns podem dizer que a arquitetura de software fala "apenas em como" fazer arquitetura. E não, não é, e o fato de que a analogia faz parecer ser uma boa prática, indiscutivelmente, resultou em um monte de danos. De forma relacionada, software em containers são frequentemente difundido como a capacidade de fornecer e de mover software em torno de "apenas como" os containers movem mercadorias em seu entorno. Não é bem assim. Ou pelo menos é mas a analogia faz com que percamos um monte de detalhes.

Os containers para mercadorias e os containers de software compartilham muito em comum. containers para mercadorias - com sua forma e tamanho padrão - permitem com que tenhamos economias poderosas em escala e padronização. Já os containers de software prometem muitos dos mesmos benefícios. Mas, esta é uma analogia superficial - um objetivo em vez de um fato.

Para realmente entender o que um container é no mundo do software, precisamos entender o que ele faz. E isso é o que este artigo busca explicar. Vamos falar sobre containers vs containerização, sobre os containers no Linux (incluindo namespaces, cgroups e sistemas de arquivos em camadas), então vamos percorrer algum código para construir um recipiente simples a partir do zero e, finalmente, falar sobre o que tudo isso realmente significa.

O que realmente é um container?

Gostaríamos de iniciar com um jogo. Imagine uma situação em sua cabeça neste momento, diga-nos o que é um "container". Pronto? Está bem. Deixe-nos ver se podemos adivinhar o que você imaginou:

Você poderia responder com uma ou mais das seguintes respostas:

● Uma forma de compartilhar recursos

● Um processo de isolamento

● Um tipo de virtualização leve

● A ação de empacotar um sistema de arquivos e seus metadados de uma única vez

● Um tipo de jaula como o chroot do linux

● Alguma coisa transportando alguma coisa como um container

● Alguma coisa que o docker faz

São várias opções de significado para uma mesma palavra! A palavra "container" começou a ser usada para um monte de (que por vezes se sobrepõem) conceitos. Ela é usada para fazer uma analogia com contentorização, e para as tecnologias usadas para implementá-lo. Se considerarmos estes separadamente, temos uma imagem mais clara. Então, vamos falar sobre o porquê de os containers, e depois o como. (Então voltaremos a este ponto, mais uma vez).

No começo

No começo, havia um programa. Vamos chamar este programa de run.sh e o que fazíamos era copiá-lo para um servidor remoto, o qual gostaríamos de executá-lo. No entanto, a execução de um código arbitrário em computadores remotos é insegura e difícil de gerenciar e dimensionar. Então nós inventamos os servidores virtuais privados e as permissões de usuário. E as coisas funcionavam bem.

Mas o pequeno run.sh possuia dependências. Era necessário que algumas bibliotecas existissem no servidor remoto. E este tipo de dependência nunca funcionou muito bem tanto remotamente como localmente. (Nos interrompa caso você nunca tenha escutado esta música). Então, inventamos as AMIs (Amazon Machine Images) e as VMDKs (imagens VMware) e também os Vagrantfiles e assim por diante, e as coisas também funcionavam.

Desta forma, estas soluções eram boas enquanto foi conveniente. Os pacotes eram grandes e era difícil de enviá-los aos servidores de forma eficaz, porque eles não eram muito padronizados. E assim, nós inventamos o cache.

E, mais uma vez, as coisas funcionavam.

O Caching é o que torna as imagens no Docker muito mais eficazes com relação aos vmdks ou vagrantfiles. Ele nos permite enviar os deltas e mais algumas imagens base comuns, em vez de mover as imagens inteiramente para um ambiente. Isto significa que podemos nos dar ao luxo de enviar todo o ambiente de um lugar para outro. É por isso que quando você "executa qualquer docker que seja" ele inicia quase que imediatamente, mesmo que seja necessário descrever a totalidade da imagem de um sistema operacional completo. Vamos falar mais detalhadamente sobre como isso funciona posteriormente.

E, realmente, é isso que os containers são. Eles são como uma agregação de dependências que podemos enviar o código relacionado de uma forma que pode ser executado e seguro. Mas isso é um objetivo de alto nível, não uma definição. Então vamos falar sobre a realidade.

Construindo um container

Então(falando a verdade desta vez!) O que é um container? Seria bom se a criação de um container fosse tão simples como uma chamada de sistema como por exemplo create_container. Mas não é. Mas, sinceramente, é bem próximo disto.

Para falar sobre containers em um nível bem baixo, temos de falar sobre três coisas. E essas coisas são namespaces, cgroups e sistemas de arquivos em camadas. Há outras coisas, mas estas três representam a maioria da magia envolvida nesta tecnologia.

Namespaces

Namespaces fornecem o isolamento necessário para executar vários containers em uma máquina, enquanto damos a cada um o que parece ser o seu próprio ambiente. Existem - até o momento da escrita deste artigo - seis namespaces. Cada um pode ser independentemente solicitado e equivale a dar a um processo (e seus filhos) uma visão dos subconjuntos de recursos de uma máquina.

Os namespaces são formados pelos seguintes arranjos:

● PID: O namespace pid dá a um processo e a seus filhos sua própria visão do subconjunto de processos do sistema. Pense nisso como uma tabela de mapeamento. Quando um processo em um namespace do tipo pid pede ao kernel uma lista de processos, o kernel procura na tabela de mapeamento. Se o processo existe na tabela de mapeamento de ID ele é usado no lugar do seu ID real. Se ele não existir na tabela de mapeamento, o kernel finge que não existe em nenhum lugar. O namespace pid faz com que o primeiro processo criado dentro dele seja o pid 1 (por mapeamento seja qual for a seu ID ele será sempre 1), dando a aparência de uma árvore de processo isolado no container.

● MNT: De certa forma, este é o namespace mais importante. O namespace de montagem dá ao processo nele contido sua própria tabela de montagem. Isso significa que eles podem montar e desmontar os diretórios sem afetar outros namespaces (incluindo o namespace de hospedagem). E o ,mais importante, em combinação com o pivot_root_syscall - como veremos posteriormente - ele permite que um processo tenha seu próprio sistema de arquivos. Esta é a maneira como podemos ter um processo pensando que está em execução no ubuntu, no busybox, ou no alpino - Mesmo trocando o sistema de arquivos, o container é capaz de visualizar.

● NET: O namespace de rede dá aos processos que o utilizam a sua própria pilha de rede. Em geral apenas o namespace de rede principal (aquele que os processos iniciam quando iniciamos o uso de um computador) realmente terá quaisquer placas de rede físicas conectadas no computador. Mas podemos criar pares ethernet virtuais - placas de rede ligadas, onde uma extremidade pode ser colocada em um namespace de rede e ao mesmo tempo ligado a outros namespaces de rede criando uma ligação virtual entre os namespaces de rede. É como ter várias pilhas IP falando entre si em um mesmo host. Com um pouco da mágica do roteamento esta técnica permite que cada container converse com o mundo real enquanto mantém isolado a sua própria pilha de rede.

● UTS: O namespace UTS oferece aos seus processos sua própria visão do nome do host e nome do domínio do sistema. Após introduzir um namespace UTS, definir o nome de um host ou o nome de um domínio não afetará outros processos.

● IPC: O Namespace IPC isola vários mecanismos de comunicação entre processos, tais como filas de mensagens. Veja a documentação sobre Namespaces para mais detalhes

● USUÁRIO: O namespace de usuário foi o mais novo adicionado ao conjunto de namespaces, e é provavelmente o mais poderoso a partir de uma perspectiva de segurança. O namespace de usuário mapeia os uids que um processo visualiza para um conjunto diferente de uids (e gids) no host. Isto é extremamente útil. Usando um namespace de usuário, podemos mapear o ID de usuário raiz do container (isto é, o ID 0) a um uid arbitrário (e sem privilégios) no host. Isto significa que podemos deixar um container pensar que possui acesso de usuário root - podemos até mesmo realmente dar-lhe permissões como se fosse um usuário root sobre recursos específicos n o container - sem realmente prover acesso e quaisquer privilégios na raiz do namespace. O container é livre para executar processos como uid 0 - que normalmente seria um sinônimo de ter permissões de root - mas o kernel na verdade está fazendo um mapeamento de uid por baixo dos panos fazendo uso de um uid real porém, sem privilégios. A maioria dos sistemas de containers não mapeia quaisquer uid no container para uid 0 no namespace chamado: em outras palavras, simplesmente não há um uid no container que tenha permissões reais de root.

A maioria das tecnologias de containers coloca um usuário de processo em todos os namespaces acima dele e inicializa os namespaces para fornecer um ambiente padrão. Isso equivale a, por exemplo, a criação de um cartão de internet inicial na rede isolada do namespace do container com uma conectividade a uma rede real no host.

Cgroups

Cgroups poderia honestamente ser um artigo inteiro falando de si próprio (e reservamos o direito de escrever um!). Vamos tratá-los de forma justa brevemente aqui, porque não há muita coisa que você não consiga encontrar diretamente na documentação uma vez que você tenha compreendido os conceitos.

Fundamentalmente cgroups coletam um conjunto de ids de processos ou tarefas em conjunto e aplica limites para eles. Enquanto namespaces isolam um processo, cgroups impõem o justo (ou injusto - até que você fique louco) compartilhamento de recursos entre processos.

Cgroups são expostos pelo kernel como sistema de arquivos especiais que você pode montar Você adiciona um processo ou segmento a um cgroup simplesmente adicionando ids de processos para um arquivo de tarefas e, em seguida, lê e configura vários valores editando essencialmente arquivos nesse diretório.

Sistemas de arquivos em camadas

Namespaces e cgroups são o isolamento e o compartilhamento de recursos na tecnologia de containers. Eles são as grandes paredes de metal e a segurança nas docas. Sistemas de arquivos em camadas são como nós podemos mover de forma eficiente imagens de máquinas inteiras em um ambiente: eles são o motivo pelo qual os navios flutuam ao invés de afundar.

Em um nível básico, sistemas de arquivos em camadas agregam a otimização necessária para realizar uma chamada para criar uma cópia do sistema de arquivos raiz para cada container. Existem inúmeras maneiras de se fazer isso. Como exemplo, o Btrfs utiliza cópia durante a escrita em um sistema de arquivos em camadas. Já o Aufs faz uso de uma "união de montagens". Uma vez que existem muitas maneiras de conseguir este passo, este artigo só vai usar algo terrivelmente simples: nós vamos realmente fazer uma cópia. É lento, mas funciona.

Construindo um container

Primeiro Passo: Configurando o esqueleto

Vamos apenas levar em conta a estrutura do esqueleto de nosso container em primeiro lugar. Supondo que tenhamos em mãos a versão mais recente do SDK da linguagem de programação golang instalado, em seguida, abra um editor e copie a listagem a seguir.

package main

import (
   	"fmt"
   	"os"
   	"os/exec"
   	"syscall"
)

func main() {
   	switch os.Args[1] {
   	case "run":
          	parent()
   	case "child":
          	child()
   	default:
          	panic("what should I do")
   	}
}

func parent() {
   	cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
   	cmd.Stdin = os.Stdin
   	cmd.Stdout = os.Stdout
   	cmd.Stderr = os.Stderr

   	if err := cmd.Run(); err != nil {
          	fmt.Println("ERROR", err)
          	os.Exit(1)
   	}
}

func child() {
   	cmd := exec.Command(os.Args[2], os.Args[3:]...)
   	cmd.Stdin = os.Stdin
   	cmd.Stdout = os.Stdout
   	cmd.Stderr = os.Stderr

   	if err := cmd.Run(); err != nil {
          	fmt.Println("ERROR", err)
          	os.Exit(1)
   	}
}

func must(err error) {
   	if err != nil {
          	panic(err)
   	}
}

Então, o que isso faz? Bem, começamos pelo main.go e lemos o primeiro argumento. Se ele é um comando 'run', então em seguida, executamos o método parent (), se for o método child () executamos o método child. Com isto, o método pai executa "/proc/self/exe" o qual trata-se de um arquivo especial que contém uma imagem na memória do executável atual. Em outras palavras, re-executamos nós mesmos, mas passando child como o primeiro argumento.

O que significa toda esta loucura? Bem, neste momento, não muito. Isso apenas nos permite executar outro programa que executa um programa solicitado pelo usuário (fornecido em "os.Args [2:]"). Com esta, embora simples plataforma, já somos capazes de criar um container.

Segundo Passo: Adicionando namespaces

Para adicionar alguns namespaces para o nosso programa, nós só precisamos adicionar uma única linha. On Line. Na segunda linha do método parent (), basta adicionar esta linha para dizer ao Go que ele precisa passar alguns parâmetros extras quando o processo child for executado.

cmd.SysProcAttr = &syscall.SysProcAttr{
   	Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}

Se você executar o programa neste momento, o programa será executado dentro dos namespaces UTS, PID e MNT

Terceiro Passo: O sistema de arquivos raiz

Atualmente o processo está em um conjunto isolado de namespaces (sinta-se livre para experimentar com a adição de outros namespaces os Cloneflags descritos anteriormente). Mas o sistema de arquivos parece o mesmo que o host. Isso ocorre porque estamos em um namespace de montagem, mas as montagens iniciais são herdados do namespace de criação.

Vamos mudar isso. Precisamos das seguintes quatro simples linhas para que possamos substituir em um sistema de arquivos raiz. Colocá-los logo no início da função child ().

must(syscall.Mount("rootfs", "rootfs", "", syscall.MS_BIND, ""))
   	must(os.MkdirAll("rootfs/oldrootfs", 0700))
   	must(syscall.PivotRoot("rootfs", "rootfs/oldrootfs"))
   	must(os.Chdir("/"))

As duas últimas linhas são um pouco importantes, elas contam para o sistema operacional que ele precisa mover o diretório atual em "/" para "rootfs/oldrootfs" e que precisa mudar o novo diretório rootfs para "/". Após a chamada "pivotroot" ser concluída, o diretório / no container irá se referir ao diretório rootfs. (A chamada bind mount é necessária para satisfazer alguns requisitos do comando "pivotroot" - o sistema operacional exige que "pivotroot" seja usado para trocar dois sistemas de arquivos que não fazem parte de uma mesma árvore, e se vinculam montando o diretório rootfs para entre si mesmos. Sim, isto é insignificante).

Quarto Passo: Inicialização do mundo do container

Neste ponto, você tem um processo em execução em um conjunto de namespaces isolados, com o sistema de arquivos raiz de sua escolha. Nós pulamos a configuração de cgroups, embora isso seja bastante simples, e ignoramos a gestão do sistema de arquivos raiz que permite de forma eficiente fazer o download e o armazenamento em cache das imagens do sistema de arquivos raiz que dentro de "pivotroot".

Nós também ignoramos a configuração do container . O que temos até aqui é um novo container em namespaces isolados. Criamos a montagem do namespace mudando para os rootfs, mas os outros namespaces têm seu conteúdo padrão. Em um container real, tínhamos necessidade de configurar o "mundo" para o container antes de executar o processo de usuário. Assim, por exemplo, nós configuramos a rede, mudamos para o uid correto antes da execução do processo, configuramos quaisquer outros limites que queremos (como a quebrando a capacidade e definindo rlimits) e assim por diante. Isto, por sua vez, pode muito bem deslocar-nos mais de 100 linhas

Quinto Passo: Colocando tudo junto

Então aqui está, um container super super simples, em (a caminho) menos de 100 linhas em Go. Obviamente isto é intencionalmente simples. Se você usar isto em produção, você é louco e, mais importante, por sua conta e risco. Mas em minha opinião, vendo algo simples e passível de ser feito (hacky) dá uma visão muito útil sobre o que está acontecendo. Então, vamos olhar através da Listagem A.

package main

import (
   	"fmt"
   	"os"
   	"os/exec"
   	"syscall"
)

func main() {
   	switch os.Args[1] {
   	case "run":
          	parent()
   	case "child":
          	child()
   	default:
          	panic("wat should I do")
   	}
}

func parent() {
   	cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
   	cmd.SysProcAttr = &syscall.SysProcAttr{
          	Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
   	}
   	cmd.Stdin = os.Stdin
   	cmd.Stdout = os.Stdout
   	cmd.Stderr = os.Stderr

   	if err := cmd.Run(); err != nil {
          	fmt.Println("ERROR", err)
   	   	os.Exit(1)
   	}
}

func child() {
   	must(syscall.Mount("rootfs", "rootfs", "", syscall.MS_BIND, ""))
   	must(os.MkdirAll("rootfs/oldrootfs", 0700))
   	must(syscall.PivotRoot("rootfs", "rootfs/oldrootfs"))
   	must(os.Chdir("/"))

   	cmd := exec.Command(os.Args[2], os.Args[3:]...)
   	cmd.Stdin = os.Stdin
   	cmd.Stdout = os.Stdout
   	cmd.Stderr = os.Stderr

   	if err := cmd.Run(); err != nil {
          	fmt.Println("ERROR", err)
          	os.Exit(1)
   	}
}

func must(err error) {
   	if err != nil {
          	panic(err)
   	}
}

Então, o que isso significa?

Aqui é onde eu vou ser um pouco controverso. Para mim, um container é uma forma fantástica de enviar as coisas e executar código de forma barata com uma boa dose de isolamento, mas isso não é o fim da conversa. Containers são uma tecnologia, não uma experiência do usuário.

Como um usuário que não quer empurrar os recipientes em produção mais do que um cliente usando amazon.com quer realmente telefonar para as docas para organizar transporte de suas mercadorias. Containers são uma tecnologia fantástica para construir em cima, mas não deve ser distraído por uma capacidade de mover imagens da máquina em torno da necessidade de construir realmente grandes experiências para desenvolvedores.

Plataformas como Serviço (PaaS) construídas em cima de containers, tais como o Cloud Foundry,começam com uma experiência de usuário com base no código em vez de containers. Para a maioria dos desenvolvedores, o que eles querem fazer é carregar o seu código e executá-lo. Nos bastidores, o Cloud Foundry - e a maioria dos outros PaaS - pegam este código e criam uma imagem em container que é escalada e gerenciada. No caso do Cloud Foundry é utilizado um buildpack, mas podemos pular esta etapa e carregar uma imagem Docker criado a partir de um Dockerfile também.

Com um PaaS, todas as vantagens dos containers ainda estão presentes Ambientes consistentes, gestão eficiente dos recursos etc - mas controlando a experiência do usuário um PaaS tanto pode oferecer uma experiência de usuário mais simples para um desenvolvedor e realizar alguns truques extras, como remendar o sistema de arquivos raiz quando há vulnerabilidades de segurança. Além do mais, plataformas oferecem coisas como bancos de dados e filas de mensagens como os serviços que você pode conectar a seus aplicativos, eliminando a necessidade de pensar em tudo como containers.

Com isto, examinamos o que são os containers. Agora, o que vamos fazer com eles?


Sobre o autor

 Julian Friedman trabalha na IBM e atua como líder de engenharia no projeto Garden, a tecnologia de containers da Cloud Foundry.  Antes da Cloud Foundry Julian trabalhou em um grande número de projetos de tecnologia emergentes, incluindo atuações na  performance do IBM Watson - o perigo de se jogar no computador - para algumas das primeiras iterações de tecnologias da IBM em  nuvem. Ele também concluiu recentemente um doutorado na área de Map/Reduce, de modo que agora tem a intenção, se possível,  de passar o resto da sua vida sem nunca mais pensar em Map/Reduce novamente. Você pode encontrar ele no Twitter na conta @doctor_julz.

 

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT