Published on

bash set -euo pipefail로 스크립트 폭발 막기

Authors

서버 운영이나 CI/CD에서 bash 스크립트는 “작고 빠르게” 문제를 해결해주지만, 반대로 “조용히 실패(silent failure)”하기도 쉽습니다. 예를 들어 curl이 실패했는데도 다음 단계가 실행되거나, 오타로 비어 있는 변수를 사용해 엉뚱한 경로를 지우는 식의 사고가 대표적입니다. 이런 폭발을 막기 위한 최소한의 안전장치가 바로 set -euo pipefail입니다.

이 글에서는 각 옵션이 실제로 어떤 종류의 사고를 막는지, 어디서 예상치 못한 동작이 나오는지, 그리고 실무에서 쓰기 좋은 예외 처리/디버깅 패턴까지 정리합니다.

왜 bash는 “실패해도 계속” 진행할까

bash는 기본적으로 명령이 실패(0이 아닌 exit code)해도 스크립트를 계속 실행합니다. 이 설계는 대화형 셸에서는 편하지만, 자동화 스크립트에서는 치명적일 수 있습니다.

  • 네트워크 요청 실패 → 빈 응답을 정상으로 오인
  • 변수가 비어 있음 → 경로가 "" 또는 /로 해석되어 위험한 명령 실행
  • 파이프라인 중간 실패 → 마지막 명령만 성공하면 전체를 성공으로 판단

이런 류의 문제는 배포/운영에서 “나중에” 터집니다. 특히 GitHub Actions 같은 CI 환경에서는 로그가 길어지면 실패 지점을 놓치기 쉬워서 더 위험합니다. (관련해서 CI 디버깅 관점은 GitHub Actions OIDC로 AWS 배포 AccessDenied 해결 같은 글의 트러블슈팅 흐름도 참고할 만합니다.)

set -euo pipefail 한 줄의 의미

보통 스크립트 상단에 아래를 넣습니다.

#!/usr/bin/env bash
set -euo pipefail

각 옵션은 다음을 의미합니다.

  • -e: 어떤 명령이든 실패하면 즉시 종료(단, 예외 케이스 존재)
  • -u: 정의되지 않은 변수 사용 시 오류로 종료
  • -o pipefail: 파이프라인에서 중간 명령이 실패해도 전체를 실패 처리

이 조합은 “실패를 빨리, 크게, 눈에 띄게” 만드는 방향입니다. 운영 자동화에서는 대개 이게 맞습니다.

-e (errexit): 실패 즉시 중단, 하지만 예외가 많다

set -e는 강력하지만, bash의 문법 구조 때문에 “실패로 보지 않는” 상황이 존재합니다. 대표적으로 조건문/논리 연산자와 함께 쓰일 때입니다.

기본 동작

#!/usr/bin/env bash
set -e

echo "1) 시작"
false
echo "2) 여긴 실행되지 않음"

false가 1을 반환하므로 스크립트는 즉시 종료합니다.

-e가 무시되는 대표 패턴: if/while/until 조건

#!/usr/bin/env bash
set -e

if grep -q "needle" ./file.txt; then
  echo "있음"
else
  echo "없음(여기까지는 정상 흐름)"
fi

echo "계속 진행"

grep이 못 찾으면 exit code 1이지만, if 조건식 내부에서는 그 실패가 “제어 흐름”으로 사용되므로 -e가 스크립트를 죽이지 않습니다.

-e가 무시되는 대표 패턴: cmd1 || cmd2

#!/usr/bin/env bash
set -e

mkdir /tmp/app || true
# 이미 존재해도 계속 진행하고 싶을 때 의도적으로 사용

여기서는 실패를 삼키는 것이 목적이므로 괜찮습니다. 문제는 이런 패턴이 많아지면 “실패를 숨기는 코드”가 늘어난다는 점입니다.

권장: 실패를 삼킬 땐 의도를 드러내기

실패를 무시해야 한다면 주석/함수로 의도를 표현해 두는 편이 좋습니다.

#!/usr/bin/env bash
set -euo pipefail

