Python으로 Linux 시스템 정보 수집 자동화

서버 상태를 점검할 때 uname -a, free -h, df -h, ip addr를 하나씩 따로 실행하고 눈으로 훑어보는 방식은 서버가 한두 대일 때는 문제없지만, 배포 전 자동 점검이나 정기 헬스체크처럼 반복적으로 돌려야 하는 상황에서는 금방 한계에 부딪힌다. 각 명령의 출력 포맷이 제각각이라 파싱하기도 번거롭고, 로그로 남기거나 다른 스크립트에 값을 넘기려면 결국 문자열을 다시 쪼개야 한다. Python 표준 라이브러리와 psutil을 조합하면 이런 흩어진 명령들을 하나의 스크립트로 묶어 구조화된 JSON 형태로 시스템 정보를 뽑아낼 수 있다. 이 글에서는 OS/CPU/메모리/디스크/네트워크 정보를 수집해 자동화 가능한 형태로 저장하는 방법을 정리한다.

핵심 개념 — 정보별 수집 소스

시스템 정보는 항목별로 가져오는 소스가 다르다. 표준 라이브러리만으로 되는 것도 있고, psutil처럼 별도 패키지가 필요한 것도 있다.

항목모듈/함수비고
OS/커널 버전platform.uname()표준 라이브러리, uname -a와 동일한 정보
호스트명, IPsocket.gethostname(), socket.gethostbyname()표준 라이브러리
CPU 코어 수/사용률os.cpu_count(), psutil.cpu_percent()사용률은 psutil 필요
메모리 사용량psutil.virtual_memory()/proc/meminfo를 psutil이 내부적으로 파싱
디스크 사용량shutil.disk_usage()표준 라이브러리, 경로별 조회
네트워크 인터페이스psutil.net_if_addrs()인터페이스별 IP/MAC 목록
부팅 후 경과 시간psutil.boot_time()epoch 타임스탬프 반환, 직접 계산 필요

psutil은 표준 라이브러리가 아니므로 pip install psutil로 먼저 설치해야 한다. 반대로 OS 정보나 디스크 사용량처럼 표준 라이브러리만으로 충분한 항목까지 psutil에 의존하면 불필요한 의존성이 늘어나므로, 가능한 부분은 표준 라이브러리를 우선 쓰는 편이 자동화 스크립트 배포 시 관리하기 편하다.

실전 코드

먼저 OS와 호스트 정보를 모으는 함수다. platform.uname()은 named tuple을 반환하므로 필드 이름으로 바로 접근할 수 있다.

import platform
import socket

def collect_os_info() -> dict:
    uname = platform.uname()
    return {
        'system': uname.system,
        'node': uname.node,
        'release': uname.release,
        'version': uname.version,
        'machine': uname.machine,
        'python_version': platform.python_version(),
    }

def collect_network_basic() -> dict:
    hostname = socket.gethostname()
    try:
        ip_addr = socket.gethostbyname(hostname)
    except socket.gaierror:
        ip_addr = None
    return {'hostname': hostname, 'ip_address': ip_addr}

socket.gethostbyname()/etc/hosts나 DNS 설정에 따라 루프백 주소(127.0.1.1 등)만 돌려주는 환경도 있다. 실제 외부로 나가는 IP가 필요하다면 psutil.net_if_addrs()로 인터페이스별 주소를 직접 순회하는 편이 더 정확하다.

다음은 CPU, 메모리, 디스크 사용량을 모으는 함수다. 디스크는 여러 마운트 포인트를 한 번에 확인할 수 있도록 경로 목록을 인자로 받는다.

import os
import shutil
import psutil

def collect_cpu_memory() -> dict:
    mem = psutil.virtual_memory()
    return {
        'cpu_count': os.cpu_count(),
        'cpu_percent': psutil.cpu_percent(interval=1),
        'mem_total_gb': round(mem.total / (1024 ** 3), 2),
        'mem_used_gb': round(mem.used / (1024 ** 3), 2),
        'mem_percent': mem.percent,
    }

def collect_disk(paths: list[str]) -> dict:
    result = {}
    for path in paths:
        usage = shutil.disk_usage(path)
        result[path] = {
            'total_gb': round(usage.total / (1024 ** 3), 2),
            'used_gb': round(usage.used / (1024 ** 3), 2),
            'percent': round(usage.used / usage.total * 100, 1),
        }
    return result

psutil.cpu_percent(interval=1)은 1초간 CPU 사용률을 측정하므로 호출 시점에 스크립트가 1초간 블로킹된다. interval을 생략하면 이전 호출 이후 누적된 값을 즉시 반환하는데, 스크립트 실행 직후 첫 호출에서는 기준점이 없어 0.0이 나올 수 있으니 최초 호출에는 interval을 명시하는 편이 안전하다.

이제 위 함수들을 모아 하나의 리포트로 조립하고, 타임스탬프가 붙은 JSON 파일로 저장하는 부분이다.

import json
from datetime import datetime, timezone
from pathlib import Path

def build_report(disk_paths: list[str]) -> dict:
    return {
        'collected_at': datetime.now(timezone.utc).isoformat(),
        'os': collect_os_info(),
        'network': collect_network_basic(),
        'resources': collect_cpu_memory(),
        'disk': collect_disk(disk_paths),
    }

def save_report(report: dict, output_dir: str = './sysinfo_logs') -> Path:
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime('%Y%m%d_%H%M%S')
    out_path = Path(output_dir) / f'sysinfo_{ts}.json'
    out_path.write_text(json.dumps(report, indent=2, ensure_ascii=False))
    return out_path

