Empacotamento e Distribuição do Projeto FastAPI Mínimo
Este é o segundo da série de artigos sobre a construção do projeto FastAPI mínimo. Enquanto o artigo anterior tratava de estrutura de diretórios, ambiente virtual, linting de código, integração contínua, controle de versão e testes automatizados, este artigo trata da distribuição da aplicação na forma de contêineres Docker
.
Como resultado final, teremos a aplicação FastAPI
rodando em um contêiner Docker
, acessível através do Caddy
como proxy reverso e com os serviços orquestrados pelo Docker Compose
. Essa solução pode ser usada diretamente em produção ou como base para projetos mais complexos. Além disso, funciona da mesma forma em desenvolvimento e em produção.
Introdução
A aplicação está pronta, testada e funcionando no ambiente de desenvolvimento. Agora, precisamos colocá-la em produção mas fazer isso do zero requer várias etapas:
- Adquirir e configurar um domínio
- Configurar o provedor de hospedagem
- Instalar e configurar a aplicação
- Instalar e configurar o servidor web (proxy reverso)
- Monitorar e gerenciar a aplicação
As ações 1, 2 e 5 não estão diretamente relacionadas à aplicação e não vamos abordá-las neste artigo, mas 3 e 4 podem fazer parte do projeto se a aplicação e suas dependências forem distribuídas na forma de contêineres Docker.
O empacotamento e distribuição de uma aplicação em contêineres é uma prática comum em ambientes de produção. As principais razões para isso são que os contêineres são encapsulados e portáteis, podendo ser implantados em diferentes plataformas e infraestruturas.
Para o projeto FastAPI mínimo, vamos precisar de dois contêineres:
- Um contêiner para a aplicação
- Um contêiner para o servidor web (proxy reverso)
O proxy reverso é um servidor web que atua como intermediário entre os clientes e o servidor de aplicação. Ele recebe solicitações de clientes (geralmente na Internet) e encaminha essas solicitações para os servidores ou serviços de destino com base em regras de roteamento predefinidas.
Vamos usar o Caddy como proxy reverso por ser fácil de configurar e oferecer recursos avançados tal como suportar automaticamente HTTPS usando Let's Encrypt.
A orquestração será feita pelo Docker Compose, que não é a ferramenta mais poderosa para orquestração de contêineres existente, mas é uma ótima escolha para desenvolvimento e testes locais, e implantações em que a simplicidade e a facilidade de uso são prioritárias.
Orquestração dos Contêineres
Vamos começar pela orquestração porque teremos uma visão geral de como será a implantação da aplicação. O docker-compose.yml
para o projeto FastAPI mínimo é apresentada abaixo:
Caddy
Usaremos uma configuração inspirada no exemplo da documentação da imagem do Caddy no Docker Hub:
A imagem caddy:alpine
(linha 4) é a mais adequada para o projeto FastAPI mínimo porque não precisamos instalar nenhum módulo ou plugin adicional do Caddy, por enquanto.
A linha 6, init: true
, inicia um processo init que garante que o encerramento do contêiner será gerenciado corretamente quando o processo principal do aplicativo dentro do contêiner for encerrado.
As linhas 8 a 10 habilitam o Caddy a escutar as portas 80
e 443
para poder atender requisições HTTP
e HTTPS
.
A linha 12 mapeia Caddyfile
, que é o arquivo de configuração do Caddy
, para seu caminho esperado dentro do contêiner (/etc/caddy/Caddyfile
). Dessa forma, a configuração do Caddy
pode ser alterada sem a necessidade de reconstruir a imagem do contêiner.
Conforme recomendado na documentação, o diretório /data
deve ser persistido em um volume (linhas 13 e 32) para que os dados importantes tais como certificados TLS
não sejam perdidos quando o contêiner for reiniciado.
A variável de ambiente DOMAIN
(linha 15) define o domínio que será usado no Caddyfile
.
App
O serviço app
é o contêiner da aplicação FastAPI:
A linha 23 indica que a imagem do contêiner será construída a partir do diretório corrente, que deve conter o arquivo Dockerfile
.
A aplicação estará escutando na porta 5000
(linha 26).
A linha 27 indica que o contêiner será reiniciado automaticamente em caso de falha ou quando o Docker for reiniciado.
A linha 29 define a variável de ambiente ENV
com o valor production
se não tiver sido definida anteriormente.
As linhas 30 a 36 definem o comando que será executado quando o contêiner for iniciado. A diferença com o comando original é a inclusão da opção --root-path=/api
, para que seja possível acessar a documentação da API através do caminho /api/docs
ao invés de /docs
, já que a aplicação será acessada através do proxy reverso a partir de /api
, conforme será apresentado na seção seguinte, sobre configuração do Caddy.
Configuração do Caddy
O Caddy
é configurado através de um arquivo de configuração chamado Caddyfile
. Para o projeto FastAPI mínimo, temos a seguinte configuração:
|
{$DOMAIN:localhost} {
|
|
handle_path /api* {
|
|
reverse_proxy h2c://app:5000
|
|
}
|
|
handle {
|
|
redir https://{host}/api/docs
|
|
}
|
|
}
|
A linha 1 define o endereço do domínio. Se não houver uma variável de ambiente DOMAIN
definida, o domínio será localhost
.
Na linha 2, handle_path /api
captura todas as requisições que começam com /api
e exclui esse prefixo implicitamente. A diretiva reverse_proxy encaminha as requisições para o servidor de aplicação (linha 3), que está escutando na porta 5000
do serviço app
, através do protocolo h2c
.
O esquema h2c:// se refere ao protocolo HTTP/2 Cleartext
, que é uma versão do protocolo HTTP/2 sem criptografia. A vantagem do h2c
é que possui os benefícios do HTTP/2
mas sem a sobrecarga da encriptação do TLS
. Não é seguro para use externo, mas é adequado para uso interno porque o Caddy
e a aplicação estarão rodando no mesmo servidor. Em outras palavras, toda a comunicação será feita em HTTP/2
mas apenas a primeira etapa (entre o cliente e o proxy) será criptografada.
Por fim, vamos usar a página da documentação da API
como página inicial da aplicação: A diretiva handle (linha 5) trata todas as requisições que não forem capturadas anteriormente e redir as redirecionas para https://{host}/api/docs
(linha 6).
E aí está a página da documentação da API como página inicial do projeto:
Criação do Contêiner da Aplicação
A construção de um container envolve dois arquivos: Dockerfile
e .dockerignore
.
.dockerignore
O projeto FastAPI Mínimo é bastante simples. Além de pyproject.toml
e poetry.lock
, usados na instalação das dependências, os únicos arquivos realmente necessários para a construção da imagem Docker são:
fastapi-minimum ├── hypercorn.toml ├── pyproject.toml ├── poetry.lock └── fastapi-minimum ├── __init__.py ├── config.py ├── main.py ├── resources.py └── routers ├── __init__.py └── hello.py
Porém, o diretório de trabalho não é assim. Considerando arquivos e diretórios do ambiente virtual, controle de versão, cache, log, testes, etc. é provável que a estrutura do projeto seja:
fastapi-minimum ├── .coverage ├── Dockerfile ├── .dockerignore ├── .github │ └── workflows │ └── continuous_integration.yml ├── .hg │ └── ... ├── .hgignore ├── htmlcov │ └── ... ├── hypercorn.toml ├── Makefile ├── fastapi-minimum │ ├── config.py │ ├── __init__.py │ ├── main.py │ ├── __pycache__ │ │ ├── config.cpython-311.pyc │ │ ├── __init__.cpython-311.pyc │ │ ├── main.cpython-311.pyc │ │ └── resources.cpython-311.pyc │ ├── resources.py │ └── routers │ ├── hello.py │ ├── __init__.py │ └── __pycache__ │ ├── hello.cpython-311.pyc │ └── __init__.cpython-311.pyc ├── .mypy_cache │ └── ... ├── poetry.lock ├── pyproject.toml ├── .pytest_cache │ └── ... ├── README.rst ├── .ruff_cache │ └── ... ├── scripts │ └── install_hooks.sh ├── tests │ ├── conftest.py │ ├── __init__.py │ └── routers │ ├── __init__.py │ └── test_hello.py └── .venv ├── bin ├── .gitignore ├── lib └── pyvenv.cfg
Para garantir que apenas o necessário seja copiado na criação da imagem Docker, precisamos de um arquivo .dockerignore com a lista de padrões de nomes a serem ignorados.
Os padrões para o projeto FastAPI Mínimo são:
.coverage Dockerfile .dockerignore .git .github .hg .hgignore htmlcov Makefile .mypy_cache .pytest_cache .ruff_cache README.rst scripts tests .venv **/__pycache__
Uma outra forma de especificar é listando o que deve ser copiado. Para isso, ignore todos os arquivos e diretórios, exceto aqueles são necessários para a construção da imagem:
O padrão *
na linha 1 indica que todos os arquivos e diretórios devem ser ignorados. As linhas iniciadas com !
são exceções à regra. Portanto, as linhas 2 a 5 especificam os arquivos e diretórios que serão incluídos. O padrão **/__pycache__
deve ficar na última linha para ignorar todos os subdiretórios __pycache__
dos diretórios incluídos no projeto.
Dockerfile
O Dockerfile
que usaremos segue as recomendações presentes em 1, 2 e 3:
Multi-Estágios
A construção da imagem é feita em dois estágios: builder
e final
. O primeiro estágio é responsável pela construção e instalação das dependências em um ambiente virtual (linhas 1 a 18). O segundo estágio copia o ambiente virtual do estágio anterior, copia os demais arquivos do projeto a partir do computador hospedeiro e configura a aplicação para ser usada em produção (linhas 21 a 31).
A construção em multi-estágios faz com que a imagem final fique o menor possível porque as ferramentas e bibliotecas usadas na construção não são incluídas na imagem final.
Uso de Imagens Oficiais, Pequenas e Seguras
Os dois estágios são baseados na imagem oficial do Python 3.11-slim
, que é a melhor imagem para uma aplicação Python disponível no momento por ser segura e pequena.
Instalação de Dependências
|
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
|
apt-get install -y --no-install-recommends build-essential curl
|
A linha 3 instala as ferramentas e bibliotecas necessárias para a compilação e instalação das dependências do projeto: build-essential
contém compiladores e ferramentas de construção e curl
será usado na instalação do poetry
.
O comando apt-get update
atualiza o índice de pacotes, DEBIAN_FRONTEND=noninteractive
e a opção -y
evitam a necessidade de interação do usuário durante a instalação dos pacotes, e a opção --no-install-recommends
evita a instalação de pacotes desnecessários.
Dependendo das dependências da aplicação, será necessário incluir bibliotecas adicionais. Por exemplo, se a aplicação usasse Postgres
como banco de dados, seria necessário adicionar libpq-dev
à instalação para compilar o pacote pyscopg2-binary
.
Evitando a Criação de Arquivos .pyc
A existência da variável de ambiente PYTHONDONTWRITEBYTECODE
impede que o Python crie arquivos .pyc
(linha 6). Esses arquivos são específicos do ambiente em que foram criados porque contêm metadados tais como caminhos absolutos de módulos importados, timestamps e outras informações específicas do sistema. Importar arquivos .pyc
gerados em outro estágio do Dockerfile pode levar a problemas de execução se os metadados originais não forem compatíveis com o ambiente final.
De qualquer maneira, não há necessidade de manter arquivos .pyc
na imagem nem de transportá-los de um estágio para outro porque esses arquivos podem ser gerados novamente durante a primeira execução do código Python.
Forçando o Buffer de Saída do Python
Definir PYTHONUNBUFFERED=1
(linha 7) é uma boa prática para garantir que a saída do Python seja exibida imediatamente no log de construção da imagem, sem que nenhuma informação seja perdida ou atrasada em um buffer em caso de erros de compilação ou execução.
Criação do Ambiente Virtual
A linha 8 cria o ambiente virtual do Python em /venv
, onde serão instaladas as dependências do projeto. Este é o único diretório que será copiado para a imagem final.
Instalação do poetry
|
ENV POETRY_VERSION=1.5.1
|
|
ENV POETRY_HOME=/opt/poetry
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
RUN curl -sSL https://install.python-poetry.org | python -
|
O poetry
é a ferramenta de gerenciamento de dependências usado pelo projeto FastAPI Mínimo.
Tal como as dependências usadas no projeto, é importante fixar a versão do poetry
para garantir que as dependências sejam instaladas sempre da mesma forma. A versão do poetry
usada e testada no projeto é a 1.5.1
e é fixada através da variável de ambiente POETRY_VERSION
(linha 10).
A localização do diretório de instalação do poetry
está definida variável de ambiente POETRY_HOME
(linha 11), e a instalação do poetry propriamente acontece na linha 13. A linha 12 é uma sugestão do hadolint para detectar problemas durante a concatenação de comandos bash através do |
(pipe) (DL4006).
Instalação das Dependências do Projeto
|
WORKDIR /app
|
|
COPY pyproject.toml poetry.lock ./
|
|
RUN . /venv/bin/activate; \
|
|
$POETRY_HOME/bin/poetry install --only main --no-interaction
|
A instalação das dependências acontece entre as linhas 15 a 18. A diretiva WORKDIR
define o diretório corrente. Na linha 16, os arquivos pyproject.toml
e poetry.lock
são copiados para o diretório corrente. Em seguida, o ambiente virtual é ativado através do comando . /venv/bin/activate
(linha 17), que altera o PATH
e força a instalação das dependências no diretório /venv
.
Finalmente, o comando poetry install --only main --no-interaction
instala apenas as dependências do grupo principal e sem interação com o usuário durante a instalação (linha 18).
De todos os arquivos do projeto, apenas pyproject.toml
(lista de dependências do projeto) e poetry.lock
(lista das versões exatas necessárias) são usados no primeiro estágio porque essas listas de dependências mudam com pouca frequência, enquanto as partes do projeto que mais mudam são copiadas apenas no segundo estágio. Essa separação é importante porque o Docker usa um cache para evitar a reexecução de comandos que não foram alterados. Assim, em caso de reconstrução da imagem, o processo de instalação das dependências não será executado novamente a não ser que pyproject.toml
ou poetry.lock
tenham sido alterados, o que torna o processo de construção da imagem muito mais rápido.
Cópia e Configuração do Ambiente Virtual
No segundo estágio, o ambiente virtual é copiado para a imagem final (linha 23) e habilitado através da inclusão do seu caminho no começo da variável de ambiente PATH
(linha 24).
Diretório de Trabalho e Usuário Não-Privilegiado
Por uma questão de segurança, é recomendável que a aplicação rode com um usuário não-privilegiado ao invés de root
, que é o usuário padrão dentro de um contêiner Docker. O ideal é criar um usuário e um grupo específicos para cada aplicação de modo que um eventual ataque não tenha acesso a outros processos ou arquivos do sistema do mesmo usuário/grupo. Porém, como só haverá um processo rodando no container, podemos simplesmente usar o usuário noboby
e o grupo nogroup
, que estão disponíveis em distribuições Linux baseadas em Debian, tal qual o python:3.11-slim
usado neste projeto.
Assim, a linha 27 define nobody
como usuário padrão e as linhas 28 e 29 copiam os demais arquivos do projeto para o diretório de trabalho /app
já pertencendo a nobody:nogroup
.
Execução da Aplicação
A linha 31 contém o comando para iniciar a aplicação individualmente. No entanto, é sobrescrito na orquestração para que a aplicação seja acessada através do proxy reverso a partir de /api
, conforme visto anteriormente.
Assim, mantemos um Dockerfile
genérico que pode ser usado em diferentes ambientes, bastando apenas alterar o comando de execução da aplicação.
Configuração da Orquestração Para Desenvolvimento
Durante o desenvolvimento, é comum usar um servidor web que reinicie automaticamente a aplicação sempre que um arquivo do projeto for alterado. Para conseguir esse comportamento na solução baseada em contêineres, vamos precisar de uma configuração diferente para ser usada apenas durante o desenvolvimento. Essa configuração será mantida em um arquivo separado, de nome docker-compose.dev.yml
:
Há duas mudanças necessárias: alterar o CMD
do contêiner da aplicação para incluir a opção --reload
ao hypercorn
(linha 8) e mapear o diretório do projeto para que as alterações locais sejam acompanhadas de dentro do contêiner (linhas 11 e 12).
O modo de desenvolvimento da orquestração é ativado através do comando docker compose -f docker-compose.yml -f docker-compose.dev.yml up
, que mescla as configurações de docker-compose.yml
e docker-compose.dev.yml
.
Novas Tarefas no Makefile
Com a inclusão dos contêineres, do proxy reverso e do docker compose
no projeto FastAPI Mínimo, algumas tarefas do Makefile
precisam ser incluídas ou atualizadas:
A tarefa run
inicia os containers em modo de produção.
A tarefa run_dev
inicia os containers em modo de desenvolvimento.
smoke_test
executa um teste rápido para verificar se a aplicação está funcionando corretamente. Para isso, inicia os containers em modo de produção, espera 4 segundos para que a aplicação seja iniciada, e executa uma requisição HTTP/2
para o endpoint /api/hello
. Se a requisição for mal sucedida, a tarefa é encerrada com código de erro 1. Caso contrário, é exibida uma mensagem de sucesso e a tarefa é encerrada com código 0.
O comando trap 'docker compose down' EXIT
garante que os containers serão encerrados mesmo que a tarefa seja interrompida por um erro ou pelo usuário.
Para testar o endpoint foi usado o comando httpx
porque já é uma dependência do projeto FastAPI mínimo. Não é preciso instalar nenhuma outra ferramenta. O comando httpx
é executado com as opções --http2
para usar o protocolo HTTP/2
e --no-verify
para ignorar a verificação do certificado TLS. A opção -v
é usada para exibir o log da requisição.
As demais tarefas do Makefile
permanecem inalteradas.
Integração Contínua
A última alteração será a inclusão de um novo job
de nome Smoke Test
no final do arquivo .github/workflows/continuous_integration.yml
:
Considerações Finais
O projeto FastAPI mínimo continua com o mesmo objetivo de ser um ponto de partida para projetos mais complexos, mas agora teve a orquestração de contêineres incluída para facilitar o desenvolvimento e a implantação da aplicação.
É possível executar a aplicação em qualquer servidor que tenha o Docker instalado, sem a necessidade de instalar o Python ou qualquer outra dependência. E essa mesma solução torna o ambiente de desenvolvimento e o de produção praticamente idênticos, conforme recomendado pelo item X. Paridade entre desenvolvimento e produção do manifesto The Twelve-Factor APP
.
O gabarito do projeto está disponibilizado no GitHub. Para instanciar um novo projeto, é necessário usar o cookiecutter. Recomendo combiná-lo com pipx:
Referências
1 | Articles: Production-ready Docker packaging for Python developers |
---|
2 | Docker Best Practices for Python Developers |
---|
3 | Best practices for writing Dockerfiles |
---|
Artigo anterior: Projeto FastAPI Mínimo
Comentários
Comments powered by Disqus