Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: SebastiĂĄn RamĂrez <tiangolo@gmail.com>
37 KiB
FastAPI em contĂȘineres - Docker
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 đ
FROM python:3.9
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --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"]
O que Ă© um ContĂȘiner
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.
O que Ă© uma Imagem de ContĂȘiner
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.
Imagens de contĂȘiner
Docker tem sido uma das principais ferramentas para criar e gerenciar imagens de contĂȘiner e contĂȘineres.
E existe um Docker Hub pĂșblico com imagens de contĂȘiner oficiais prĂ©-prontas para diversas ferramentas, ambientes, bancos de dados e aplicaçÔes.
Por exemplo, hĂĄ uma Imagem Python oficial.
E existe muitas outras imagens para diferentes coisas, como bancos de dados, por exemplo:
- PostgreSQL
- MySQL
- MongoDB
- Redis, etc.
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.
ContĂȘineres e Processos
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.
Construindo uma Imagem Docker para FastAPI
Okay, vamos construir algo agora! đ
Eu vou mostrar como construir uma imagem Docker para FastAPI do zero, baseado na imagem oficial do Python.
Isso Ă© o que vocĂȘ quer fazer na maioria dos casos, por exemplo:
- Usando Kubernetes ou ferramentas similares
- Quando rodando em uma Raspberry Pi
- Usando um serviço em nuvem que irĂĄ rodar uma imagem de contĂȘiner para vocĂȘ, etc.
O Pacote Requirements
VocĂȘ normalmente teria os requisitos do pacote para sua aplicação em algum arquivo.
Isso pode depender principalmente da ferramenta que vocĂȘ usa para instalar esses requisitos.
O caminho mais comum de fazer isso é ter um arquivo requirements.txt com os nomes dos pacotes e suas versÔes, um por linha.
VocĂȘ, naturalmente, usaria as mesmas ideias que vocĂȘ leu em Sobre VersĂ”es do FastAPI{.internal-link target=_blank} para definir os intervalos de versĂ”es.
Por exemplo, seu requirements.txt poderia parecer com:
fastapi>=0.68.0,<0.69.0
pydantic>=1.8.0,<2.0.0
uvicorn>=0.15.0,<0.16.0
E vocĂȘ normalmente instalaria essas dependĂȘncias de pacote com pip, por exemplo:
$ pip install -r requirements.txt
---> 100%
Successfully installed fastapi pydantic uvicorn
!!! info HĂĄ outros formatos e ferramentas para definir e instalar dependĂȘncias de pacote.
Eu vou mostrar um exemplo depois usando Poetry em uma seção abaixo. đ
Criando o CĂłdigo do FastAPI
- Crie um diretĂłrio
appe entre nele. - Crie um arquivo vazio
__init__.py. - Crie um arquivo
main.pycom:
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}
Dockerfile
Agora, no mesmo diretĂłrio do projeto, crie um arquivo Dockerfile com:
# (1)
FROM python:3.9
# (2)
WORKDIR /code
# (3)
COPY ./requirements.txt /code/requirements.txt
# (4)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# (5)
COPY ./app /code/app
# (6)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
-
Inicie a partir da imagem base oficial do Python.
-
Defina o diretĂłrio de trabalho atual para
/code.Esse Ă© o diretĂłrio onde colocaremos o arquivo
requirements.txte o diretĂłrioapp. -
Copie o arquivo com os requisitos para o diretĂłrio
/code.Copie somente o arquivo com os requisitos primeiro, nĂŁo o resto do cĂłdigo.
Como esse arquivo nĂŁo muda com frequĂȘncia, o Docker irĂĄ detectĂĄ-lo e usar o cache para esse passo, habilitando o cache para o prĂłximo passo tambĂ©m.
-
Instale as dependĂȘncias de pacote vindas do arquivo de requisitos.
A opção
--no-cache-dirdiz aopippara nĂŁo salvar os pacotes baixados localmente, pois isso sĂł aconteceria sepipfosse executado novamente para instalar os mesmos pacotes, mas esse nĂŁo Ă© o caso quando trabalhamos com contĂȘineres.!!! note
--no-cache-dirĂ© apenas relacionado aopip, nĂŁo tem nada a ver com Docker ou contĂȘineres.A opção
--upgradediz aopippara atualizar os pacotes se eles jĂĄ estiverem instalados.Por causa do passo anterior de copiar o arquivo, ele pode ser detectado pelo cache do Docker, esse passo tambĂ©m usarĂĄ o cache do Docker quando disponĂvel.
Usando o cache nesse passo irĂĄ salvar muito tempo quando vocĂȘ for construir a imagem repetidas vezes durante o desenvolvimento, ao invĂ©s de baixar e instalar todas as dependĂȘncias toda vez.
-
Copie o diretĂłrio
./appdentro do diretĂłrio/code.Como isso tem todo o cĂłdigo contendo o que muda com mais frequĂȘncia, o cache do Docker nĂŁo serĂĄ usado para esse passo ou para qualquer passo seguinte facilmente.
EntĂŁo, Ă© importante colocar isso perto do final do
Dockerfile, para otimizar o tempo de construção da imagem do contĂȘiner. -
Defina o comando para rodar o servidor
uvicorn.CMDrecebe uma lista de strings, cada uma dessas strings Ă© o que vocĂȘ digitaria na linha de comando separado por espaços.Esse comando serĂĄ executado a partir do diretĂłrio de trabalho atual, o mesmo diretĂłrio
/codeque vocĂȘ definiu acima comWORKDIR /code.Porque o programa serĂĄ iniciado em
/codee dentro dele estĂĄ o diretĂłrio./appcom seu cĂłdigo, o Uvicorn serĂĄ capaz de ver e importarappdeapp.main.
!!! tip Revise o que cada linha faz clicando em cada bolha com o nĂșmero no cĂłdigo. đ
Agora vocĂȘ deve ter uma estrutura de diretĂłrio como:
.
âââ app
â  âââ __init__.py
â âââ main.py
âââ Dockerfile
âââ requirements.txt
Por Trås de um Proxy de Terminação TLS
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.
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
Cache Docker
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:
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
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.
COPY ./app /code/app
Construindo a Imagem Docker
Agora que todos os arquivos estĂŁo no lugar, vamos construir a imagem do contĂȘiner.
- VĂĄ para o diretĂłrio do projeto (onde estĂĄ o seu
Dockerfile, contendo o diretĂłrioapp). - Construa sua imagem FastAPI:
$ docker build -t myimage .
---> 100%
!!! tip
Note o . no final, Ă© equivalente a ./, ele diz ao Docker o diretĂłrio a ser usado para construir a imagem do contĂȘiner.
Nesse caso, Ă© o mesmo diretĂłrio atual (`.`).
Inicie o contĂȘiner Docker
- Execute um contĂȘiner baseado na sua imagem:
$ docker run -d --name mycontĂȘiner -p 80:80 myimage
Verifique
VocĂȘ deve ser capaz de verificar isso no URL do seu contĂȘiner Docker, por exemplo: http://192.168.99.100/items/5?q=somequery ou http://127.0.0.1/items/5?q=somequery (ou equivalente, usando seu host Docker).
VocĂȘ verĂĄ algo como:
{"item_id": 5, "q": "somequery"}
Documentação interativa da API
Agora vocĂȘ pode ir para http://192.168.99.100/docs ou http://127.0.0.1/docs (ou equivalente, usando seu host Docker).
VocĂȘ verĂĄ a documentação interativa automĂĄtica da API (fornecida pelo Swagger UI):
Documentação alternativa da API
E vocĂȘ tambĂ©m pode ir para http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou equivalente, usando seu host Docker).
VocĂȘ verĂĄ a documentação alternativa automĂĄtica (fornecida pela ReDoc):
Construindo uma Imagem Docker com um Arquivo Ănico FastAPI
Se seu FastAPI for um Ășnico arquivo, por exemplo, main.py sem um diretĂłrio ./app, sua estrutura de arquivos poderia ser assim:
.
âââ Dockerfile
âââ main.py
âââ requirements.txt
EntĂŁo vocĂȘ sĂł teria que alterar os caminhos correspondentes para copiar o arquivo dentro do Dockerfile:
FROM python:3.9
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# (1)
COPY ./main.py /code/
# (2)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
-
Copie o arquivo
main.pypara o diretĂłrio/codediretamente (sem nenhum diretĂłrio./app). -
Execute o Uvicorn e diga a ele para importar o objeto
appdemain(em vez de importar deapp.main).
EntĂŁo ajuste o comando Uvicorn para usar o novo mĂłdulo main em vez de app.main para importar o objeto FastAPI app.
Conceitos de Implantação
Vamos falar novamente sobre alguns dos mesmos Conceitos de Implantação{.internal-link target=_blank} 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:
- HTTPS
- Executando na inicialização
- ReinicializaçÔes
- Replicação (nĂșmero de processos rodando)
- MemĂłria
- Passos anteriores antes de começar
HTTPS
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).
Executando na inicialização e reinicializaçÔes
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. âš
Replicação - NĂșmero de Processos
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.
Balanceamento de Carga
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.
Um Processo por ContĂȘiner
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:
Um Aplicativo Simples
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.
Docker Compose
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.
Prometheus and Outros Motivos
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:
- Segurança - HTTPS
- Executando na inicialização
- ReinicializaçÔes
- Replicação (o nĂșmero de processos em execução)
- MemĂłria
- Passos anteriores antes de inicializar
MemĂłria
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.
ContĂȘineres MĂșltiplos
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.
ContĂȘiner Ănico
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.
Imagem Oficial do Docker com Gunicorn - Uvicorn
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{.internal-link target=_blank}.
Essa imagem seria Ăștil principalmente nas situaçÔes descritas acima em: ContĂȘineres com MĂșltiplos Processos e Casos Especiais.
!!! warning Existe uma grande chance de que vocĂȘ nĂŁo precise dessa imagem base ou de qualquer outra semelhante, e seria melhor construir a imagem do zero, como descrito acima em: Construa uma Imagem Docker para o FastAPI.
Essa imagem tem um mecanismo de auto-ajuste incluĂdo para definir o nĂșmero de processos trabalhadores com base nos nĂșcleos de CPU disponĂveis.
Isso tem padrĂ”es sensĂveis, mas vocĂȘ ainda pode alterar e atualizar todas as configuraçÔes com variĂĄveis de ambiente ou arquivos de configuração.
Hå também suporte para executar passos anteriores antes de iniciar com um script.
!!! tip Para ver todas as configuraçÔes e opçÔes, vå para a pågina da imagem Docker: tiangolo/uvicorn-gunicorn-fastapi.
NĂșmero de Processos na Imagem Oficial do Docker
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). đš
Criando um Dockerfile
Aqui estĂĄ como vocĂȘ criaria um Dockerfile baseado nessa imagem:
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
COPY ./app /app
AplicaçÔes Maiores
Se vocĂȘ seguiu a seção sobre a criação de AplicaçÔes Maiores com MĂșltiplos Arquivos{.internal-link target=_blank}, seu Dockerfile pode parecer com isso:
```Dockerfile hl_lines="7"
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
COPY ./requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt
COPY ./app /app/app
Quando Usar
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.
Deploy da Imagem do ContĂȘiner
Depois de ter uma imagem de contĂȘiner (Docker), existem vĂĄrias maneiras de implantĂĄ-la.
Por exemplo:
- Com Docker Compose em um Ășnico servidor
- Com um cluster Kubernetes
- Com um cluster Docker Swarm Mode
- Com outra ferramenta como o Nomad
- Com um serviço de nuvem que pega sua imagem de contĂȘiner e a implanta
Imagem Docker com Poetry
Se vocĂȘ usa Poetry para gerenciar as dependĂȘncias do seu projeto, pode usar a construção multi-estĂĄgio do Docker:
# (1)
FROM python:3.9 as requirements-stage
# (2)
WORKDIR /tmp
# (3)
RUN pip install poetry
# (4)
COPY ./pyproject.toml ./poetry.lock* /tmp/
# (5)
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
# (6)
FROM python:3.9
# (7)
WORKDIR /code
# (8)
COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt
# (9)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
# (10)
COPY ./app /code/app
# (11)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
-
Esse Ă© o primeiro estĂĄgio, ele Ă© chamado
requirements-stage. -
Defina
/tmpcomo o diretĂłrio de trabalho atual.Aqui Ă© onde geraremos o arquivo
requirements.txt -
Instale o Poetry nesse estĂĄgio do Docker.
-
Copie os arquivos
pyproject.tomlepoetry.lockpara o diretĂłrio/tmp.Porque estĂĄ usando
./poetry.lock*(terminando com um*), nĂŁo irĂĄ falhar se esse arquivo ainda nĂŁo estiver disponĂvel. -
Gere o arquivo
requirements.txt. -
Este Ă© o estĂĄgio final, tudo aqui serĂĄ preservado na imagem final do contĂȘiner.
-
Defina o diretĂłrio de trabalho atual como
/code. -
Copie o arquivo
requirements.txtpara o diretĂłrio/code.Essse arquivo sĂł existe no estĂĄgio anterior do Docker, Ă© por isso que usamos
--from-requirements-stagepara copiĂĄ-lo. -
Instale as dependĂȘncias de pacote do arquivo
requirements.txtgerado. -
Copie o diretĂłrio
apppara o diretĂłrio/code. -
Execute o comando
uvicorn, informando-o para usar o objetoappimportado deapp.main.
!!! tip 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.
Por trås de um proxy de terminação TLS - Poetry
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:
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
Recapitulando
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. đ€

