Introdução

Se você já trabalhou com DevOps ou automação de tarefas, provavelmente já passou pela frustração de ter um script que funciona perfeitamente no terminal do servidor, mas falha misteriosamente quando executado via pipeline do Azure DevOps. Essa situação é mais comum do que parece e acontece por motivos bem fundamentados.

Neste artigo, vamos explorar em detalhes o que muda quando você executa um script Linux diretamente no terminal versus quando o mesmo script roda dentro de uma pipeline de CI/CD. Você vai entender as diferenças de usuário, ambiente, permissões, rede e efemeridade que causam esses comportamentos distintos. Ao final, terá em mãos um checklist de diagnóstico, um conjunto de boas práticas e um template Bash pronto para criar scripts que funcionam de forma consistente em ambos os cenários.

Este conteúdo é direcionado a desenvolvedores, engenheiros DevOps e administradores de sistemas que precisam garantir que seus scripts sejam reprodutíveis e confiáveis tanto localmente quanto em pipelines automatizadas. Se você quer eliminar o clássico “funciona na minha máquina”, continue lendo.


Visão Geral — O Que Muda

A tabela abaixo resume as principais diferenças entre executar um script no terminal do servidor e via pipeline do Azure DevOps. Cada aspecto pode impactar diretamente o comportamento do seu código.

AspectoTerminal do servidorPipeline Azure DevOps
Usuário/SessãoSeu usuário (ex.: lincoln), com perfil interativoUsuário do agente (ex.: vsts/azure-pipelines), tipicamente não-interativo
ShellO shell padrão do usuário (ex.: /bin/bash), com dotfiles carregadosShell invocado pela task (ex.: bash), sem .bashrc/.profile (ou parcialmente)
Ambiente (ENV)Variáveis definidas localmente, PATH “rico”Ambiente controlado pela pipeline: PATH diferente, variáveis de Secrets mapeadas, menos lixo
Diretório de trabalhoOnde você está (ex.: /home/lincoln)Workspace do agente (ex.: /_work/1/s), ou pasta do job
Permissões/SudoVocê pode ter sudo e TTYAgente geralmente sem TTY; sudo pode exigir NOPASSWD
DependênciasUsa libs, ferramentas e versões do servidorDepende do que está instalado no agente (MS-hosted ou self-hosted)
InteratividadePode responder prompts, ver menusExecução não-interativa; qualquer prompt bloqueia
Rede/ProxyUsa rede do servidorPode haver proxy corporativo do agente; bloqueios de saída
Arquivos & CaminhosPaths absolutos do servidorPaths relativos ao repositório/artefatos da pipeline
Tempo/RecursosSem timeout (em geral)Timeouts, quotas de CPU/Mem, paralelismo controlado
Logs e AuditoriaLogs locais (syslog, history)Logs centralizados, mascaramento de segredos, retenção
EfemeridadeEstado persistenteAgentes (especialmente hosted) são efêmeros; não guardam estado
Confiabilidade/ReprodutibilidadePode “funcionar só na sua máquina”Pipeline força reprodutibilidade e idempotência

ℹ️ Informação: Entender cada linha desta tabela é essencial para diagnosticar problemas. Sempre que um script falhar na pipeline, volte a esta referência e identifique qual aspecto diverge do seu ambiente local.


Por Que Isso Causa Diferença

As diferenças listadas acima não são aleatórias — elas existem porque o contexto de execução é fundamentalmente diferente. Abaixo, detalhamos cada uma das causas principais.

Usuário e TTY

No terminal, o comando roda com seu usuário e um TTY (terminal interativo). Várias ferramentas detectam a presença do TTY e habilitam cores, prompts e até fluxos de autenticação interativos.

No Azure DevOps, a execução é não-interativa. Qualquer script que espere input (ex.: read, prompts de sudo, wizard de az login) vai travar ou falhar. Essa é uma das causas mais frequentes de erros em pipelines, especialmente para quem está migrando scripts legados. Conforme a documentação oficial do Azure Pipelines, os agentes executam jobs sem interação humana.

Ambiente e PATH

Localmente, seu PATH carrega /usr/local/bin, versões específicas do Node/.NET/Java, alias e funções do shell definidas em .bashrc ou .profile.

No agente, o PATH pode ser completamente diferente. Ferramentas “globais” podem não existir ou ter outra versão. Isso muda o comportamento de forma sutil — por exemplo: node 18 no servidor vs node 20 no agente pode gerar incompatibilidades em dependências.

Diretório e Caminhos Relativos

Scripts que assumem . como um diretório específico, ou que dependem de cd implícito, podem quebrar na pipeline. Na pipeline, o working directory geralmente é a pasta do código clonado ($(Build.SourcesDirectory)), e artefatos ficam em $(Build.ArtifactStagingDirectory).

⚠️ Atenção: Nunca assuma que o diretório atual é o que você espera. Use caminhos absolutos ou variáveis de ambiente da pipeline como $(Build.SourcesDirectory) para referências explícitas.

