Lucas Simon

Web Developer. lucassrod@gmail.com

Série API em Flask - Parte 13 - Criando um container Docker

Do a request in container

Neste capítulo, vamos aprender a como criar um container docker para desenvolvimento e para deploy da nossa aplicação futuramente em produção.

Para entendimento, segue os capítulos que serão abordados.

O repositório com todo o código fonte esta aqui. Os capítulos estão em branches.

Antes…

Houveram algumas mudanças no projeto para adequar a este capítulo. Vou passar aqui rapidamente as mudanças e seguir com o objetivo do artigo.

Alterado a classe Config em config.py adicionando valores padrões e alterado o APP_PORT para somente PORT. Também foi criada uma nova configuração chamada ProductionConfig.

class Config:
    SECRET_KEY = getenv('SECRET_KEY') or 'uma string randômica e gigante'
    PORT = int(getenv('PORT', 5000))
    DEBUG = getenv('DEBUG') or False
    MONGODB_HOST = getenv('MONGODB_URI', 'mongodb://localhost:27017/api-users')
    JWT_ACCESS_TOKEN_EXPIRES = timedelta(
        minutes=int(getenv('JWT_ACCESS_TOKEN_EXPIRES', 20))
    )
    JWT_REFRESH_TOKEN_EXPIRES = timedelta(
        days=int(getenv('JWT_REFRESH_TOKEN_EXPIRES', 30))
    )

# ...

class ProductionConfig(Config):
    FLASK_ENV = 'production'
    TESTING = False
    DEBUG = False


config = {
    'production': ProductionConfig,
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'default': DevelopmentConfig
}

Por conta dessa mudança no application.py tive de alterar a port.

if __name__ == '__main__':
    ip = '0.0.0.0'
    port = app.config['PORT']

Outras mudanças substanciais ocorreram no arquivo Makefile.

Construindo nosso container de desenvolvimento

O primeiro passo antes de criar o nosso container é criar uma imagem do nosso aplicativo. Essa imagem vai ter todas os pacotes do sistema operacional que serão compiladas por alguns pacotes Python.

Para isso escrever um script no dockerfile que será interpretado pelo comando docker build. Basicamente esse dockerfile é uma sequência de etapas para se criar a imagem.

