PRODPlataforma BaaS europeia soberanaAbra o painel →

Engenharia · 10 minutos de leitura

Espaços de trabalho de carga para um back-end Rust multisserviço

Affane Daylami · Fondateur · 12 de agosto de 2026

Voltar ao blog

Um back-end multisserviço em Rust levanta rapidamente a mesma questão: um depósito por serviço ou um único espaço de trabalho Cargo? Aurabase decidiu pela segunda opção. Onze serviços, uma CLI e cinco bibliotecas compartilhadas residem em uma única raiz Cargo.toml, com um único Cargo.lock para todos. Veja como este espaço de trabalho é construído, lendo-o linha por linha.

Este texto em inglês foi gerado automaticamente a partir do original em francês e ainda não foi revisado.
Esta página foi traduzida automaticamente. A versão em inglês é oficial.

Este não é um exemplo educativo inventado para a ocasião. Cada trecho de código abaixo vem da raiz Cargo.toml do Aurabase e seus manifestos de caixa, conforme existem no repositório hoje - incluindo duas discrepâncias que encontramos ao relê-las para este artigo e que estamos documentando como estão, em vez de corrigi-las silenciosamente antes da publicação.

O essencial
  • O espaço de trabalho raiz Cargo do Aurabase declara 18 membros explícitos: 11 serviços, a CLI aura-cli, 5 bibliotecas compartilhadas e o SDK aurabase-rs — em resolver = "2" e um único Cargo.lock.
  • [workspace.dependencies] centraliza versões compartilhadas; cada caixa herda { workspace = true } em vez de definir seu próprio número - exceto quando uma caixa é substituída, silenciosamente.
  • Uma pasta em services/ não é automaticamente um membro do espaço de trabalho: a lista members é explícita, não um glob, precisamente para poder excluir código não-Rust.
  • [profile.release] se aplica apenas uma vez a todo o espaço de trabalho — uma escolha como panic = "unwind" se aplica a todos os onze serviços de uma vez.
#
Por que um espaço de trabalho

Um depósito por serviço ou apenas um Cargo.lock?

Um espaço de trabalho Cargo agrupa várias caixas em um único Cargo.lock e um único diretório de destino - a mesma funcionalidade que o gerenciador de pacotes do Rust fornece para este caso. Onze binários Rust separados, cada um em seu próprio repositório, parecem mais independentes à primeira vista. Na prática, isso significa onze Cargo.lockdistintos, onze resoluções de versão que podem divergir ao longo do tempo e nenhuma garantia de que dois serviços compilem a mesma versão deaxum ou sqlx.

Um espaço de trabalho Cargo resolve isso no nível do gerenciador de pacotes, não no nível de disciplina da equipe. Todos os membros compartilham um único Cargo.lock na raiz: uma dependência comum é resolvida uma vez, para uma versão idêntica, para todo o grafo. É também isso que torna um refatorador entre serviços (alterando uma assinatura em aura-core, por exemplo) visível para um único cargo build --workspace, em vez de ser descoberto serviço por serviço na produção. Esta é uma das opções que distingue nosso núcleo Rust 100% unificado de uma pilha heterogênea montada serviço por serviço.

#
Anatomia

A raiz Cargo.toml: resolvedor e membros