Permissões e SELinux/AppArmor

No servidor, você pode ter sudo sem senha, ACLs permissivas, ou SELinux em modo permissivo. No agente, sudo pode precisar de NOPASSWD configurado no /etc/sudoers, e o serviço roda com um usuário independente. Acesso a /etc ou serviços do host pode ser negado.

Dependências e Versões

“Funciona no meu servidor” costuma significar acoplamento ao ambiente daquele host específico. No DevOps, cada agente tem seu próprio conjunto de ferramentas. Se não for um agente self-hosted idêntico ao servidor, espere divergências de versões e disponibilidade de pacotes.

Rede e Credenciais

Chamadas externas (ex.: Docker registries, APIs) podem depender de proxy, certificados corporativos ou regras de firewall. O Azure DevOps gerencia secrets via variáveis mascaradas. Scripts que procuram ~/.aws/credentials ou ~/.npmrc podem não encontrar esses arquivos no agente.

Para quem trabalha com segurança em aplicações, o gerenciamento correto de credenciais em pipelines é um ponto crítico que merece atenção especial.

Efemeridade e Estado

Scripts que escrevem em /var/tmp e esperam encontrar os dados depois, ou que contam com caches locais, falham em agentes efêmeros. Na pipeline, o ideal é usar artefatos, caches gerenciados e etapas declarativas.


Exemplos Práticos — Mão na Massa

A seguir, apresentamos três cenários reais que ilustram as diferenças mais comuns entre terminal e pipeline, com suas respectivas soluções.

1) sudo em Pipeline

1
2
# Falha na pipeline porque não há TTY e o sudo pede senha
sudo systemctl restart myservice

Solução: configure /etc/sudoers com NOPASSWD para o usuário do agente, ou execute via um service account. Evite operações de host em agentes hosted — use self-hosted.

1
2
# /etc/sudoers — permitir restart sem senha para o usuário do agente
vsts ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myservice

2) Variáveis e PATH

1
2
# Funciona no servidor porque 'jq' está em /usr/local/bin
jq '.version' package.json

Na pipeline: instale jq no agente (self-hosted) ou adicione uma etapa que garanta a ferramenta. Também é possível usar um container de job com jq pré-instalado.

1
2
3
4
5
6
7
8
# azure-pipelines.yml — instalar jq antes de usar
steps:
  - script: |
      sudo apt-get update && sudo apt-get install -y jq
    displayName: 'Instalar jq'
  - script: |
      jq '.version' package.json
    displayName: 'Executar script com jq'

3) Prompt Invisível

1
2
# Script pede confirmação — trava a pipeline
read -p "Continuar? (y/n) " ans

Na pipeline: remova prompts; use flags não-interativas (ex.: --yes, --force) e passe parâmetros via variáveis de pipeline.

1
2
3
4
5
6
# Versão não-interativa — segura para pipeline
SKIP_CONFIRM="${SKIP_CONFIRM:-1}"
if [[ "$SKIP_CONFIRM" != "1" ]]; then
    read -p "Continuar? (y/n) " ans
    [[ "$ans" != "y" ]] && exit 0
fi

💡 Dica: Sempre projete seus scripts com uma flag de “modo não-interativo”. Isso os torna compatíveis com pipelines sem sacrificar a usabilidade no terminal.


Azure DevOps — Pontos Específicos

Se você trabalha com Azure DevOps, existem particularidades que valem destaque especial. Conhecê-las ajuda a evitar armadilhas comuns.

Hosted vs Self-hosted Agents

  • Hosted: ambiente gerenciado pela Microsoft; efêmero; conjunto de ferramentas pré-definido. Ideal para projetos que seguem padrões comuns e não têm dependências exóticas.
  • Self-hosted: você controla o SO, versões e pacotes instalados — melhor para paridade com o servidor de produção.

Tasks e Shells

  • Use a task Bash@3 para scripts .sh.
  • Especifique o workingDirectory quando necessário.
  • Evite depender de .bashrc; inicialize o ambiente no próprio script.

Secrets e Service Connections

Timeouts e Retentativas

  • Configure timeoutInMinutes no job para evitar builds presos.
  • Implemente retry com backoff em chamadas instáveis (APIs externas, downloads de pacotes).

📌 Exemplo: Um timeout de 60 minutos é comum para jobs de build. Para deploys que envolvem provisionamento de infra, considere valores maiores (90-120 minutos).


Checklist de Diagnóstico

Quando um script falhar na pipeline, percorra sistematicamente esta lista para identificar a causa raiz. Cada item pode ser verificado adicionando um echo ou printenv temporário ao seu script.

  1. Quem está rodando? id -u -n, whoami, echo $SHELL, tty.
  2. Ambiente env | sort, echo $PATH, locale.
  3. Diretório pwd, ls -la, ver se paths relativos existem.
  4. Ferramentas command -v <tool>, --version de cada binário crítico.
  5. Permissões Tente sudo -n <cmd> (sem prompt). Veja /etc/sudoers.
  6. Rede/Proxy curl -I https://..., variáveis HTTP_PROXY/HTTPS_PROXY, certificados.
  7. Logs Verifique logs da pipeline e da aplicação (journalctl, arquivos em /var/log).
  8. Efemeridade O script persiste algo? Precisa de cache/artefatos?

