지난 글에서는 시놀로지 NAS의 스케줄러(Cron)와 Cloudflare Pages를 연결하여, 옵시디언에서 글을 작성하고 저장하기만 하면 자동으로 블로그가 배포되는 파이프라인을 구축했습니다.

이전 글 요약

옵시디언 글 작성 ➔ NAS 스케줄러 감지 ➔ GitHub로 자동 Push ➔ Cloudflare 감지 후 자동 Build & Deploy

하지만 이 시스템에는 한 가지 모니터링 관점의 맹점이 존재합니다. “자동화가 백그라운드에서 실행되기 때문에, 배포의 성공/실패 여부를 즉각적으로 알 수 없다” 는 것입니다. 서버에 접속해 로그를 뒤지거나 웹사이트에 직접 들어가서 확인해야만 배포 상태를 알 수 있다는 불편함이 있습니다.

이번 글에서는 이 파이프라인에 텔레그램 봇 API를 연동하여, 서버에서의 Push 상태와 클라우드 환경(Cloudflare)의 Deploy 상태를 스마트폰으로 실시간 모니터링하는 것을 설정해보려 합니다.

1. 텔레그램 봇 API 발급 및 설정

가장 먼저 알림 메시지를 전송해 줄 텔레그램 봇을 생성하고, 메시지를 수신할 고유 ID를 확보해야 합니다.

  1. 봇 토큰 발급

    • 텔레그램 앱에서 BotFather 를 검색하여 대화를 시작합니다.

    • /newbot 명령어를 입력하고, 안내에 따라 봇의 이름과 유저네임(반드시 bot으로 끝나야 함)을 설정합니다.

    • 생성이 완료되면 HTTP API 토큰(예: 123456789:ABCdefGHIjklMNOpqrSTUvwxYZ)을 발급해 줍니다. 이 토큰은 외부 유출되지 않게 메모해 둡니다.

  2. Chat ID(수신자 ID) 확인

    • 알림을 수신할 본인 계정의 고유 ID가 필요합니다.

    • 텔레그램 검색창에 @userinfobot 을 검색하여 대화를 시작합니다.

    • /start를 입력하면 본인의 Chat ID(숫자 형태, 예: 123456789)를 확인할 수 있습니다.

2. 1단계 알림: Git Push 상태 모니터링

먼저 서버에서 GitHub로 소스코드를 전송(Push)할 때의 상태를 모니터링합니다. 기존에 작성했던 quartz_sync.sh 쉘 스크립트를 수정하여, Push 성공 시 변경된 파일 목록을 보내고 실패 시 에러 로그를 전송하도록 작성합니다.

SSH로 접속하여 /root/quartz_sync.sh 파일을 아래와 같이 수정합니다.

#!/bin/bash
# ================================================================
# [초기 설정]
# set -e: 오류 발생 시 즉시 중단 (단, 에러 로그를 잡아야 하는 부분에서는 잠시 끕니다)
# (주의: 로그를 깔끔하게 남기기 위해 기존의 -x(디버그 모드)는 제거했습니다.)
# ================================================================
set -e  
 
# ================================================================
# [사용자 변수 세팅]
# ================================================================
 
# 작업할 폴더 경로 (Obsidian 볼트가 있는 Quartz 리포지토리 경로)
WORK_DIR="Quartz 볼트가 있는 경로 입력"
 
# 로그가 저장될 '폴더' 지정 (기존에는 단일 파Q일명이었음)
LOG_DIR="로그 경로 입력"
 
# 폴더가 없으면 자동으로 생성
mkdir -p "$LOG_DIR"
 
# 오늘 날짜(YYYY-MM-DD)를 구해서 매일 새로운 로그 파일명 지정
TODAY=$(date +'%Y-%m-%d')
LOG_FILE="$LOG_DIR/quartz_sync_${TODAY}.log"
 
# 3. 텔레그램 봇 정보Q
TG_TOKEN="여기에 BotFather에서 발급받은 http API 토큰 입력"
TG_CHAT_ID="여기에 Chat ID 입력"
 
# ================================================================
# [로그 기록 함수]
# ================================================================
 
# echo -e 명령어와 tee를 사용해 터미널 화면과 로그 파일에 동시에 기록.
write_log() {
    echo -e "$1" | tee -a "$LOG_FILE"
}
 
# ================================================================
# [동기화 작업 시작]
# ================================================================
cd "$WORK_DIR" || exit 1
 
# 현재 시간을 YYYY-MM-DD HH:MM:SS 형태로 저장
CURRENT_TIME=$(date +'%Y-%m-%d %H:%M:%S')
 
# ------------------------------------------
# Git 상태 확인: 보낼 변경사항이 있는지 확인.
# ------------------------------------------
STATUS_CHECK=$(git -c core.quotePath=false status -s content)
 
if [[ -z "$STATUS_CHECK" ]]; then
  # 변경사항이 없으면 종료
  # 로그 파일이 너무 길어지는 것을 방지하기 위해 이 부분은 기록하지 않음
  exit 0
fi
 