FROM python:3.6-alpine
RUN rm -rf /var/cache/apk/* && \
    apk update && \
    apk add make && \
    apk add build-base && \
    apk add gcc && \
    apk add python3-dev && \
    apk add libffi-dev && \
    apk add musl-dev && \
    apk add openssl-dev && \
    apk del build-base && \
    rm -rf /var/cache/apk/*

ENV HOME=/home/api FLASK_APP=application.py FLASK_ENV=development PORT=5000
RUN adduser -D api
USER api
WORKDIR $HOME
COPY --chown=api:api . $HOME

RUN python -m venv venv && \
    venv/bin/pip install --upgrade pip && \
    venv/bin/pip install -r requirements/dev.txt

EXPOSE 5000
ENTRYPOINT [ "./boot.sh" ]

O primeiro comando FROM referencia a uma imagem base ja existente para construir a imagem da nossa aplicação. Foi escolhido o python:3.6-alpine pois ele pertence a uma distribuição linux conhecida como alpine pois ela possui um tamanho bem menor se comparada ao ubuntu ou debian. Além disso essa imagem por padrão já vem com o python3.6 instalado além dos comandos básicos de shell cat,ls, find, sh e etc....

Uma observação importante é que o tamanho da imagem importa e muito quando for realizar um deploy para uma AWS, Azure, GoogleCloud. Quanto maior o tamanho maior será o download pela plataforma.

Por isso ao executar o comando RUN abaixo estou fazendo três coisas.

RUN rm -rf /var/cache/apk/* && \
    apk update && \
    apk add make && \
    apk add build-base && \
    apk add gcc && \
    apk add python3-dev && \
    apk add libffi-dev && \
    apk add musl-dev && \
    apk add openssl-dev && \
    apk del build-base && \
    rm -rf /var/cache/apk/*
  1. Limpando o cache do apk usando o rm -rf /var/cache/apk/*

  2. Instalando os pacotes que preciso para compilar pacotes python apk add ....

  3. Removendo pacotes apk del e limpando o cache novamente.

Tudo isso para reduzir o tamanho da imagem da aplicação. Você deve se perguntar o por que de ter colocado todos esses comandos em uma única entrada RUN e separando por && \. A resposta é que o docker e feito por camadas ou melhor blocos que compoem uma imagem. Se fosse várias instruções RUN isso iria criar várias camadas e eu teria de limpar o cache após cada uma dessas instruções.

Com o comando ENV eu defino algumas váriaveis para serem utilizadas na aplicação e no shell da imagem.

ENV HOME=/home/api FLASK_APP=application.py FLASK_ENV=development PORT=5000

No bloco abaixo eu crio uma instrução, RUN, para criar um usuário chamado api. A instrução USER faz logon com o usuário criado e WORKDIR seta o home directory no caminho da váriavel $HOME definida anteriormente.

RUN adduser -D api
USER api
WORKDIR $HOME

Em seguida, copiamos todo o conteúdo da minha pasta (projeto/aplicação/api) para o container docker.

COPY --chown=api:api . $HOME

Duas coisas extremamente importantes:

  1. O parâmetro --chown=api:api realiza a cópia da minha pasta local para o container associando o dono para usuário api e o grupo também. Caso contrário todos os arquivos e deretórios iriam pertencer ao usuário root.

  2. O arquivo oculto .dockerignore. Esse arquivo tem como objetivo ignorar os arquivos e pastas ali contidos. Ele é bem similar ao .gitignore conhecido por todos nós.

.ash_history
.cache
.coverage
.editorconfig
.env
.env-example
.git
.gitignore
.pytest_cache
.vscode
.idea
.cache/
__pycache__/
*.pytest_cache/
.venv
env/
venv/
.DS_Store
*.eggs
*.egg-info
*.py[cod]
*.swp
*.log

Seguindo com a explicação do Dockerfile. Temos novamente a instrução RUN, para criar um virtualenv, atualizar o pip e instalar as dependências de desenvolvimento da api

RUN python -m venv venv && \
    venv/bin/pip install --upgrade pip && \
    venv/bin/pip install -r requirements/dev.txt

Depois expômos, EXPOSE, a porta 5000 do container pelas regras de rede do docker.

EXPOSE 5000

E definimos através da instrução, ENTRYPOINT, o comando padrão quando nosso container for incializado.

ENTRYPOINT [ "./boot.sh" ]

O boot.sh

Este comando ou script é responsável por iniciar a aplicação do Flask e é bem simples de entender.

#!/bin/sh
source venv/bin/activate

case "$FLASK_ENV" in
    production)
        # exec gunicorn -b :$PORT --access-logfile - --error-logfile - "apps:create_app('production')"
        exec gunicorn -w $WORKERS -b :$PORT --access-logfile - --error-logfile - application:app
        ;;
    *) exec make run ;;
esac

Primeiro ativo o virtualenv previamente criado. Depois verifico através de um case/esac o conteúdo da variaǘel $FLASK_ENV.

Caso seja production, executo o gunicorn com as respectivas flags.

  • -w: Número de workers que vou levantar

  • -b: Realizo um bind para a porta 5000 ou o que vier da variável $PORT

  • -access-logfile -: Envia os logs de acesso para saída padrão do console. Nesse caso do docker logs

  • -error-logfile -: Envia os logs de erro para saída padrão do console. Nesse caso do docker logs

  • application:app: Este é o módulo application.py da raíz do meu projeto e :app é a instancia do flask criada pelo create_app() e app.run()

Vamos testar? Execute o comando docker build -t api_users:latest .

$ docker build -t api_users:latest .
Sending build context to Docker daemon  121.9kB
Step 1/10 : FROM python:3.6-alpine

 ---> 9f65fe00d268
Successfully built 9f65fe00d268
Successfully tagged api_users:latest

Criando nosso container.

Ok, criamos a imagem mas quero ver o container. Simples, execute o comando abaixo:

$ docker run -itd --name flask_api_users_latest -p 5001:5000  -e SECRET_KEY=hard-secret-key --link mongo-latest:dbserver -e MONGODB_URI=mongodb://dbserver:27017/api-users api_users:latest
  • -itd: –interactive, –tty, –detach. Cria o container iterativo, com um terminal tty e executa ele em background

  • --name: Seta um nome para o container

  • -p: Realiza um roteamento de portas local (5001) para responder na porta (5000) do container.

  • -e: Passa variáveis de ambiente para o container

  • --link: Esse é importante. Eu tenho um container do MongoDB chamado mongo-latest. O que eu fiz aqui é dizer que esse container possui conexão de rede com este container e ele terá um hostname dbserver. Posteriormente esse dbserver irá compôr na variável de ambiente MONGODB_URI

  • api_users:latest: É a imagem que criamos anteriormente.

Nossa API funcionando em containers

Dois comandos muito úteis.

  1. docker ps, para ver containers ativos e docker ps -a para ver todos os containers (ativos e inativos)
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                      NAMES
3898548e4c9c        api_users:latest    "./boot.sh"              8 minutes ago       Up 8 minutes        0.0.0.0:5001->5000/tcp     flask_api_users_latest
508ffdbcce60        mongo               "docker-entrypoint.s…"   4 months ago        Up 5 hours          0.0.0.0:27017->27017/tcp   mongo-latest
  1. docker logs 3898548e4c9c -f. Muito útil para ver o console do container. A opção -f é similar a do tail -f ou seja fica observando a saida logs.
$ docker logs 3898548e4c9c -f
python application.py
 * Serving Flask app "api-users" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 276-699-376

Do a request in container

Dockerfile de produção

Nossa imagem para produção é levemente diferente do desenvolvimento. Observe as mudanças:

FROM python:3.6-alpine
RUN rm -rf /var/cache/apk/* && \
    apk update && \
    apk add make && \
    apk add build-base && \
    apk add gcc && \
    apk add python3-dev && \
    apk add libffi-dev && \
    apk add musl-dev && \
    apk add openssl-dev && \
    apk del build-base && \
    rm -rf /var/cache/apk/*

ENV HOME=/home/api FLASK_APP=application.py FLASK_ENV=production WORKERS=4 PORT=5000
RUN adduser -D api
USER api
WORKDIR $HOME
COPY --chown=api:api . $HOME

RUN python -m venv venv && \
    venv/bin/pip install --upgrade pip && \
    venv/bin/pip install -r requirements/prod.txt

EXPOSE 5000
ENTRYPOINT [ "./boot.sh" ]

Na variável ENV adicionamos mais alguns itens para serem passados par ao gunicorn. E instalamos agora as dependências de produção.

Um ponto a observar que chamamos esse arquivo de Dockerfile-prd. Para construir a imagem de produção para este arquivo utilizamos a opção -f.

$ docker build -t api_users_prd -f Dockerfile-prd

Fazendo um teste:

172.17.0.1 - - [19/Oct/2018:01:27:00 +0000] "GET / HTTP/1.1" 200 27 "-" "HTTPie/0.9.8"
[2018-10-19 01:54:54 +0000] [1] [INFO] Handling signal: term
[2018-10-19 01:54:54 +0000] [11] [INFO] Worker exiting (pid: 11)
[2018-10-19 01:54:54 +0000] [10] [INFO] Worker exiting (pid: 10)
[2018-10-19 01:54:54 +0000] [9] [INFO] Worker exiting (pid: 9)
[2018-10-19 01:54:54 +0000] [12] [INFO] Worker exiting (pid: 12)
[2018-10-19 01:54:54 +0000] [1] [INFO] Shutting down: Master
[2018-10-19 23:05:13 +0000] [1] [INFO] Starting gunicorn 19.9.0
[2018-10-19 23:05:13 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
[2018-10-19 23:05:13 +0000] [1] [INFO] Using worker: sync
[2018-10-19 23:05:13 +0000] [8] [INFO] Booting worker with pid: 8
[2018-10-19 23:05:13 +0000] [9] [INFO] Booting worker with pid: 9
[2018-10-19 23:05:14 +0000] [10] [INFO] Booting worker with pid: 10
[2018-10-19 23:05:14 +0000] [11] [INFO] Booting worker with pid: 11
172.17.0.1 - - [19/Oct/2018:23:05:46 +0000] "GET / HTTP/1.1" 200 27 "-" "HTTPie/0.9.8"

Do a request in container prd

Finalizando as configurações do docker podemos dar um passo para o deploy da api e adentrar ao mundo DevOps em Cloud. Agradeço a todos e podem me mandar dúvidas, criticas e sugestões. Abraços a todos.

Próximo artigo: Arquivos de configuração para Deploy na Digital Ocean