서버 상태 점검 스크립트

유정빈·2026년 5월 6일

이 글은 2편 시리즈의 1편입니다.

  • 1편: 도구 소개 — 왜 만들었는지, 어떻게 동작하는지
  • 2편: 검증 — stress-ng로 직접 장애를 주입하고 탐지/판정/대응 루프 확인

들어가며

서버를 운영하다 보면 이런 상황이 생각보다 자주 발생합니다.

상황 1. 신규 서버 세팅 후 보안 점검을 한 번도 안 했다

Ubuntu를 설치하면 기본적으로 패스워드 만료 정책이 99999일로 설정되어 있고, 세션 타임아웃도 없습니다. 아무도 신경 쓰지 않으면 그 상태로 몇 달이 지나갑니다.

상황 2. 디스크가 서서히 차오르는데 아무도 몰랐다

df -h로 봤을 때 오늘 60%였던 디스크가 한 달 뒤에 92%가 되어 있습니다. 로그 로테이션을 설정하지 않은 채로 서비스를 올린 결과입니다.

상황 3. CPU가 정상인데 서버가 느리다

top을 켜보면 CPU는 12%입니다. 그런데 응답이 느립니다. wa(I/O Wait)가 40%인 게 문제인데, 이걸 top 화면에서 바로 읽는 사람은 생각보다 많지 않습니다.


이 세 가지 상황의 공통점은 각각의 명령어를 따로따로 보면 놓치기 쉽다는 겁니다.

df -h는 숫자를 보여주지만 지난달과 비교하지 않습니다. top은 지금 이 순간만 보여줍니다. sshd_config는 직접 열어봐야 알 수 있습니다.


이 스크립트가 하는 것

크게 세 가지입니다.

  1. 수집: CPU, 메모리, 디스크, 포트, 보안 설정을 한 번에 수집
  2. 판정: 100점 기준으로 점수를 매기고, 과거 이력과 비교해서 추세 계산
  3. 대응: 문제가 있으면 어떤 명령어로 고치면 되는지 같이 출력

아키텍처: Bash + Python을 왜 같이 쓰는가

./health-check.sh run
         │
         ▼