Tudo começa com a declaração [workspace] e sua lista members. Na Aurabase, esta lista é escrita à mão, agrupada por função — serviços, ferramentas, bibliotecas — em vez de gerada por um padrão genérico como services/*.

Cargo.tomltoml
[workspace]
resolver = "2"

members = [
    # Serviços
    "services/aura-gateway",
    "services/aura-auth",
    "services/aura-db",
    # … 8 outros serviços (aura-realtime, aura-storage, aura-ai,…)
    # Ferramentas
    "aura-cli",
    # Bibliotecas
    "libs/aura-core",
    # …4 outras bibliotecas
    "aurabase-rs",
]

resolver = "2" não é um detalhe cosmético. O resolvedor v2 do Cargo isola a funcionalidade de build-dependencies e dependências específicas do alvo (target.'cfg(windows)', por exemplo) do resto do gráfico - elas não vazam mais para o binário final. Também unifica as versões da mesma dependência em todos os membros do espaço de trabalho que a compartilham: um único axum, apenas uma vez, e não onze resoluções independentes.

#
Legado

workspace.package: uma versão, uma edição, em princípio compartilhada

[workspace.package] declara uma vez os campos comuns — versão, edição, autores, licença — que cada caixa pode herdar com version.workspace = true em vez de copiá-los.

Cargo.toml (raiz)toml
[workspace.package]
version = "0.1.1"
edition = "2021"

# serviços/aura-gateway/Cargo.toml
[package]
name = "aura-gateway"
version.workspace = true

Todos os onze serviços do Aurabase seguem esse padrão. Duas caixas divergem, e esta é a primeira das duas discrepâncias encontradas durante a preparação deste artigo: aura-cli declara version = "0.2.0" em cópia impressa e a lib libs/aura-migrations declara version = "0.1.0" — ambos diferentes do 0.1.1 do espaço de trabalho.

O que isso significa

version.workspace = true é opcional, campo por campo, caixa por caixa. Nada impede que uma caixa mantenha sua própria numeração — voluntariamente (ferramenta publicada separadamente, por exemplo) ou por esquecimento. Uma auditoria do espaço de trabalho deve verificar este campo, caixa por caixa, e não assumir herança.

#
Desduplicação

workspace.dependencies: uma fonte de verdade, a menos que uma caixa a ignore

[workspace.dependencies] centraliza dependências compartilhadas por várias caixas. Cada serviço refere-se a ele com { workspace = true } em vez de definir sua própria restrição de versão.

Cargo.toml (raiz)toml
[workspace.dependencies]
axum = { version = "0.8", features = ["ws", "multipart", "macros"] }
sqlx = { version = "0.8", default-features = false, features = […] }
tower_governor = { version = "0.8", features = ["axum"] }
governor = "0.10"

# services/aura-gateway/Cargo.toml — NÃO herda o governador do espaço de trabalho
governor = "0.8"  # versão local, diferente

Esta é a segunda discrepância real: o espaço de trabalho centraliza governor na versão 0.10, mas aura-gateway redeclara sua própria linha governor = "0.8" em vez de herdar - o gateway aplica sua limitação de taxa com a caixa governor simples, quando aura-auth, aura-ai, aura-db e aura-functions passam tower_governor em middleware. Um espaço de trabalho não impede a divergência: apenas a torna visível, se nos dermos ao trabalho de comparar.

Porém, nem todas as dependências merecem ser centralizadas. wasmtime não aparece em nenhum lugar em [workspace.dependencies]: apenas uma caixa, aura-functions, usa-o para seu tempo de execução WASM, portanto permanece declarado localmente. A regra que aplicamos: aumente uma dependência para o nível do espaço de trabalho a partir do momento em que duas ou mais caixas a compartilham, não antes.

#
Gráfico interno

libs/ para serviços/: que depende do que

As cinco bibliotecas compartilhadas (aura-core, aura-crypto, aura-db-adapters, aura-migrations, aura-telemetry) são declaradas como dependências de caminho em [workspace.dependencies] — aura-core = { path = "libs/aura-core" } — então cada serviço escolhe quais realmente precisa.

O gráfico resultante permanece legível de um arquivo para outro. aura-gateway depende apenas de três das cinco bibliotecas internas — aura-core, aura-crypto e aura-telemetry. Basta rotear, assinar um JWT para o sidecar PostgREST e exportar seus traces, sem tocar em aura-db-adapters nem aura-migrations. aura-provisioneradiciona aura-migrations: reproduz as migrações de esquema resultantes da criação de um projeto.

O caso mais restrito é aura-migrator, o binário dedicado que executa migrações de CI/CD. Entre as bibliotecas internas do Aurabase, sua produção [dependencies] lista apenasaura-migrations — aura-core só aparece em seu [dev-dependencies], para teste. O binário entregue à produção, portanto, não inclui nenhum códigoaura-core; ele só existe durante cargo test.

#
Caso concreto

Uma caixa, vários binários: divisão aura em tempo real

Um espaço de trabalho não exige a criação de um novo membro sempre que você desejar um novo processo implementável. aura-realtime permanece um único membro do espaço de trabalho, mas seu Cargo.toml declara três tabelas [[bin]] distintas em torno do mesmo [lib]compartilhado.

Cargo.tomltoml
[lib]
name = "aura_realtime"

[[bin]]
name = "aura-realtime"

[[bin]]
name = "aura-realtime-cdc-worker"
path = "src/bin/cdc_worker.rs"

[[bin]]
name = "aura-realtime-ws-front"
path = "src/bin/ws_front.rs"

O próprio manifesto documenta a fronteira entre os dois. cdc-worker captura alterações do Postgres por meio de pesquisa e publica em NATS no núcleo pub/sub (Client::publish_with_headers), sem nunca abrir um servidor WebSocket - JetStream, neste mesmo serviço, atende exclusivamente o KV de presença entre instâncias, não o fan-out do CDC. ws-front consome apenas NATS e mantém conexões WebSocket/SSE, sem nunca tocar no CDC — os detalhes completos estão em a documentação do mecanismo em tempo real. Ambos os binários compartilham o mesmo código de filtragem RLS via [lib], mas implantam e escalam de forma independente no Kubernetes. Este é o sinal certo para escolher vários binários em uma caixa em vez de um novo membro do espaço de trabalho: mesma lógica interna, diferentes topologias de implantação.

#
Armadilha para evitar

Um arquivo em services/ não é necessariamente um membro Cargo

A pasta aura-edge-runtime existe no repositório Aurabase. No entanto, ele não contém nenhum Cargo.toml - apenas TypeScript (index.ts, envelope.ts) e não aparece em nenhum lugar da lista members do espaço de trabalho raiz.

Astuce

É exatamente por isso que a lista members do Aurabase é escrita à mão em vez de substituída por um padrão genérico como services/*. Um glob teria tentado incluir esta pasta não Rust no espaço de trabalho, resultando em uma falha de resolução. Uma lista explícita permite que pastas que não falam o mesmo idioma coexistam, sob o mesmo pai services/.

A lição generaliza: contar os serviços de um backend Rust listando as subpastas de services/ fornece um número falso. Somente a raiz Cargo.toml tem autoridade sobre o que realmente é compilado no espaço de trabalho.

#
Compilação

[profile.release]: uma configuração única para todo o espaço de trabalho

Um [profile.release] declarado na raiz do espaço de trabalho se aplica a todos os membros compilados no modo release — apenas um local para ajustar, não onze. Cargo documenta todas as chaves disponíveis em sua referência de perfil de compilação ; Aurabase ativa apenas cinco.

Cargo.toml (raiz)toml
[profile.release]
lto = "thin"          # Caixas cruzadas LTO, equilíbrio entre desempenho e tempo de construção
codegen-units = 1     # melhor inlining geral
panic = "unwind"   # um pânico isolado por tokio/axum → 500, não um crash global
strip = "symbols"
opt-level = 3

A escolha de panic = "unwind" em vez de "abort" está documentada diretamente no comentário do arquivo: um pânico em um manipulador axum é interceptado por tokio, a tarefa retorna 500 e o processo continua a atender outras solicitações simultâneas. O ganho de desempenho deabort não compensa a perda do isolamento entre consultas.

#
Na prática

Congelar o conjunto de ferramentas e os comandos importantes

Um espaço de trabalho unificado define versões de dependência, mas não a versão do compilador em si. rust-toolchain.toml, na raiz, congela o conjunto de ferramentas (channel = "1.93" hoje) para qualquer chamada direta para cargo ou rustc no repositório.

Este arquivo existe justamente porque ocorreu um desvio silencioso: uma ação de CI instalou o canal stable do dia sem ler rust-toolchain.toml, enquanto a imagem do Docker de lançamento compilava com a versão congelada. Um commit poderia passar em todos os testes com o Rust estável de hoje e depois quebrar na construção da imagem – descoberto após a fusão, não antes. rustuprespeita este arquivo para qualquer comando direto: foi a única correção necessária, sem afetar os fluxos de trabalho de CI.

terminalbash
# Compile todo o espaço de trabalho
cargo build --workspace

# Teste uma única caixa (não todo o espaço de trabalho)
cargo test -p aura-auth

# Lint estrito em todo o espaço de trabalho, avisos = erros
cargo clippy --workspace --all-targets -- -D warnings

# Formatação
cargo fmt --all

Um último detalhe, para quem lê um manifesto antes de executá-lo: a caixa aura-cli compila um binário cuja tabela [[bin]] o nomeia aurabase, não aura. O wrapper npm publicado, @aurabase/cli, expõe os dois comandos – aura e aurabase ambos apontam para o mesmo script. O nome de uma carga [[bin]], o nome da caixa e o nome exposto por um wrapper npm são três coisas distintas; nenhum pode ser adivinhado pelos outros dois.

#
Recapitulação

Checklist: onde declarar o quê

Cinco decisões surgem cada vez que você adiciona uma caixa a um espaço de trabalho do Cargo. Aqui é onde cada um é declarado, com base no exemplo do Aurabase abordado acima.

Lista de membrosmembros do [espaço de trabalho]Root — lista explícita, nunca um globo
Versão/edição compartilhada[espaço de trabalho.pacote]Root — version.workspace = true, por caixa, opcional
Dependência compartilhada por mais de 2 caixas[espaço de trabalho.dependências]Root — então {workspace = true } em cada caixa
Dependência de um único consumidor[dependências] da caixaLocalmente, sem passar pelo espaço de trabalho
Construir perfil[perfil.lançamento]Apenas root – aplica-se a todos os membros

Para auditar um espaço de trabalho existente do Cargo – o seu ou de um projeto que você está assumindo – quatro verificações são suficientes para encontrar o tipo de discrepância documentada acima:

  1. Compare [workspace.package].version com version de cada caixa — um valor diferente não é necessariamente um bug, mas vale a pena documentar.
  2. Compare [workspace.dependencies] com as dependências declaradas localmente por cada caixa — identifique os nomes presentes em ambos os lados com versões diferentes.
  3. Compare a lista members na raiz Cargo.toml com as subpastas reais no repositório - uma pasta ausente em members não é necessariamente um descuido.
  4. Verifique o nome real de cada binário compilado ([[bin]] name) em vez de assumir que ele corresponde ao nome da caixa ou ao nome exposto por um possível wrapper npm.

PRONTO PARA IMPLEMENTAR?

Seu back-end em cinco minutos.

Não é necessário cartão de crédito · 500 MB grátis · 50.000 MAU