infra
Platform

모듈 맵

[Infra Ops] 실무 백엔드 자동화를 위한 Bash 쉘 스크립트 작성 기법

0 / 52 완료

펼치기
0 / 52 완료0%

인프라 운영 & SRE · 05 / 52

[Infra Ops] 실무 백엔드 자동화를 위한 Bash 쉘 스크립트 작성 기법

인프라 엔지니어가 현장에서 실제로 쓰는 Shell 스크립트 패턴 — 변수, 조건문, 반복문, 에러 처리, 다중 서버 점검, 배포 자동화를 작성합니다

🚨INCIDENT ALERT
HIGH

배포는 항상 긴장됩니다. 서버 3대에 WAR 파일을 배포하고, 각각 서비스를 재시작하고, 로그를 확인하고, 문제가 있으면 롤백해야 합니다. 매번 수동으로 하다 보면 한 서버를 빠뜨리거나 순서를 틀리는 실수가 납니다.

스크립트로 자동화하면 사람의 실수가 사라지고, 실행 기록이 남고, 언제든 같은 방식으로 반복할 수 있습니다.

이번 챕터에서 배울 것
  • 1변수, 인자($1/$2), 조건문([[ ]]), 반복문(for/while/read)을 올바르게 작성할 수 있다
  • 2set -e와 set -o pipefail로 에러 안전한 스크립트를 만들 수 있다
  • 3trap으로 스크립트 실패 시 정리 작업을 수행할 수 있다
  • 4logger로 스크립트 실행 기록을 syslog에 남길 수 있다
  • 5디스크 임계치 알림, WAR 배포 자동화, 서비스 기동 점검 스크립트를 작성할 수 있다
실습 환경 준비
bash 버전 확인
bash --version | head -1
스크립트 실습 디렉터리 생성
mkdir -p ~/scripts && ls -la ~/scripts
logger 동작 확인
logger -t test 'shell scripting module started' && sudo tail -2 /var/log/syslog 2>/dev/null || sudo tail -2 /var/log/messages 2>/dev/null

스크립트 기본 구조

💡개념

셸이 스크립트 한 줄을 실행하는 순서 — 읽기부터 종료코드까지 5단계

cp $SRC "$DST" 한 줄을 셸은 그냥 실행하지 않습니다. 줄을 읽고 → 토큰으로 파싱하고(인용부호 해석) → 확장(변수·글롭·명령치환)을 정해진 순서로 펼친 뒤 → 명령을 실행하고 → 종료코드를 남깁니다. 이 순서를 알면 따옴표를 왜 씌우는지, set -e가 왜 어떤 자리에서는 안 멈추는지, cron에서 왜 경로가 깨지는지가 한꺼번에 설명됩니다. 셸 스크립트 사고의 대부분은 문법을 몰라서가 아니라 이 실행·확장 순서를 오해해서 납니다.

