셸 스크립트는 기본적으로 명령이 실패해도 다음 줄을 계속 실행한다. 백업 스크립트에서 덤프가 실패했는데 그다음 줄이 옛 백업을 지워 버리는 식의 사고가 여기서 나온다. 기본값을 "실패하면 멈춘다" 로 바꾸고, 실패해도 괜찮은 줄만 따로 표시하는 편이 안전하다.
#!/usr/bin/env bash
set -euo pipefail
| 옵션 | 동작 |
|---|---|
-e |
명령이 0 이 아닌 코드로 끝나면 스크립트를 종료 |
-u |
정의되지 않은 변수를 참조하면 오류 |
-o pipefail |
파이프라인의 어느 단계가 실패해도 전체를 실패로 봄 |
pipefail 이 없으면 dump | gzip > out.gz 에서 dump 가 죽어도 gzip 이 성공하므로 파이프라인 전체가 성공으로 잡힌다. 빈 파일이 정상 백업으로 남는다.
set -e 는 만능이 아니다. 다음 위치에서는 실패해도 멈추지 않는다.
if, while, until 의 조건부&&, || 의 왼쪽! 로 부정한 명령그래서 반환값을 반드시 봐야 하는 명령은 set -e 에 기대지 말고 직접 검사한다.
# 실패해도 계속 진행
rm -f /tmp/lock || true
# 아무것도 하지 않는 내장 명령 : 를 써도 같다
systemctl stop legacy.service || :
# 실패를 처리하고 넘어가기
if ! curl -fsS "${URL}" -o /tmp/payload; then
echo "download failed, skipping" >&2
else
process /tmp/payload
fi
curl 은 HTTP 404 를 받아도 종료 코드 0 을 내므로 -f 를 반드시 붙인다.
스크립트가 어떤 경로로 끝나든 정리 작업을 보장할 때 쓴다.
TMPDIR="$(mktemp -d)"
cleanup() { rm -rf "${TMPDIR}"; }
trap cleanup EXIT
trap 'echo "failed at line ${LINENO}" >&2' ERR
trap ... ERR 안에서 continue 나 break 를 쓰는 예제를 종종 보는데, 반복문 밖에서 continue 는 동작하지 않으며 오류가 난다. ERR 트랩은 로그를 남기거나 정리하는 용도로 쓰고, 흐름 제어는 if 로 한다.
빌드 산출물의 체크섬을 비교해 변한 경우에만 배포를 이어 간다.
#!/usr/bin/env bash
set -euo pipefail
TARGET="/opt/build/app-$(date +%Y%m%d).jar"
NEW_SHA="/opt/build/new.sha256"
OLD_SHA="/opt/build/old.sha256"
[[ -f "${TARGET}" ]] || { echo "ERROR: ${TARGET} not found" >&2; exit 1; }
# 이전 결과를 밀어 두고 새로 계산한다. 첫 실행이면 비어 있는 파일을 만든다.
[[ -f "${NEW_SHA}" ]] && mv -f "${NEW_SHA}" "${OLD_SHA}" || : > "${OLD_SHA}"
sha256sum "${TARGET}" | awk '{print $1}' > "${NEW_SHA}"
if cmp -s "${NEW_SHA}" "${OLD_SHA}"; then
echo "unchanged. stop."
exit 0
fi
echo "changed. continue."
scp "${TARGET}" deploy@192.168.0.20:/opt/app/
해시 값만 잘라서 저장하는 것이 핵심이다. sha256sum 의 출력은 <해시> <파일명> 형태라 파일명에 날짜가 들어가면 내용이 같아도 줄 전체는 매일 달라진다. 줄 전체를 비교하면 변경이 없어도 항상 "변경됨" 으로 판정한다.
cmp -s 는 내용만 비교하고 아무것도 출력하지 않는다. diff 보다 가볍고 스크립트에 맞는다.