[Bash] 데이터 수집 (modules/*.sh)
  resource.sh  → CPU, 메모리, 디스크, Load Average
  network.sh   → 포트(22/80/443), 외부 Ping
  security.sh  → SSH 설정, 패스워드 정책, TMOUT, faillock
         │
         │  JSON으로 묶어서 stdin 파이프로 전달
         ▼
[Python] 분석 (analyzer/)
  score.py    → 가중치 × 페널티로 100점에서 차감
  trend.py    → 최근 5회 이력으로 추세 계산 (SPIKE / TREND_UP / STABLE)
  reporter.py → 컬러 CLI 출력, JSON 리포트 저장
         │
         ▼
Exit Code (0=OK / 1=WARNING / 2=CRITICAL / 3=EMERGENCY)

Bash는 top, df, ss 같은 시스템 명령어를 외부 패키지 없이 어디서나 실행할 수 있습니다. Python은 소수점 계산, 표준편차, JSON 직렬화를 Bash보다 훨씬 깔끔하게 처리합니다. 두 언어를 stdin 파이프(|)로 연결해서 각자 잘하는 역할만 맡겼습니다.

역할언어이유
시스템 데이터 수집Bash외부 패키지 없이 모든 Linux에서 동작
점수 계산 / 추세 분석Python복잡한 수식, 유지보수성, JSON 처리
Exit Code 반환BashPython 결과를 OS로 전달

점검 항목

Resource (자원)

항목수집 방식비고
CPU 사용률vmstat (없으면 top 자동 전환)2-sample 평균으로 순간값 오차 제거
메모리 사용률cgroup v2 → v1 → /proc/meminfo 순서 시도컨테이너 환경 자동 감지
디스크 사용률df /루트 파티션 기준
Inode 사용률df -i /파일 수 고갈 감지
Load Average/proc/loadavg ÷ CPU 코어 수서버 사양 무관한 비율로 환산

Load Average를 코어 수로 나누는 이유:

4코어 서버의 Load 2.0과 1코어 서버의 Load 2.0은 완전히 다른 상황입니다.

단순 숫자로 보면 같아 보이지만, 코어 대비 비율로 환산하면 서버 사양에 관계없이 동일한 기준으로 판단할 수 있습니다.

Network (네트워크)

  • 포트 점검: nc → 없으면 /dev/tcp 방식으로 자동 전환 (설치 없이 동작)
  • 외부 Ping: 8.8.8.8 (설정에서 변경 가능)
  • API Endpoint: config/services.txt에 적힌 주소를 curl로 확인

포트 점검이 실제로 유용한 경우:

  • 배포 직후 nginx가 설정 오류로 안 뜬 경우 → 80 포트 닫힘 → 즉시 감지
  • 방화벽 설정 바꾸다가 443을 막은 경우 → 443 경고
  • 서버 outbound가 막힌 경우 → ping 실패 → 외부 API 호출 불가 상황 인지

포트가 열려 있다고 서비스가 정상인 건 아닙니다.
nginx 프로세스는 살아있는데 DB 연결이 끊겨서 500을 반환하는 경우, 포트 점검만으로는 잡히지 않습니다. 더 정확한 확인이 필요하면 config/services.txt에 실제 헬스체크 엔드포인트를 등록하세요.

# config/services.txt 예시
http://localhost/health
http://localhost:8080/api/status

Security (보안)

이 부분이 실제로 가장 많이 놓치는 항목들입니다.

항목확인 내용문제 시 상태
Root SSH 로그인PermitRootLogin이 yes로 설정된 경우CRITICAL
패스워드 만료 정책PASS_MAX_DAYS > 90일이면 경고WARNING
세션 타임아웃TMOUT 환경변수 미설정 또는 600초 초과WARNING
계정 잠금 정책pam_faillock 미설정WARNING
SUID/SGID 파일 수전체 파일시스템에서 20개 초과 시WARNING

Ubuntu를 처음 설치하면 PASS_MAX_DAYS=99999, TMOUT 미설정, faillock 미설정이 기본값입니다. 이 세 가지를 그냥 올리면 보안 점검에서 WARNING 3개가 한 번에 나옵니다.


실행해보기

# 클론 및 권한 부여
git clone https://github.com/nangman-infra/linux-health-checker.git
cd linux-health-checker
chmod +x health-check.sh

# 기본 점검
./health-check.sh run

# 보안 전체 점검 포함 (sudo 필요)
sudo ./health-check.sh run

./health-check.sh run 실행 결과 전체 화면

sudo 없이 실행하면 PARTIAL MODE로 동작합니다. SUID/SGID 파일 검사, faillock 등은 root 권한이 없으면 조회가 안 됩니다. 이런 항목은 UNKNOWN으로 표시되고 점수에서 5점씩 차감됩니다.



sudo ./health-check.sh run 결과 — SUID 파일 수, faillock 상태 포함


Health Score 계산 방식

100점에서 시작해서 문제 항목마다 페널티 × 가중치를 차감합니다.

Health Score = 100 - Σ(penalty × weight) / Σ(max_penalty × weight) × 100
지표가중치WARNING 페널티CRITICAL 페널티
디스크 사용률5-10점-30점
메모리 사용률4-10점-30점
보안 설정4-10점-30점
CPU 사용률3-10점-30점
네트워크 포트3-10점
Load Average2-10점
Inode 사용률2-30점

가중치를 다르게 설정한 이유:

  • 디스크(5): 100% 차는 순간 프로세스가 파일을 못 쓰고 즉시 죽습니다. 서비스 즉각 중단.
  • 메모리(4): OOM-Killer가 아무 프로세스나 골라서 죽입니다. 예측이 불가능합니다.
  • CPU(3): 100%여도 서비스가 느려지는 것이지, 즉시 죽지는 않습니다.

최종 상태 판정:

CRITICAL 항목이 하나라도 있으면  → CRITICAL (Exit 2)
Score < 50                      → CRITICAL (Exit 2)
Score < 80 또는 WARNING 2개 이상 → WARNING  (Exit 1)
나머지                           → OK       (Exit 0)

88점이 나오는 경우 실제 계산 과정:

아래는 실제 서버에서 WARNING 4개 + UNKNOWN 1개가 나왔을 때의 계산입니다.

항목상태페널티가중치가중 페널티
HTTPS(443)WARNING10330
패스워드 만료 정책WARNING10440
세션 타임아웃(TMOUT)WARNING10440
계정 잠금(faillock)WARNING10440
SUID/SGID 검사UNKNOWN5420
나머지 정상 항목OK00
합계170

전체 항목이 모두 CRITICAL일 때의 최대 가중 페널티: 약 1,260

점수 = 100 - (170 ÷ 1,260) × 100 = 100 - 13.5 ≈ 88점

단순히 100 - 4×10 - 1×5 = 55가 아닌 이유: 분모가 전체 항목이 전부 CRITICAL이었을 때의 최대값이기 때문입니다. 이렇게 설계한 이유는 WARNING 하나에 점수가 과하게 떨어지지 않도록 하기 위해서입니다.


추세 분석: 지금이 정상인지보다 방향이 중요하다

실행할 때마다 결과를 reports/history/에 JSON으로 저장합니다. 이력이 2개 이상 쌓이면 CPU, 메모리, 디스크의 추세를 자동으로 계산합니다.

SPIKE: 갑자기 튀는 경우

평소 CPU가 2~4%였는데 갑자기 95%가 된 경우입니다. 이동평균 대비 20% 이상 급등하거나, 2 표준편차를 초과하면 SPIKE로 판정합니다.

주로 발생 원인: 배포 직후 버그, 배치 잡 충돌, 외부 공격

TREND_UP: 서서히 올라가는 경우

디스크가 60% → 65% → 70% → 75%로 매주 조금씩 올라가는 경우입니다. 최근 이력이 꾸준히 상승하는 패턴을 감지하면 TREND_UP을 알립니다.

주로 발생 원인: 로그 로테이션 미설정, 메모리 누수, 데이터 계속 적재

TREND_UP이 없으면 이런 일이 생깁니다:

실행 시점디스크df -h만 볼 때이 도구
1주차60%괜찮다STABLE
2주차65%괜찮다STABLE
3주차70%아직 괜찮다TREND_UP ← 여기서 경고
5주차80%이제 경고다WARNING
7주차90%서비스 영향CRITICAL

TREND_UP은 80% WARNING보다 2주 일찍 알려줍니다. 로그 로테이션을 설정할 시간이 있을 때 잡는 거냐, 서비스 터지고 나서 잡는 거냐의 차이입니다.


역할별 임계치 시스템

서버 종류마다 "정상"의 기준이 다릅니다.

Redis 서버는 메모리를 95% 쓰는 게 정상입니다. 데이터를 메모리에 올려두는 게 목적이니까요. 하지만 이걸 기본 기준으로 보면 CRITICAL 경고가 뜹니다.

config/roles/ 폴더에 파일을 하나 추가하면 새 역할이 생기는 구조입니다.

config/
├── default.conf     ← 공통 기본값
└── roles/
    ├── web.conf     ← 웹 서버 (CPU 기준 강화, 70% 경고)
    ├── db.conf      ← DB 서버 (메모리 기준 완화, 90% 정상)
    ├── cache.conf   ← 캐시 서버 (메모리 95%도 정상)
    ├── batch.conf   ← 배치 서버 (CPU 90%도 정상)
    ├── proxy.conf   ← 로드밸런서
    ├── storage.conf ← 스토리지 (디스크 60%부터 경고)
    └── ci.conf      ← CI/CD 서버

db.conf의 경우 이렇게 생겼습니다.

# DB 서버는 Buffer Pool로 메모리를 의도적으로 많이 씁니다.
memory_warning=90    # default: 80
memory_critical=97   # default: 95
disk_warning=70      # 데이터 파일이 빠르게 증가하므로 더 빨리 경고
disk_critical=85     # default: 90

default.conf를 먼저 로드하고, 그 위에 역할 파일을 덮어씌우는 방식입니다. 파일에 적힌 값만 바뀌고 나머지는 기본값 그대로입니다.

# 직접 지정
./health-check.sh run --role db

# 서버에 한 번 설정해두면 이후엔 자동 적용
echo "db" | sudo tee /etc/health-check.role
./health-check.sh run   # --role 없어도 db 기준으로 점검

자동 역할 감지 순서:

  1. /etc/health-check.role 파일에 적힌 값
  2. 실행 중인 프로세스 확인 (mysqld → db, nginx → web 등)
  3. 복수의 역할 프로세스가 감지되면 샌드박스로 판단 → default 사용

CI/CD

CI/CD 파이프라인은 코드를 빌드하고 서버에 배포하는 과정을 자동화한 것입니다.
Jenkins, GitHub Actions, GitLab CI 같은 도구가 이 역할을 합니다.

이 헬스체커는 그 파이프라인 안에서 "지금 배포해도 되는 상태인가?"를 확인하는 한 단계로 생각하면 됩니다.

[ 배포 파이프라인 흐름 ]

① 코드 빌드
② ./health-check.sh run   ← 이 단계에서 서버 상태 점검
   ├─ Exit 0 (OK)       → 정상, ③으로 진행
   ├─ Exit 1 (WARNING)  → 알림 발송 후 ③으로 진행
   └─ Exit 2 (CRITICAL) → 여기서 파이프라인 중단
③ 서버에 배포
④ ./health-check.sh run   ← 배포 후 다시 한번 확인
   └─ Exit 2 (CRITICAL) → 배포 후 문제 발생, 롤백 트리거

왜 이게 가능한가?

Shell 스크립트는 종료할 때 Exit Code(종료 코드)를 반환합니다. CI/CD 도구들은 이 숫자를 보고 다음 단계를 계속할지 멈출지 결정합니다. Exit Code가 0이 아니면 자동으로 실패로 처리되고, 파이프라인이 멈춥니다.

이 도구가 반환하는 Exit Code:

Exit Code상태의미
0OK모든 항목 정상
1WARNING주의 필요, 당장 서비스에 영향은 없음
2CRITICAL서비스에 영향을 주는 문제 존재
3EMERGENCY자동 복구 실패, 수동 확인 필요

실제 Jenkins Pipeline에서는 이렇게 씁니다:

stage('서버 상태 점검') {
    steps {
        sh './health-check.sh run'
        // Exit 2면 여기서 파이프라인이 자동으로 멈춥니다
    }
}

stage('배포') {
    steps {
        sh './deploy.sh'
    }
}

별도로 조건문을 쓰지 않아도 됩니다. CI/CD 도구가 Exit Code를 보고 알아서 처리합니다.

점수나 상태를 다른 곳에 보내고 싶을 때

Exit Code는 파이프라인을 멈추거나 계속하는 데 쓰고, 점수나 상태 같은 데이터를 다른 시스템에 넘길 때는 JSON 모드를 씁니다.

# 점수만 추출
./health-check.sh run --json 2>/dev/null | jq '.health_score'
# → 88

# 상태만 추출 (.status 가 아니라 .final_status 입니다)
./health-check.sh run --json 2>/dev/null | jq '.final_status'
# → "WARNING"

# 사용 가능한 키 전체 확인
./health-check.sh run --json 2>/dev/null | jq 'keys'

[INFO] 로그와 JSON 결과는 별도 채널(stderr / stdout)로 분리되어 있어서 2>/dev/null로 로그를 버리고 jq가 숫자만 가져올 수 있습니다.

업로드중..


실행 이력 확인

실행할 때마다 logs/audit.log에 기록이 남습니다.

[2026-05-05 09:13:53] run executed | role=default | fix=false | dry_run=false
[2026-05-05 09:14:47] run executed | role=db     | fix=false | dry_run=false
[2026-05-05 09:15:02] [FIX] TMOUT 설정 완료 → /etc/profile.d/timeout.sh
[2026-05-05 09:15:02] [FIX] 원본 백업 → /tmp/sshd_config.bak.1234

각 줄의 의미:

의미
run executedrun 명령어로 실행됨
role=default역할 기준을 따로 지정하지 않음. --role db로 실행하면 role=db로 기록됨
fix=false--fix 옵션 없이 실행. 서버 설정을 자동으로 변경하지 않음
dry_run=false실제 실행. --dry-run이면 변경 없이 "어떻게 고칠지"만 출력
[FIX] TMOUT 설정 완료--fix로 실행했을 때 세션 타임아웃이 없어서 자동으로 설정 파일을 생성함
[FIX] 원본 백업파일을 수정하기 전에 원본을 /tmp에 백업해둔 경로. 문제가 생기면 이 파일로 복원 가능
# 최근 실행 이력 확인
tail -20 logs/audit.log

# 가장 최근 점검 결과 JSON 확인
cat reports/history/$(ls -t reports/history/ | head -1) | python3 -m json.tool

마무리

CPU 2%, 메모리 22%, 디스크 13% — top이랑 df -h만 보면 아무 문제 없는 서버입니다.
근데 헬스체커를 돌리면 88점 WARNING이 나옵니다. 패스워드 정책이 99999일이고, 세션 타임아웃 없고, faillock 비활성화 상태이기 때문입니다.

반대로 DB 서버에서 메모리 85%가 나왔을 때, --role db 하나로 기준 자체를 바꿀 수 있는 게 생각보다 편했습니다. 같은 숫자인데 어떤 서버냐에 따라 판단이 달라지기 때문입니다.

2편에서는 직접 부하를 줘서 SPIKE와 TREND_UP이 실제로 잡히는지 확인합니다.


다음 글: [2편 — 직접 장애를 주입하고 헬스체커 동작 검증]

GitHub: nangman-infra/linux-health-checker

0개의 댓글