TEXT
[셸]  cp "$SRC"/*.war "$DST"
   │
   ① 읽기·라인 분리        (스크립트를 줄 단위로 읽어 명령을 구성)
   │
   ② 파싱·토큰화           (단어·연산자 분리, 인용부호 해석)
   │
   ③ 확장(정해진 순서)      (중괄호 → 틸드 → 변수·명령치환·산술 → 단어분리 → 글롭)
   │    → 이 시점에 "$SRC"가 실제 경로로, *.war가 실제 파일 목록으로 바뀜
   │
   ④ 명령 실행            (PATH에서 cp를 찾아 확장된 인자로 실행)
   │
   ⑤ 종료코드 전파         ($?에 결과 저장 → set -e·|| 가 이 값으로 판단)
   ▼
[셸]  다음 줄로 (set -e면 0이 아닐 때 여기서 종료)

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① 읽기스크립트를 줄 단위로 읽어 하나의 명령으로 구성shebang(#!/usr/bin/env bash) 없거나 CRLF 줄바꿈 → bad interpreter·^M 오류
② 파싱·인용단어·연산자를 나누고 따옴표를 해석따옴표 짝 안 맞음 → unexpected EOF / 작은따옴표 안은 확장 안 됨(의도와 달리 리터럴로)
③ 확장변수·명령치환·글롭을 정해진 순서로 펼침따옴표 없는 $VAR에 공백·빈값 → 단어분리로 인자가 쪼개지고 엉뚱한 글롭 / set -u면 미정의 변수 → unbound variable
④ 실행PATH에서 명령을 찾아 확장된 인자로 실행PATH 누락(cron 환경) → command not found / 실행권한 없음 → Permission denied
⑤ 종료코드결과를 $?에 남기고 다음 흐름이 판단local V=$(실패명령)·파이프 중간·if/&& 앞은 실패해도 $?가 0으로 덮여 set -e가 못 잡음

즉 스크립트 사고의 대부분은 이 순서를 오해한 데서 옵니다 — 확장(③)이 실행(④)보다 먼저라, 따옴표 없는 변수는 이미 여러 인자로 쪼개진 뒤 명령에 넘어갑니다(그래서 rm $f가 위험). 그리고 set -e는 종료코드(⑤)를 보는데, if·&& 앞이나 local V=$(...)는 애초에 그 자리에서 0으로 판정돼 실패가 가려집니다. 이상 동작을 만나면 bash -x로 ③ 확장 결과(펼쳐진 실제 인자)를 눈으로 확인하는 것이 첫 단계입니다 — 대개 변수가 예상과 다르게 확장돼 있습니다. 아래에서는 이 종료코드(⑤)를 안전장치로 삼는 스크립트 기본 틀부터 봅니다.

💡개념

에러 안전한 스크립트 기본 틀

인프라 스크립트는 실패해도 멈추지 않으면 더 큰 사고를 만들 수 있습니다. set -e 없는 배포 스크립트는 중간 단계가 실패해도 다음 명령을 계속 실행하고, 그 결과는 종종 서비스 장애로 이어집니다. 어떤 상황에서도 안전하게 동작하는 스크립트를 만들려면 처음부터 올바른 틀 위에서 시작해야 합니다.

Shell 스크립팅 — 안전한 스크립트 구조와 실무 패턴확대

배포 스크립트가 WAR 파일 복사에 실패했는데 오류를 무시하고 Tomcat을 재시작해버렸습니다. 이전 WAR 파일로 서비스가 올라왔고, "배포가 됐는데 변경 사항이 반영이 안 돼요"라는 제보가 들어왔습니다. set -e가 없는 스크립트는 중간에 실패해도 멈추지 않습니다. 로그도 없으면 언제, 어디서 실패했는지 추적조차 불가능합니다. 모든 인프라 스크립트는 이 틀로 시작합니다.

로컬 터미널
#!/usr/bin/env bash
# 파일명: deploy-app.sh
# 설명: 애플리케이션 배포 스크립트

# --- 안전장치 ---
set -e              # 명령어 실패 시 즉시 종료
set -o pipefail     # 파이프라인 중간 실패도 감지
set -u              # 정의되지 않은 변수 사용 시 오류

# --- 상수 정의 ---
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_TAG="deploy"
readonly TIMESTAMP="$(date +%Y%m%d_%H%M%S)"

# --- 함수 정의 ---
log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
  logger -t "$LOG_TAG" "$*"    # syslog에도 기록
}

error() {
  echo "[ERROR] $*" >&2
  logger -t "$LOG_TAG" "ERROR: $*"
  exit 1
}

# --- 트랩 설정 (스크립트 종료 시 항상 실행) ---
cleanup() {
  local exit_code=$?
  if [[ $exit_code -ne 0 ]]; then
    log "Script failed with exit code $exit_code"
  fi
}
trap cleanup EXIT

# --- 인자 검증 ---
if [[ $# -lt 1 ]]; then
  error "Usage: $SCRIPT_NAME <war_file>"
fi

WAR_FILE="$1"

if [[ ! -f "$WAR_FILE" ]]; then
  error "File not found: $WAR_FILE"
fi

log "Starting deployment: $WAR_FILE"
# ... 실제 작업
log "Deployment completed"

왜 이 구조가 중요한가:

  • set -e: 중간에 실패해도 계속 진행하지 않음 → 망가진 상태 방지
  • set -u: $DEPLOY_DIR을 잘못 쓰면 의도치 않게 /에 작업하는 사고 방지
  • trap cleanup EXIT: 스크립트가 어떻게 종료되든 정리 작업 실행
  • logger: 크론탭으로 실행된 경우도 흔적이 남음

Shell 스크립트 안전 기본 틀 — set -euo pipefail확대

💡개념

변수, 인자, 조건문, 반복문

서버 10대에 동일한 명령을 수동으로 실행하다가 한 대를 빠뜨렸습니다. 다음 날 그 서버만 구버전으로 돌아가고 있었고, 버그 수정이 반영되지 않아 오류가 계속 나왔습니다. 반복문으로 서버 목록을 돌리고 조건문으로 이상 유무를 체크했다면 이 실수는 구조적으로 방지됩니다. 변수와 흐름 제어를 제대로 쓰지 못하면 스크립트는 반자동화에 머뭅니다.

변수와 인자:

로컬 터미널
# 변수 선언 (따옴표 항상 사용)
APP_DIR="/opt/tomcat"
APP_NAME="myapp"
MAX_RETRY=3

# 인자 처리
SCRIPT_NAME="$0"    # 스크립트 이름
ARG1="$1"           # 첫 번째 인자
ARG2="${2:-default}" # 두 번째 인자 (없으면 "default")
ARG_COUNT="$#"      # 인자 개수

# 명령어 출력을 변수에 저장
CURRENT_DATE=$(date +%Y%m%d)
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')

echo "오늘 날짜: $CURRENT_DATE, 디스크 사용: ${DISK_USAGE}%"

조건문:

로컬 터미널
# 파일/디렉터리 존재 확인
if [[ -f "/opt/app/config.yml" ]]; then
  echo "설정 파일 존재"
elif [[ -d "/opt/app" ]]; then
  echo "디렉터리만 존재"
else
  echo "경로 없음"
fi

# 변수 비어있는지 확인
if [[ -z "$WAR_FILE" ]]; then
  error "WAR_FILE이 비어있습니다"
fi

# 숫자 비교
if [[ "$DISK_USAGE" -ge 85 ]]; then
  echo "경고: 디스크 사용률 ${DISK_USAGE}%"
fi

# 문자열 비교
if [[ "$ENV" == "production" ]]; then
  echo "운영 환경"
fi

# 명령어 성공/실패 확인 (set -e 없이 수동 처리)
if systemctl is-active --quiet nginx; then
  echo "nginx 실행 중"
else
  echo "nginx 중지됨"
fi

반복문:

로컬 터미널
# for 반복문
for server in web01 web02 was01 was02; do
  echo "점검 중: $server"
  ssh "$server" "uptime && df -h / | tail -1"
done

# 파일 목록 반복
for war_file in /tmp/*.war; do
  [[ -f "$war_file" ]] || continue    # 파일 없으면 건너뜀
  echo "처리: $war_file"
done

# while + read (파일에서 서버 목록 읽기)
while IFS= read -r server; do
  [[ -z "$server" || "$server" =~ ^# ]] && continue  # 빈 줄, 주석 건너뜀
  echo "접속: $server"
  ssh -o ConnectTimeout=5 "$server" "hostname"
done < /etc/server-list.txt

# C-style for (카운터)
for ((i=1; i<=3; i++)); do
  echo "시도 $i/3"
  curl -sf http://localhost:8080/health && break
  sleep 5
done
스크립트 기본 문법 실습
1

1. set -euo pipefail 동작 확인

bash -c 'set -euo pipefail; echo "시작"; false; echo "이 줄은 출력 안 됨"'; echo "종료 코드: $?"
2

2. 변수와 조건문 실습

DISK_USE=$(df / | awk 'NR==2 {print $5}' | tr -d '%'); echo "루트 파티션: ${DISK_USE}%"; [[ $DISK_USE -gt 5 ]] && echo '사용 중' || echo '거의 비어 있음'
3

3. 반복문으로 다중 서비스 점검

for svc in nginx sshd cron; do systemctl is-active "$svc" &>/dev/null && echo "$svc: 실행 중" || echo "$svc: 중지됨"; done
🔍실행 후 확인할 것
  • set -e 스크립트에서 false 명령어 실행 후 echo가 출력되지 않고 즉시 종료되는지 먼저 확인 — 출력이 나오면 set -e가 적용되지 않은 것으로 #!/usr/bin/env bash 다음 줄에 위치했는지 점검
  • DISK_USE=${use%%%} 패턴에서 변수에 숫자만 남아야 정상 — % 기호가 남아있으면 [ "$pct" -gt "$THRESHOLD" ] 비교에서 integer expression error 발생
  • 반복문에서 실제 서버에 없는 서비스가 "중지됨"으로 표시되면 정상 — "명령어 없음" 오류가 나면 systemctl이 PATH에 없거나 sudo 권한 필요. 서비스 5개 이상 점검 시 병렬 실행(&) 고려

실무 스크립트 3가지

💡개념

실무 스크립트 1: 디스크 임계치 알림

매일 아침 수동으로 df -h를 확인하다가 하루를 빠뜨렸는데, 그날 디스크가 100%가 됐습니다. 로그가 넘쳐서 새 파일이 만들어지지 않았고, Tomcat이 로그를 쓰지 못해 장애로 이어졌습니다. 임계치를 자동으로 감지해서 알려주는 스크립트가 있었다면 아무도 없던 새벽에 미리 알 수 있었습니다.

로컬 터미널
#!/usr/bin/env bash
# disk-check.sh — 디스크 사용률 점검 및 알림

set -euo pipefail

readonly THRESHOLD=85    # 경고 기준 (%)
readonly CRITICAL=95     # 위험 기준 (%)
readonly LOG_TAG="disk-check"
readonly HOSTNAME=$(hostname)

log() { logger -t "$LOG_TAG" "$*"; echo "[$(date '+%H:%M:%S')] $*"; }

check_disk() {
  local mount_point="$1"
  local usage

  usage=$(df -h "$mount_point" | awk 'NR==2 {print $5}' | tr -d '%')

  if [[ "$usage" -ge "$CRITICAL" ]]; then
    log "CRITICAL: $HOSTNAME $mount_point 디스크 ${usage}% — 즉시 조치 필요"
    # 실제 환경: curl -X POST $SLACK_WEBHOOK -d '{"text":"..."}'
    return 1
  elif [[ "$usage" -ge "$THRESHOLD" ]]; then
    log "WARNING: $HOSTNAME $mount_point 디스크 ${usage}%"
    return 0
  else
    log "OK: $mount_point ${usage}%"
    return 0
  fi
}

# inode 사용률도 함께 체크
check_inode() {
  local mount_point="$1"
  local inode_usage

  inode_usage=$(df -i "$mount_point" | awk 'NR==2 {print $5}' | tr -d '%')

  if [[ "$inode_usage" -ge "$THRESHOLD" ]]; then
    log "WARNING: $HOSTNAME $mount_point inode ${inode_usage}%"
  fi
}

# 점검할 마운트 포인트 목록
MOUNT_POINTS=("/" "/var" "/opt" "/data")

exit_code=0
for mp in "${MOUNT_POINTS[@]}"; do
  # 마운트 포인트가 존재하는지 확인
  mountpoint -q "$mp" 2>/dev/null || continue
  check_disk "$mp" || exit_code=1
  check_inode "$mp"
done

exit $exit_code
로컬 터미널
# crontab에 등록 (매 10분마다 실행)
# crontab -e
*/10 * * * * /opt/scripts/disk-check.sh >> /var/log/disk-check.log 2>&1
디스크 점검 스크립트 작성 실습
1