# 변경사항이 발견되면, 로그 파일에 작업 시작 기록
write_log "\n=================================================="
write_log "🚀 [시작] Quartz 자동 동기화"
write_log "🕒 시간: $CURRENT_TIME"
write_log "--------------------------------------------------"
write_log "📝 [변경 파일 목록]\n$STATUS_CHECK"
write_log "--------------------------------------------------"
 
# 파일 add, 오늘 날짜로 commit.
/usr/bin/git add content/
/usr/bin/git commit -m "Auto-sync: $CURRENT_TIME" || true
 
# GitHub에 새로 업데이트된 규칙이나 변경사항이 있는지 먼저 pull 합니다.
write_log "🔄 Repository Branch 동기화(Pull) 진행 중..."
/usr/bin/git pull --rebase origin main
 
# ================================================================
# Push 및 결과 보고 (성공/실패 분기)]
# ================================================================
write_log "📤 GitHub Push 시도 중..."
 
# 1. 에러가 나도 스크립트가 죽지 않도록 잠시 안전장치를 풉니다.
set +e
 
# 2. Push를 실행하고, 정상 메시지와 에러 메시지(2>&1)를 모두 PUSH_LOG 변수에 담기.
PUSH_LOG=$(/usr/bin/git push origin main 2>&1)
PUSH_STATUS=$? # 푸시 결과 코드 저장 (성공 시 0, 실패 시 1 이상)
 
# 3. 안전장치를 다시 켭니다.
set -e
 
# 4. 결과에 따른 분기 처리 (로그 기록 & 텔레그램 발송)
if [ $PUSH_STATUS -eq 0 ]; then
    # -------------------------
    # ✅ 1. 푸시 성공 시
    # -------------------------
    # 로그 파일에 성공 기록
    write_log "✅ [성공] 푸시 완료 Cloudflare 배포 예정"
    write_log "==================================================\n"
 
    # 텔레그램으로 보낼 성공 메시지 작성
    TG_MSG="✅ [Quartz Push Succeeded]
Quartz 변경점 Push 성공
Cloudflare 배포 예정
🕒 시간: $CURRENT_TIME
📝 반영된 파일:
$STATUS_CHECK"
 
else
    # -------------------------
    # 🚨 2. 푸시 실패 시
    # -------------------------
    # 업무 일지(로그 파일)에 실패 기록 및 에러 내용 첨부
    write_log "🚨 [실패] 푸시 중 에러 발생"
    write_log "--------------------------------------------------"
    write_log "$PUSH_LOG"
    write_log "==================================================\n"
 
    # 텔레그램으로 보낼 실패 메시지 작성 (에러 로그 포함)
    TG_MSG="🚨 [Quartz Push Failed]
Quartz Push 실패
🕒 시간: $CURRENT_TIME
📜 에러 로그:
$PUSH_LOG"
 
fi
 
# ================================================================
# [텔레그램 발송]
# --data-urlencode 옵션을 사용하여 에러 로그 속 특수문자가 깨지지 않게 전송
# ================================================================
 
curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
     -d chat_id="${TG_CHAT_ID}" \
     --data-urlencode text="$TG_MSG" > /dev/null
  
exit 0

쉘 스크립트 핵심 로직

PUSH_LOG=$(... 2>&1) 처리를 통해 정상적인 결과 메시지뿐만 아니라, Git에서 발생하는 에러 메시지(표준 에러)까지 모두 캡처하여 텔레그램 알림 내용에 포함시킬 수 있습니다.

3. 2단계 알림: Cloudflare Pages 빌드 상태 모니터링

GitHub에 코드가 정상적으로 전달되었다면, Cloudflare가 이를 감지하여 배포를 시작합니다.

초기에는 Cloudflare 대시보드에 직접 curl 스크립트를 한 줄로 길게 입력하려 했으나, 특수문자와 이모지로 인해 Cloudflare 방화벽에서 API Request Failed 에러를 발생시키는 문제가 있었습니다.

이를 해결하고 보안성을 높이기 위해, CI/CD 환경의 정석적인 방식인 ‘환경 변수 분리’‘명령어 스크립트화(build.sh)’ 를 적용합니다.

3-1. Cloudflare 환경 변수(Environment Variables) 등록

코드에 API 토큰을 하드코딩하지 않고, Cloudflare의 보안 환경 변수로 등록합니다.

  1. Cloudflare Pages 대시보드 ➔ 설정(Settings)환경 변수(Environment variables)

  2. 프로덕션 환경에 두 개의 변수를 추가합니다.

    • 변수명: TG_TOKEN / 값: 1단계에서_받은_토큰 (※ 앞에 bot을 붙이지 않도록 주의)

    • 변수명: TG_CHAT_ID / 값: 1단계에서_받은_ID

3-2. 리포지토리 최상위에 build.sh 생성

텔레그램 발송 로직이 포함된 빌드 쉘 스크립트를 GitHub 리포지토리에 추가합니다.

  1. my-quartz-blog 리포지토리의 최상위(Root) 디렉터리build.sh 파일을 생성합니다.

  2. 아래 코드를 입력하고 커밋합니다.

#!/bin/bash
 
