Conteúdos

Subindo uma aplicação PHP no Kubernetes com Gitlab-CI - Parte 1 - Construindo a imagem da aplicação

Subindo uma aplicação PHP no Kubernetes com Gitlab-CI - Parte 1 - Construindo a imagem da aplicação

Este é o primeiro de 3 artigos que pretendo escrever e nele quero compartilhar um pouco sobre a construção de imagens otimizadas utilizando o Builder Pattern.

Parte 2 - Criando os arquivos de manifesto da Aplicação (Kubernetes)

Parte 3 - Criando uma pipeline para o deploy com Gitlab-CI

Por que PHP?

O artigo poderia ser escrito utilizando qualquer outra tecnologia como Golang, Python, Java, Node, etc… A escolha pelo PHP se dá por 2 motivos. O primeiro e mais óbvio é porque é a tecnologia que utilizo no dia-a-dia, no trabalho e tenho mais afinidade. O segundo motivo foi uma das razões que me levaram à escrever o artigo, justamente a menor quantidade de artigos e exemplos utilizando PHP. Durante meus estudos notei que era mais fácil encontrar informações sobre outras tecnologias sendo “deployadas” (se me permitem o neologismo) no Kubernetes do que PHP, por isso a escolha por ele.

Antes de começar

Se você está lendo este artigo eu presumo que já esteja familiarizado com a utilização de containers com Docker, caso este não seja o seu caso eu sugiro fortemente que você assista a série “Descomplicando Docker” do canal LinuxTips.

É importante antes de seguir lendo o artigo que você se familiarize com o conceito de imagens do Docker.

Se você já tem conhecimento sobre utilização de containers com Docker, abaixo eu vou demonstrar uma forma de construir imagens menores para sua aplicação utilizando uma boa prática para criação de containers chamada Builder Pattern. Para saber um pouco mais sobre a técnica, vale a pena assistir este vídeo do Sandeep Dinesh do Google. Aliás, recomendo assistir a toda série “Kubernetes Best Practices” apresentada por ele. Agora, sem mais delongas vamos ao que interessa

O problema de utilizar uma imagem padrão

Primeiramente precisamos entender qual é a necessidade de criar imagens otimizadas sendo que seria muito mais fácil utilizar as imagens padrão encontradas no Docker Hub.

https://gustavoantao.github.io/assets/la_vem_historia.jpg

Qual é o problema disso? Bem, vamos criar uma situação hipotética onde você tem um cluster Kubernetes rodando sua principal aplicação. Seu software é uma plataforma de comércio eletrônico rodando milhares de lojas de diversos tamanhos, desde pequenas lojas com 40 a 50 produtos até grandes lojas com milhares de produtos e vendendo milhões de reais por mês.

Pensar no gerenciamento de escala manual de um ambiente como este já é de arrepiar os cabelos, mas felizmente o Kubernetes nos oferece o autoscaling horizontal de pods e os Cloud Providers em suas soluções gerenciadas de K8s oferecem o autoscaling de worker nodes, permitindo que elasticidade e escalabilidade não sejam problemas. O ambiente naturalmente se estica e diminui automaticamente conforme a carga que recebe, mantendo sua plataforma estável e ao mesmo tempo economizando recursos em períodos de ociosidade. Tudo maravilhoso, cada vez que sobe a quantidade de requests a um nível que a plataforma não vai aguentar o autoscaling sobe novos nodes que baixam a imagem da sua aplicação e provisionam novos pods para atender estas requisições. E é aí que mora o problema! Imagine que seu maior cliente, resolve fazer uma ação de marketing no intervalo do jogo da final da copa e anuncia um desconto de 50% justamente nos itens que ele mais vende. Ah.. ele esqueceu de te informar a respeito.

https://gustavoantao.github.io/assets/meteoro.jpeg

O resultado será uma avalanche de requisições repentinamente chegando no seu cluster, logicamente que com o crescimento das requests o autoscaling provisionará novos nodes, talvez dezenas, talvez centenas, que terão cada um deles que fazer o download da imagem da sua app antes de provisionar os pods e começar a atender as requisições, é muito provável que durante algum tempo (talvez segundos, talvez minutos) requests sejam perdidas e seu cliente deixará de vender. É uma situação que não o deixará muito contente, lembre-se é seu maior cliente, que investiu pesado em marketing e teve prejuízo ao não conseguir atender a toda demanda que sua ação de marketing conseguiu atrair.

Utilizando imagens menores para sua app o tempo para subir um novo node será muito reduzido, e se não conseguir evitar 100% situações como a exemplificada acima, certamente vai minimizar muito o impacto.

Diferença de tamanho entre as imagens oficiais

Vamos primeiramente baixar a imagem oficial do PHP-FPM do Docker Hub:

$ docker pull php:7.3-fpm

