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 , 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. ambientes virtuais Python 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. é uma plataforma popular para desenvolver, implantar e executar aplicativos em contêineres, e o é 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 ). Embora existam soluções alternativas, como , para simplificar, continuarei usando o Docker e o Docker Compose neste exemplo. O Docker Docker Compose docker-compose.yml minikube 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. Configuração do projeto Para ilustrar isso com um exemplo, definirei um aplicativo Python simples que usa a estrutura da Web leve do Python, , para criar uma API RESTful para consultar informações sobre autores e suas postagens. A API tem um único endpoint, , 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 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 . Flask /authors/{author_id} solicitações 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 , e o segundo . Dentro de cada um desses diretórios, criarei 3 arquivos: authors_service posts_service 1. : 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. app.py 2. : um arquivo de texto simples que especifica os pacotes Python necessários para a execução do aplicativo. requirements.txt 3. : 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. Dockerfile Em cada arquivo, vou implementar um microsserviço Flask com a lógica desejada. app.py Para , a arquivo tem a seguinte aparência: authors_service app.py app = flask.Flask(__name__) author = { } response = requests.get( ) app.run( ) import os import flask import requests from faker import Faker @app.route( "/authors/<string:author_id>" , methods=[ "GET" ] ) def get_author_by_id ( author_id: str ): "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 ): f' {os.environ[ "POSTS_SERVICE_URL" ]} / {author_id} ' return response.json() if __name__ == "__main__" : 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 . 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. Note que a lógica acima depende do , e pacotes. Para contabilizar isso, vou adicioná-los ao serviço de autores arquivo, da seguinte forma: /authors/{author_id} flask requests Faker requirements.txt 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 , tem a seguinte aparência: posts_service app.py app = flask.Flask(__name__) posts = [ { } ] app.run( ) import os import uuid from random import randint import flask from faker import Faker @app.route( '/posts/<string:author_id>' , methods=[ 'GET' ] ) def get_posts_by_author_id ( author_id: str ): "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__' : host=os.environ[ 'SERVICE_HOST' ], port= int (os.environ[ 'SERVICE_PORT' ]) Neste código, quando um cliente (ou seja, ) envia uma solicitação GET para a rota , a função é chamado com o especificado 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. authors_service /posts/{author_id} get_posts_by_author_id author_id Também precisarei adicionar os pacotes flask e Faker aos serviços de postagens arquivo, da seguinte forma: requirements.txt 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 e para definir o soquete no qual o servidor Flask será iniciado. Enquanto não é um problema (vários serviços podem escutar no mesmo host), 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, e ) 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. SERVICE_HOST SERVICE_PORT SERVICE_HOST SERVICE_PORT AUTHORS_SERVICE_PORT POSTS_SERVICE_PORT A conteinerização ajuda a evitar problemas como esse . Neste caso, posso definir o 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. Para conteinerizar os serviços, criarei um novo arquivo chamado no diretório de cada serviço. O conteúdo deste arquivo (para ambos os serviços) é o seguinte: configurando o ambiente a ser adaptado para o aplicativo, e não o contrário SERVICE_PORT Dockerfile WORKDIR /app COPY . /app/ FROM python: 3.8 RUN mkdir /app COPY requi rements.txt /app/ RUN pip install -r requi rements.txt CMD [ "python" , "app.py" ] Esta baseia-se em uma do Python 3.8 e configura um diretório para o aplicativo no contêiner. Em seguida, ele copia o 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. Dockerfile imagem pai requirements.txt Em seguida, criarei um arquivo chamado 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 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: docker-compose.yml docker-compose.yml --- # 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 Executando o aplicativo Os contêineres podem ser iniciados com o comando. Na primeira vez que isso for executado, as imagens do docker serão criadas automaticamente. docker-compose up Isso satisfaz o primeiro requisito básico acima de "Executar". reimplantando Observe que no arquivo, as montagens de volume são usadas para compartilhar os diretórios de código-fonte para o e 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). docker-compose.yml authors_service posts_service Por exemplo, a linha a seguir monta o diretório na máquina host para o diretório no recipiente: ./authors_service /app authors_service 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 recriar a imagem, satisfazendo efetivamente o segundo requisito principal de "implantar". sem Depurando É 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 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. sys.settrace() 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 como editor de escolha para demonstrar como fazer isso. Depois, explicarei como trabalhar de maneira semelhante com . VSCode 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 para VSCode esteja instalada e habilitada. extensão do Docker 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 , 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 arquivo pode ser modificado da seguinte forma: Python docker-compose.yml --- # 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 no Linux e macOS, ou no Windows. ~/.vscode/extensions %USERPROFILE%\.vscode\extensions 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 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. outra instância É 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 ao 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 arquivos: debugpy requirements.txt requirements.txt debugpy == 1 . 6 . 4 Agora preciso reconstruir as imagens para instalar o debugpy nas imagens do Docker para cada serviço. vou correr o comando para fazer isso. Então eu vou correr para lançar os contêineres. docker-compose build docker-compose up 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 função, que inicia um servidor debugpy que escuta conexões de um depurador no número de porta especificado (neste caso, 5678). listen Se eu quisesse depurar o , eu poderia colocar o snippet acima logo antes do declaração de função dentro do arquivo - da seguinte forma: authors_service get_author_by_id app.py app = flask.Flask(__name__) ... import os import flask import requests from faker import Faker 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 script é executado. app.py 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 . Então, dentro desta pasta, criarei um arquivo chamado , com o seguinte conteúdo: .vscode launch.json { } } ] } { "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 função acima. debugpy.listen 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 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. debugpy.listen Isso satisfaz o requisito de "Depuração" acima. Desenvolvimento remoto e depuração com Jetbrains Pycharm IDE De acordo com os , há duas maneiras de fazer isso ao usar o PyCharm: um interpretador pode ser recuperado de uma imagem do Docker usando o recurso e/ou uma . 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. documentos oficiais de intérprete remoto configuração de servidor de depuração remota 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 e, em seguida, selecione no menu pop-up. Add new interpreter On docker compose... 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 ). Uma vez selecionado o intérprete, clique em "Criar". /usr/local/bin/python 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 arquivo(s): e . 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 arquivos: requirements.txt pydevd pydevd_pycharm requirements.txt 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 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). no próprio contêiner, A etapa final é definir uma configuração de execução que o PyCharm possa usar para se conectar ao servidor de depuração remoto. Para fazer isso, eu vou: 1. Abra a caixa de diálogo Configuração de execução/depuração selecionando > no menu principal. 2. Clique no botão no canto superior esquerdo da caixa de diálogo e selecione no menu suspenso. 3. No campo , insira um nome para a configuração de execução. 4. No campo , especifique o caminho para o script que desejo depurar. 5. No campo , insira o endereço IP da máquina host onde o servidor do depurador será executado. Neste exemplo, é "localhost". Executar Editar configurações + Python Remote Debug Nome Caminho do script Host 6. No campo , insira o número da porta na qual o servidor do depurador atenderá. Neste exemplo, é 5678. Porta 7. Na seção , 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 na máquina host, para no contêiner para depuração de author_service ou para no contêiner para depurar posts_service (precisariam ser duas configurações de execução separadas). Mapeamentos de caminho path/to/project/on/host/authors_service /app path/to/project/on/host/posts_service /app 8. Clique em para salvar a configuração de execução. Para iniciar a depuração, selecionarei a configuração de execução acima no menu suspenso , clicarei no botão e, em seguida, ativarei o(s) contêiner(es) relevante(s) com o comando. O depurador PyCharm será anexado ao script e pausará a execução na linha onde o função é chamada, permitindo-me começar a destruir esses bugs. OK Executar Depurar docker-compose up pydevd_pycharm.settrace Resumindo 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!