- Published on
Bash set -euo pipefail 함정과 안전한 예외처리
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
운영 자동화, CI 파이프라인, 배포 스크립트에서 set -euo pipefail은 사실상 표준처럼 쓰입니다. 실패를 조기에 감지하고, 조용히 넘어가던 오류를 크게 드러내기 때문입니다.
하지만 이 조합은 “안전장치”이면서 동시에 “발목잡이”가 되기도 합니다. 특히 -e의 동작은 직관적이지 않고, pipefail은 파이프라인의 의미를 바꿔버리며, -u는 환경변수/배열 처리에서 자주 폭발합니다. 결과적으로 정상적으로 처리하고 싶었던 예외 상황(예: 파일이 없으면 생성, 없으면 무시, grep이 못 찾으면 1 반환 등)까지 스크립트를 즉시 종료시켜 장애를 만들 수 있습니다.
이 글에서는 set -euo pipefail의 핵심 동작과 대표 함정, 그리고 “실패는 빨리, 예외는 명시적으로” 처리하는 안전한 패턴을 코드로 정리합니다. 운영에서 이런 스크립트가 실패하면 곧바로 서비스 장애로 이어질 수 있으니, 장애 원인 추적 관점에서는 Kubernetes CrashLoopBackOff 원인 12가지와 진단처럼 로그와 종료 코드를 체계적으로 보는 습관도 같이 가져가면 좋습니다.
set -euo pipefail 각각의 의미
보통 아래처럼 한 줄로 켭니다.
#!/usr/bin/env bash
set -euo pipefail
각 옵션은 다음 의미를 가집니다.
set -e(errexit): “단순 명령(simple command)”이 0이 아닌 종료 코드를 반환하면 스크립트를 종료합니다.set -u(nounset): 정의되지 않은 변수를 참조하면 에러로 처리하고 종료합니다.set -o pipefail: 파이프라인(cmd1 | cmd2 | cmd3)의 종료 코드를 “마지막 명령”이 아니라 “중간에 실패한 명령”까지 반영합니다. 즉, 어느 하나라도 실패하면 파이프라인이 실패로 간주됩니다.
문제는 “단순 명령”의 범위, if/while/&&/|| 같은 조건식에서의 예외, 커맨드 치환($(...)) 내부에서의 전파 등 세부 규칙이 직관과 다르다는 점입니다.
함정 1: grep의 정상적인 1이 치명적 실패가 된다
grep은 매칭을 못 찾으면 종료 코드 1을 반환합니다. 에러가 아니라 “결과 없음”일 뿐인데, set -e에서는 즉시 종료될 수 있습니다.
#!/usr/bin/env bash
set -euo pipefail
echo "hello" > /tmp/sample.txt
# 매칭이 없으면 grep은 1을 반환
grep "world" /tmp/sample.txt
echo "여기는 실행되지 않을 수 있음"
안전한 처리 패턴
의도적으로 “없으면 없는대로” 진행하려면 예외를 코드에 명시해야 합니다.
# 1) 결과 없음은 허용
if grep -q "world" /tmp/sample.txt; then
echo "found"
else
echo "not found (ok)"
fi
# 2) or-리스트로 명시적 무시
grep -q "world" /tmp/sample.txt || true
|| true는 흔히 쓰지만 남용하면 진짜 오류까지 숨길 수 있습니다. 가능하면 if로 의도를 드러내는 편이 유지보수에 유리합니다.
함정 2: 파이프라인에서 앞단 실패가 뒤늦게 폭발한다 (pipefail)
pipefail을 켜면, 파이프라인 중간 명령이 실패해도 전체가 실패로 간주됩니다. 이는 보안/정확성 측면에서는 훌륭하지만, “앞단이 실패해도 뒷단이 처리해주길 기대했던” 패턴이 깨집니다.
#!/usr/bin/env bash
set -euo pipefail
# 파일이 없으면 cat은 1을 반환
# pipefail이 없으면 wc -l의 0이 전체 종료코드가 될 수 있음
count=$(cat /no/such/file | wc -l)
echo "count=${count}"
안전한 처리 패턴: 파이프라인을 분해하고 오류를 다룬다
#!/usr/bin/env bash
set -euo pipefail
file="/no/such/file"
if [[ -f "$file" ]]; then
count=$(wc -l < "$file")
else
count=0
fi
echo "count=${count}"
파이프라인을 분해하면 “무엇이 실패해도 괜찮은지”를 분기에서 명시할 수 있어 예외가 안전해집니다.
함정 3: -u로 인해 환경변수 기본값 처리조차 실패한다
-u는 매우 유용하지만, 선택적 환경변수를 자주 쓰는 배포 스크립트에서는 빈번히 터집니다.
#!/usr/bin/env bash
set -euo pipefail
# DEPLOY_ENV가 없으면 바로 종료
echo "env is $DEPLOY_ENV"
안전한 처리 패턴: 파라미터 확장으로 기본값/필수값 강제
#!/usr/bin/env bash
set -euo pipefail
# 기본값
DEPLOY_ENV="${DEPLOY_ENV:-dev}"
# 필수값(없으면 메시지와 함께 종료)
AWS_REGION="${AWS_REGION:?AWS_REGION is required}"
echo "DEPLOY_ENV=$DEPLOY_ENV"
echo "AWS_REGION=$AWS_REGION"
이 방식은 “필수 설정 누락”을 빠르게 잡아주므로 CI에서 특히 유용합니다. 예를 들어 권한/자격증명 누락으로 배포가 실패하는 경우엔 GitHub Actions OIDC AssumeRole 실패 해결 가이드 같은 체크리스트와 함께 보면 원인 좁히기가 빨라집니다.
함정 4: set -e는 생각보다 예외가 많고, 그래서 더 위험하다
set -e는 “항상 실패하면 종료”가 아닙니다. 조건식 내부, &&/|| 리스트, if의 조건, while 조건 등에서는 동작이 달라집니다. 이 일관성 부족 때문에 “어떤 실패는 죽고 어떤 실패는 안 죽는” 스크립트가 만들어지고, 그게 더 위험해집니다.
대표적으로 아래는 많은 사람이 기대와 다르게 이해합니다.
#!/usr/bin/env bash
set -euo pipefail
false && echo "never"
echo "여기는 실행됨"
false가 실패해도 && 리스트의 일부이기 때문에 즉시 종료되지 않습니다. 반대로, 같은 false가 단독으로 실행되면 종료됩니다.
권장: set -e에만 기대지 말고, 명시적 에러 핸들링을 추가
- “실패하면 종료”해야 하는 구간은
cmd뒤에 명시적으로 메시지를 붙입니다. - “실패해도 괜찮음”은
if로 감싸 의도를 표현합니다.
#!/usr/bin/env bash
set -euo pipefail
die() {
echo "ERROR: $*" >&2
exit 1
}
run() {
"$@" || die "command failed: $*"
}
run mkdir -p /tmp/app
run touch /tmp/app/ready
이 패턴은 로그 품질을 올려서, 장애 시 원인 파악 시간을 줄여줍니다. 특히 컨테이너 환경에서 종료 코드만 남기고 로그가 빈약하면 CrashLoopBackOff로 이어지기 쉬우니(원인 탐색이 어려움), 실패 메시지를 남기는 습관이 중요합니다.
함정 5: trap과 결합하지 않으면 “어디서 죽었는지” 모른다
set -e로 조기 종료는 되지만, 어디서 죽었는지 모르면 운영에서는 의미가 없습니다. trap을 붙여 스택과 라인 정보를 남기면 디버깅이 급격히 쉬워집니다.
#!/usr/bin/env bash
set -euo pipefail
on_err() {
local exit_code="$?"
local line_no="${BASH_LINENO[0]}"
local cmd="${BASH_COMMAND}"
echo "ERROR: exit_code=${exit_code} line=${line_no} cmd=${cmd}" >&2
}
trap on_err ERR
step1() {
echo "step1"
}
step2() {
echo "step2"
grep -q "needle" /tmp/no-such-file
}
step1
step2
추가로 set -E (errtrace)를 켜면 함수/서브셸에서도 ERR 트랩 전파가 더 잘 됩니다.
set -Eeuo pipefail
trap on_err ERR
안전한 예외처리의 핵심: “예외는 지역화(localize)하고, 전역 상태를 건드리지 않는다”
실무에서 가장 많이 하는 실수는, 특정 구간에서만 -e를 끄려고 전역으로 set +e를 해놓고 다시 켜는 것을 잊는 것입니다. 이러면 스크립트의 안전성이 구간에 따라 달라지고, 리뷰어도 놓치기 쉽습니다.
패턴 1: 예외가 필요한 명령을 if로 감싼다
#!/usr/bin/env bash
set -euo pipefail
# 파일이 없으면 생성하는 로직: 실패가 아니라 분기
file="/tmp/app.conf"
if [[ ! -f "$file" ]]; then
echo "creating $file"
printf '%s\n' "key=value" > "$file"
fi
패턴 2: “실패를 값으로 다루는” 함수로 캡슐화
예외를 허용하되, 그 결과를 명확히 반환값으로 받아 처리합니다.
#!/usr/bin/env bash
set -euo pipefail
try_grep() {
local pattern="$1"
local file="$2"
if grep -q "$pattern" "$file"; then
return 0
else
return 1
fi
}
if try_grep "enabled=true" "/etc/myapp.conf"; then
echo "enabled"
else
echo "disabled or missing (ok)"
fi
패턴 3: 서브셸로 옵션 변경을 격리한다
특정 블록에서만 -e를 끄고 싶다면, 전역을 오염시키지 말고 서브셸로 격리합니다.
#!/usr/bin/env bash
set -euo pipefail
# 이 블록 안에서만 -e를 끈다(바깥에는 영향 없음)
(
set +e
curl -fsS "https://example.invalid/health" >/dev/null 2>&1
rc="$?"
if [[ "$rc" -ne 0 ]]; then
echo "health check failed but continuing (rc=$rc)" >&2
fi
)
echo "main flow continues safely"
서브셸은 변수 변경이 바깥으로 나오지 않는다는 점도 장점입니다.
실전 템플릿: 안전한 Bash 스크립트 골격
아래는 운영/CI에서 자주 쓰는 “기본 골격”입니다.
- 엄격 모드:
set -Eeuo pipefail - 에러 트랩: 라인/커맨드/코드 출력
- 필수 변수 강제:
${VAR:?} - 예외는
if또는 서브셸로 지역화
#!/usr/bin/env bash
set -Eeuo pipefail
on_err() {
local code="$?"
echo "ERROR: code=$code line=${BASH_LINENO[0]} cmd=${BASH_COMMAND}" >&2
}
trap on_err ERR
die() {
echo "ERROR: $*" >&2
exit 1
}
# required config
ENV_NAME="${ENV_NAME:?ENV_NAME is required}"
log() {
printf '%s\n' "$*" >&2
}
main() {
log "starting deploy for ENV_NAME=$ENV_NAME"
# 예외 허용: 없으면 0으로 처리
local count
if [[ -f "/tmp/items.txt" ]]; then
count=$(wc -l < "/tmp/items.txt")
else
count=0
fi
log "items=$count"
# 실패하면 즉시 중단되어야 하는 커맨드는 명시적으로 메시지 강화
mkdir -p "/tmp/app" || die "failed to create /tmp/app"
}
main "$@"
체크리스트: set -euo pipefail을 쓸 때의 운영 관점 점검
- “결과 없음”을 의미하는 종료 코드
1이 존재하는 명령(grep등)을 조건식으로 감쌌는가 - 파이프라인은 가능한 분해했는가, 또는
pipefail로 인해 실패가 전파되는 것이 의도인가 - 선택적 환경변수는
${VAR:-default}로, 필수 변수는${VAR:?}로 처리했는가 trap ERR로 죽은 위치를 남기는가 (set -E포함)- 예외 처리를 위해
set +e를 전역으로 켰다 껐다 하지 않았는가(서브셸로 격리했는가)
set -euo pipefail은 “켜면 안전해진다”가 아니라, “실패와 예외의 경계를 코드로 더 명확히 그리도록 강제한다”에 가깝습니다. 예외를 숨기지 말고, 예외를 의도적으로 표현하는 스크립트가 결국 가장 안전합니다.