Embora trabalhar com Python tenha sido, na maioria das vezes, uma experiência fantástica para mim, o gerenciamento de ambientes de desenvolvimento complexos sempre apresentou alguns desafios à medida que os projetos aumentavam.
Para citar apenas alguns exemplos, aqui estão os 3 principais problemas com o Python que encontrei:
1. Aplicativos que dependem de variáveis de ambiente podem precisar que essas variáveis sejam definidas antes que o aplicativo possa ser executado.
2. As aplicações que utilizam certificados de autenticação para comunicação entre diferentes serviços, podem requerer a geração destes certificados localmente antes da execução da aplicação.
3. Conflitos de versão de dependência podem ocorrer entre diferentes microsserviços dentro do mesmo projeto.
As coisas podem ficar especialmente complicadas ao trabalhar com vários microsserviços que dependem uns dos outros e, francamente, como desenvolvedor, não quero realmente gerenciar toda essa sobrecarga apenas para começar a trabalhar. Isso é especialmente verdadeiro se eu estiver apenas iniciando um novo projeto.
Uma solução comum que vi usada ao desenvolver aplicativos Python é usar ambientes virtuais Python , que são ambientes isolados que contêm uma instalação Python e pacotes necessários. No entanto, o gerenciamento de vários ambientes virtuais e outras configurações relacionadas ao ambiente ainda pode ser demorado e complicado, pois o ambiente virtual fornece apenas isolamento no nível do interpretador Python. Isso significa que outras configurações relacionadas ao ambiente, como variáveis de ambiente e alocação de porta, ainda são compartilhadas globalmente para todos os componentes do projeto.
A solução que demonstrarei neste artigo é o uso de conteinerização, que é um método de empacotar um aplicativo e suas dependências em uma unidade independente que pode ser facilmente implantada e executada em qualquer plataforma. O Docker é uma plataforma popular para desenvolver, implantar e executar aplicativos em contêineres, e o Docker Compose é uma ferramenta que facilita a definição e a execução de aplicativos Docker de vários contêineres usando um único arquivo YAML (normalmente denominado
docker-compose.yml
). Embora existam soluções alternativas, como minikube , para simplificar, continuarei usando o Docker e o Docker Compose neste exemplo.Demonstrarei como configurar e usar um ambiente de desenvolvimento conteinerizado usando Docker e Docker Compose. Também discutirei alguns dos desafios de usar um ambiente de desenvolvimento conteinerizado e como superá-los configurando o Docker e o Docker Compose para atender aos seguintes requisitos principais para um ambiente de desenvolvimento eficaz:
1. Executar - Executar cenários de ponta a ponta que simulam a execução no ambiente de produção de destino.
2. Implantar - Fazer alterações de código e reimplementá-lo rapidamente, como em uma pilha de tempo de execução de aplicativos não conteinerizados.
3. Depurar - definir pontos de interrupção e usar um depurador para percorrer o código, como acontece com uma pilha de tempo de execução de aplicativo sem contêiner, para identificar e corrigir erros.
Para ilustrar isso com um exemplo, definirei um aplicativo Python simples que usa a estrutura da Web leve do Python, Flask , para criar uma API RESTful para consultar informações sobre autores e suas postagens. A API tem um único endpoint,
/authors/{author_id}
, que pode ser usado para recuperar informações sobre um determinado autor especificando seu ID como um parâmetro de caminho. O aplicativo usa o módulo de solicitações para fazer solicitações HTTP para um serviço de postagens separado, que deve fornecer uma lista de postagens desse autor. Para manter o código conciso, todos os dados serão gerados aleatoriamente em tempo real usando a biblioteca Faker .Para começar, vou inicializar e abrir um diretório vazio para o projeto. Em seguida, criarei dois subdiretórios: o primeiro será chamado
authors_service
, e o segundo posts_service
. Dentro de cada um desses diretórios, criarei 3 arquivos:1.
app.py
: o ponto de entrada principal para o aplicativo Flask, que define o aplicativo, configura rotas e especifica as funções a serem chamadas quando uma solicitação é feita para essas rotas.2.
requirements.txt
: um arquivo de texto simples que especifica os pacotes Python necessários para a execução do aplicativo.3.
Dockerfile
: Um arquivo de texto contendo instruções para criar uma imagem do Docker, que, conforme mencionado acima, é um pacote leve, autônomo e executável que inclui tudo o que é necessário para executar o aplicativo, incluindo código, tempo de execução, bibliotecas, variáveis de ambiente, e praticamente qualquer outra coisa.Em cada
app.py
arquivo, vou implementar um microsserviço Flask com a lógica desejada.Para
authors_service
, a app.py
arquivo tem a seguinte aparência: import os import flask import requests from faker import Faker app = flask.Flask(__name__) @app.route( "/authors/<string:author_id>" , methods=[ "GET" ] )
def get_author_by_id ( author_id: str ):
author = { "id" : author_id, "name" : Faker().name(), "email" : Faker().email(), "posts" : _get_authors_posts(author_id) } return flask.jsonify(author) def _get_authors_posts ( author_id: str ):
response = requests.get( f' {os.environ[ "POSTS_SERVICE_URL" ]} / {author_id} '
) return response.json() if __name__ == "__main__" : app.run( host=os.environ[ 'SERVICE_HOST' ], port= int (os.environ[ 'SERVICE_PORT' ]) )
Este código configura um aplicativo Flask e define uma rota para lidar com solicitações GET para o endpoint
/authors/{author_id}
. Quando esse ponto de extremidade é acessado, ele gera dados fictícios para um autor com o ID fornecido e recupera uma lista de postagens desse autor do serviço de postagens separado. Em seguida, ele executa o aplicativo Flask, ouvindo o nome do host e a porta especificados nas variáveis de ambiente correspondentes. flask
, requests
e Faker
pacotes. Para contabilizar isso, vou adicioná-los ao serviço de autores requirements.txt
arquivo, da seguinte forma: flask == 2 . 2 . 2
requests == 2 . 28 . 1
Faker == 15 . 3 . 4
Observe que não há requisitos específicos de versão de pacote para nenhuma das dependências mencionadas neste guia. As versões usadas foram as mais recentes disponíveis no momento da redação.
Para o
posts_service
, app.py
tem a seguinte aparência: import os import uuid from random import randint import flask from faker import Faker app = flask.Flask(__name__) @app.route( '/posts/<string:author_id>' , methods=[ 'GET' ] )
def get_posts_by_author_id ( author_id: str ):
posts = [ { "id:" : str (uuid.uuid4()), "author_id" : author_id, "title" : Faker().sentence(), "body" : Faker().paragraph() } for _ in range (randint( 1 , 5 )) ] return flask.jsonify(posts) if __name__ == '__main__' : app.run( host=os.environ[ 'SERVICE_HOST' ], port= int (os.environ[ 'SERVICE_PORT' ]) )
Neste código, quando um cliente (ou seja,
authors_service
) envia uma solicitação GET para a rota /posts/{author_id}
, a função get_posts_by_author_id
é chamado com o especificado author_id
como parâmetro. A função gera dados simulados para entre 1 e 5 postagens escritas pelo autor usando a biblioteca Faker e retorna a lista de postagens como uma resposta JSON ao cliente.Também precisarei adicionar os pacotes flask e Faker aos serviços de postagens
requirements.txt
arquivo, da seguinte forma: flask == 2 . 2 . 2
Faker == 15 . 3 . 4
Antes de colocar esses serviços em contêineres, vamos considerar um exemplo de por que eu gostaria de empacotá-los e executá-los isoladamente um do outro.
Ambos os serviços usam as variáveis de ambiente
SERVICE_HOST
e SERVICE_PORT
para definir o soquete no qual o servidor Flask será iniciado. Enquanto SERVICE_HOST
não é um problema (vários serviços podem escutar no mesmo host), SERVICE_PORT
pode causar problemas. Se eu instalasse todas as dependências em um ambiente Python local e executasse ambos os serviços, o primeiro serviço a iniciar usaria a porta especificada, fazendo com que o segundo serviço travasse porque não poderia usar a mesma porta. Uma solução simples é usar variáveis de ambiente separadas (por exemplo, AUTHORS_SERVICE_PORT
e POSTS_SERVICE_PORT
) em vez de. No entanto, modificar o código-fonte para se adaptar às restrições ambientais pode se tornar complexo ao aumentar a escala.A conteinerização ajuda a evitar problemas como esse configurando o ambiente a ser adaptado para o aplicativo, e não o contrário . Neste caso, posso definir o
SERVICE_PORT
variável de ambiente para um valor diferente para cada serviço, e cada serviço poderá usar sua própria porta sem interferência de outros serviços. Dockerfile
no diretório de cada serviço. O conteúdo deste arquivo (para ambos os serviços) é o seguinte: FROM python: 3.8
RUN mkdir /app WORKDIR /app COPY requi rements.txt /app/
RUN pip install -r requi rements.txt
COPY . /app/ CMD [ "python" , "app.py" ]
Esta
Dockerfile
baseia-se em uma imagem pai do Python 3.8 e configura um diretório para o aplicativo no contêiner. Em seguida, ele copia o requirements.txt
arquivo da máquina host para o contêiner e instala as dependências listadas nesse arquivo. Por fim, ele copia o restante do código do aplicativo da máquina host para o contêiner e executa o script principal do aplicativo quando o contêiner é iniciado.Em seguida, criarei um arquivo chamado
docker-compose.yml
no diretório raiz do projeto. Conforme mencionado brevemente acima, esse arquivo é usado para definir e executar aplicativos Docker de vários contêineres. No docker-compose.yml
arquivo, posso definir os serviços que compõem o aplicativo, especificar as dependências entre eles e configurar como eles devem ser construídos e executados. Nesse caso, fica assim: ---
# Specify the version of the Docker Compose file format
version: '3.9'
services:
# Define the authors_service service
authors_service:
# This service relies on, and is therefor dependent on, the below `posts_service` service
depends_on:
- posts_service
# Specify the path to the Dockerfile for this service
build:
context: ./authors_service
dockerfile: Dockerfile
# Define environment variables for this service
environment:
- SERVICE_HOST=0.0.0.0
- PYTHONPATH=/app
- SERVICE_PORT=5000
- POSTS_SERVICE_URL=http://posts_service:6000/posts
# Map port 5000 on the host machine to port 5000 on the container
ports:
- "5000:5000"
# Mount the authors_service source code directory on the host to the working directory on the container
volumes:
- ./authors_service:/app
# Define the posts_service service
posts_service:
# Specify the path to the Dockerfile for this service
build:
context: ./posts_service
dockerfile: Dockerfile
# Define environment variables for this service
environment:
- PYTHONPATH=/app
- SERVICE_HOST=0.0.0.0
- SERVICE_PORT=6000
# Mount the posts_service source code directory on the host to the working directory on the container
volumes:
- ./posts_service:/app
Os contêineres podem ser iniciados com o
docker-compose up
comando. Na primeira vez que isso for executado, as imagens do docker serão criadas automaticamente.Isso satisfaz o primeiro requisito básico acima de "Executar".
Observe que no
docker-compose.yml
arquivo, as montagens de volume são usadas para compartilhar os diretórios de código-fonte para o authors_service
e posts_service
serviços entre a máquina host e os contêineres. Isso permite que o código seja editado na máquina host com as alterações refletidas automaticamente nos contêineres (e vice-versa).Por exemplo, a linha a seguir monta o
./authors_service
diretório na máquina host para o /app
diretório no authors_service
recipiente: volumes: - . /authors_service:/ app
As alterações feitas na máquina host ficam imediatamente disponíveis no contêiner, e as alterações feitas no contêiner são imediatamente mantidas no diretório de código-fonte da máquina host. Isso permite a reimplantação rápida das alterações reiniciando o contêiner relevante sem recriar a imagem, satisfazendo efetivamente o segundo requisito principal de "implantar".
É aqui que se envolve um pouco mais. Os depuradores em Python usam as ferramentas de depuração fornecidas pelo interpretador para pausar a execução de um programa e inspecionar seu estado em determinados pontos. Isso inclui definir uma função de rastreamento com
sys.settrace()
em cada linha de código e verificando pontos de interrupção, além de usar recursos como a pilha de chamadas e a inspeção de variáveis. A depuração de um interpretador Python em execução em um contêiner pode adicionar complexidade em comparação à depuração de um interpretador Python em execução em uma máquina host. Isso ocorre porque o ambiente do contêiner é isolado da máquina host.Para superar isso, uma das duas trilhas gerais a seguir pode ser tomada: O código pode ser depurado de dentro do próprio contêiner ou pode ser depurado usando um servidor de depuração remoto.
Primeiro, usarei o VSCode como editor de escolha para demonstrar como fazer isso. Depois, explicarei como trabalhar de maneira semelhante com o JetBrains PyCharm .
Depurando o código de dentro do próprio contêiner
Para desenvolver e depurar o código de um contêiner docker em execução usando o VSCode, irei:
1. Certifique-se de que a extensão do Docker para VSCode esteja instalada e habilitada.
2. Certifique-se de que o contêiner ao qual desejo anexar esteja funcionando.
3. Abra a visualização do explorador da extensão do Docker clicando no ícone do Docker na barra lateral esquerda.
4. Na visualização do explorer, expanda a seção "Executando contêineres" e selecione o contêiner ao qual deseja anexar.
5. Clique com o botão direito do mouse no contêiner e selecione a opção "Anexar código do Visual Studio" no menu de contexto.
Isso anexará o Visual Studio Code ao contêiner selecionado e abrirá uma nova janela do VSCode dentro do contêiner. Nesta nova janela, posso escrever, executar e depurar código como faria normalmente em um ambiente local.
Para evitar a instalação de extensões do VSCode, como Python , toda vez que o contêiner é reiniciado, posso montar um volume dentro do contêiner que armazena as extensões do VSCode. Dessa forma, quando o container for reiniciado, as extensões ainda estarão disponíveis porque estão armazenadas na máquina host. Para fazer isso usando o docker compose neste projeto de demonstração, o
docker-compose.yml
arquivo pode ser modificado da seguinte forma: ---
# Specify the version of the Docker Compose file format
version: '3.9'
services:
# Define the authors_service service
authors_service:
...
# Mount the authors_service source code directory on the host to the working directory on the container
volumes:
- ./authors_service:/app
# Mount the vscode extension directory on the host to the vscode extension directory on the container
- /path/to/host/extensions:/root/.vscode/extensions
# Define the posts_service service
posts_service:
...
Observe que as extensões do VSCode normalmente podem ser encontradas em
~/.vscode/extensions
no Linux e macOS, ou %USERPROFILE%\.vscode\extensions
no Windows.Usando um servidor de depuração Python remoto
O método de depuração acima funciona bem para scripts autônomos ou para escrever, executar e depurar testes. No entanto, a depuração de um fluxo lógico envolvendo vários serviços executados em diferentes contêineres é mais complexa.
Quando um contêiner é iniciado, o serviço que ele contém normalmente é iniciado imediatamente. Nesse caso, os servidores Flask em ambos os serviços já estão em execução no momento em que o VSCode é anexado, portanto, clicar em "Executar e depurar" e iniciar outra instância do servidor Flask não é prático, pois resultaria em várias instâncias do mesmo serviço em execução no mesmo contêiner e competindo entre si, o que geralmente não é um fluxo de depuração confiável.
Isso me leva à opção número dois; usando um servidor de depuração Python remoto. Um servidor de depuração Python remoto é um interpretador Python executado em um host remoto e configurado para aceitar conexões de um depurador. Isso permite o uso de um depurador que está sendo executado localmente, para examinar e controlar um processo Python que está sendo executado em um ambiente remoto.
É importante observar que o termo "remoto" não se refere necessariamente a uma máquina fisicamente remota ou mesmo a um ambiente local, mas isolado, como um contêiner Docker em execução em uma máquina host. Um servidor de depuração remoto Python também pode ser útil para depurar um processo Python que está sendo executado no mesmo ambiente que o depurador. Nesse contexto, usarei um servidor de depuração remoto que está sendo executado no mesmo contêiner do processo que estou depurando. A principal diferença entre esse método e a primeira opção de depuração que abordamos é que estarei anexando a um processo pré-existente em vez de criar um novo sempre que quiser executar e depurar o código.
Para começar, o primeiro passo é adicionar o pacote debugpy ao
requirements.txt
arquivos para ambos os serviços. debugpy é um depurador Python de código aberto e de alto nível que pode ser usado para depurar programas Python local ou remotamente. Vou adicionar a seguinte linha a ambos requirements.txt
arquivos: debugpy == 1 . 6 . 4
Agora preciso reconstruir as imagens para instalar o debugpy nas imagens do Docker para cada serviço. vou correr o
docker-compose build
comando para fazer isso. Então eu vou correr docker-compose up
para lançar os contêineres.Em seguida, anexarei o VSCode ao contêiner em execução que contém o processo que desejo depurar, como fiz acima.
Para anexar um depurador ao aplicativo python em execução, precisarei adicionar o seguinte snippet ao código no ponto a partir do qual desejo iniciar a depuração:
import debugpy; debugpy.listen( 5678 )
Este snippet importa o módulo debugpy e chama o
listen
função, que inicia um servidor debugpy que escuta conexões de um depurador no número de porta especificado (neste caso, 5678).Se eu quisesse depurar o
authors_service
, eu poderia colocar o snippet acima logo antes do get_author_by_id
declaração de função dentro do app.py
arquivo - da seguinte forma: import os import flask import requests from faker import Faker app = flask.Flask(__name__) import debugpy; debugpy.listen( 5678 ) @app.route( "/authors/<string:author_id>" , methods=[ "GET" ] )
def get_author_by_id ( author_id: str ):
...
Isso iniciaria um servidor de depuração na inicialização do aplicativo como o
app.py
script é executado.A próxima etapa é criar uma configuração de inicialização do VSCode para depurar o aplicativo. No diretório raiz do serviço ao qual anexei o contêiner (e no qual estou executando a janela do VSCode), criarei uma pasta chamada
.vscode
. Então, dentro desta pasta, criarei um arquivo chamado launch.json
, com o seguinte conteúdo: { "version" : "0.2.0" , "configurations" : [ { "name" : "Python: Remote Attach" , "type" : "python" , "request" : "attach" , "connect" : { "host" : "localhost" , "port" : 5678
} } ] }
Essa configuração especifica que o VSCode deve ser anexado a um depurador Python em execução na máquina local (ou seja, o contêiner atual) na porta 5678 - que, principalmente, foi a porta especificada ao chamar o
debugpy.listen
função acima.Em seguida, salvarei todas as alterações. Na exibição do explorador da extensão do Docker, clicarei com o botão direito do mouse no contêiner ao qual estou anexado e selecionarei "Reiniciar contêiner" no menu de contexto (feito na instância local do VSCode). Depois de reiniciar o contêiner, a janela do VSCode dentro do contêiner exibirá uma caixa de diálogo perguntando se desejo recarregar a janela - a resposta correta é sim.
Agora só falta vê-lo em ação!Para iniciar a depuração, dentro da instância do VSCode em execução no contêiner, abro o script que desejo depurar e aperto F5 para iniciar o depurador. O depurador irá anexar ao script e pausar a execução na linha onde o
debugpy.listen
função é chamada. Os controles do depurador na guia Depurar agora podem ser usados para percorrer o código, definir pontos de interrupção e inspecionar variáveis.Isso satisfaz o requisito de "Depuração" acima.
De acordo com os documentos oficiais , há duas maneiras de fazer isso ao usar o PyCharm: um interpretador pode ser recuperado de uma imagem do Docker usando o recurso de intérprete remoto e/ou uma configuração de servidor de depuração remota . Observe que essas duas opções não são mutuamente exclusivas. Pessoalmente, normalmente confio principalmente no recurso de intérprete remoto para desenvolvimento e uso uma configuração de servidor de depuração remota se e quando necessário.
Configurando um intérprete remoto
Para configurar um intérprete remoto no PyCharm, irei:
1. Clique no menu pop-up da guia intérpretes no canto inferior direito da janela do IDE.
2. Clique em Add new interpreter e, em seguida, selecione On docker compose... no menu pop-up.
3. Na próxima janela pop-up, selecione o arquivo de composição do docker relevante e selecione o serviço relevante no menu suspenso. O PyCharm agora tentará se conectar à imagem do docker e recuperar os interpretadores python disponíveis.
4. Na próxima janela, selecione o interpretador python que desejo usar (por exemplo
/usr/local/bin/python
). Uma vez selecionado o intérprete, clique em "Criar".O PyCharm irá então indexar o novo interpretador, após o qual posso executar ou depurar o código como de costume - o PyCharm orquestrará a composição do docker nos bastidores para mim sempre que eu desejar.
Definindo uma configuração de servidor de depuração remota
Para definir uma configuração de servidor de depuração remota, primeiro preciso adicionar duas dependências ao
requirements.txt
arquivo(s): pydevd e pydevd_pycharm . Eles são semelhantes em função ao pacote debugpy demonstrado acima, mas, como o próprio nome sugere, pydevd_pycharm é projetado especificamente para depuração com PyCharm. No contexto deste projeto de demonstração, adicionarei as duas linhas a seguir a ambos requirements.txt
arquivos: pydevd ~= 2 . 9 . 1
pydevd -pycharm== 223 . 8214 . 17
Depois que isso for feito e as imagens do docker forem reconstruídas, posso incorporar o seguinte trecho de código no código para iniciar um servidor de depuração pydevd_pycharm no ponto do código a partir do qual desejo iniciar a depuração:
import pydevd_pycharm; pydevd_pycharm.settrace( 'host.docker.internal' , 5678 )
Observe que, diferentemente do debugpy, aqui especifiquei um endereço de nome de host com o valor "host.docker.internal", que é um nome DNS que resolve o endereço IP interno da máquina host de um contêiner Docker. Isso ocorre porque não estou executando o PyCharm no contêiner; em vez disso, estou efetivamente configurando o servidor de depuração para escutar na porta 5678 da máquina host .
Essa opção também existe com o debugpy, mas como nesse caso eu estava executando uma instância do VSCode no próprio contêiner, simplificou as coisas para deixar o endereço do nome do host padrão como "localhost" (ou seja, a interface de loopback do próprio contêiner, não o máquina hospedeira).
Para fazer isso, eu vou:
1. Abra a caixa de diálogo Configuração de execução/depuração selecionando Executar > Editar configurações no menu principal.
2. Clique no botão + no canto superior esquerdo da caixa de diálogo e selecione Python Remote Debug no menu suspenso.
3. No campo Nome , insira um nome para a configuração de execução.
4. No campo Caminho do script , especifique o caminho para o script que desejo depurar.
5. No campo Host , insira o endereço IP da máquina host onde o servidor do depurador será executado. Neste exemplo, é "localhost".
6. No campo Porta , insira o número da porta na qual o servidor do depurador atenderá. Neste exemplo, é 5678.
7. Na seção Mapeamentos de caminho , posso especificar como os caminhos na máquina host são mapeados para os caminhos dentro do contêiner. Isso é útil se estou depurando o código que é montado no contêiner do host, pois os caminhos podem não ser os mesmos em ambos os ambientes. Neste exemplo, quero mapear
path/to/project/on/host/authors_service
na máquina host, para /app
no contêiner para depuração de author_service ou path/to/project/on/host/posts_service
para /app
no contêiner para depurar posts_service (precisariam ser duas configurações de execução separadas). 8. Clique em OK para salvar a configuração de execução.
Para iniciar a depuração, selecionarei a configuração de execução acima no menu suspenso Executar , clicarei no botão Depurar e, em seguida, ativarei o(s) contêiner(es) relevante(s) com o
docker-compose up
comando. O depurador PyCharm será anexado ao script e pausará a execução na linha onde o pydevd_pycharm.settrace
função é chamada, permitindo-me começar a destruir esses bugs.Neste guia, dei uma visão geral, porém prática, do que são os ambientes de desenvolvimento python em contêiner, por que são úteis e como escrever, implantar e depurar código python usando-os. Observe que este não é de forma alguma um guia abrangente para trabalhar com esses ambientes. É apenas um ponto de partida para expandir. Aqui estão alguns links úteis para esse fim:
1. Visão geral da conteinerização por redhat
3. Documentos oficiais do Docker
4. Documentos oficiais do JetBrains PyCharm para depuração remota
5.Documentos oficiais do VSCode para desenvolver Python em contêineres de desenvolvimento
Espero que você tenha achado este guia útil - obrigado pela leitura!