1. 스크립트 디렉터리 생성

mkdir -p ~/scripts && echo '디렉터리 준비 완료'
2

2. 디스크 점검 스크립트 생성

cat > ~/scripts/disk-check.sh << 'EOF' #!/usr/bin/env bash set -euo pipefail THRESHOLD=${1:-80} echo "=== 디스크 점검 (임계: ${THRESHOLD}%) ===" df -h | awk 'NR>1 && /^\/dev/' | while read fs size used avail use mp; do pct=${use%%%} if [[ $pct -ge $THRESHOLD ]]; then echo "[경고] $mp: ${pct}% (용량: $size, 사용: $used)" else echo "[정상] $mp: ${pct}%" fi done EOF chmod +x ~/scripts/disk-check.sh && echo '생성 완료'
3

3. 스크립트 실행

bash ~/scripts/disk-check.sh 50
🔍실행 후 확인할 것
  • chmod +x 없이 실행 시 Permission denied 오류 먼저 확인 — ls -la 로 실행 권한(-rwxr-xr-x)이 있는지 본 다음 스크립트 내용 점검으로 넘어간다
  • 임계값 50으로 실행 시 50% 이상 파티션이 "[경고]"로 표시되면 정상 — 아무것도 표시 안 되면 df -h 파싱이 잘못된 것. pct=${use%%%} 위치가 use 변수 할당 이후인지 확인
  • pct에 숫자 대신 "%"가 포함된 채로 비교하면 integer expression expected 오류 — 이 오류는 조용히 스크립트를 종료하지 않아 잘못된 알림이 발송됨. $1로 임계값을 인자로 받으면 재사용성과 테스트 용이성 모두 높아짐