💡 Dica: Inclua uma etapa de diagnóstico no início da sua pipeline que imprima whoami, pwd, env | sort e as versões das ferramentas críticas. Em caso de falha, você já terá as informações necessárias nos logs.


Dicas e Boas Práticas

As práticas abaixo ajudam a minimizar as diferenças entre terminal e pipeline, garantindo que seus scripts sejam portáveis e confiáveis.

  • Padronize o ambiente: use containers como runtime do job (cada pipeline roda a mesma imagem). Ou use self-hosted agent no mesmo SO e versão do servidor.
  • Declare dependências no script: cheque e instale faltantes (ou falhe com mensagem clara). Trave versões (ex.: node 20.x, dotnet 8.0.x).
  • Evite interatividade: use flags --non-interactive, --yes. Remova read, select e menus de scripts que rodam em pipeline.
  • Gerencie credenciais de forma segura: use variáveis secret, Key Vault e service connections. Não dependa de arquivos no ~ do usuário.
  • Use caminhos explícitos: prefira paths absolutos para binários críticos. Defina workingDirectory na task.
  • Torne o script idempotente: rodar duas vezes não deve quebrar o estado. Verifique se um serviço já está no estado desejado antes de agir.
  • Log detalhado: ative set -euo pipefail e set -x em modo debug. Garanta saída clara de erros e códigos de retorno corretos.

⚠️ Atenção: set -euo pipefail é essencial para scripts em pipeline. Sem essa diretiva, erros silenciosos podem propagar um estado corrompido por todo o restante do job.


Template Bash — Script à Prova de Pipeline

Abaixo, um template que incorpora todas as boas práticas discutidas neste artigo. Use-o como ponto de partida para qualquer script que precise funcionar tanto no terminal quanto na pipeline.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env bash
# Template Bash "à prova de pipeline"
# Funciona de forma consistente no terminal e no Azure DevOps
set -euo pipefail

# Debug opcional — ative com DEBUG=1
[[ "${DEBUG:-0}" == "1" ]] && set -x

# Normalize PATH (inclua binários conhecidos do servidor/agente)
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"

# Working dir explícito (ou receba por variável de pipeline)
WORKDIR="${WORKDIR:-$(pwd)}"
cd "$WORKDIR"

# Checagem de dependências — falha rápido com mensagem clara
need() {
    command -v "$1" >/dev/null 2>&1 || {
        echo "ERRO: Falta '$1' no PATH. Instale antes de continuar."
        exit 127
    }
}
need jq
need awk

# Não-interativo por padrão (pipeline-friendly)
NON_INTERACTIVE="${NON_INTERACTIVE:-1}"

# Função de retry com backoff exponencial
retry() {
    local n=0
    until "$@"; do
        n=$((n + 1))
        [[ $n -ge 3 ]] && { echo "ERRO: Falha após 3 tentativas: $*"; exit 1; }
        echo "Tentativa $n falhou. Aguardando $((n * 2))s..."
        sleep $((n * 2))
    done
}

# Uso de secrets via env (injetados pela pipeline ou exportados localmente)
API_TOKEN="${API_TOKEN:?ERRO: Variável API_TOKEN não definida}"
retry curl -sS -H "Authorization: Bearer $API_TOKEN" https://api.exemplo.com/health

echo "OK — script finalizado com sucesso."

ℹ️ Informação: Este template pode ser adaptado para qualquer linguagem ou contexto. O princípio é o mesmo: normalizar o ambiente, verificar dependências e evitar interatividade.


Conclusão

Ao longo deste artigo, vimos que as diferenças entre executar um script no terminal do servidor e via pipeline do Azure DevOps não são bugs — são consequências naturais de contextos de execução distintos. O terminal oferece um ambiente interativo, com seu usuário, seu PATH e suas configurações carregadas. A pipeline, por outro lado, prioriza reprodutibilidade, segurança e automação, operando com um usuário de serviço em um ambiente controlado e frequentemente efêmero.

Os principais pontos de atenção são: a ausência de TTY (que elimina interatividade), as diferenças no PATH e variáveis de ambiente, o gerenciamento de permissões via sudo, e a efemeridade dos agentes hosted. Para cada um desses desafios, apresentamos soluções práticas e um template reutilizável.

Se você aplicar as boas práticas discutidas — padronizar o ambiente, declarar dependências, evitar interatividade e tornar os scripts idempotentes — vai eliminar a grande maioria dos problemas de “funciona na minha máquina”. Scripts bem projetados para pipeline também são scripts melhores para qualquer ambiente.

Gostou deste conteúdo? Deixe um comentário com suas experiências ou dúvidas. Se quiser aprofundar em temas relacionados, confira os artigos sugeridos abaixo.


Leia Também


Referências