Vamos ver o tamanho da imagem default (baseada no Debian)

https://gustavoantao.github.io/assets/default_debian.png

Podemos observar que a imagem padrão, sem adição de nenhuma extensão do PHP possui 398MB de tamanho.

A imagem default do PHP é baseada no Debian, mas já é bem sabido que utilizar a distro Alpine Linux como base traz uma enorme economia de espaço na imagem e esta é a forma mais simples de se reduzir o tamanho da sua app. Vamos ver qual é o tamanho da imagem PHP-FPM oficial utilizando o Alpine:

$ docker pull php:7.3-fpm-alpine

https://gustavoantao.github.io/assets/default_alpine.png

Como podemos observar a imagem com Alpine tem apenas 74.6MB uma economia de 323.4MB se comparada à imagem Debian. Mas quem trabalha com PHP sabe que para levar uma aplicação à produção será necessário instalar diversas extensões que o PHP possui, seja para conexão com banco de dados, autenticação com LDAP, compactação, manipulação de imagens etc…

Para termos uma noção eu criei um Dockerfile utilizando a imagem base padrão do PHP-FPM e adicionando algumas extensões que são frequentemente usadas:

  • mbstring
  • zip
  • intl
  • gd
  • imap
  • xml
  • mysqli
  • json
  • bcmath
  • bz2
  • pdo_mysql
  • ldap

Além dessas extensões deixei a imagem preparada com o composer já instalado para o gerenciamento de dependências da aplicação. Vejamos o Dockerfile dessa imagem:

FROM php:7.3-fpm

RUN apt-get update && apt-get upgrade

# PHP MBSTRING
RUN docker-php-ext-install mbstring

# PHP-ZIP
RUN apt-get install zlib1g-dev libzip-dev -y
RUN docker-php-ext-install zip

# PHP-INTL
RUN apt-get install icu-devtools libicu-dev
RUN docker-php-ext-configure intl && docker-php-ext-install intl

# PHP-GD
RUN apt-get install libpng-dev -y
RUN docker-php-ext-configure gd && docker-php-ext-install gd

# PHP-IMAP
RUN apt-get install libc-client-dev libkrb5-dev -y
RUN docker-php-ext-configure imap --with-kerberos --with-imap-ssl && docker-php-ext-install imap

# PHP-XML
RUN apt-get install libxml2-dev
RUN docker-php-ext-configure xml && docker-php-ext-install xml

# PHP-MYSQLi
RUN docker-php-ext-configure mysqli && docker-php-ext-install mysqli

# PHP-JSON
RUN docker-php-ext-configure json && docker-php-ext-install json

# PHP-BCMATH
RUN docker-php-ext-configure bcmath && docker-php-ext-install bcmath

# PHP-BZ2
RUN apt-get install libbz2-dev -y
RUN docker-php-ext-configure bz2 && docker-php-ext-install bz2

# PHP-PDO
RUN docker-php-ext-configure pdo && docker-php-ext-install pdo
RUN docker-php-ext-configure pdo_mysql && docker-php-ext-install pdo_mysql

# PHP-LDAP
RUN apt-get install libldap2-dev
RUN docker-php-ext-configure ldap && docker-php-ext-install ldap

# Instalando o composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php && \
php -r "unlink('composer-setup.php');" && \
mv composer.phar /usr/local/bin/composer && \
/usr/local/bin/composer global require hirak/prestissimo

Para “buildar” esse Dockerfile executamos:

$ docker build -t php7.3-ext-debian .

Vejamos com que tamanho essa imagem ficou agora:

https://gustavoantao.github.io/assets/ext_debian.png

São 126MB à mais na imagem oficial Debian após a adição das extensões, qual será o resultado dessas adições na imagem alpine? O Dockerfile Alpine ficou assim:

FROM php:7.3-fpm-alpine

RUN apk add --update php libstdc++

# PHP MBSTRING
RUN docker-php-ext-install mbstring

# PHP-ZIP
RUN apk add --update zlib-dev libzip-dev
RUN docker-php-ext-install zip

# PHP-INTL
RUN apk add --update icu-dev php7-intl
RUN docker-php-ext-configure intl && docker-php-ext-install intl

# PHP-GD
RUN apk add --update libgd libpng-dev
RUN docker-php-ext-configure gd && docker-php-ext-install gd

# PHP-IMAP
RUN apk add --update imap-dev
RUN docker-php-ext-configure imap && docker-php-ext-install imap

# PHP-XML
RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing libxml++-dev
RUN docker-php-ext-configure xml && docker-php-ext-install xml

# PHP-MYSQLi
RUN docker-php-ext-configure mysqli && docker-php-ext-install mysqli

# PHP-JSON
RUN docker-php-ext-configure json && docker-php-ext-install json