ignore_failure() {
  "$@" || {
    echo "[warn] ignoring failure: $*" >&2
    return 0
  }
}

ignore_failure rm -f /tmp/nonexistent

-u (nounset): 오타/빈 값으로 인한 사고를 막는다

set -u는 정의되지 않은 변수를 참조하면 즉시 종료합니다.

#!/usr/bin/env bash
set -eu

echo "ENV=$ENV"  # ENV가 정의되지 않았다면 여기서 종료

이 옵션이 특히 유용한 이유는, bash에서 빈 문자열은 많은 명령에서 “합법적인 인자”로 처리되기 때문입니다. 예를 들어 경로 조합을 잘못하면 위험해집니다.

안전한 기본값 패턴

정의되지 않았을 수도 있는 변수는 기본값을 명시하세요.

#!/usr/bin/env bash
set -euo pipefail

REGION="${REGION:-ap-northeast-2}"
TIMEOUT="${TIMEOUT:-10}"

echo "region=$REGION timeout=$TIMEOUT"

필수 변수 강제 패턴

필수 입력은 “없으면 즉시 실패”가 더 낫습니다.

#!/usr/bin/env bash
set -euo pipefail

: "${DATABASE_URL:?DATABASE_URL is required}"
: "${S3_BUCKET:?S3_BUCKET is required}"

echo "OK"

:는 no-op 명령이고, ${VAR:?msg}는 VAR이 비어 있거나 unset이면 msg를 출력하고 종료합니다.

pipefail: 파이프라인 중간 실패를 놓치지 않는다

bash에서 기본 파이프라인의 exit code는 마지막 명령의 exit code입니다. 즉, 중간이 실패해도 마지막이 성공하면 전체가 성공으로 보입니다.

pipefail이 없을 때의 함정

#!/usr/bin/env bash
set -eu

# curl이 실패해도 jq가 빈 입력을 처리하며 0으로 끝나면 성공처럼 보일 수 있음
curl -fsS "https://example.invalid/api" | jq -r '.version'

echo "배포 계속 진행..."  # 위험

pipefail을 켜면

#!/usr/bin/env bash
set -euo pipefail

curl -fsS "https://example.invalid/api" | jq -r '.version'

echo "여긴 curl 실패 시 실행되지 않음"

pipefail은 파이프라인 내 어떤 명령이든 실패하면(정확히는 마지막이 아닌 실패 상태를 반영) 전체 파이프라인을 실패로 만들어 -e와 결합 시 즉시 중단시킵니다.

추천 템플릿: set + trap + 에러 메시지까지

set -euo pipefail만으로도 효과는 크지만, 실제 현장에서는 “어디서 실패했는지”까지 빠르게 보이게 만드는 게 중요합니다. 아래 템플릿을 많이 씁니다.

#!/usr/bin/env bash
set -euo pipefail

# 에러 발생 시, 실패한 명령과 라인 번호를 출력
trap 'echo "[error] line=$LINENO cmd=$BASH_COMMAND" >&2' ERR

log() { echo "[info] $*" >&2; }

die() { echo "[fatal] $*" >&2; exit 1; }

log "start"

: "${ENV:?ENV is required}"

log "done"
  • trap ... ERR는 에러 시점의 LINENO, BASH_COMMAND를 찍어주어 CI 로그에서 역추적이 훨씬 쉬워집니다.
  • 운영 스크립트는 실패했을 때 “조용히” 끝나는 게 최악이므로, 표준 에러로 명확히 남기는 게 좋습니다.

예외 처리: 실패를 ‘의도적으로’ 허용해야 할 때

모든 실패가 치명적인 것은 아닙니다. 예를 들어 “리소스가 없으면 생성” 같은 흐름에서는 실패를 정상 분기로 다루기도 합니다.

패턴 1) if로 감싸기

#!/usr/bin/env bash
set -euo pipefail

if aws s3 ls "s3://$BUCKET" >/dev/null 2>&1; then
  echo "bucket exists"
else
  echo "bucket missing, creating..."
  aws s3 mb "s3://$BUCKET"
fi

패턴 2) 실패 허용 범위를 좁히기 (set +e 구간)