💡개념

실무 스크립트 2: WAR 배포 자동화

배포할 때마다 "백업, 중지, 복사, 시작, 확인"을 수동으로 하다 보면 한 단계씩 빠뜨리게 됩니다. 백업을 빠뜨리면 배포 후 문제가 생겼을 때 이전 버전으로 돌아가지 못합니다. 확인 단계가 없으면 서비스가 실제로 살아났는지 모른 채 배포가 완료됐다고 보고하게 됩니다. 이 스크립트 하나로 사람의 실수를 없애고 동일한 순서를 보장합니다.

로컬 터미널
#!/usr/bin/env bash
# deploy-war.sh — Tomcat WAR 파일 배포 스크립트
# 사용: ./deploy-war.sh /tmp/myapp-1.2.3.war [서버명]

set -euo pipefail

# --- 설정 ---
readonly TOMCAT_DIR="/opt/tomcat"
readonly WEBAPPS_DIR="$TOMCAT_DIR/webapps"
readonly BACKUP_DIR="$TOMCAT_DIR/webapps-backup"
readonly TOMCAT_SERVICE="tomcat"
readonly TOMCAT_USER="tomcat"
readonly LOG_TAG="deploy-war"
readonly TIMESTAMP=$(date +%Y%m%d_%H%M%S)

log()   { echo "[$(date '+%H:%M:%S')] $*"; logger -t "$LOG_TAG" "$*"; }
error() { echo "[ERROR] $*" >&2; logger -t "$LOG_TAG" "ERROR: $*"; exit 1; }

