Extração de dados

Executando Scrapers em CI/CD: Credenciais de Proxy, Segredos e Execuções de Teste

Como executar scrapers em CI sem expor credenciais de proxy ou gastar largura de banda em cada push: armazenamento de segredos, níveis de teste, sessões, verificações de cota e rotação.

Chris Collins

Chris Collins

21 de setembro de 2026 · 11 min de leitura

Um scraper que funciona no laptop geralmente encontra o CI de uma de duas maneiras. Ou alguém cola a senha do proxy no arquivo de workflow “só para ficar verde”, ou o pipeline executa um scrape completo em produção a cada push e gasta silenciosamente um mês de banda até quarta-feira. Ambas são evitáveis, e corrigi-las é basicamente uma questão de decidir, de antemão, quais execuções realmente precisam de um proxy ao vivo.

Este tutorial cobre onde as credenciais de proxy devem viver no CI, como mantê-las fora dos logs, como dividir os testes para que a maioria das execuções nunca toque na rede, e como rotacionar uma credencial sem correria. Os exemplos usam GitHub Actions e GitLab CI com o gateway residencial da Shifter, mas a estrutura se aplica a qualquer runner.

O que dá errado

Quatro modos de falha respondem por quase todos os incidentes de credenciais em CI com scrapers:

FalhaComo acontece
Credencial commitadaUm arquivo .env ou uma URL de proxy fixa no código acaba no repositório
Credencial nos logsUma linha de debug imprime a URL do proxy, ou um cliente HTTP registra os cabeçalhos da requisição
Credencial exposta a código não confiávelUm pull request de fora do time roda com os secrets disponíveis
Banda desperdiçadaCada push executa um scrape ao vivo contra sites reais

Os três primeiros são problemas de segurança. O quarto é um problema de custo que também torna o pipeline instável, porque sites reais mudam e seu build não deveria falhar quando a página de outra pessoa falha.

Passo 1: armazene a credencial como um secret, nunca no código

Seu nome de usuário e senha residenciais estão na página do plano no painel. Armazene-os como dois secrets de CI:

  • SHIFTER_USERNAME: o nome de usuário completo como exibido, por exemplo customer-USERNAME
  • SHIFTER_PASSWORD: a senha

GitHub Actions. Adicione ambos em Settings do repositório, Secrets and variables, Actions. O GitHub oculta valores de secrets nos logs, e, com exceção do GITHUB_TOKEN, os secrets não são passados para o runner quando um workflow é acionado a partir de um repositório fork. Essa segunda regra importa para repositórios públicos: o pull request de um colaborador externo não consegue ler a senha do seu proxy, mas também não consegue executar seus testes ao vivo, o que você tratará no passo 3.

GitLab CI. Adicione ambos como variáveis de CI/CD, marque-os como masked, e marque-os como protected para que fiquem disponíveis apenas a pipelines em branches ou tags protegidas. Uma pegadinha específica merece destaque: o GitLab só consegue mascarar um valor que seja uma única linha com 8 caracteres ou mais. As senhas residenciais da Shifter podem ser mais curtas que isso, e o GitLab vai se recusar a mascará-las. A solução é armazenar o par como uma única variável mascarada:

SHIFTER_PROXY_AUTH=customer-USERNAME:PASSWORD

Esse valor fica confortavelmente acima de 8 caracteres e usa apenas caracteres que o GitLab permite em variáveis mascaradas. Divida-o pelo último dois-pontos no seu código.

Adicione .env ao .gitignore no mesmo commit, para que um arquivo local nunca acompanhe a credencial até o repositório.

Passo 2: monte a URL do proxy no código, e nunca a imprima

Monte a URL do proxy em tempo de execução a partir do ambiente, em um único lugar, para que os flags de segmentação e sessão sejam adicionados de forma consistente e nada mais lide com a senha bruta:

import os

GATEWAY = "p.shifter.io:443"


def shifter_credentials():
    if "SHIFTER_PROXY_AUTH" in os.environ:
        username, password = os.environ["SHIFTER_PROXY_AUTH"].rsplit(":", 1)
        return username, password
    return os.environ["SHIFTER_USERNAME"], os.environ["SHIFTER_PASSWORD"]


def proxy_url(country=None, session=None, ttl=None):
    username, password = shifter_credentials()
    if country:
        username += f"-country-{country}"
    if session:
        username += f"-sid-{session}"
        if ttl:
            username += f"-ttl-{ttl}"
    return f"http://{username}:{password}@{GATEWAY}"


def redact(url):
    # Seguro para logar: mantém os flags, remove a senha.
    creds, host = url.rsplit("@", 1)
    return f"{creds.rsplit(':', 1)[0]}:***@{host}"

Registre redact(url) se precisar ver quais flags uma execução usou. Nunca registre a URL em si.

