Introdução
Um pipeline de CI/CD (Continuous Integration / Continuous Deployment) bem configurado é a espinha dorsal de qualquer projeto profissional em Rust. Ele garante que cada mudança no código seja compilada, testada e analisada automaticamente antes de chegar à produção.
Rust apresenta desafios únicos para CI/CD: tempos de compilação longos, binários grandes e a necessidade de cross-compilation para diferentes plataformas. Neste artigo, vamos construir pipelines otimizados que superam esses desafios, usando GitHub Actions como plataforma principal, com Docker multi-stage builds para deploy eficiente.
O Problema: CI Lento e Deploy Manual
Não Faça Isso: Workflow sem Cache
# ERRADO: .github/workflows/ci.yml — sem cache, sem otimização
name: CI
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: rustup update stable
- run: cargo build # Baixa e compila TODAS as dependências do zero
- run: cargo test # Compila tudo novamente
- run: cargo clippy # E mais uma vez...
# Tempo total: 15-30 minutos para cada push
Não Faça Isso: Dockerfile Não Otimizado
# ERRADO: Imagem gigante, recompila tudo a cada mudança
FROM rust:latest
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/meu-app"]
# Resultado: Imagem de 2GB+, rebuild completo a cada alteração
A Solução: Pipeline Otimizado e Profissional
Faça Isso: GitHub Actions com Cache e Paralelismo
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-Dwarnings"
jobs:
check:
name: Verificar compilação
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Instalar Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Verificar formatação
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features
test:
name: Testes
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v4
- name: Instalar Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Instalar cargo-nextest
uses: taiki-e/install-action@nextest
- name: Executar testes
run: cargo nextest run --all-features
- name: Doc tests
run: cargo test --doc
security:
name: Auditoria de segurança
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
coverage:
name: Cobertura de código
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Instalar Rust
uses: dtolnay/rust-toolchain@stable
- name: Instalar cargo-tarpaulin
run: cargo install cargo-tarpaulin
- name: Gerar relatório de cobertura
run: cargo tarpaulin --out xml --output-dir coverage
- name: Upload cobertura
uses: codecov/codecov-action@v4
with:
files: coverage/cobertura.xml
Faça Isso: Testes Rápidos com cargo-nextest
cargo-nextest executa testes até 3x mais rápido que cargo test:
# Instalar
cargo install cargo-nextest
# Executar testes (paralelo por padrão)
cargo nextest run
# Com retry para testes flaky
cargo nextest run --retries 2
# Apenas testes que falharam na última execução
cargo nextest run --run-ignored=only
Configure o nextest para seu projeto:
# .config/nextest.toml
[profile.default]
retries = 0
slow-timeout = { period = "60s", terminate-after = 2 }
fail-fast = true
[profile.ci]
retries = 2
fail-fast = false
# No GitHub Actions
- name: Testes no CI
run: cargo nextest run --profile ci
Docker Multi-Stage Build Otimizado
Faça Isso: Dockerfile com Cache de Dependências
# Dockerfile
# ===== Stage 1: Builder =====
FROM rust:1.85-bookworm AS builder
WORKDIR /app
# Truque: copiar apenas os manifestos primeiro para cachear dependências
COPY Cargo.toml Cargo.lock ./
# Criar um projeto dummy para compilar dependências
RUN mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release && \
rm -rf src
# Agora copiar o código real (dependências já estão compiladas e cacheadas)
COPY src/ src/
# Tocar no main.rs para forçar recompilação apenas do nosso código
RUN touch src/main.rs && \
cargo build --release
# ===== Stage 2: Runtime =====
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Criar usuário não-root
RUN useradd --create-home appuser
USER appuser
COPY --from=builder /app/target/release/meu-app /usr/local/bin/meu-app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["meu-app"]
Resultado: Imagem de ~80MB (em vez de 2GB+), e rebuilds só recompilam seu código (não as dependências).
Faça Isso: Build Ainda Menor com Musl (Binário Estático)
# Dockerfile.musl
FROM rust:1.85-bookworm AS builder
RUN rustup target add x86_64-unknown-linux-musl
RUN apt-get update && apt-get install -y musl-tools
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl && \
rm -rf src
COPY src/ src/
RUN touch src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl
# Imagem final: ~10MB com scratch
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/meu-app /meu-app
EXPOSE 8080
ENTRYPOINT ["/meu-app"]
Cross-Compilation para Múltiplas Plataformas
GitHub Actions para Releases Multi-Plataforma
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
artifact: meu-app
- target: x86_64-apple-darwin
os: macos-latest
artifact: meu-app
- target: aarch64-apple-darwin
os: macos-latest
artifact: meu-app
- target: x86_64-pc-windows-msvc
os: windows-latest
artifact: meu-app.exe
steps:
- uses: actions/checkout@v4
- name: Instalar Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Upload artefato
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: target/${{ matrix.target }}/release/${{ matrix.artifact }}
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
- name: Criar release
uses: softprops/action-gh-release@v2
with:
files: |
x86_64-unknown-linux-gnu/meu-app
x86_64-apple-darwin/meu-app
aarch64-apple-darwin/meu-app
x86_64-pc-windows-msvc/meu-app.exe
Cross-Compilation Local com cross
# Instalar cross (usa Docker para cross-compilation)
cargo install cross
# Compilar para Linux ARM64
cross build --release --target aarch64-unknown-linux-gnu
# Compilar para Windows a partir do Linux
cross build --release --target x86_64-pc-windows-gnu
Otimização do Cache no CI
Faça Isso: Cache Inteligente com sccache
# .github/workflows/ci.yml — com sccache
jobs:
test:
runs-on: ubuntu-latest
env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
- uses: actions/checkout@v4
- name: Instalar Rust
uses: dtolnay/rust-toolchain@stable
- name: Configurar sccache
uses: mozilla-actions/sccache-action@v0.0.6
- name: Build e teste
run: |
cargo build --release
cargo test --release
- name: Estatísticas do cache
run: sccache --show-stats
Armadilhas Comuns
1. Cache Invalidado com Frequência
# ERRADO: Cache por hash de todo o diretório
key: ${{ runner.os }}-cargo-${{ hashFiles('**/*.rs') }}
# Qualquer mudança em qualquer .rs invalida o cache
# CORRETO: Cache por hash do Cargo.lock (dependências)
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Invalidado apenas quando dependências mudam
2. Testes que Dependem de Serviços Externos
# CORRETO: Use service containers
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: teste
POSTGRES_DB: app_teste
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Rodar testes
env:
DATABASE_URL: postgres://postgres:teste@localhost:5432/app_teste
run: cargo nextest run
3. Builds de Release sem Otimização
# Cargo.toml — certifique-se de que o profile release está otimizado
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = true
4. Ignorar Testes de Documentação
# Doc tests são testes legítimos — não os ignore
- name: Testes de documentação
run: cargo test --doc
Exemplo do Mundo Real: Pipeline Completo
# .github/workflows/full-pipeline.yml
name: Pipeline Completo
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-Dwarnings"
jobs:
# Job 1: Verificações rápidas (< 2 min)
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/
~/.cargo/git/
target/
key: ${{ runner.os }}-lint-${{ hashFiles('**/Cargo.lock') }}
- run: cargo fmt --check
- run: cargo clippy --all-targets --all-features
# Job 2: Testes (paralelo ao lint)
test:
name: Testes (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/
~/.cargo/git/
target/
key: ${{ matrix.os }}-test-${{ hashFiles('**/Cargo.lock') }}
- uses: taiki-e/install-action@nextest
- run: cargo nextest run --all-features
- run: cargo test --doc
# Job 3: Segurança (paralelo)
security:
name: Segurança
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Job 4: Build de release (após testes)
build:
name: Build Release
needs: [lint, test, security]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry/
~/.cargo/git/
target/
key: ${{ runner.os }}-release-${{ hashFiles('**/Cargo.lock') }}
- run: cargo build --release
- uses: actions/upload-artifact@v4
with:
name: release-binary
path: target/release/meu-app
# Job 5: Deploy (após build)
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: release-binary
- name: Deploy para produção
run: echo "Implemente seu deploy aqui (Docker push, SSH, AWS, etc.)"
Checklist de CI/CD para Projetos Rust
- Cache do Cargo — Use
actions/cachebaseado emCargo.lock - cargo fmt –check — Formatação consistente
- cargo clippy — Linting com
RUSTFLAGS="-Dwarnings" - cargo nextest run — Testes rápidos e paralelos
- cargo test –doc — Não esqueça dos doc tests
- cargo audit — Vulnerabilidades de segurança
- Docker multi-stage — Imagens pequenas e seguras
- Cross-compilation — Binários para múltiplas plataformas
- sccache — Cache de compilação entre jobs
- Profile release otimizado — LTO, codegen-units=1, strip
Veja Também
- Instalação: GitHub Actions — Configure Rust no GitHub Actions
- Instalação: Docker — Rust com Docker passo a passo
- Segurança em Rust — Automatize auditorias de segurança no CI
- Otimização de Performance — Otimize seus builds de release
- Documentação em Rust — Doc tests como parte do CI
- Gerenciamento de Dependências — Lockfile e versionamento no CI