# PHP-BCMATH
RUN docker-php-ext-configure bcmath && docker-php-ext-install bcmath

# PHP-BZ2
RUN docker-php-ext-configure bz2 && docker-php-ext-install bz2

# PHP-PDO
RUN docker-php-ext-configure pdo && docker-php-ext-install pdo
RUN docker-php-ext-configure pdo_mysql && docker-php-ext-install pdo_mysql

# PHP-LDAP
RUN apk add --update openldap-dev
RUN docker-php-ext-configure ldap && docker-php-ext-install ldap

# Instalando o composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php && \
php -r "unlink('composer-setup.php');" && \
mv composer.phar /usr/local/bin/composer && \
/usr/local/bin/composer global require hirak/prestissimo

E o resultado:

https://gustavoantao.github.io/assets/ext_alpine.png

Com 304MB a imagem está muito maior que que a imagem original Alpine mas ainda 220MB menor que a imagem Debian com as extensões e ainda consegue ser menor que a imagem Debian original.

Mas é possível fazer melhor, como disse no início do artigo, ao utilizarmos o Builder Pattern para a criação das imagens podemos economizar ainda mais espaço em disco.

Builder Pattern

Mas o que cargas d’água vem a ser esse tal padrão?

Bem, ao realizarmos as instalações das extensões (ou a compilação para o caso de linguagens compiladas) o sistema baixa diversas bibliotecas e dependências que somente serão utilizadas durante a instalação, inflando a imagem com arquivos que serão desnecessários para o funcionamento da aplicação. O Docker oferece um recurso chamado de multi-stage build, que nos permite fazer toda a geração de dependências em uma imagem temporária e em seguida copiar apenas os arquivos que interessam para a imagem final, tornando-a muito menor. Este é o Builder Pattern.

Mas como fazemos isso?

Um Dockerfile normalmente possui uma instrução FROM que determina qual será a imagem base utilizada seguida das outras instruções para construção da imagem (COPY, RUN, CMD, etc).

No caso do Builder Pattern utiliza-se um ou mais comandos FROM pra gerar as dependências que posteriormente serão copiadas para a imagem final.

Veja o exemplo:

###############################################################################
# Imagem PHP-7.3 otimizada para uso no cluster
# Multi-stage build (builder pattern)
# Primeiro realiza o build das extensões necessárias

FROM php:7.3-fpm-alpine AS extensions_source

ENV EXT_DIR=/usr/src/php/ext
RUN mkdir -p ${EXT_DIR}

RUN apk add --update php libstdc++

# PHP MBSTRING
RUN docker-php-ext-install mbstring

# PHP-ZIP
RUN apk add --update zlib-dev libzip-dev
RUN docker-php-ext-install zip

# PHP-INTL
RUN apk add --update icu-dev php7-intl
RUN docker-php-ext-configure intl && docker-php-ext-install intl

# PHP-GD
RUN apk add --update libgd libpng-dev
RUN docker-php-ext-configure gd && docker-php-ext-install gd

# PHP-IMAP
RUN apk add --update imap-dev
RUN docker-php-ext-configure imap && docker-php-ext-install imap

# PHP-XML
RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing libxml++-dev
RUN docker-php-ext-configure xml && docker-php-ext-install xml

# PHP-MYSQLi
RUN docker-php-ext-configure mysqli && docker-php-ext-install mysqli

# PHP-JSON
RUN docker-php-ext-configure json && docker-php-ext-install json

# PHP-BCMATH
RUN docker-php-ext-configure bcmath && docker-php-ext-install bcmath

# PHP-BZ2
RUN docker-php-ext-configure bz2 && docker-php-ext-install bz2

# PHP-PDO
RUN docker-php-ext-configure pdo && docker-php-ext-install pdo
RUN docker-php-ext-configure pdo_mysql && docker-php-ext-install pdo_mysql

# PHP-LDAP
RUN apk add --update openldap-dev
RUN docker-php-ext-configure ldap && docker-php-ext-install ldap

# Estágio 2
# Faz o build da imagem limpa, apenas copiando os arquivos necessários da imagem temporária

FROM php:7.3-fpm-alpine

ENV EXT_DIR=/usr/local/lib/php/extensions/no-debug-non-zts-20180731/
ENV LIB_DIR=/usr/lib

COPY --from=extensions_source \
${EXT_DIR}/mbstring.so \
${EXT_DIR}/zip.so \
${EXT_DIR}/intl.so \
${EXT_DIR}/gd.so \
${EXT_DIR}/imap.so \
${EXT_DIR}/xml.so \
${EXT_DIR}/mysqli.so \
${EXT_DIR}/json.so \
${EXT_DIR}/bcmath.so \
${EXT_DIR}/bz2.so \
${EXT_DIR}/pdo.so \
${EXT_DIR}/pdo_mysql.so \
${EXT_DIR}/ldap.so ${EXT_DIR}/