정말로 특정 구간만 실패를 허용해야 한다면, 범위를 최소화합니다.

#!/usr/bin/env bash
set -euo pipefail

set +e
output=$(some_flaky_command)
rc=$?
set -e

if [[ $rc -ne 0 ]]; then
  echo "[warn] flaky command failed, continue" >&2
fi

이 방식은 “실패를 허용한다”는 사실이 코드 구조로 드러나서 리뷰/유지보수에 유리합니다.

자주 같이 쓰는 안전 옵션/관례

set -euo pipefail 외에도 안정성을 올리는 관례가 있습니다.

IFS 고정(단, 신중히)

공백/개행 분리로 인한 버그를 줄이기 위해 IFS를 제한하기도 합니다.

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

다만 IFS 변경은 예상치 못한 부작용이 있을 수 있어, 팀 규약이 없다면 “문자열/배열을 명확히 다루는 습관(따옴표, 배열 사용)”을 우선 권합니다.

항상 따옴표로 감싸기

rm -rf "$target_dir"   # O
rm -rf $target_dir      # X (공백/글로빙 위험)

임시 디렉터리 안전하게 만들기

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

EXIT 트랩은 성공/실패/중단 모두에서 정리(cleanup)를 보장합니다.

실전 예시: 배포 전 체크 스크립트(안전장치 포함)

아래는 “환경변수 검증 → API 헬스체크 → 아티팩트 존재 확인” 같은 전형적인 흐름을 안전하게 작성한 예시입니다.

#!/usr/bin/env bash
set -euo pipefail
trap 'echo "[error] line=$LINENO cmd=$BASH_COMMAND" >&2' ERR

log() { echo "[info] $*" >&2; }

: "${API_BASE_URL:?API_BASE_URL is required}"
: "${ARTIFACT_PATH:?ARTIFACT_PATH is required}"

log "health check: $API_BASE_URL/health"
status=$(curl -fsS "$API_BASE_URL/health" | jq -r '.status')
[[ "$status" == "ok" ]] || { echo "[fatal] health not ok: $status" >&2; exit 1; }

log "check artifact: $ARTIFACT_PATH"
[[ -f "$ARTIFACT_PATH" ]] || { echo "[fatal] artifact missing" >&2; exit 1; }

log "all checks passed"
  • curl -f로 HTTP 오류를 실패로 만들고
  • pipefailcurl|jq 중간 실패를 전파하며
  • 필수 변수는 ${VAR:?}로 초기에 차단합니다.

이런 “초기 실패(fail fast)” 구조는 운영에서 장애를 줄이는 데 직접적인 효과가 있습니다. 특히 권한/정책 문제로 배포가 실패하는 케이스는 원인이 다양하니, 실패 지점을 빨리 고정하는 게 중요합니다. (S3 권한 이슈를 30분 내 진단하는 흐름은 S3 AccessDenied 403 급발생 - OAC·정책·KMS 30분 진단도 함께 보면 좋습니다.)

체크리스트: set -euo pipefail 도입 시 흔한 함정

  • grep/test 같은 “찾지 못함”을 1로 반환하는 명령을 무작정 쓰면, 의도와 다르게 종료될 수 있음 → if grep ...; then 구조로 처리
  • cmd || true가 남발되면 실패가 매몰됨 → ignore_failure 같은 래퍼로 의도 노출
  • 파이프라인은 반드시 pipefail과 함께 사용(특히 curl | jq, kubectl | grep)
  • trap ERR를 추가해 실패 위치를 로그로 남기기

마무리

set -euo pipefail은 bash 스크립트를 “좀 더 엄격한 언어”처럼 만들어, 실패가 조용히 누적되는 것을 막습니다. 핵심은 단순히 옵션을 켜는 데서 끝나지 않고, 실패를 허용해야 하는 지점은 명확한 구조(if, 제한된 set +e, 래퍼 함수)로 표현하는 것입니다.

운영/배포 자동화 스크립트가 늘어날수록, 이 한 줄이 막아주는 사고의 크기도 같이 커집니다. 스크립트가 길어지기 전에 템플릿으로 고정해 두는 것을 권합니다.