Série API em Flask - Parte 13 - Criando um container Docker
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.
Capítulo 1: Introdução, configuração e Hello World
Capítulo 2: Organizando as dependências e requerimentos
Capítulo 3: Configurando o pytest e nosso primeiro teste
Capítulo 4: Configurando o Makefile
Capítulo 5: Adicionando o MongoDB
Capítulo 6: Criando e testando o modelo de usuários
Capítulo 7: Criando usuários
Capítulo 8: Listando usuários
Capítulo 9: Buscando usuários
Capítulo 10: Editando um usuário
Capítulo 11: Deletando um usuário
Capítulo 12: Autênticação por JWT
Capítulo 13: Criando um container Docker Estamos aqui
Capítulo 14: Arquivos de configuração para Deploy na Digital Ocean
Capítulo 15: Automatizando o processo de deploy com Fabric
Capítulo 16: CI e CD com Jenkins, Python, Flask e Fabric
Capítulo 17: Utilizando o RabbitMQ com Flask e Sendgrid para enviar e-mails de boas vindas e ativar a conta do usuário
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/*
Limpando o cache do
apk
usando orm -rf /var/cache/apk/*
Instalando os pacotes que preciso para compilar pacotes python
apk add ....
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:
O parâmetro
--chown=api:api
realiza a cópia da minha pasta local para o container associando o dono para usuárioapi
e o grupo também. Caso contrário todos os arquivos e deretórios iriam pertencer ao usuárioroot
.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 dodocker logs
-error-logfile -
: Envia os logs de erro para saída padrão do console. Nesse caso dodocker logs
application:app
: Este é o móduloapplication.py
da raíz do meu projeto e:app
é a instancia do flask criada pelocreate_app()
eapp.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 chamadomongo-latest
. O que eu fiz aqui é dizer que esse container possui conexão de rede com este container e ele terá um hostnamedbserver
. Posteriormente essedbserver
irá compôr na variável de ambienteMONGODB_URI
api_users:latest
: É a imagem que criamos anteriormente.
Nossa API funcionando em containers
Dois comandos muito úteis.
docker ps
, para ver containers ativos edocker 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
docker logs 3898548e4c9c -f
. Muito útil para ver o console do container. A opção-f
é similar a dotail -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
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"
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