Criando uma nova imagem no Docker, Parte 2: Agora com argumentos.

Criando uma nova imagem no Docker, Parte 2: Agora com argumentos.

Overview

Seja bem-vindo a um mergulho mais profundo no universo Docker com este guia ilustrativo, onde evoluímos da criação básica de imagens para algo muito mais flexível e poderoso. Desta vez, abordaremos como tornar uma imagem Docker não apenas funcional, mas também versátil através do uso inteligente de variáveis e a adoção do gunicorn como webserver, solucionando problemas de conexões concorrentes. Tudo isso enquanto mantemos a leveza e a eficiência no desenvolvimento de aplicações Flask. Acompanhe este passo a passo e supere os desafios comuns do deploy!

No último post, mostrei como fazer uma imagem do docker utilizando arquivos locais e buscando do Git. Neste eu evoluo um pouco este processo. A imagem criada neste post utiliza arquivos locais e tem a vantagem da flexibilidade de variáveis.

Bom, esta é uma pergunta um tanto quanto obvia, mas é valida. A ideia é flexibilizar a criação da imagem e passar valores padrões para variáveis de ambiente. É claro que você pode ser criativo e achar outras utilidades, mas estas são as mais obvias e são bem flexíveis.

No último post, a imagem criada foi para uma API dummy especifica que eu fiz. Sendo assim, o nome do arquivo (.py) é fixo e a imagem só serve para aquela API específica. Outra desvantagem daquela imagem é que estou executando a aplicação Flask através do próprio script e isso é um problema, pois desta forma chamadas concorrentes vão fazer a API cair.

A ideia desta imagem é faze-la genérica o suficiente para que possa ser utilizada com qualquer aplicação Flask e utilizar o gunicorn como webserver, o que vai acabar com o problema de conexões concorrentes.

Vamos a imagem.

# Base for this image
FROM python:3.7-slim

# A few labels...
LABEL description="my flask application"
LABEL maintainer="Breno RdV <breno@raccoon.ninja>"
bash

Nesta primeira parte do script, estou definindo:

  1. FROM: A imagem que será utilizada com base;
  2. LABEL description: Label com descrição da imagem;
  3. LABEL maintainer: Label com informação do criador/autor da imagem;

Uma explicação: Na última imagem, utilizei o Alpine como base, mas agora utilizei o python:3.7-slim. Esta imagem é uma versão bem leve do Debian, que vem com o Python 3.7 instalado. Fiz isso por que nos poupa o trabalho de fazer estas instalações manualmente e por que, de acordo com algumas pesquisas que fiz, o Alpine é mais leve, mas também é mais propenso a ser o culpado por falhas em execução de scripts/aplicações Python.

# Source folder
ARG source_folder_app=/app
ENV source_folder=${source_folder_app}

# Target folder (where it the code will be copied.)
ARG target_folder_app=/app
ENV target_folder=${target_folder_app}

# Host
ARG host_addr="0.0.0.0"
ENV host=${host_addr}

# Port
ARG port_num=20042
ENV port=${port_num}

# Bind
ENV bind="${host}:${port}"

# Number of workers
ARG qty_workers=3
ENV workers=${qty_workers}

# App file
ARG app_file_name="web_isn_batch_add_processo"
ENV app_file=${app_file_name}

# Variable name
ARG app_var_name="app"
ENV app_var=${app_var_name}

# Variable that will be used as the APP_MODULE value.
ENV full_app_var="${app_file}:${app_var}"
bash

No código acima, criei 9 variáveis de ambiente e 7 argumento de build.

  • As variáveis de ambiente são as que começam com ENV;
  • Os argumentos de build são os que começam com ARG;
  1. O argumento de build só pode ser definido e acessado na hora da criação da imagem. (docker build …);
  2. Já as variáveis de ambiente são, literalmente, variáveis de sistema que estarão presentes no container.

Se você reparar, quase todas as variáveis de ambiente (ENV) possuem um par (ARV). Exemplo: ARG port_num e ENV port.