# 1. Quartz 블로그 인쇄(빌드) 시작
if npx quartz build; then
    # 2-A. 성공 시 텔레그램 발송 (-sS 옵션으로 에러 시 로그 표시)
    curl -sS -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
         -d chat_id="${TG_CHAT_ID}" \
         --data-urlencode text="✅ [Quartz Deploy Succeeded]
Quartz 배포 성공🚀"
else
    # 2-B. 실패 시 에러 발송
    curl -sS -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
         -d chat_id="${TG_CHAT_ID}" \
         --data-urlencode text="🚨 [Quartz Deploy Failed]
Quartz 빌드 실패
Cloudflare Pages 빌드 로그 확인 필요"
    # 중요: Cloudflare 시스템에도 실패했다고 알려주기 위해 1을 반환하며 종료
    exit 1
fi

(참고: 이 스크립트를 Cloudflare 빌드 명령어를 변경하여 호출하기 때문에 Cloudflare에 설정한 환경 변수값이 이 스크립트에 호출되어 사용되므로 보안상 안전.)

3-3. Cloudflare 빌드 명령어 수정

마지막으로 Cloudflare가 기존 npx quartz build 대신 방금 만든 스크립트를 실행하도록 설정합니다.

  1. Cloudflare Pages 대시보드 ➔ 설정빌드 및 배포빌드 구성 편집

  2. 빌드 명령어를 bash build.sh 로 변경하고 저장합니다.

    (주의: 빌드 출력 디렉터리인 public은 절대 변경하지 않습니다.)

🤔 구축 과정에서의 Q&A 및 트러블슈팅

시스템을 구축하며 고민했던 부분들과 마주쳤던 에러에 대한 해결 과정을 정리했습니다.

Q1. build.sh 파일을 GitHub에 그대로 올려도 보안상 문제가 없나요? 왜 NAS 쉘 스크립트와 방식이 다른가요?

A. 로컬(On-Premise) 환경과 클라우드(Cloud) 환경의 보안 처리 기준이 다르기 때문입니다.

NAS에 위치한 스크립트는 해커가 관리자 권한으로 내부망을 뚫고 들어오지 않는 한 유출될 위험이 없는 ‘닫힌 공간’입니다. 따라서 변수에 토큰을 직접 기입해도 비교적 안전합니다.

반면, build.sh는 인터넷을 통해 GitHub를 거쳐 Cloudflare로 이동하는 파일입니다. 리포지토리 설정 실수 등 다양한 요인으로 유출될 수 있으므로, 파일 내부에 직접 토큰을 적지 않고 Cloudflare 시스템 내부의 ‘환경 변수(Environment Variables)’ 기능을 사용하여 실행 시점에만 암호를 꺼내 쓰도록 분리하는 것이 CI/CD 보안의 정석(Best Practice)입니다.

Q2. build.sh를 프로젝트 최상위에 넣으면, Cloudflare가 알아서 인식하고 실행하나요?

A. 네, 그렇습니다. CI/CD 파이프라인의 구동 원리 덕분입니다.

Cloudflare는 원격으로 명령을 내리는 것이 아니라, 빌드가 트리거되면 자체적인 가상 컨테이너(Runner)를 생성하고 GitHub 리포지토리의 전체 파일을 해당 컨테이너로 클론(Clone) 해옵니다. 파일 복사가 끝난 후 우리가 지정한 bash build.sh를 실행하게 되므로, 최상위 디렉터리에 위치한 쉘 스크립트를 정상적으로 찾아 실행할 수 있습니다.

Q3. Cloudflare 빌드 명령어 칸에 curl을 직접 넣었을 때 API Request Failed 에러가 났던 이유는 무엇인가요?

A. Cloudflare 대시보드의 방화벽 룰 때문입니다.

웹 대시보드 입력 필드에 줄바꿈 기호(\n), 마크다운 문법, 다양한 특수문자와 이모지가 포함된 긴 코드를 직접 주입하려고 하면 보안 시스템이 이를 비정상적인 요청(Injection 시도 등)으로 간주하여 거부합니다. 따라서 build.sh라는 외부 파일로 로직을 분리하여 우회하는 것이 가장 깔끔한 해결책입니다.

Q4. 분명 설정을 다 마쳤는데 텔레그램 메시지가 오지 않습니다. 원인이 무엇일까요?

A. 텔레그램 API URL의 문법 오염 또는 토큰 포맷 오류를 점검해야 합니다.

가장 흔한 실수는 Cloudflare 환경 변수 값을 오지정하거나 빌드 명령을 수정하지 않는 경우 입니다. 그리고 API URL 형태가 정확히 https://api.telegram.org/bot${TG_TOKEN}/sendMessage로 구성되었는지 확인하세요.

스크립트의 curl 명령어에 -sS 옵션을 추가하면 실패 시 Cloudflare 빌드 로그에 에러 메시지가 상세히 출력되므로 디버깅이 수월해집니다.

이제 모든 설정이 완료되었습니다. 옵시디언에서 편하게 글을 작성하고 저장만 하면, 백그라운드에서 동기화와 빌드가 이루어지며 스마트폰으로 진행 상황을 즉각 보고받을 수 있는 블로그 자동화 환경이 완성되었습니다.