배포는 항상 긴장됩니다. 서버 3대에 WAR 파일을 배포하고, 각각 서비스를 재시작하고, 로그를 확인하고, 문제가 있으면 롤백해야 합니다. 매번 수동으로 하다 보면 한 서버를 빠뜨리거나 순서를 틀리는 실수가 납니다.
스크립트로 자동화하면 사람의 실수가 사라지고, 실행 기록이 남고, 언제든 같은 방식으로 반복할 수 있습니다.
- 1변수, 인자($1/$2), 조건문([[ ]]), 반복문(for/while/read)을 올바르게 작성할 수 있다
- 2set -e와 set -o pipefail로 에러 안전한 스크립트를 만들 수 있다
- 3trap으로 스크립트 실패 시 정리 작업을 수행할 수 있다
- 4logger로 스크립트 실행 기록을 syslog에 남길 수 있다
- 5디스크 임계치 알림, WAR 배포 자동화, 서비스 기동 점검 스크립트를 작성할 수 있다
bash --version | head -1mkdir -p ~/scripts && ls -la ~/scriptslogger -t test 'shell scripting module started' && sudo tail -2 /var/log/syslog 2>/dev/null || sudo tail -2 /var/log/messages 2>/dev/null스크립트 기본 구조
에러 안전한 스크립트 기본 틀
인프라 스크립트는 실패해도 멈추지 않으면 더 큰 사고를 만들 수 있습니다. set -e 없는 배포 스크립트는 중간 단계가 실패해도 다음 명령을 계속 실행하고, 그 결과는 종종 서비스 장애로 이어집니다. 어떤 상황에서도 안전하게 동작하는 스크립트를 만들려면 처음부터 올바른 틀 위에서 시작해야 합니다.

배포 스크립트가 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: 크론탭으로 실행된 경우도 흔적이 남음

변수, 인자, 조건문, 반복문
서버 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
- false 명령어 이후 'echo 이 줄은 출력 안 됨'이 실행되지 않고 바로 종료된다
- DISK_USE 변수에 숫자만 들어가고 % 기호가 제거된 것을 확인
- 반복문 결과에서 실제 서버에 없는 서비스는 '중지됨'으로 표시된다
실무 스크립트 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
- chmod +x 없이 실행하면 Permission denied — 반드시 실행 권한 부여 필요
- 임계값 50으로 실행하면 50% 이상 파티션이 '[경고]'로 표시된다
- pct=${use%%%} 에서 % 기호를 제거하지 않으면 비교 연산자가 오류남
- 스크립트에 인자($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 파일 자동 압축 해제 방지)
rm -rf "$WEBAPPS_DIR/$APP_NAME"
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"
- 백업 파일명에 타임스탬프(YYYYMMDD_HHMMSS)가 붙어 덮어쓰기 방지됨
- 배포 후 webapps/app.war 내용이 'v2.0-new'로 변경된 것을 확인
- backup 디렉터리에 이전 버전이 남아 롤백 가능 상태
- || { echo '실패'; exit 1; } 패턴이 없으면 cp 실패 시에도 스크립트가 계속 진행됨
실무 스크립트 3: 서비스 기동 상태 점검
WAS 서버 8대 중 한 대에서 서비스가 내려가 있었는데
, 로드밸런서가 해당 서버를 계속 살아있다고 판단해 트래픽을 보내고 있었습니다. 수동으로 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=no -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
- disk-simple.sh 실행 시 80% 이상 파티션이 있으면 WARNING 메시지가 출력된다
- bash -c 'set -e; false; echo test' 실행 시 'test'가 출력되지 않고 종료된다
- logger 명령어 실행 후 journalctl 또는 /var/log/syslog에 해당 태그 메시지가 보인다
- 스크립트 실행 후 Exit code: 0이 표시되면 정상 완료
실제 업무에서 이 지식이 쓰이는 상황:
인프라 엔지니어가 작성하는 스크립트는 크게 세 종류입니다:
- 일상 점검 스크립트 (disk-check, service-check): 크론탭으로 실행, 이상 감지 시 알림
- 배포 스크립트 (deploy-war, deploy-static): 배포 프로세스의 표준화, 사람의 실수 제거
- 긴급 대응 스크립트 (restart-all, cleanup-logs): 장애 시 빠른 대응용
스크립트를 ~/scripts/ 또는 /opt/scripts/에 정리하고 Git으로 관리하면, 팀원 누구나 같은 방식으로 작업할 수 있고 변경 이력도 추적됩니다.
작은 스크립트부터 시작하세요. 매주 수동으로 하는 작업 하나를 스크립트로 만들면 3개월 후에는 운영의 많은 부분이 자동화됩니다.
다음 모듈에서는 Web/WAS 구조 — Nginx, Apache, Tomcat의 역할과 실제 설정을 다룹니다.