# 인자 검증
[[ $# -ge 1 ]] || error "Usage: $(basename $0) <war_file>"
WAR_FILE="$1"
[[ -f "$WAR_FILE" ]] || error "WAR 파일 없음: $WAR_FILE"

APP_NAME=$(basename "$WAR_FILE" .war)  # myapp-1.2.3
log "배포 시작: $APP_NAME"

# 1. 사전 백업
mkdir -p "$BACKUP_DIR"
if [[ -d "$WEBAPPS_DIR/$APP_NAME" ]]; then
  tar -czf "$BACKUP_DIR/${APP_NAME}_${TIMESTAMP}.tar.gz" \
    -C "$WEBAPPS_DIR" "$APP_NAME"
  log "기존 버전 백업: ${APP_NAME}_${TIMESTAMP}.tar.gz"
fi
if [[ -f "$WEBAPPS_DIR/${APP_NAME}.war" ]]; then
  cp "$WEBAPPS_DIR/${APP_NAME}.war" \
     "$BACKUP_DIR/${APP_NAME}.war.${TIMESTAMP}"
fi

# 2. Tomcat 중지
log "Tomcat 중지 중..."
sudo systemctl stop "$TOMCAT_SERVICE"

# 중지 확인 (최대 30초 대기)
for i in $(seq 1 30); do
  if ! systemctl is-active --quiet "$TOMCAT_SERVICE"; then
    break
  fi
  sleep 1
done
systemctl is-active --quiet "$TOMCAT_SERVICE" && error "Tomcat 중지 실패"

# 3. 기존 배포 디렉터리 삭제 (war 파일 자동 압축 해제 방지)
TARGET_DIR="${WEBAPPS_DIR}/${APP_NAME}"
case "$TARGET_DIR" in
  /opt/tomcat/webapps/*) ;;
  *) error "허용되지 않은 배포 경로: $TARGET_DIR" ;;
esac
test -n "$APP_NAME" && test "$APP_NAME" != "ROOT" || error "APP_NAME 확인 필요"
test "$(readlink -f "$TARGET_DIR")" = "$TARGET_DIR" || error "심볼릭 링크/경로 검증 실패"
test -d "$TARGET_DIR" && rm -rf -- "$TARGET_DIR"
rm -f "$WEBAPPS_DIR/${APP_NAME}.war"

# 4. WAR 파일 복사 및 권한 설정
log "WAR 파일 배포 중..."
cp "$WAR_FILE" "$WEBAPPS_DIR/"
chown "${TOMCAT_USER}:${TOMCAT_USER}" "$WEBAPPS_DIR/${APP_NAME}.war"
chmod 644 "$WEBAPPS_DIR/${APP_NAME}.war"

# 5. Tomcat 시작
log "Tomcat 시작 중..."
sudo systemctl start "$TOMCAT_SERVICE"

# 6. 기동 확인 (최대 60초 대기)
log "서비스 기동 확인 중..."
for i in $(seq 1 60); do
  if curl -sf http://localhost:8080/${APP_NAME}/health > /dev/null 2>&1; then
    log "배포 완료! ($i초 소요)"
    logger -t "$LOG_TAG" "SUCCESS: $APP_NAME deployed in ${i}s"
    exit 0
  fi
  sleep 1
done

# 7. 기동 실패 → 자동 롤백
error "서비스 기동 실패 — 롤백을 수동으로 수행하세요: $BACKUP_DIR"
WAR 배포 스크립트 실습 (시뮬레이션)
1

1. 배포 디렉터리 구조 준비

mkdir -p /tmp/deploy-test/{webapps,backup} && echo 'v1.0-original' > /tmp/deploy-test/webapps/app.war && echo '준비 완료'
2

2. 배포 핵심 로직 실습

WAR_DIR=/tmp/deploy-test/webapps; BACKUP_DIR=/tmp/deploy-test/backup; TS=$(date +%Y%m%d_%H%M%S); [[ -f $WAR_DIR/app.war ]] && cp $WAR_DIR/app.war $BACKUP_DIR/app-${TS}.war && echo "백업 완료: app-${TS}.war"; echo 'v2.0-new' > /tmp/new-app.war; cp /tmp/new-app.war $WAR_DIR/app.war && echo '배포 완료'; ls -la $BACKUP_DIR/
3

3. 배포 검증

cat /tmp/deploy-test/webapps/app.war && echo '' && ls /tmp/deploy-test/backup/
🔍실행 후 확인할 것
  • backup 디렉터리에서 타임스탬프 포함 파일명을 먼저 확인(ls -lh backup/) — 파일이 없으면 백업 단계에서 실패한 것. set -e 없이 작성했으면 cp 실패 후에도 배포가 진행돼 롤백 불가 상태
  • 배포 후 webapps/app.war 내용이 v2.0-new로 변경됐는지 확인 — 변경 안 됐으면 cp 명령의 소스/대상 경로 오류. set -euo pipefail 없는 스크립트는 cp 실패를 무시하고 계속 실행됨
  • || { echo "실패"; exit 1; } 패턴이 각 핵심 명령어(cp, systemctl)마다 있어야 중간 실패 감지 가능 — 패턴 없이 set -e만 의존하면 서브셸이나 파이프에서 발생한 에러를 놓칠 수 있음
💡개념

실무 스크립트 3: 서비스 기동 상태 점검

WAS 서버 8대 중 한 대에서 서비스가 내려가 있었는데

인프라 스크립트 3종 — 용도와 패턴확대 , 로드밸런서가 해당 서버를 계속 살아있다고 판단해 트래픽을 보내고 있었습니다. 수동으로 8대를 일일이 확인하는 루틴을 만들어두지 않아서 30분 뒤에야 발견했습니다. 모든 서버를 동시에 점검하고 이상 서버만 출력하는 스크립트가 있으면 문제를 빠르게 찾을 수 있습니다.

로컬 터미널
#!/usr/bin/env bash
# service-check.sh — 다중 서버 서비스 상태 일괄 점검

set -euo pipefail

# --- 설정 ---
readonly SERVER_LIST="/etc/infra/was-servers.txt"  # 서버 목록 파일
readonly SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=yes -o BatchMode=yes"
readonly LOG_TAG="service-check"

log() { logger -t "$LOG_TAG" "$*"; }

# 단일 서버 점검 함수
check_server() {
  local server="$1"
  local result=""
  local status="OK"

  # SSH로 원격 점검 명령 실행
  result=$(ssh $SSH_OPTS "$server" '
    # 서비스 상태
    TOMCAT=$(systemctl is-active tomcat 2>/dev/null || echo "unknown")
    NGINX=$(systemctl is-active nginx 2>/dev/null || echo "not-installed")

    # 헬스체크 URL 응답
    HEALTH=$(curl -sf -o /dev/null -w "%{http_code}" http://localhost:8080/health 2>/dev/null || echo "fail")

    # 리소스 상태
    DISK=$(df -h / | awk "NR==2{print \$5}")
    MEM_AVAIL=$(free -m | awk "NR==2{print \$7}")
    LOAD=$(uptime | awk -F"average:" "{print \$2}" | awk "{print \$1}" | tr -d ",")

    echo "tomcat=${TOMCAT} nginx=${NGINX} health=${HEALTH} disk=${DISK} mem_avail=${MEM_AVAIL}MB load=${LOAD}"
  ' 2>/dev/null || echo "SSH_FAILED")

  # 결과 파싱 및 이상 여부 판단
  if [[ "$result" == "SSH_FAILED" ]]; then
    status="SSH_FAIL"
  elif echo "$result" | grep -q "tomcat=failed\|tomcat=inactive"; then
    status="TOMCAT_DOWN"
  elif echo "$result" | grep -q "health=fail\|health=503\|health=502"; then
    status="HEALTH_FAIL"
  fi

  printf "%-15s %-10s %s\n" "$server" "[$status]" "$result"
  log "$server $status: $result"
}

# 서버 목록 파일 생성 (없는 경우 예시)
if [[ ! -f "$SERVER_LIST" ]]; then
  echo "서버 목록 파일 없음: $SERVER_LIST"
  echo "예시 파일 형식:"
  echo "  10.0.1.11  # was01-prd"
  echo "  10.0.1.12  # was02-prd"
  exit 0
fi

echo "=================================================="
echo " 서비스 기동 상태 점검 — $(date '+%Y-%m-%d %H:%M:%S')"
echo "=================================================="
printf "%-15s %-10s %s\n" "SERVER" "STATUS" "DETAIL"
echo "--------------------------------------------------"

FAIL_COUNT=0
while IFS= read -r line; do
  # 빈 줄, 주석 건너뜀
  [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue

  server=$(echo "$line" | awk '{print $1}')
  result_line=$(check_server "$server")
  echo "$result_line"

  echo "$result_line" | grep -qv '\[OK\]' && ((FAIL_COUNT++)) || true
done < "$SERVER_LIST"

echo "=================================================="
echo "점검 완료: 이상 서버 ${FAIL_COUNT}대"
log "점검 완료: 이상 ${FAIL_COUNT}대"

[[ "$FAIL_COUNT" -gt 0 ]] && exit 1 || exit 0
로컬 터미널
# 사용법
chmod +x ~/scripts/service-check.sh

# 직접 실행
~/scripts/service-check.sh

# crontab — 매 5분마다 실행
*/5 * * * * /opt/scripts/service-check.sh >> /var/log/service-check.log 2>&1
디스크 점검 스크립트 실습
1

스크립트 파일 생성

cat > ~/scripts/disk-simple.sh << 'EOF' #!/usr/bin/env bash set -euo pipefail THRESHOLD=80 df -h | awk 'NR>1 && /^\/dev/' | while read fs size used avail use mp; do pct=${use%%%} if [[ $pct -ge $THRESHOLD ]]; then echo "WARNING: $mp 디스크 ${pct}% (임계: ${THRESHOLD}%)" logger -t disk-check "WARNING: $(hostname) $mp ${pct}%" fi done EOF chmod +x ~/scripts/disk-simple.sh
2

스크립트 실행 테스트

bash ~/scripts/disk-simple.sh; echo 'Exit code:' $?
3

syslog 기록 확인

logger -t test-script 'practice session running' && sudo grep 'test-script' /var/log/syslog 2>/dev/null || sudo grep 'test-script' /var/log/messages 2>/dev/null || journalctl -t test-script -n 3 --no-pager
4

set -e 동작 확인

bash -c 'set -e; echo start; false; echo this_should_not_print'; echo "exit: $?"
5

조건문 및 파일 확인 실습

FILE=/etc/hostname; if [[ -f $FILE ]]; then echo "$FILE 존재: $(cat $FILE)"; else echo "$FILE 없음"; fi
🔍실행 후 확인할 것
  • disk-simple.sh 실행 시 출력을 먼저 확인 — 80% 이상 파티션에 WARNING이 나오면 정상. 아무 출력도 없으면 df -h 파싱 실패 또는 임계값 조건이 맞지 않는 것
  • bash -c "set -e; false; echo test" 실행 시 "test"가 출력되지 않아야 set -e 정상 동작 — 출력되면 false가 파이프나 && 뒤에 있어 set -e가 무시된 구조
  • logger 실행 후 journalctl -t <태그명> --since "1 min ago"로 즉시 확인 — 로그가 없으면 systemd-journald가 실행 중인지 확인. 로그가 있고 Exit code: 0이면 스크립트 정상 완료로 cron 등록 준비 완료

심화 — 안전장치의 한계: set -e는 만능이 아니다

💡개념

심화: set -e의 함정 — 켜 놨는데도 안 멈추는 자리들

set -euo pipefail을 스크립트 맨 위에 넣는 습관은 옳습니다. 하지만 그것을 '무조건 실패하면 멈추는 스위치'로 믿으면, 정작 실패가 조용히 지나가는 자리에서 사고가 납니다. set -e일부러 발동하지 않는 경우들을 알아야 진짜 안전한 스크립트가 됩니다.

  • 조건으로 쓰인 명령: if cmd; then ..., while cmd; do ...cmd는 실패해도 종료되지 않습니다(성공/실패를 판단하는 게 목적이니 당연). until, ! 뒤도 마찬가지입니다.
  • &&·|| 체인의 앞부분: cmd1 && cmd2에서 cmd1이 실패하면 set -e가 안 잡고 그냥 cmd2를 건너뜁니다. A || B 형태도 A의 실패를 '처리된 것'으로 봅니다.
  • 파이프라인 중간: pipefail이 없으면 a | b의 exit code는 마지막 b만 따릅니다. pipefail을 켜야 중간 실패도 잡힙니다(이 모듈 본문에서 다룬 이유).
  • local/declare와 명령 치환의 결합: 가장 악명 높은 함정입니다. local OUT=$(cmd)cmd가 실패해도 local 선언 자체의 exit code(0) 가 최종값이 돼 $?가 0으로 덮입니다. 선언과 대입을 나눠 local OUT; OUT=$(cmd)로 써야 cmd의 실패가 드러납니다.

그래서 성숙한 스크립트는 set -e에만 기대지 않고, 정말 실패하면 안 되는 핵심 단계에는 명시적으로 || error "..." 를 붙입니다. set -e는 '깜빡한 실패'를 잡는 그물이지, 설계된 방어선이 아닙니다.

상황: 배포 스크립트에 set -euo pipefail을 분명히 넣었습니다. 그런데 헬스체크 URL이 죽어 있었는데도 스크립트가 멈추지 않고 끝까지 진행돼 '배포 성공'으로 보고했습니다. 정작 실패해야 할 곳에서 안 멈춘 것입니다.

원인: 문제의 줄이 함수 안에서 local STATUS=$(curl -fs "$HEALTH_URL") 형태였습니다. curl -f는 HTTP 오류에서 실패를 반환하지만, local STATUS=$(...)처럼 선언과 명령 치환을 한 줄에 쓰면 최종 exit code가 local 명령의 성공(0)으로 덮여 set -e가 실패를 못 봅니다. 결과적으로 죽은 헬스체크가 '통과'로 처리됐습니다.

진단: 실패가 무시된 정확한 줄을 좁힙니다. bash -x script.sh로 추적하면 curl이 비정상 종료했는데도 흐름이 이어지는 지점이 보입니다. local/declare$(...)가 한 줄에 있는 곳, 그리고 if·&&·파이프 안에 숨은 핵심 명령을 훑습니다.

해결: 선언과 대입을 분리합니다 — local STATUS; STATUS=$(curl -fs "$HEALTH_URL"). 그러면 curl 실패가 그대로 드러나 set -e가 멈춥니다. 더 확실하게는 핵심 단계에 명시적 검증을 답니다: curl -fs "$HEALTH_URL" || error "헬스체크 실패". set -e는 보조 그물일 뿐, 배포 성패를 가르는 단계는 손으로 한 번 더 확인하는 게 안전합니다.

💼
실무 맥락
현업 패턴

실제 업무에서 이 지식이 쓰이는 상황:

인프라 엔지니어가 작성하는 스크립트는 크게 세 종류입니다:

  1. 일상 점검 스크립트 (disk-check, service-check): 크론탭으로 실행, 이상 감지 시 알림
  2. 배포 스크립트 (deploy-war, deploy-static): 배포 프로세스의 표준화, 사람의 실수 제거
  3. 긴급 대응 스크립트 (restart-all, cleanup-logs): 장애 시 빠른 대응용

스크립트를 ~/scripts/ 또는 /opt/scripts/에 정리하고 Git으로 관리하면, 팀원 누구나 같은 방식으로 작업할 수 있고 변경 이력도 추적됩니다.

작은 스크립트부터 시작하세요. 매주 수동으로 하는 작업 하나를 스크립트로 만들면 3개월 후에는 운영의 많은 부분이 자동화됩니다.

명령어·단축키 빠른 참조

에러 안전한 인프라 스크립트 작성에서 쓴 bash 옵션·구문·명령을 모았습니다.

명령어/단축키용도자주 쓰는 예
set -euo pipefail안전 3종(실패 즉시 종료·파이프·미정의 변수)모든 스크립트 2번째 줄에 배치
trap … EXIT종료 시 정리 작업 보장trap cleanup EXIT
logger -t실행 기록을 syslog에 남김logger -t deploy "started", 확인 journalctl -t deploy
[[ … ]]조건 검사(파일·변수·비교)[[ -f "$f" ]], [[ -z "$v" ]], [[ "$n" -ge 85 ]]
${VAR:-기본값}미정의 변수 기본값(set -u 안전)THRESHOLD=${1:-80}
for / while read서버·파일 목록 반복for s in web01 web02; do …, while IFS= read -r s; do … done < list
local VAR; VAR=$(…)명령 치환 실패 감지(한 줄 결합 금지)local OUT; OUT=$(curl -f …)
systemctl is-active --quiet서비스 상태로 분기systemctl is-active --quiet nginx && echo up
curl -sf헬스체크(실패 시 비정상 종료)curl -sf http://localhost:8080/health || error 실패
ssh -o …원격 일괄 점검 옵션ssh -o ConnectTimeout=5 -o BatchMode=yes "$s" uptime
awk / tr -d출력 파싱(사용률 추출)df -h / | awk 'NR==2{print $5}' | tr -d '%'
bash -x스크립트 실행 추적 디버깅bash -x deploy.sh
chmod +x스크립트 실행 권한 부여chmod +x ~/scripts/disk-check.sh
crontab */N * * * *스크립트 정기 실행 등록*/10 * * * * /opt/scripts/disk-check.sh >> log 2>&1

관련 모듈로 더 깊이:

다음 모듈에서는 Web/WAS 구조 — Nginx, Apache, Tomcat의 역할과 실제 설정을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

set -e와 set -o pipefail을 함께 사용하는 이유는?

Q2

logger 명령어를 스크립트에서 사용하는 이유는?

Q3

인프라 스크립트에서 파일 존재 여부 확인에 사용하는 올바른 조건문은?

Q4

while read 루프에서 다중 서버에 명령을 실행할 때 SSH 옵션으로 -o StrictHostKeyChecking=no를 추가하는 이유와 위험성은?

Q5

셸 스크립트에서 변수를 "$var"처럼 따옴표로 감싸는 이유는?

Q6

배포 스크립트를 여러 번 돌려도 안전하게(멱등) 만들려면?

Q7

[심화] set -e가 켜져 있어도 명령 실패가 스크립트를 멈추지 못하는 경우로 옳은 것은?

Q8

[심화] set -euo pipefail을 넣었는데도 함수 안 local OUT=$(curl -f ...)의 실패가 안 잡히고 다음 줄이 실행된다. 원인과 수정은?

0 / 8 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

Linux 서버에 Nginx를 설치하고 systemd 서비스로 등록하여 80포트에서 응답하는 상태까지 만든다.

40📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요