RUN ln -s ${LIB_DIR}/libzip.so ${LIB_DIR}/libzip.so.5 \
&& ln -s ${LIB_DIR}/libcrypto.so.1.1 ${LIB_DIR}/libcrypto.so \
&& ln -s ${LIB_DIR}/libssl.so.1.1 ${LIB_DIR}/libssl.so \
&& ln -s ${LIB_DIR}/libbz2.so ${LIB_DIR}/libbz2.so.1 \
&& ln -s ${LIB_DIR}/libicuio.so ${LIB_DIR}/libicuio.so.64 \
&& ln -s ${LIB_DIR}/libicui18n.so ${LIB_DIR}/libicui18n.so.64 \
&& ln -s ${LIB_DIR}/libicuuc.so ${LIB_DIR}/libicuuc.so.64 \
&& ln -s ${LIB_DIR}/libicudata.so ${LIB_DIR}/libicudata.so.64 \
&& ln -s ${LIB_DIR}/libldap.so ${LIB_DIR}/libldap-2.4.so.2 \
&& ln -s ${LIB_DIR}/liblber.so ${LIB_DIR}/liblber-2.4.so.2 \
&& ln -s ${LIB_DIR}/libsasl2.so ${LIB_DIR}/libsasl2.so.3 \
&& ln -s ${LIB_DIR}/libpng.so ${LIB_DIR}/libpng16.so \
&& ln -s ${LIB_DIR}/libpng.so ${LIB_DIR}/libpng16.so.16 \
&& ln -s ${LIB_DIR}/libc-client.so ${LIB_DIR}/libc-client.so.1

COPY --from=extensions_source \
${LIB_DIR}/libzip.so \
${LIB_DIR}/../../lib/libcrypto.so.1.1 \
${LIB_DIR}/../../lib/libssl.so.1.1 \
${LIB_DIR}/libstdc* \
${LIB_DIR}/libicudata.so \
${LIB_DIR}/libicui18n.so \
${LIB_DIR}/libicuio.so \
${LIB_DIR}/libicutest.so \
${LIB_DIR}/libicutu.so \
${LIB_DIR}/libicuuc.so \
${LIB_DIR}/libgcc_s.so.1 \
${LIB_DIR}/libpng.so \
${LIB_DIR}/libc-client.so \
${LIB_DIR}/libbz2.so \
${LIB_DIR}/libldap.so \
${LIB_DIR}/libldap_r-2.4.so.2 \
${LIB_DIR}/libldap_r.so \
${LIB_DIR}/liblber.so \
${LIB_DIR}/libsasl2.so ${LIB_DIR}/

RUN docker-php-ext-enable zip mbstring intl gd imap xml mysqli json bcmath \
bz2 pdo pdo_mysql ldap

# Refaço a cópia pois o comando docker-php-ext-enable faz purge de algumas libs
COPY --from=extensions_source ${LIB_DIR}/libstdc* ${LIB_DIR}/libgcc_s.so.1 ${LIB_DIR}/

# Instalando o composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php && \
php -r "unlink('composer-setup.php');" && \
mv composer.phar /usr/local/bin/composer && \
/usr/local/bin/composer global require hirak/prestissimo

# Limpando a imagem
RUN rm -rf /var/cache/apk/* /tmp/* /usr/share/man /usr/local/lib/php/doc/*

Note que primeiramente eu declaro um FROM e o nomeio utilizando o parametro AS e nesse estágio são realizadas as instalações das extensões necessárias. Concluídas as instalações, é realizada a declaração de um novo FROM que fará o build da imagem final. Neste estágio utilizando o parâmetro COPY –from=nome_do_estágio é possível referenciar o estágio anterior e copiar apenas os arquivos que nos interessam. Adicionalmente, eu adiciono ao final do arquivo um comando para excluir arquivos de cache, temporários e documentação.

Vejamos o resultado:

https://gustavoantao.github.io/assets/builder_alpine.png

A imagem gerada utilizando o Builder Pattern ficou com 156MB. São 148MB a menos que a imagem Alpine e 368MB de diferença para a imagem Debian. Uma redução de 70% no tamanho original da imagem Debian com as extensões.

Conclusão

Vimos que:

  1. Com o simples fato de optarmos pela distro Alpine Linux na hora de construir nossos containers podemos criar imagens muito mais enxutas;

  2. Que com um pouquinho de trabalho, usando o Builder Pattern é possível otimizar ainda mais estas imagens;

  3. Quando estamos falando de imagens de container para o Kubernetes, tamanho é sim importante :fontawesome-regular-grin-squint-tears:.

No próximo post, entrarei no mundo do Kubernetes propriamente dito criando os arquivos de manifesto para o deploy de uma app PHP no K8s.

Até lá!