Ao fazer o deploy de aplicações FastAPI uma abordagem comum é construir uma imagem de contêiner Linux. Isso normalmente é feito usando o Docker. Você pode a partir disso fazer o deploy dessa imagem de algumas maneiras.
Usando contêineres Linux você tem diversas vantagens incluindo segurança, replicabilidade, simplicidade, entre outras.
Dica
Está com pressa e já sabe dessas coisas? Pode ir direto para Dockerfile abaixo 👇.
Visualização do Dockerfile 👀
FROMpython:3.9WORKDIR/codeCOPY./requirements.txt/code/requirements.txt
RUNpipinstall--no-cache-dir--upgrade-r/code/requirements.txt
COPY./app/code/app
CMD["uvicorn","app.main:app","--host","0.0.0.0","--port","80"]# If running behind a proxy like Nginx or Traefik add --proxy-headers# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"]
Contêineres (especificamente contêineres Linux) são um jeito muito leve de empacotar aplicações contendo todas as dependências e arquivos necessários enquanto os mantém isolados de outros contêineres (outras aplicações ou componentes) no mesmo sistema.
Contêineres Linux rodam usando o mesmo kernel Linux do hospedeiro (máquina, máquina virtual, servidor na nuvem, etc). Isso simplesmente significa que eles são muito leves (comparados com máquinas virtuais emulando um sistema operacional completo).
Dessa forma, contêineres consomem poucos recursos, uma quantidade comparável com rodar os processos diretamente (uma máquina virtual consumiria muito mais).
Contêineres também possuem seus próprios processos (comumente um único processo), sistema de arquivos e rede isolados simplificando deploy, segurança, desenvolvimento, etc.
Um contêiner roda a partir de uma imagem de contêiner.
Uma imagem de contêiner é uma versão estática de todos os arquivos, variáveis de ambiente e do comando/programa padrão que deve estar presente num contêiner. Estática aqui significa que a imagem de contêiner não está rodando, não está sendo executada, somente contém os arquivos e metadados empacotados.
Em contraste com a "imagem de contêiner" que contém os conteúdos estáticos armazenados, um "contêiner" normalmente se refere à instância rodando, a coisa que está sendo executada.
Quando o contêiner é iniciado e está rodando (iniciado a partir de uma imagem de contêiner), ele pode criar ou modificar arquivos, variáveis de ambiente, etc. Essas mudanças vão existir somente nesse contêiner, mas não persistirão na imagem subjacente do container (não serão salvas no disco).
Uma imagem de contêiner é comparável ao arquivo de programa e seus conteúdos, ex.: python e algum arquivo main.py.
E o contêiner em si (em contraste à imagem de contêiner) é a própria instância da imagem rodando, comparável a um processo. Na verdade, um contêiner está rodando somente quando há um processo rodando (e normalmente é somente um processo). O contêiner finaliza quando não há um processo rodando nele.
Usando imagens de contêiner pré-prontas é muito fácil combinar e usar diferentes ferramentas. Por exemplo, para testar um novo banco de dados. Em muitos casos, você pode usar as imagens oficiais precisando somente de variáveis de ambiente para configurá-las.
Dessa forma, em muitos casos você pode aprender sobre contêineres e Docker e re-usar essa experiência com diversos componentes e ferramentas.
Então, você rodaria vários contêineres com coisas diferentes, como um banco de dados, uma aplicação Python, um servidor web com uma aplicação frontend React, e conectá-los juntos via sua rede interna.
Todos os sistemas de gerenciamento de contêineres (como Docker ou Kubernetes) possuem essas funcionalidades de rede integradas a eles.
Uma imagem de contêiner normalmente inclui em seus metadados o programa padrão ou comando que deve ser executado quando o contêiner é iniciado e os parâmetros a serem passados para esse programa. Muito similar ao que seria se estivesse na linha de comando.
Quando um contêiner é iniciado, ele irá rodar esse comando/programa (embora você possa sobrescrevê-lo e fazer com que ele rode um comando/programa diferente).
Um contêiner está rodando enquanto o processo principal (comando ou programa) estiver rodando.
Um contêiner normalmente tem um único processo, mas também é possível iniciar sub-processos a partir do processo principal, e dessa forma você terá vários processos no mesmo contêiner.
Mas não é possível ter um contêiner rodando sem pelo menos um processo rodando. Se o processo principal parar, o contêiner também para.
Se você está executando seu contêiner atrás de um Proxy de Terminação TLS (load balancer) como Nginx ou Traefik, adicione a opção --proxy-headers, isso fará com que o Uvicorn confie nos cabeçalhos enviados por esse proxy, informando que o aplicativo está sendo executado atrás do HTTPS, etc.
Existe um truque importante nesse Dockerfile, primeiro copiamos o arquivo com as dependências sozinho, não o resto do código. Deixe-me te contar o porquê disso.
COPY./requirements.txt/code/requirements.txt
Docker e outras ferramentas constróem essas imagens de contêiner incrementalmente, adicionando uma camada em cima da outra, começando do topo do Dockerfile e adicionando qualquer arquivo criado por cada uma das instruções do Dockerfile.
Docker e ferramentas similares também usam um cache interno ao construir a imagem, se um arquivo não mudou desde a última vez que a imagem do contêiner foi construída, então ele irá reutilizar a mesma camada criada na última vez, ao invés de copiar o arquivo novamente e criar uma nova camada do zero.
Somente evitar a cópia de arquivos não melhora muito as coisas, mas porque ele usou o cache para esse passo, ele pode usar o cache para o próximo passo. Por exemplo, ele pode usar o cache para a instrução que instala as dependências com:
O arquivo com os requisitos de pacote não muda com frequência. Então, ao copiar apenas esse arquivo, o Docker será capaz de usar o cache para esse passo.
E então, o Docker será capaz de usar o cache para o próximo passo que baixa e instala essas dependências. E é aqui que salvamos muito tempo. ✨ ...e evitamos tédio esperando. 😪😆
Baixar e instalar as dependências do pacote pode levar minutos, mas usando o cache leva segundos no máximo.
E como você estaria construindo a imagem do contêiner novamente e novamente durante o desenvolvimento para verificar se suas alterações de código estão funcionando, há muito tempo acumulado que isso economizaria.
A partir daí, perto do final do Dockerfile, copiamos todo o código. Como isso é o que muda com mais frequência, colocamos perto do final, porque quase sempre, qualquer coisa depois desse passo não será capaz de usar o cache.
Vamos falar novamente sobre alguns dos mesmos Conceitos de Implantação em termos de contêineres.
Contêineres são principalmente uma ferramenta para simplificar o processo de construção e implantação de um aplicativo, mas eles não impõem uma abordagem particular para lidar com esses conceitos de implantação e existem várias estratégias possíveis.
A boa notícia é que com cada estratégia diferente há uma maneira de cobrir todos os conceitos de implantação. 🎉
Vamos revisar esses conceitos de implantação em termos de contêineres:
Se nos concentrarmos apenas na imagem do contêiner para um aplicativo FastAPI (e posteriormente no contêiner em execução), o HTTPS normalmente seria tratado externamente por outra ferramenta.
Isso poderia ser outro contêiner, por exemplo, com Traefik, lidando com HTTPS e aquisição automática de certificados.
Tip
Traefik tem integrações com Docker, Kubernetes e outros, portanto, é muito fácil configurar e configurar o HTTPS para seus contêineres com ele.
Alternativamente, o HTTPS poderia ser tratado por um provedor de nuvem como um de seus serviços (enquanto ainda executasse o aplicativo em um contêiner).
Normalmente, outra ferramenta é responsável por iniciar e executar seu contêiner.
Ela poderia ser o Docker diretamente, Docker Compose, Kubernetes, um serviço de nuvem, etc.
Na maioria (ou em todos) os casos, há uma opção simples para habilitar a execução do contêiner na inicialização e habilitar reinicializações em falhas. Por exemplo, no Docker, é a opção de linha de comando --restart.
Sem usar contêineres, fazer aplicativos executarem na inicialização e com reinicializações pode ser trabalhoso e difícil. Mas quando trabalhando com contêineres em muitos casos essa funcionalidade é incluída por padrão. ✨
Se você tiver um cluster de máquinas com Kubernetes, Docker Swarm Mode, Nomad ou outro sistema complexo semelhante para gerenciar contêineres distribuídos em várias máquinas, então provavelmente desejará lidar com a replicação no nível do cluster em vez de usar um gerenciador de processos (como o Gunicorn com workers) em cada contêiner.
Um desses sistemas de gerenciamento de contêineres distribuídos como o Kubernetes normalmente tem alguma maneira integrada de lidar com a replicação de contêineres enquanto ainda oferece balanceamento de carga para as solicitações recebidas. Tudo no nível do cluster.
Nesses casos, você provavelmente desejará criar uma imagem do contêiner do zero como explicado acima, instalando suas dependências e executando um único processo Uvicorn em vez de executar algo como Gunicorn com trabalhadores Uvicorn.
Quando usando contêineres, normalmente você terá algum componente escutando na porta principal. Poderia ser outro contêiner que também é um Proxy de Terminação TLS para lidar com HTTPS ou alguma ferramenta semelhante.
Como esse componente assumiria a carga de solicitações e distribuiria isso entre os trabalhadores de uma maneira (esperançosamente) balanceada, ele também é comumente chamado de Balanceador de Carga.
Tip
O mesmo componente Proxy de Terminação TLS usado para HTTPS provavelmente também seria um Balanceador de Carga.
E quando trabalhar com contêineres, o mesmo sistema que você usa para iniciar e gerenciá-los já terá ferramentas internas para transmitir a comunicação de rede (por exemplo, solicitações HTTP) do balanceador de carga (que também pode ser um Proxy de Terminação TLS) para o(s) contêiner(es) com seu aplicativo.
Um Balanceador de Carga - Múltiplos Contêineres de Workers¶
Quando trabalhando com Kubernetes ou sistemas similares de gerenciamento de contêiner distribuído, usando seus mecanismos de rede internos permitiria que o único balanceador de carga que estivesse escutando na porta principal transmitisse comunicação (solicitações) para possivelmente múltiplos contêineres executando seu aplicativo.
Cada um desses contêineres executando seu aplicativo normalmente teria apenas um processo (ex.: um processo Uvicorn executando seu aplicativo FastAPI). Todos seriam contêineres idênticos, executando a mesma coisa, mas cada um com seu próprio processo, memória, etc. Dessa forma, você aproveitaria a paralelização em núcleos diferentes da CPU, ou até mesmo em máquinas diferentes.
E o sistema de contêiner com o balanceador de carga iria distribuir as solicitações para cada um dos contêineres com seu aplicativo em turnos. Portanto, cada solicitação poderia ser tratada por um dos múltiplos contêineres replicados executando seu aplicativo.
E normalmente esse balanceador de carga seria capaz de lidar com solicitações que vão para outros aplicativos em seu cluster (por exemplo, para um domínio diferente, ou sob um prefixo de URL diferente), e transmitiria essa comunicação para os contêineres certos para esse outro aplicativo em execução em seu cluster.
Nesse tipo de cenário, provavelmente você desejará ter um único processo (Uvicorn) por contêiner, pois já estaria lidando com a replicação no nível do cluster.
Então, nesse caso, você não desejará ter um gerenciador de processos como o Gunicorn com trabalhadores Uvicorn, ou o Uvicorn usando seus próprios trabalhadores Uvicorn. Você desejará ter apenas um único processo Uvicorn por contêiner (mas provavelmente vários contêineres).
Tendo outro gerenciador de processos dentro do contêiner (como seria com o Gunicorn ou o Uvicorn gerenciando trabalhadores Uvicorn) só adicionaria complexidade desnecessária que você provavelmente já está cuidando com seu sistema de cluster.
Contêineres com Múltiplos Processos e Casos Especiais¶
Claro, existem casos especiais em que você pode querer ter um contêiner com um gerenciador de processos Gunicorn iniciando vários processos trabalhadores Uvicorn dentro.
Nesses casos, você pode usar a imagem oficial do Docker que inclui o Gunicorn como um gerenciador de processos executando vários processos trabalhadores Uvicorn, e algumas configurações padrão para ajustar o número de trabalhadores com base nos atuais núcleos da CPU automaticamente. Eu vou te contar mais sobre isso abaixo em Imagem Oficial do Docker com Gunicorn - Uvicorn.
Aqui estão alguns exemplos de quando isso pode fazer sentido:
Você pode querer um gerenciador de processos no contêiner se seu aplicativo for simples o suficiente para que você não precise (pelo menos não agora) ajustar muito o número de processos, e você pode simplesmente usar um padrão automatizado (com a imagem oficial do Docker), e você está executando em um único servidor, não em um cluster.
Você pode estar implantando em um único servidor (não em um cluster) com o Docker Compose, então você não teria uma maneira fácil de gerenciar a replicação de contêineres (com o Docker Compose) enquanto preserva a rede compartilhada e o balanceamento de carga.
Então você pode querer ter um único contêiner com um gerenciador de processos iniciando vários processos trabalhadores dentro.
Você também pode ter outros motivos que tornariam mais fácil ter um único contêiner com múltiplos processos em vez de ter múltiplos contêineres com um único processo em cada um deles.
Por exemplo (dependendo de sua configuração), você poderia ter alguma ferramenta como um exportador do Prometheus no mesmo contêiner que deve ter acesso a cada uma das solicitações que chegam.
Nesse caso, se você tivesse múltiplos contêineres, por padrão, quando o Prometheus fosse ler as métricas, ele receberia as métricas de um único contêiner cada vez (para o contêiner que tratou essa solicitação específica), em vez de receber as métricas acumuladas de todos os contêineres replicados.
Então, nesse caso, poderia ser mais simples ter um único contêiner com múltiplos processos, e uma ferramenta local (por exemplo, um exportador do Prometheus) no mesmo contêiner coletando métricas do Prometheus para todos os processos internos e expor essas métricas no único contêiner.
O ponto principal é que nenhum desses são regras escritas em pedra que você deve seguir cegamente. Você pode usar essas idéias para avaliar seu próprio caso de uso e decidir qual é a melhor abordagem para seu sistema, verificando como gerenciar os conceitos de:
Se você executar um único processo por contêiner, terá uma quantidade mais ou menos bem definida, estável e limitada de memória consumida por cada um desses contêineres (mais de um se eles forem replicados).
E então você pode definir esses mesmos limites e requisitos de memória em suas configurações para seu sistema de gerenciamento de contêineres (por exemplo, no Kubernetes). Dessa forma, ele poderá replicar os contêineres nas máquinas disponíveis levando em consideração a quantidade de memória necessária por eles e a quantidade disponível nas máquinas no cluster.
Se sua aplicação for simples, isso provavelmente não será um problema, e você pode não precisar especificar limites de memória rígidos. Mas se você estiver usando muita memória (por exemplo, com modelos de aprendizado de máquina), deve verificar quanta memória está consumindo e ajustar o número de contêineres que executa em cada máquina (e talvez adicionar mais máquinas ao seu cluster).
Se você executar múltiplos processos por contêiner (por exemplo, com a imagem oficial do Docker), deve garantir que o número de processos iniciados não consuma mais memória do que o disponível.
Passos anteriores antes de inicializar e contêineres¶
Se você estiver usando contêineres (por exemplo, Docker, Kubernetes), existem duas abordagens principais que você pode usar.
Se você tiver múltiplos contêineres, provavelmente cada um executando um único processo (por exemplo, em um cluster do Kubernetes), então provavelmente você gostaria de ter um contêiner separado fazendo o trabalho dos passos anteriores em um único contêiner, executando um único processo, antes de executar os contêineres trabalhadores replicados.
Info
Se você estiver usando o Kubernetes, provavelmente será um Init Container.
Se no seu caso de uso não houver problema em executar esses passos anteriores em paralelo várias vezes (por exemplo, se você não estiver executando migrações de banco de dados, mas apenas verificando se o banco de dados está pronto), então você também pode colocá-los em cada contêiner logo antes de iniciar o processo principal.
Se você tiver uma configuração simples, com um único contêiner que então inicia vários processos trabalhadores (ou também apenas um processo), então poderia executar esses passos anteriores no mesmo contêiner, logo antes de iniciar o processo com o aplicativo. A imagem oficial do Docker suporta isso internamente.
Há uma imagem oficial do Docker que inclui o Gunicorn executando com trabalhadores Uvicorn, conforme detalhado em um capítulo anterior: Server Workers - Gunicorn com Uvicorn.
O número de processos nesta imagem é calculado automaticamente a partir dos núcleos de CPU disponíveis.
Isso significa que ele tentará aproveitar o máximo de desempenho da CPU possível.
Você também pode ajustá-lo com as configurações usando variáveis de ambiente, etc.
Mas isso também significa que, como o número de processos depende da CPU do contêiner em execução, a quantidade de memória consumida também dependerá disso.
Então, se seu aplicativo consumir muito memória (por exemplo, com modelos de aprendizado de máquina), e seu servidor tiver muitos núcleos de CPU mas pouca memória, então seu contêiner pode acabar tentando usar mais memória do que está disponível e degradar o desempenho muito (ou até mesmo travar). 🚨
Você provavelmente não deve usar essa imagem base oficial (ou qualquer outra semelhante) se estiver usando Kubernetes (ou outros) e já estiver definindo replicação no nível do cluster, com vários contêineres. Nesses casos, é melhor construir uma imagem do zero conforme descrito acima: Construindo uma Imagem Docker para FastAPI.
Essa imagem seria útil principalmente nos casos especiais descritos acima em Contêineres com Múltiplos Processos e Casos Especiais. Por exemplo, se sua aplicação for simples o suficiente para que a configuração padrão de número de processos com base na CPU funcione bem, você não quer se preocupar com a configuração manual da replicação no nível do cluster e não está executando mais de um contêiner com seu aplicativo. Ou se você estiver implantando com Docker Compose, executando em um único servidor, etc.
Clique nos números das bolhas para ver o que cada linha faz.
Um estágio do Docker é uma parte de um Dockerfile que funciona como uma imagem temporária do contêiner que só é usada para gerar alguns arquivos para serem usados posteriormente.
O primeiro estágio será usado apenas para instalar Poetry e para gerar o requirements.txt com as dependências do seu projeto a partir do arquivo pyproject.toml do Poetry.
Esse arquivo requirements.txt será usado com pip mais tarde no próximo estágio.
Na imagem final do contêiner, somente o estágio final é preservado. Os estágios anteriores serão descartados.
Quando usar Poetry, faz sentido usar construções multi-estágio do Docker porque você realmente não precisa ter o Poetry e suas dependências instaladas na imagem final do contêiner, você apenas precisa ter o arquivo requirements.txt gerado para instalar as dependências do seu projeto.
Então, no próximo (e último) estágio, você construiria a imagem mais ou menos da mesma maneira descrita anteriormente.
Novamente, se você estiver executando seu contêiner atrás de um proxy de terminação TLS (balanceador de carga) como Nginx ou Traefik, adicione a opção --proxy-headers ao comando:
Usando sistemas de contêiner (por exemplo, com Docker e Kubernetes), torna-se bastante simples lidar com todos os conceitos de implantação:
HTTPS
Executando na inicialização
Reinícios
Replicação (o número de processos rodando)
Memória
Passos anteriores antes de inicializar
Na maioria dos casos, você provavelmente não desejará usar nenhuma imagem base e, em vez disso, construir uma imagem de contêiner do zero baseada na imagem oficial do Docker Python.
Tendo cuidado com a ordem das instruções no Dockerfile e o cache do Docker, você pode minimizar os tempos de construção, para maximizar sua produtividade (e evitar a tédio). 😎
Em alguns casos especiais, você pode querer usar a imagem oficial do Docker para o FastAPI. 🤓