배경
집에서 운영 중인 Proxmox 호스트에는 이미 VM 상태와 로컬 저장소 사용량을 정리해서 매일 메일로 보내주는 쉘 스크립트가 하나 있었다. qm list, pvesh 같은 Proxmox 전용 명령으로 VM별 CPU/메모리/디스크 정보를 뽑고, df로 로컬 스토리지 현황을 더해 HTML 메일로 발송하는 구조였다.
문제는 이 스크립트가 딱 “이 서버 자신”의 정보만 알고 있다는 점이었다. 클라우드에 따로 띄워둔 인스턴스 몇 대의 CPU·메모리·디스크 상태는 여전히 각 서버에 접속해서 일일이 확인해야 했다. 이번에 이 부분을 하나로 합쳐봤다.
전체 구조
핵심 아이디어는 단순하다. Proxmox 호스트가 클라이언트가 되어 SSH로 원격 서버에 접속하고, 명령 실행 결과만 받아온다. 원격 서버 쪽에는 에이전트나 모니터링 프로그램을 따로 설치할 필요가 없다.
[Proxmox 호스트]
├─ 로컬 정보 수집 (qm list, pvesh, df) — 바로 실행
├─ SSH 접속 → 원격서버 A → CPU/MEM/DISK 조회 → 결과만 회수
├─ SSH 접속 → 원격서버 B → 동일하게 조회
├─ SSH 접속 → 원격서버 C → 동일하게 조회
├─ 위 결과를 모두 HTML 하나로 조합
└─ ssmtp로 메일 발송
로컬 실행 → 원격 SSH 수집 → HTML 조합 → 메일 발송, 이 네 단계가 전부다.
SSH 키 인증 설정
크론으로 자동 실행되어야 하는 스크립트라 비밀번호 입력 없이 접속되는 게 필수였다. SSH의 공개키 인증을 이용했다.
ssh-keygen -t ed25519 -f ~/.ssh/id_rsa_monitor -N ""
-t ed25519: 최근 권장되는 키 타입 (짧고 빠름)-N "": 패스프레이즈 없이 생성 (크론에서 사람 입력 없이 돌아가야 하므로 필수)
생성하면 개인키(id_rsa_monitor)와 공개키(id_rsa_monitor.pub) 한 쌍이 나온다. 원리는 이렇다.
- 개인키는 접속을 시도하는 쪽(Proxmox 호스트)에만 존재
- 공개키는 접속을 받는 쪽(원격 서버)의
~/.ssh/authorized_keys에 등록 - 접속 시 원격 서버가 임의의 값을 공개키로 암호화해서 보내고, 클라이언트가 그걸 개인키로 풀어서(또는 서명해서) 돌려주면 서버가 공개키로 검증 → 통과하면 “이 사람은 진짜 그 개인키를 가진 사람”이라고 확신하고 접속 허용
- “무언가를 안다(비밀번호)”가 아니라 “무언가를 가지고 있다(개인키)”로 신원을 증명하는 방식
공개키 등록은 ssh-copy-id로 간단히 끝난다.
ssh-copy-id -i ~/.ssh/id_rsa_monitor.pub 계정명@원격서버IP
이후 ~/.ssh/config에 별칭을 등록해두면 관리가 훨씬 편해진다.
Host remote-a
HostName 원격서버A_IP
User ubuntu
IdentityFile ~/.ssh/id_rsa_monitor
Host remote-b
HostName 원격서버B_IP
User ubuntu
IdentityFile ~/.ssh/id_rsa_monitor
이렇게 해두면 ssh remote-a 한 줄로 접속되고, 스크립트에서도 IP나 키 경로를 직접 적을 필요 없이 이 별칭만 참조하면 된다.
접속이 키 인증으로 됐는지 비밀번호로 됐는지는 서버의 인증 로그로 확인할 수 있다.
sudo grep sshd /var/log/auth.log | tail -20
Accepted publickey for ubuntu from ...가 찍히면 키 인증 성공, Accepted password for ...가 찍히면 비밀번호로 인증된 것이다.
스크립트 구현에서 부딪힌 문제들
1. bash 연관배열은 순서를 보장하지 않는다
원격 서버 목록을 관리할 때 처음엔 이렇게 썼다.
declare -A REMOTE_HOSTS=(
["Server-A"]="remote-a"
["Server-B"]="remote-b"
["Server-C"]="remote-c"
)
코드에는 A→B→C 순서로 적었는데, 실제 출력은 뒤죽박죽으로 나왔다. bash의 연관배열(declare -A)은 해시 기반이라 순서를 보장하지 않기 때문이다. 순서가 중요하다면 인덱스 배열로 바꿔야 한다.
REMOTE_HOSTS=(
"Server-A|remote-a"
"Server-B|remote-b"
"Server-C|remote-c"
)
for entry in "${REMOTE_HOSTS[@]}"; do
display_name="${entry%%|*}" # 구분자 앞부분
ssh_target="${entry##*|}" # 구분자 뒷부분
...
done
|로 구분한 문자열을 배열에 순서대로 나열하고, %%/## 파라미터 확장으로 분리하는 방식이다. 인덱스 배열은 정의한 순서 그대로 순회되므로 이 문제가 해결된다.
2. 원격 서버 응답이 없을 때의 장애 허용 처리
원격 서버 중 하나가 다운되거나 방화벽에 막혀 있으면, SSH 접속이 무한 대기하거나 스크립트 전체가 멈춰버릴 위험이 있다. 이를 방지하려고 두 가지를 넣었다.
result=$(ssh -o ConnectTimeout=5 -o BatchMode=yes "$ssh_target" "$REMOTE_CMD" 2>/dev/null)
if [ -z "$result" ]; then
echo "<tr><td>${display_name}</td><td colspan='5'>접속 실패</td></tr>" >> "$REPORT_FILE"
continue
fi
ConnectTimeout=5: 5초 안에 접속이 안 되면 포기하고 다음으로 넘어감BatchMode=yes: 키 인증이 실패해도 비밀번호 프롬프트로 넘어가지 않고 즉시 실패 처리 (크론 환경에서 사람이 없으므로 무한 대기를 막기 위함)- 결과가 비어있으면 해당 서버 행에만 경고 표시하고
continue로 다음 서버로 넘어감
이렇게 하면 3대 중 1~2대가 죽어도 나머지 정보 + 메일 발송 자체는 정상적으로 진행된다. 오히려 어떤 서버가 문제인지 메일에서 바로 확인할 수 있어 장애 감지용으로도 쓸 수 있다.
3. 웹메일 클라이언트가 <style> 블록을 제거한다
HTML 메일에 <head><style>...</style></head>로 CSS를 정의하고 class 속성으로 스타일을 입혔더니, 브라우저에서 미리보기할 때는 예쁘게 나오는데 실제 메일함(네이버 메일 등)에서 열어보면 여백도 색상도 다 날아간 밋밋한 표로 나왔다.
원인은 대부분의 웹메일 서비스가 보안상 <head> 안의 <style> 태그를 통째로 제거하기 때문이다. 클래스 기반 CSS는 이 태그 안에 정의되므로 몽땅 무시된다.
해결책은 모든 스타일을 태그에 직접 붙이는 인라인 스타일로 바꾸는 것이다.
TH_STYLE="background-color:#f2f4f6; border:1px solid #d0d5da; padding:10px 14px; font-weight:bold;"
TD_STYLE="border:1px solid #d0d5da; padding:10px 14px; line-height:1.6;"
echo "<th style=\"$TH_STYLE\">항목</th>"
반복되는 스타일 문자열은 변수로 미리 정의해두면 관리가 한결 편해진다. 짝수/홀수 행 배경색처럼 CSS의 :nth-child 선택자로 처리하던 부분은 인라인 스타일에서 쓸 수 없으므로, 반복문 안에서 카운터를 두고 두 스타일을 번갈아 적용하는 방식으로 바꿨다.
row_idx=0
for ...; do
cell_style="$TD_STYLE"
[ $((row_idx % 2)) -eq 1 ] && cell_style="$TD_STYLE_ALT"
...
row_idx=$((row_idx + 1))
done
HTML 이메일을 만들 때는 처음부터 인라인 스타일로 작성하는 걸 권장한다. 대부분의 웹메일·모바일 메일 클라이언트가 <style> 블록을 신뢰하지 않는다.
마무리
결과적으로 로컬 VM/저장소 정보에 원격 클라우드 서버 3대의 자원 현황까지 합쳐진 리포트가 매일 하나의 메일로 도착하게 됐다. 핵심은 세 가지였다.
- SSH 공개키 인증으로 사람 개입 없이 원격 정보를 안전하게 가져오기
- 순서·장애 허용을 고려한 견고한 반복문 설계
- HTML 이메일은 인라인 스타일로 작성해야 웹메일에서도 깨지지 않는다는 것
인프라가 늘어날수록 “한눈에 보이는 대시보드 하나”의 가치가 커지는 걸 새삼 느낀 작업이었다.