Alisson Machado
13 January 2018

Heroku, Python e Gunicorn: Deploy de Aplicações em Containers

Heroku é atualmente uma das melhores opções PaaS ( Plataform as a Service ) para desenvolvedores fazerem o deploy de suas aplicações, essa plataforma é muito utilizada nas startups pois utilizando ela não é necessário se preocupar com gerenciamento de infraestrutura, monitoração de disco, cpu, memória e entre outros. Nesse post vou mostrar como eu utilizo dessa plataforma para fazer o deploy dos meus projetos pessoais em ambientes production-like. O código fonte da aplicação está publicado no link abaixo: https://github.com/Responsus/TerminusNG Essa aplicação é uma versão um pouco melhorada do meu tcc da faculdade, que foi um sistema para gerenciamento de projetos, inicialmente esse sistema foi escrito utilizando um framework chamado Pyramid e nessa nova versão foi escrita utilizando o microframework Flask. O Heroku possui vários planos que podem ser usados, eu uso o plano free, que disponibiliza um container com apenas 1 Worker e desliga após 30 minutos de inatividade, que é o suficiente para testar aplicações em ambientes não produção. Todos os planos estão disponíveis no link abaixo: https://www.heroku.com/pricing Agora, o primeiro passo é criar uma conta no Heroku. https://signup.heroku.com/login Após a sua conta criada, crie um novo app, clicando no botão New no topo da página conforme a imagem abaixo: Ao criar o novo app, dê um nome único a ele. Após a criação do seu app, você será redirecionado para uma outra página e nela clique em Settings, logo abaixo você encontrará um link para o seu repositório no Heroku, salve esse link que será usado mais a frente. Agora é necessário instalar o heroku-cli na máquina para publicar o projeto. Nesse link temos todos os links para download em vários sistemas operacionais. https://devcenter.heroku.com/articles/heroku-cli Eu instalei a versão para windows, então os exemplos que vou usar em diante serão digitados usando o Windows Powershell. Abaixo uma lista dos comandos disponíveis para o Heroku-cli.
PS C:\Users\1511 MXTI\Documents> heroku --help
Usage: heroku COMMAND

Help topics, type heroku help TOPIC for more details:

 access          manage user access to apps
 addons          tools and services for developing, extending, and operating your app
 apps            manage apps
 auth            heroku authentication
 authorizations  OAuth authorizations
 buildpacks      manage the buildpacks for an app
 certs           a topic for the ssl plugin
 ci              run an application test suite on Heroku
 clients         OAuth clients on the platform
 config          manage app config vars
 container       Use containers to build and deploy Heroku apps
 domains         manage the domains for an app
 drains          list all log drains
 features        manage optional features
 git             manage local git repository for app
 keys            manage ssh keys
 labs            experimental features
 local           run heroku app locally
 logs            display recent log output
 maintenance     manage maintenance mode for an app
 members         manage organization members
 notifications   display notifications
 orgs            manage organizations
 pg              manage postgresql databases
 pipelines       manage collections of apps in pipelines
 plugins         manage plugins
 ps              manage dynos (dynos, workers)
 redis           manage heroku redis instances
 regions         list available regions
 releases        manage app releases
 run             run a one-off process inside a Heroku dyno
 sessions        OAuth sessions
 spaces          manage heroku private spaces
 status          status of the Heroku platform
 teams           manage teams
 update          update CLI
 webhooks        setup HTTP notifications of app activity
Primeiro vou fazer o git clone da aplicação.
PS C:\Users\1511 MXTI> git clone https://github.com/Responsus/TerminusNG.git
Cloning into 'TerminusNG'...
remote: Counting objects: 816, done.
remote: Compressing objects: 100% (23/23), done.
remote: Total 816 (delta 14), reused 21 (delta 10), pack-reused 783
Receiving objects: 100% (816/816), 4.21 MiB | 422.00 KiB/s, done.
Resolving deltas: 100% (130/130), done.
PS C:\Users\1511 MXTI>
Com a aplicação já clonada, entrei no diretório e fiz o login do heroku.
PS C:\Users\1511 MXTI> cd .\TerminusNG\
PS C:\Users\1511 MXTI\TerminusNG> heroku login
Enter your Heroku credentials:
Email: alisson.copyleft@gmail.com
Password: *****************
Logged in as alisson.copyleft@gmail.com
PS C:\Users\1511 MXTI\TerminusNG>