Criei desta forma para fazer com que as variáveis de ambiente tenham valores que possam ser definidos no momento da criação da imagem, dando mais flexibilidade para ela.

Uma breve explicação de cada um dos parâmetros. Primeiro vou falar o que o parametro faz e depois mostrar qual a variável de ambiente (ENV) e argumento de build (ARG) correspondentes:

  1. Pasta, na maquina local, onde está a aplicação que será utilizada na imagem
    Importante: O caminho não pode ser fora do diretório onde está o Dockerfile. Até onde me consta, o Docker não consegue enxergar pastas fora deste contexto.
    • ARG source_folder_app
    • ENV source_folder
  2. Pasta destino, para onde a aplicação será copiada
    • ARG target_folder_app
    • ENV target_folder
  3. IP/Host que será utilizado no bind.
    • ARG host_addr
    • ENV host
  4. Porta que será utilizada no bind.
    • ARG port_num
    • ENV port
  5. Quantidade de workers que estarão funcionando no container do gunicorn
    • ARG qty_workers
    • ENV workers
  6. Nome do arquivo python onde está a variável da aplicação Flask
    (nome do arquivo sem a extensão .py.)
    • ARG app_file_name
    • ENV app_file
  7. Nome da variável da aplicação Flask
    • ARG app_var_name
    • ENV app_var

Com as variáveis acima, podemos definir valores padrão para cada uma delas e estes valores serão utilizados na hora de criar o container.

# Copies source files
COPY .${source_folder} ${target_folder}

# Copies gunicorn config file. (sample file, slightly modified.)
COPY ./gunicorn_conf.py /gunicorn.conf.py
bash

No código acima, faço a copia dos arquivos da pasta source para a pasta destino. Ambas foram definidas pelas variáveis do passo anterior. Respectivamente falando: source_folder e target_folder.

Depois copiamos o arquivo de configuração (gunicorn_conf.py) para a raiz da imagem, renomeando ele para gunicorn.conf.py. (Não tinha necessidade de renomeá-lo, mas resolvi fazer mesmo assim.)

# Installs stuff...
RUN pip install --upgrade pip \
    && pip install --upgrade setuptools \
    && pip install wheel \
    && pip install gunicorn \
    && pip install -r ${target_folder}/requirements.txt
bash

Agora que já temos os arquivos necessários e as variáveis criadas, basta instalarmos o que precisamos. Neste caso, estou instalando/atualizando o setuptools, wheel e o gunicorn, que é o nosso webserver. Depois eu faço a instalação dos requirements da aplicação.

# Finishing up...
WORKDIR ${target_folder}/
ENV PYTHONPATH=${target_folder}
EXPOSE ${port}
bash

Acima estão alguns preparativos finais:

  1. Definindo o “working directory” para a pasta onde está o fonte da aplicação
  2. Definindo o PYTHONPATH para a mesma pasta
  3. Expondo a porta que foi definida na variável de ambiente port.
CMD gunicorn --config /gunicorn.conf.py "${app_file}:${app_var}"
bash

A linha de comando acima passa o arquivo de configuração (/gunicorn.conf.py e depois o nome do “modulo” (arquivo python onde está a variável da aplicação) e o nome da variável em si.

Em diversos sites, você vai ver uma variante da linha de comando com a seguinte sintaxe: CMD [“gunicorn”, “arg1”, “arg2”]. Todavia, esta abordagem não funciona para deixar o nome do modulo e da variável dinâmicos.

docker build -t pyimage:latest .
bash

Lembrando que você deve rodar o comando acima com o console no diretório onde está o arquivo Dockerfile. O nome que eu escolhi para esta image foi pyimage e a tag foi latest (pyimage:latest), mas você pode escolher outro.

version: '3'
services:
  flask-app:
    image: pyimage:latest
    ports:
      - "5042:20042"
bash

Por conveniência, criei um arquivo docker-compose. Ele vai gerar uma instancia da imagem que acabamos de criar.

No meu Github existe um exemplo completo que você pode utilizar.

Este foi um post longo, mas espero ter ajudado.

Postagens nesta série