Olá Habr! Trago a sua atenção uma tradução do artigo do fundador do serviço Meetspaceapp Nick Gauthier,
“Criando contêineres mínimos do Docker para aplicativos Go” .
10 minutos para lerExistem muitos contêineres oficiais e suportados pela comunidade para várias linguagens de programação (incluindo Go). Mas esses recipientes podem ser bem grandes. Vamos primeiro comparar os métodos padrão de criação de contêineres para aplicativos Go e depois mostrarei como criar aplicativos Go em contêiner estático extremamente pequenos
Parte 1: Nossa "aplicação"
Para o teste, precisamos de um pequeno aplicativo. Vamos bifurcar google.com e gerar o tamanho HTML.
package main import ( "fmt" "io/ioutil" "net/http" "os" ) func main() { resp, err := http.Get("https://google.com") check(err) body, err := ioutil.ReadAll(resp.Body) check(err) fmt.Println(len(body)) } func check(err error) { if err != nil { fmt.Println(err) os.Exit(1) } }
Se começarmos, temos apenas um número. Eu tenho cerca de 17K. Decidi deliberadamente usar SSL, mas explicarei o motivo mais tarde.
Parte 2: dockerização
Usando a imagem oficial do Go, escrevemos o Dockerfile "onbuild":
FROM golang:onbuild
A imagem "Onbuild" pressupõe que seu projeto tenha uma estrutura padrão e criará um aplicativo Go padrão. Se você precisar de mais flexibilidade, poderá usar a imagem Go padrão e compilá-la:
FROM golang:latest RUN mkdir /app ADD . /app/ WORKDIR /app RUN go build -o main . CMD ["/app/main"]
Seria bom aqui criar um Makefile ou algo parecido que você usa para a construção do aplicativo. Podemos carregar alguns recursos da CDN ou importá-los de outro projeto, ou talvez desejemos executar testes no contêiner ...
Como você pode ver, a dockerização Go é bastante direta, especialmente quando você considera que não usamos os serviços e portas às quais precisamos nos conectar. Mas há uma falha séria nas
imagens oficiais - elas são realmente grandes. Vamos ver:
REPOSITORY SIZE TAG IMAGE ID CREATED VIRTUAL SIZE example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB example-golang latest 02e19291523e 19 minutes ago 520.7MB golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB golang 1.4.2 121a93c90463 9 days ago 514.9MB golang latest 121a93c90463 9 days ago 514.9MB
A imagem base ocupa 514,9 MB e nosso aplicativo adiciona outros 5,8 MB. Como é que nosso aplicativo compilado requer 515 MB de dependências?
O fato é que nosso aplicativo foi compilado dentro do contêiner. Isso significa que o contêiner precisa instalar o Go. Portanto, ele precisa das dependências do Go, além de um gerenciador de pacotes e um sistema operacional realmente completo. De fato, se você olhar para o Dockerfile para golang: 1.4, ele vem com o Debian Jessie, instala o compilador GCC e cria ferramentas, baixa Go e instala. Assim, obtemos todo o servidor Debian e o kit de ferramentas Go para iniciar nosso minúsculo aplicativo. O que pode ser feito sobre isso?
Parte 3: Compilar!
Você pode melhorar a situação afastando-se um pouco da abordagem usual. Para fazer isso, vamos compilar Go em nosso diretório de trabalho e adicionar o binário ao contêiner. Isso significa que uma construção simples do docker não funcionará. Precisamos de um conjunto de contêineres com várias etapas:
go build -o main . docker build -t example-scratch -f Dockerfile.scratch .
E um Dockerfile.scratch simples:
FROM scratch ADD main / CMD ["/main"]
O que é zero? O zero é uma imagem em branco especial na janela de encaixe. Seu tamanho é 0B:
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE example-scratch latest ca1ad50c9256 About a minute ago 5.60MB scratch latest 511136ea3c5a 22 months ago 0B
Como resultado, nosso contêiner ocupa apenas 5,6 MB. Ótimo! Mas há um problema:
$ docker run -it example-scratch no such file or directory
O que isso significa? Demorei um pouco para perceber que nosso binário Go estava procurando bibliotecas no sistema operacional em que estava sendo executado. Compilamos nosso aplicativo, mas ele ainda está vinculado dinamicamente às bibliotecas que precisam ser iniciadas (ou seja, a todas as bibliotecas C). Infelizmente, o scratch está vazio, portanto não há bibliotecas ou caminhos de carregamento. Precisamos modificar o script de construção para compilar estaticamente nosso aplicativo com todas as bibliotecas internas:
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
Desabilitamos o cgo, o que nos dá um binário estático. Também especificamos o Linux como sistema operacional (caso alguém o construa no Mac ou no Windows). O sinalizador -a significa reconstruir todos os pacotes que usamos, que reconstruirão todas as importações com o cgo desativado. Agora temos um binário estático. Vamos correr:
$ docker run -it example-scratch Get https://google.com: x509: failed to load system roots and no roots provided
O que é isso? Por isso, decidi usar o SSL em nosso exemplo. Este é um "problema" muito comum para esses cenários: para concluir solicitações SSL, precisamos de certificados SSL raiz. Então, como os adicionamos ao nosso contêiner?
Dependendo do sistema operacional, os certificados podem estar em lugares diferentes. Para muitas distribuições Linux, isso é
/etc/ssl/certs/ca-certificates.crt . Portanto, em primeiro lugar, copiaremos
ca-certificates.crt do nosso computador (ou de uma máquina virtual Linux ou de um provedor de certificados on-line) em nosso repositório. Em seguida, adicionamos
ADD ao nosso Dockerfile para mover esse arquivo para onde o Go espera:
FROM scratch ADD ca-certificates.crt /etc/ssl/certs/ ADD main / CMD ["/main"]
Agora apenas recrie nossa imagem e inicie-a. Isso funciona! Vamos ver o tamanho da nossa aplicação agora:
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE example-scratch latest ca1ad50c9256 About a minute ago 6.12MB example-onbuild latest 9dfb1bbac2b8 19 minutes ago 520.7MB example-golang latest 02e19291523e 19 minutes ago 520.7MB golang onbuild 3be7ee2ec1ae 9 days ago 514.9MB golang 1.4.2 121a93c90463 9 days ago 514.9MB golang latest 121a93c90463 9 days ago 514.9MB scratch latest 511136ea3c5a 22 months ago 0B
Adicionamos um pouco mais de meio megabyte (e a maioria vem de um arquivo estático, e não de certificados raiz). Temos um contêiner muito pequeno - será muito conveniente movê-lo entre registros.
Conclusão
Nosso objetivo era reduzir o tamanho do contêiner para o aplicativo Go. A peculiaridade do Go é que ele pode criar um arquivo binário vinculado estaticamente que contém completamente o aplicativo. Outros idiomas também podem fazer isso, mas de modo algum todos. A aplicação de uma técnica semelhante para reduzir o tamanho do contêiner em outros idiomas dependerá de seus requisitos mínimos. Por exemplo, um aplicativo Java ou JVM pode ser compilado fora do contêiner e, em seguida, incorporado em um contêiner que contém apenas a JVM (e suas dependências). Mas, mesmo assim, será menos que um contêiner com o JDK.