O mascaramento de secrets tem um ponto ciego que vale a pena conhecer. Ele corresponde ao valor armazenado, então um valor transformado passa despercebido. A autenticação de proxy é enviada como um cabeçalho Proxy-Authorization contendo a codificação base64 de username:password, e um log de debug dos cabeçalhos da requisição imprime essa codificação, que nenhum mascarador de CI vai reconhecer. Mantenha o log de debug em nível de cabeçalho desligado no CI. Se você precisar montar uma string sensível que não seja em si um secret, registre-a com o comando ::add-mask:: do GitHub antes que qualquer coisa possa imprimi-la.

Passe secrets para seu scraper como variáveis de ambiente, não como argumentos de linha de comando. A própria orientação do GitHub é evitar passar secrets entre processos pela linha de comando quando possível. Argumentos são fáceis de ver em listagens de processos e tendem a acabar em traces de shell.

Passo 3: divida os testes para que a maioria das execuções nunca precise de proxy

Este passo elimina a maior parte do custo e da instabilidade. Divida os testes do scraper em três níveis:

NívelO que verificaPrecisa de proxyQuando roda
Testes de parserLógica de extração contra fixtures de HTML salvasNãoA cada push e pull request
Teste de fumaça ao vivoUm punhado de requisições reais através do gatewaySimBranch principal e uma agenda
Execução completaO scrape realSimSua própria agenda, ou um gatilho manual

Testes de parser são a maior parte da sua cobertura. Salve respostas reais como arquivos de fixture, e teste se seus seletores extraem os campos corretos delas. Eles rodam em segundos, não custam nada, não precisam de secrets, e falham apenas quando seu código está errado. Quando um site muda seu layout, salve uma fixture nova e atualize o parser no mesmo pull request.

O teste de fumaça ao vivo confirma que credenciais, segmentação e conectividade funcionam, não que toda página é analisada corretamente. Mantenha-o a poucas requisições. Ele deve ser pulado, em vez de falhar, quando o secret estiver ausente, que é exatamente a situação em um pull request de um fork:

import os
import pytest
import requests

from scraper.proxy import proxy_url

live = pytest.mark.skipif(
    not (os.environ.get("SHIFTER_PASSWORD") or os.environ.get("SHIFTER_PROXY_AUTH")),
    reason="no proxy credentials in this environment",
)


@live
def test_gateway_exits_in_requested_country():
    url = proxy_url(country="de")
    r = requests.get("https://ipinfo.io/json", proxies={"http": url, "https": url}, timeout=30)
    assert r.status_code == 200
    assert r.json()["country"] == "DE"

A execução completa deve ficar em uma agenda, não em push, para que um merge não acione um scrape em escala de produção.

Passo 4: uma sessão por job, nunca compartilhada

Sessões persistentes fixam uma execução a um único IP de saída, o que é o que você quer para fluxos de múltiplas etapas como paginação. O id de sessão da Shifter é qualquer string que você escolher, com uma duração padrão de 120 segundos que ttl sobrescreve. O aviso da documentação se aplica diretamente ao CI: não reutilize um id de sessão entre workflows concorrentes, porque requisições de jobs diferentes chegando ao mesmo IP parecem suspeitas para a maioria dos sistemas anti-bot.

O CI já fornece um valor único por execução. Use-o, mais o índice do job se você executar uma matriz:

import os

run = os.environ.get("GITHUB_RUN_ID") or os.environ.get("CI_PIPELINE_ID", "local")
job = os.environ.get("JOB_INDEX", "0")
url = proxy_url(country="us", session=f"ci{run}j{job}", ttl=600)

Mantenha o id alfanumérico, já que o nome de usuário usa hífens para separar flags. Para requisições independentes umas das outras, deixe a sessão de fora completamente e cada requisição rotaciona para um IP novo.

Passo 5: verifique a cota antes de uma execução grande

Um scrape agendado que fica sem banda na metade é pior do que um que nunca começou. A API de Uso e Cota da Shifter retorna o que resta em um plano, então um passo de pré-verificação pode pular a execução de forma limpa:

import os
import sys
import requests

MIN_GB = float(os.environ.get("MIN_REMAINING_GB", "5"))

r = requests.get(
    f"https://shifter.io/api/v1/memberships/{os.environ['SHIFTER_MEMBERSHIP']}/usage",
    params={"api_token": os.environ["SHIFTER_API_TOKEN"]},
    timeout=30,
)
r.raise_for_status()
plan = r.json()["data"]

if plan["metered"] and plan["remaining_gb"] < MIN_GB:
    print(f"Only {plan['remaining_gb']} GB left, resets {plan['resets_at']}. Skipping run.")
    sys.exit(78)

O token da API é gerado no painel em Account, API Tokens. Armazene-o como um secret assim como a senha: ele pertence à sua conta e não a um workspace específico, então lê o uso de todos os workspaces dos quais você é membro. O endpoint tem limite de taxa de 60 requisições por minuto, bem mais do que uma pré-verificação precisa.

