이 글은 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는 직접 열어봐야 알 수 있습니다.
크게 세 가지입니다.
./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 반환 | Bash | Python 결과를 OS로 전달 |
| 항목 | 수집 방식 | 비고 |
|---|---|---|
| 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은 완전히 다른 상황입니다.
단순 숫자로 보면 같아 보이지만, 코어 대비 비율로 환산하면 서버 사양에 관계없이 동일한 기준으로 판단할 수 있습니다.
nc → 없으면 /dev/tcp 방식으로 자동 전환 (설치 없이 동작)config/services.txt에 적힌 주소를 curl로 확인포트 점검이 실제로 유용한 경우:
포트가 열려 있다고 서비스가 정상인 건 아닙니다.
nginx 프로세스는 살아있는데 DB 연결이 끊겨서 500을 반환하는 경우, 포트 점검만으로는 잡히지 않습니다. 더 정확한 확인이 필요하면 config/services.txt에 실제 헬스체크 엔드포인트를 등록하세요.
# config/services.txt 예시
http://localhost/health
http://localhost:8080/api/status
이 부분이 실제로 가장 많이 놓치는 항목들입니다.
| 항목 | 확인 내용 | 문제 시 상태 |
|---|---|---|
| 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 상태 포함

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 Average | 2 | -10점 | — |
| Inode 사용률 | 2 | — | -30점 |
가중치를 다르게 설정한 이유:
최종 상태 판정:
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) | WARNING | 10 | 3 | 30 |
| 패스워드 만료 정책 | WARNING | 10 | 4 | 40 |
| 세션 타임아웃(TMOUT) | WARNING | 10 | 4 | 40 |
| 계정 잠금(faillock) | WARNING | 10 | 4 | 40 |
| SUID/SGID 검사 | UNKNOWN | 5 | 4 | 20 |
| 나머지 정상 항목 | OK | 0 | — | 0 |
| 합계 | 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, 메모리, 디스크의 추세를 자동으로 계산합니다.
평소 CPU가 2~4%였는데 갑자기 95%가 된 경우입니다. 이동평균 대비 20% 이상 급등하거나, 2 표준편차를 초과하면 SPIKE로 판정합니다.
→ 주로 발생 원인: 배포 직후 버그, 배치 잡 충돌, 외부 공격
디스크가 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 기준으로 점검
자동 역할 감지 순서:
/etc/health-check.role 파일에 적힌 값mysqld → db, nginx → web 등)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 | 상태 | 의미 |
|---|---|---|
| 0 | OK | 모든 항목 정상 |
| 1 | WARNING | 주의 필요, 당장 서비스에 영향은 없음 |
| 2 | CRITICAL | 서비스에 영향을 주는 문제 존재 |
| 3 | EMERGENCY | 자동 복구 실패, 수동 확인 필요 |
실제 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 executed | run 명령어로 실행됨 |
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편 — 직접 장애를 주입하고 헬스체커 동작 검증]