Agora adicione o repositório copiado do heroku nos passos anteriores.
PS C:\Users\1511 MXTI\TerminusNG> git remote add heroku https://git.heroku.com/alissonmachado.git
PS C:\Users\1511 MXTI\TerminusNG>
Após adicionado o heroku como remote, foi feito um push para esse repositório:
PS C:\Users\1511 MXTI\TerminusNG> git add --all
PS C:\Users\1511 MXTI\TerminusNG> git commit -m "deploy heroku"
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean
PS C:\Users\1511 MXTI\TerminusNG> git push heroku
Counting objects: 816, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (645/645), done.
Writing objects: 100% (816/816), 4.21 MiB | 59.00 KiB/s, done.
Total 816 (delta 130), reused 816 (delta 130)
A saida do comando será parecida com essa:
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Python app detected
remote: -----> Installing python-3.6.4
remote: -----> Installing pip
remote: -----> Installing requirements with pip
remote:        Collecting Flask (from -r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 1))
remote:          Downloading Flask-0.12.2-py2.py3-none-any.whl (83kB)
remote:        Collecting Flask_SQLAlchemy (from -r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 2))
remote:          Downloading Flask_SQLAlchemy-2.3.2-py2.py3-none-any.whl
remote:        Collecting gunicorn (from -r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 3))
remote:          Downloading gunicorn-19.7.1-py2.py3-none-any.whl (111kB)
remote:        Collecting flask-security (from -r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading Flask_Security-3.0.0-py2.py3-none-any.whl (68kB)
remote:        Collecting flask-mongoengine (from -r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 5))
remote:          Downloading flask-mongoengine-0.9.3.tar.gz (111kB)
remote:        Collecting bcrypt (from -r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 6))
remote:          Downloading bcrypt-3.1.4-cp36-cp36m-manylinux1_x86_64.whl (54kB)
remote:        Collecting itsdangerous>=0.21 (from Flask->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 1))
remote:          Downloading itsdangerous-0.24.tar.gz (46kB)
remote:        Collecting click>=2.0 (from Flask->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 1))
remote:          Downloading click-6.7-py2.py3-none-any.whl (71kB)
remote:        Collecting Jinja2>=2.4 (from Flask->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 1))
remote:          Downloading Jinja2-2.10-py2.py3-none-any.whl (126kB)
remote:        Collecting Werkzeug>=0.7 (from Flask->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 1))
remote:          Downloading Werkzeug-0.14.1-py2.py3-none-any.whl (322kB)
remote:        Collecting SQLAlchemy>=0.8.0 (from Flask_SQLAlchemy->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 2))
remote:          Downloading SQLAlchemy-1.2.0.tar.gz (5.5MB)
remote:        Collecting passlib>=1.7 (from flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading passlib-1.7.1-py2.py3-none-any.whl (498kB)
remote:        Collecting Flask-Login>=0.3.0 (from flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading Flask-Login-0.4.1.tar.gz
remote:        Collecting Flask-Mail>=0.7.3 (from flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading Flask-Mail-0.9.1.tar.gz (45kB)
remote:        Collecting Flask-Principal>=0.3.3 (from flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading Flask-Principal-0.4.0.tar.gz
remote:        Collecting Flask-BabelEx>=0.9.3 (from flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading Flask-BabelEx-0.9.3.tar.gz (41kB)
remote:        Collecting Flask-WTF>=0.13.1 (from flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading Flask_WTF-0.14.2-py2.py3-none-any.whl
remote:        Collecting mongoengine>=0.8.0 (from flask-mongoengine->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 5))
remote:          Downloading mongoengine-0.15.0.tar.gz (144kB)
remote:        Collecting six (from flask-mongoengine->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 5))
remote:          Downloading six-1.11.0-py2.py3-none-any.whl
remote:        Collecting cffi>=1.1 (from bcrypt->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 6))
remote:          Downloading cffi-1.11.3-cp36-cp36m-manylinux1_x86_64.whl (420kB)
remote:        Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->Flask->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 1))
remote:          Downloading MarkupSafe-1.0.tar.gz
remote:        Collecting blinker (from Flask-Mail>=0.7.3->flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading blinker-1.4.tar.gz (111kB)
remote:        Collecting Babel>=1.0 (from Flask-BabelEx>=0.9.3->flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading Babel-2.5.1-py2.py3-none-any.whl (6.8MB)
remote:        Collecting speaklater>=1.2 (from Flask-BabelEx>=0.9.3->flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading speaklater-1.3.tar.gz
remote:        Collecting WTForms (from Flask-WTF>=0.13.1->flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading WTForms-2.1.zip (553kB)
remote:        Collecting pymongo>=2.7.1 (from mongoengine>=0.8.0->flask-mongoengine->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 5))
remote:          Downloading pymongo-3.6.0-cp36-cp36m-manylinux1_x86_64.whl (378kB)
remote:        Collecting pycparser (from cffi>=1.1->bcrypt->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 6))
remote:          Downloading pycparser-2.18.tar.gz (245kB)
remote:        Collecting pytz>=0a (from Babel>=1.0->Flask-BabelEx>=0.9.3->flask-security->-r /tmp/build_f5fec0d0c01ac466add1d2bf054ac08c/requirements.txt (line 4))
remote:          Downloading pytz-2017.3-py2.py3-none-any.whl (511kB)
remote:        Installing collected packages: itsdangerous, click, MarkupSafe, Jinja2, Werkzeug, Flask, SQLAlchemy, Flask-SQLAlchemy, gunicorn, passlib, Flask-Login, blinker, Flask-Mail, Flask-Principal, pytz, Babel, speaklater, Flask-BabelEx, WTForms, Flask-WTF, flask-security, pymongo, six, mongoengine, flask-mongoengine, pycparser, cffi, bcrypt
remote:          Running setup.py install for itsdangerous: started
remote:            Running setup.py install for itsdangerous: finished with status 'done'
remote:          Running setup.py install for MarkupSafe: started
remote:            Running setup.py install for MarkupSafe: finished with status 'done'
remote:          Running setup.py install for SQLAlchemy: started
remote:            Running setup.py install for SQLAlchemy: finished with status 'done'
remote:          Running setup.py install for Flask-Login: started
remote:            Running setup.py install for Flask-Login: finished with status 'done'
remote:          Running setup.py install for blinker: started
remote:            Running setup.py install for blinker: finished with status 'done'
remote:          Running setup.py install for Flask-Mail: started
remote:            Running setup.py install for Flask-Mail: finished with status 'done'
remote:          Running setup.py install for Flask-Principal: started
remote:            Running setup.py install for Flask-Principal: finished with status 'done'
remote:          Running setup.py install for speaklater: started
remote:            Running setup.py install for speaklater: finished with status 'done'
remote:          Running setup.py install for Flask-BabelEx: started
remote:            Running setup.py install for Flask-BabelEx: finished with status 'done'
remote:          Running setup.py install for WTForms: started
remote:            Running setup.py install for WTForms: finished with status 'done'
remote:          Running setup.py install for mongoengine: started
remote:            Running setup.py install for mongoengine: finished with status 'done'
remote:          Running setup.py install for flask-mongoengine: started
remote:            Running setup.py install for flask-mongoengine: finished with status 'done'
remote:          Running setup.py install for pycparser: started
remote:            Running setup.py install for pycparser: finished with status 'done'
remote:        Successfully installed Babel-2.5.1 Flask-0.12.2 Flask-BabelEx-0.9.3 Flask-Login-0.4.1 Flask-Mail-0.9.1 Flask-Principal-0.4.0 Flask-SQLAlchemy-2.3.2 Flask-WTF-0.14.2 Jinja2-2.10 MarkupSafe-1.0 SQLAlchemy-1.2.0 WTForms-2.1 Werkzeug-0.14.1 bcrypt-3.1.4 blinker-1.4 cffi-1.11.3 click-6.7 flask-mongoengine-0.9.3 flask-security-3.0.0 gunicorn-19.7.1 itsdangerous-0.24 mongoengine-0.15.0 passlib-1.7.1 pycparser-2.18 pymongo-3.6.0 pytz-2017.3 six-1.11.0 speaklater-1.3
remote:
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 58.4M
remote: -----> Launching...
remote:        Released v3
O log acima, mostra todo o processo de instalação de dependências para que seja criado o container e seja feito o deploy da aplicação, mas o mais importante é o final do log que aparecerá similiar ao mostrado abaixo:
remote: -----> Launching...
remote:        Released v3
remote:        https://alissonmachado.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
Foi retornado a seguinte url: https://alissonmachado.herokuapp.com/. E pronto, agora é só acessar a aplicação que já estará funcionando. Mas como foi feito o deploy? Ao se fazer uma aplicação que será publicada no heroku, ela já deve ser construída pensada em heroku, assim como qualquer outra plataforma DevOps que faz o deploy em containers. Dentro do repositório existe um arquivo chamado Procfile: https://github.com/Responsus/TerminusNG/blob/master/Procfile , nesse arquivo é definido qual é o processo que deve ser iniciado, que no nosso caso é o servidor Web gunicorn e ele vai iniciar p arquivo app.py que é o principal da aplicação. Outro ponto importante é deixar o arquivo requirements.txt: https://github.com/Responsus/TerminusNG/blob/master/requirements.txt, sempre atualizado, pois ele também faz a instalação do gunicorn que é o servidor de aplicação que é usado durante o deploy. E acho que é isso ai, qualquer coisa é só mandar um salve (=