if __name__ == '__main__':
    report = build_report(disk_paths=['/', '/home'])
    saved = save_report(report)
    print(f'저장 완료: {saved}')

실행하면 다음과 비슷한 JSON 파일이 생성된다.

저장 완료: sysinfo_logs/sysinfo_20260716_010500.json

{
  "collected_at": "2026-07-15T16:05:00.123456+00:00",
  "os": {
    "system": "Linux",
    "node": "web01",
    "release": "6.14.0-29-generic",
    "version": "#29-Ubuntu SMP",
    "machine": "x86_64",
    "python_version": "3.12.3"
  },
  "network": {
    "hostname": "web01",
    "ip_address": "10.0.1.15"
  },
  "resources": {
    "cpu_count": 4,
    "cpu_percent": 12.5,
    "mem_total_gb": 7.76,
    "mem_used_gb": 3.21,
    "mem_percent": 41.4
  },
  "disk": {
    "/": {"total_gb": 49.09, "used_gb": 18.42, "percent": 37.5},
    "/home": {"total_gb": 98.18, "used_gb": 61.03, "percent": 62.2}
  }
}

이 리포트에 임계값 검사를 추가하면 단순 수집을 넘어 알림용 자동화로 바로 이어진다. 예를 들어 디스크 사용률이 특정 퍼센트를 넘으면 종료 코드를 다르게 반환해, cron에서 실패로 인식시켜 별도 알림 시스템(메일, Slack webhook 등)과 연결할 수 있다.

import sys

def check_thresholds(report: dict, disk_percent_max: float = 85.0, mem_percent_max: float = 90.0) -> list[str]:
    warnings = []
    if report['resources']['mem_percent'] > mem_percent_max:
        warnings.append(f"메모리 사용률 {report['resources']['mem_percent']}% 초과")
    for path, info in report['disk'].items():
        if info['percent'] > disk_percent_max:
            warnings.append(f"{path} 디스크 사용률 {info['percent']}% 초과")
    return warnings

if __name__ == '__main__':
    report = build_report(disk_paths=['/', '/home'])
    save_report(report)
    warnings = check_thresholds(report)
    if warnings:
        for w in warnings:
            print(f'WARNING: {w}', file=sys.stderr)
        sys.exit(1)
    sys.exit(0)

이 스크립트를 crontab에 등록해두면 별도 모니터링 에이전트 없이도 가벼운 정기 점검을 구성할 수 있다.

# 매 시간 정각에 실행, 실패(임계값 초과) 시에만 메일 발송
0 * * * * /usr/bin/python3 /opt/scripts/sysinfo_report.py || echo "sysinfo check failed" | mail -s "Alert" admin@example.com

주의사항

psutil은 별도 설치가 필요하다: 표준 라이브러리가 아니므로 스크립트를 다른 서버에 배포할 때 pip install psutil이 선행돼야 한다. 컨테이너 이미지나 가상환경 없이 시스템 Python에 바로 배포하는 환경이라면 requirements.txt로 버전을 고정해두는 편이 안전하다.

cron 환경은 로그인 셸과 다르다: cron으로 실행되는 프로세스는 로그인 셸과 PATH, 환경 변수, 로케일이 다를 수 있다. 스크립트 안에서 python3나 외부 명령을 절대 경로로 지정하고, 필요하다면 스크립트 상단에서 LANG/LC_ALL을 명시적으로 설정하는 것이 안전하다.

cpu_percent()의 블로킹 특성: interval을 주면 그 시간만큼 스크립트가 멈춘다. 여러 지표를 한 번에 빠르게 수집해야 한다면 interval=None으로 즉시 반환받고, 직전 호출과의 시간 간격을 스크립트 쪽에서 별도로 관리하는 방식을 고려해야 한다.

로그 파일이 무한히 쌓인다: save_report()는 실행할 때마다 새 JSON 파일을 만든다. 정기 실행 스크립트에 그대로 적용하면 디스크를 스스로 채우는 역설적인 상황이 생길 수 있으므로, logrotate를 연결하거나 스크립트 안에서 오래된 파일을 주기적으로 정리하는 로직을 함께 넣어야 한다.

수집한 정보의 민감도: 호스트명, 내부 IP, 디스크 경로 구조 같은 정보는 외부에 그대로 노출되면 공격자에게 정찰 정보가 될 수 있다. 리포트를 외부 알림 채널로 전송하거나 공개 저장소에 커밋하는 구성이라면 민감 필드를 걸러내는 단계를 반드시 거쳐야 한다.

마무리

OS, CPU, 메모리, 디스크, 네트워크 정보를 흩어진 명령어 대신 하나의 Python 스크립트로 모으면, 그 결과를 JSON으로 저장해 시계열로 쌓거나 다른 자동화 파이프라인의 입력으로 바로 넘길 수 있다. 여기에 임계값 검사와 종료 코드만 추가하면 cron과 조합해 별도 모니터링 에이전트 없이도 기본적인 헬스체크를 구성할 수 있다. 다만 이 방식은 어디까지나 스냅샷 수집에 가깝기 때문에, 초 단위로 변하는 지표를 실시간으로 계속 추적해야 한다면 Prometheus의 node_exporter 같은 전용 도구를 쓰는 편이 낫다.

참고

Python 공식 문서 – platform

Python 공식 문서 – shutil.disk_usage

psutil documentation

답글 남기기