Se uma execução esgotar o plano com Extra Traffic desativado, o gateway retorna 509 Bandwidth Limit Exceeded. Trate isso como uma condição de parada, não como algo para tentar de novo.

Passo 6: falhe rápido em erros de autenticação

Repetições fazem sentido para erros transitórios de rede e não fazem sentido para erros de credencial. Um 407 Proxy Authentication Required significa que o nome de usuário, a senha ou um flag está errado, e vai continuar errado na próxima tentativa. No CI, um loop de repetição em torno de um 407 transforma uma falha de um segundo em uma de dez minutos com um log confuso. Faça seu cliente tratar 407 como fatal, imprima a URL do proxy redigida, e pare. As causas usuais, e a ordem em que verificá-las, estão cobertas em corrigindo erros 407 de autenticação de proxy.

Um workflow completo do GitHub Actions

name: scraper

on:
  push:
  pull_request:
  schedule:
    - cron: "0 6 * * *"
  workflow_dispatch:

jobs:
  parser-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest tests/parsers

  smoke-test:
    if: github.ref == 'refs/heads/main'
    needs: parser-tests
    runs-on: ubuntu-latest
    env:
      SHIFTER_USERNAME: ${{ secrets.SHIFTER_USERNAME }}
      SHIFTER_PASSWORD: ${{ secrets.SHIFTER_PASSWORD }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest tests/live

  full-run:
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    needs: smoke-test
    runs-on: ubuntu-latest
    concurrency: scraper-full-run
    env:
      SHIFTER_USERNAME: ${{ secrets.SHIFTER_USERNAME }}
      SHIFTER_PASSWORD: ${{ secrets.SHIFTER_PASSWORD }}
      SHIFTER_API_TOKEN: ${{ secrets.SHIFTER_API_TOKEN }}
      SHIFTER_MEMBERSHIP: ${{ vars.SHIFTER_MEMBERSHIP }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: python scripts/check_quota.py
      - run: python -m scraper.run

Os testes de parser rodam em todo lugar sem secrets. O teste de fumaça e a execução completa só existem onde os secrets existem. O grupo concurrency impede que duas execuções agendadas se sobreponham e compartilhem um orçamento de IP. O ID do membership não é secreto, então fica em uma variável simples do repositório.

Um ajuste vale a pena fazer: como está escrito, o código de saída 78 do script de cota faz o job falhar. Se você preferir ver uma execução pulada em vez de vermelha, dê ao passo de verificação um id, faça-o escrever uma flag em $GITHUB_OUTPUT, e condicione o passo de scrape a essa saída.

Rotacionando a credencial

Rotacione em uma agenda, e imediatamente sempre que um secret puder ter vazado: um log público, um contratado saindo, um workflow forkado sobre o qual você tem dúvidas.

Na Shifter, o proprietário da conta ou um Admin do workspace pode gerar uma nova senha residencial na página do plano no painel. Membros Viewer e Billing não podem. O gateway adota a nova senha imediatamente, e a antiga deixa de funcionar no mesmo momento, então planeje a ordem:

  1. Pause o workflow agendado, ou aceite que uma execução em andamento vai falhar com um 407.
  2. Gere a nova senha no painel.
  3. Atualize o secret do CI imediatamente.
  4. Atualize todos os outros consumidores do mesmo plano. A senha pertence ao plano, não a um pipeline, então qualquer outra coisa que a use quebra no mesmo momento.
  5. Execute novamente o teste de fumaça para confirmar.

O ponto 4 é o motivo pelo qual vale a pena saber onde a credencial de um plano é usada antes de precisar rotacioná-la. Se vários pipelines independentes compartilham um plano, uma única rotação afeta todos eles.

Conclusão

A maior parte do trabalho de executar scrapers a partir do CI é manter o proxy fora das execuções que não precisam dele. Testes de parser contra fixtures cobrem a lógica em cada push, sem secrets e sem banda. Um pequeno teste de fumaça ao vivo na branch principal comprova que as credenciais ainda funcionam. O scrape real roda em uma agenda, verifica sua cota primeiro, usa um id de sessão que ninguém mais compartilha, e para no primeiro erro de autenticação em vez de tentar de novo.

A credencial em si vive no armazenamento de secrets do CI, é montada em uma única função, e nunca é impressa, inclusive em base64. Para coleta de longa duração, o lado operacional de acompanhar um pipeline está coberto em monitorando um pipeline de web scraping, e distribuir um pipeline entre regiões em failover de proxy residencial para pipelines multirregionais. A configuração de proxy no lado do cliente para scrapers baseados em navegador está em configurando proxies residenciais no Selenium e Playwright.

Pronto para começar?

Experimente os proxies residenciais da Shifter, mais de 205M IPs, mais de 195 países, a partir de $ 0,75/GB.

Começar