Seu passo de deploy trava em um nome de host interno enquanto o mesmo comando responde na hora no seu notebook, e o build do cliente é idêntico nas duas máquinas. O job roda em um runner hospedado pelo GitHub: uma máquina virtual criada para aquele único job dentro da nuvem do GitHub, sem rota para o seu espaço de endereços privado, sem entrada de resolver para a zona interna e sem certificado que o endpoint privado aceite. Apenas dois desenhos cabem nessa restrição, e nenhum deles pode ser ligado depois sem reconstruir o workflow. Escolha antes da primeira linha de YAML, porque migrar depois custa mais que a primeira tentativa.

A escolha é entre um runner colocado onde a rede já termina e um túnel que existe pela duração de um job. Ambos sustentam deploys de produção há anos, e falham de formas diferentes.

Onde fica de fato a fronteira

Tudo o que você instala no notebook para no notebook, e tudo o que você instala durante um job desaparece com o runner. Não existe namespace de rede persistente ao qual um cliente fique preso entre execuções, então cada execução começa no mesmo estado inalcançável. O Azure documenta essa fronteira para agentes hospedados pela Microsoft, onde links VPN e ExpressRoute para redes privadas não são suportados, e os runners hospedados pelo GitHub carregam esse mesmo tipo de restrição em vez de um mecanismo separado. Leia isso como uma propriedade do produto, não como um defeito a contornar.

Nenhuma configuração do repositório relaxa isso. O rótulo do job decide qual pool pega o trabalho, e um rótulo hospedado significa uma máquina fora do seu perímetro durante toda a execução.

A alcançabilidade privada precisa vir, portanto, de algo que você possua. Um runner colocado onde a rede já termina, ou um túnel que vive um job. O resto deste artigo trata de pagar por uma dessas opções de propósito em vez de descobrir a conta em produção.

Decida pela frequência de execução, não pela familiaridade

Recorra a um runner próprio dentro da VPC ou da rede do escritório quando os deploys chegam várias vezes ao dia, quando o job toca mais de uma porta privada ou quando um time de plataforma já cuida de patch e rotação de credenciais. Um job nessa máquina resolve e roteia como qualquer outro host atrás do firewall. O trabalho de rede acontece uma vez, na instalação, em vez de a cada execução.

Construa o túnel dentro do job quando o acesso é ocasional e você não quer um agente de longa duração segurando uma credencial para a sua rede. O job vira seu próprio cliente de rede por alguns minutos e não deixa nada para trás, o que muitas vezes é todo o objetivo.

Estime o custo do túnel antes de escolhê-lo. Cada execução paga a materialização do perfil, a partida do cliente, a espera pela prontidão e a desmontagem. Um túnel que sobe mas nunca é verificado gera um deploy que falha no meio do caminho e deixa o estado privado numa forma que ninguém planejou. A repetição depois disso raramente é limpa, e a segunda falha costuma ser atribuída à versão do cliente.

Escrever o perfil para que o passo não possa abortar

Mantenha perfis e credenciais fora do repositório e materialize-os a partir de secrets no passo que precisa deles. A ordem decide se o job sobrevive: um arquivo criado sob sudo pertence ao root, e o shell sem privilégios do passo não consegue mais escrever nele.

- name: Bring up OpenVPN
  run: |
    umask 077
    profile="$RUNNER_TEMP/client.ovpn"
    printf '%s\n' "$VPN_PROFILE" > "$profile"
    : > "$RUNNER_TEMP/openvpn.log"
    printf '%s\n' "$VPN_CREDENTIALS" > "$RUNNER_TEMP/credentials.txt"
    sudo openvpn --daemon openvpn-job \
      --config "$profile" \
      --auth-user-pass "$RUNNER_TEMP/credentials.txt" \
      --writepid "$RUNNER_TEMP/openvpn.pid" \
      --log "$RUNNER_TEMP/openvpn.log"

Os dois detalhes desse bloco existem para passos posteriores. Com umask 077 o perfil fica no modo 0600 e pertence ao usuário do runner, o sudo ainda consegue lê-lo, e o cliente escreve seu log num arquivo que o runner já possui, então o loop de prontidão consegue ler esse log sem um segundo sudo. --writepid registra o PID que a desmontagem vai mirar.

Um perfil que autentica com certificado de cliente precisa da chave escrita da mesma forma, e um perfil de desafio estático precisa do modo de autenticação correspondente. Perfis que empurram opções de DNS precisam de openvpn-systemd-resolved no Ubuntu e no Debian, porque o push sozinho não muda nada numa imagem base.

Prontidão é uma sonda, não uma pausa

Um sleep fixo aguenta até o concentrador ficar lento, e aí o passo de deploy roda antes de a rota existir. A falha então aparece dentro da ferramenta de deploy como timeout contra o host privado, o que não diz nada sobre o túnel.

- name: Await private endpoint
  run: |
    for attempt in $(seq 1 30); do
      if getent hosts private.example.internal > /dev/null && \
         timeout 3 bash -c 'exec 3<>/dev/tcp/private.example.internal/8443' 2>/dev/null; then
        echo "endpoint answered after $attempt attempts"
        exit 0
      fi
      sleep 2
    done
    grep -Ei 'error|verify|auth' "$RUNNER_TEMP/openvpn.log" | tail -40
    exit 1

O loop observa dois sinais porque qualquer um deles pode ser a metade quebrada. getent hosts prova que o resolver empurrado responde por aquele nome, e a abertura do socket prova que algo escuta na porta que o deploy vai usar; um perfil só de rota precisa do endereço literal e da linha getent apagada.

Quando o loop expira, o log é a única coisa que separa três falhas diferentes: um certificado rejeitado, um modo de autenticação errado e uma rota que nunca chegou. Filtrar antes do tail mantém essa saída curta o bastante para ler enquanto o job ainda está falhando. Leia o primeiro erro em vez da última linha, porque um cliente que não consegue autenticar se repete até o loop desistir. ip -4 route show dev tun0 responde à pergunta mais simples de se alguma rota foi empurrada, e custa uma linha dentro do mesmo loop.

Desmontagem e a questão do artefato

Feche o job com um passo protegido por if: always() que execute sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" e nada mais. Num runner próprio compartilhado, nunca mate por nome de processo: outro job pode ter um cliente com o mesmo binário e argumentos parecidos. A falha cai então no job que rodou em segundo, e o log dele aponta para um workflow que não fez nada errado.

A proteção importa porque um deploy malsucedido ainda deixa um túnel vivo. Sem always() o passo de limpeza é pulado exatamente quando é necessário, e a próxima execução começa contra um cliente já conectado.

Resta uma pergunta antes de adotar qualquer um dos dois desenhos: o estágio de build precisa mesmo de uma rota privada? Um agente de deployment que já está dentro da rede consegue baixar um artefato aprovado, o que mantém os builds comuns longe do caminho privado. Menos jobs segurando um túnel significa menos lugares onde uma rota velha pode se esconder.

Artigos relacionados