← 개발 로그 목록

website: 배포 스크립트 실패 흡수 방지 + 백업 성공 신호 모니터링 추가

/ 8분 분량 / 개발 로그

CD 파이프라인에서 배포 스크립트가 중간에 실패해도 그냥 넘어가던 문제를 고쳤고, 백업 스크립트가 조용히 실패하는 상황을 감지하기 위해 OCI Monitoring에 성공 신호를 찍는 dead man's switch를 추가했습니다.

요약

2026년 7월 12일, fix(cd): 배포 스크립트 git pull 실패 흡수 방지 + 서버·git 파일 불일치 해소라는 제목의 PR(#104)을 머지했습니다. 이번 작업은 두 갈래로 나뉩니다. 하나는 GitHub Actions CD 워크플로우에서 배포 스크립트 실패를 제대로 감지하도록 고친 것이고, 다른 하나는 백업 스크립트가 성공했다는 신호를 OCI Monitoring에 보내서 "언제 마지막으로 백업이 됐는지" 추적할 수 있게 만든 것입니다. .github/workflows/cd.yml, infra/.gitattributes, infra/backup-db.sh, infra/push-backup-metric.py 총 4개 파일이 바뀌었고, 새로 추가된 코드만 86줄입니다.

배경 및 목적

커밋 메시지에 담긴 두 가지 문제를 각각 짚어보면 이렇습니다.

첫째, "배포 스크립트 git pull 실패 흡수 방지". CD 파이프라인의 배포 스크립트 안에서 여러 명령이 순차적으로 실행되는데, 이 중 하나(예: git pull)가 실패해도 셸 스크립트가 그냥 다음 줄로 넘어가버리는 구조였습니다. 배포가 절반만 되거나 이전 코드가 그대로 남은 채로 "성공"이라고 리포트되는 상황이 생길 수 있는 거죠.

둘째, 백업 관련 이슈(#83 관측 미션)입니다. 매일 새벽 cron으로 DB 백업이 돌아가고 있었지만, cron이 안 돌았거나 서버가 죽었거나 스크립트가 중간에 실패했을 때 이걸 알아챌 방법이 없었습니다. 백업이 "안 됐다"는 걸 능동적으로 알려주는 로그는 없고, 백업이 필요한 순간에야 문제를 발견하는 구조였던 셈입니다.

구현 내용

1. CD 워크플로우 - set -euo pipefail 추가

.github/workflows/cd.yml의 배포 스크립트 블록 맨 앞에 딱 한 줄을 추가했습니다.

script: |
  set -euo pipefail

  # GHCR 인증 — 서버에서 private 이미지 pull 권한 획득
  echo "GHCR_USER" --password-stdin

set -e는 명령이 실패하면 즉시 스크립트를 종료시키고, -u는 정의되지 않은 변수를 참조할 때 에러를 내며, -o pipefail은 파이프라인 중간 명령이 실패해도 그 실패가 무시되지 않게 합니다. 이 세 옵션을 조합하면 스크립트 안 어느 지점에서 실패하든 CI가 실패로 잡아내게 됩니다. 코드 자체는 짧지만, 배포 신뢰성 측면에서는 꽤 중요한 한 줄입니다.

2. 백업 성공 신호 - push-backup-metric.py (dead man's switch)

이번 커밋에서 가장 비중 있는 부분입니다. infra/push-backup-metric.py는 78줄짜리 새 스크립트로, 백업이 성공적으로 끝날 때마다 OCI Monitoring에 "나 방금 성공했어"라는 신호(값은 의미 없이 1로 고정)를 찍습니다.

핵심 아이디어는 "존재 알람(Absence Alarm)"입니다. 값 자체를 확인하는 게 아니라, 이 메트릭이 일정 시간(예: 26시간) 이상 안 들어오면 알람이 뜨는 방식입니다. 즉 cron이 안 돌았든, 서버가 죽었든, 스크립트가 중간에 죽었든 원인이 뭐든 간에 "정상적으로 끝까지 실행됐는지"를 하나의 신호로 확인할 수 있게 됩니다.

인스턴스 정보를 조회하는 부분이 흥미로웠습니다.

def instance_metadata():
    """인스턴스가 자기 자신의 OCID/compartment를 IMDS에서 런타임에 조회.

    하드코딩하지 않는 이유: 이 값들을 소스에 박아두면 gitleaks가 OCI
    OCID로 탐지해 CI를 막고(#83 PR에서 실제로 걸림), 인스턴스가 교체되면
    코드도 같이 고쳐야 함 - 둘 다 IMDS 조회로 피할 수 있음.
    """
    req = urllib.request.Request(_IMDS_URL, headers={"Authorization": "Bearer Oracle"})
    with urllib.request.urlopen(req, timeout=5) as resp:
        data = json.loads(resp.read())
    return data["id"], data["compartmentId"]

OCID를 코드에 하드코딩하지 않고 IMDS(Instance Metadata Service)에서 런타임에 조회하는 방식을 택했습니다. 주석에 남긴 것처럼 이건 시행착오에서 나온 결정입니다. 실제로 #83 작업 중 OCID를 코드에 박아뒀다가 gitleaks가 시크릿으로 오탐해서 CI가 막힌 적이 있었다고 합니다. IMDS 조회로 바꾸면 이 문제도 없어지고, 인스턴스가 나중에 교체되더라도 코드를 안 고쳐도 되는 부수 효과까지 얻습니다.

인증은 InstancePrincipalsSecurityTokenSigner를 사용해서 별도 크레덴셜 파일 없이 인스턴스 자체 신원으로 OCI API를 호출합니다. 기존에 있던 push-disk-metric.py와 동일한 IAM 정책(likelion-monitoring-dyngroup + likelion-monitoring-policy)을 그대로 재사용해서, 새로운 권한 설정을 추가하지 않고 기존 인프라에 얹은 점도 눈에 띕니다.

3. backup-db.sh에서 신호 발송 호출 추가

backup_one() {
    ...
    python3 "{db_name}/{DATE}.db" "$snapshot"
    echo "uploaded: {db_name}-${DATE}.db"

    # 성공 신호 - OCI Monitoring Absence Alarm이 이게 26시간 이상 안 들어오면 알림
    # (cron 미실행·서버 다운·스크립트 중도실패 전부 이걸로 잡힘, #83)
    "SCRIPT_DIR/push-backup-metric.py" "$db_name"
}

DB 업로드가 끝난 직후에 성공 신호를 쏘도록 4줄을 추가했습니다. oci-monitor-venv라는 별도 가상환경의 파이썬을 명시적으로 지정해서 호출하는데, 이건 oci SDK가 시스템 파이썬 환경을 오염시키지 않도록 격리해둔 기존 관례를 따른 것으로 보입니다.

기술적 의사결정

OCI Monitoring의 Absence Alarm 방식을 택한 이유는 "성공했다는 로그를 남긴다" vs "일정 시간 신호가 없으면 알린다"의 차이에서 나옵니다. 전자는 로그를 누군가 확인해야 하지만, 후자는 문제가 생겼을 때만 능동적으로 알림이 오기 때문에 관측 부담이 훨씬 적습니다. 이미 디스크 메트릭에서 같은 패턴(push-disk-metric.py)을 쓰고 있었던 걸 백업에도 그대로 확장한 셈이라, 새로운 개념을 도입하기보다는 검증된 패턴을 재사용한 결정입니다.

IMDS 조회 vs 하드코딩도 비교해볼 만한데, 하드코딩 쪽이 코드는 단순하지만 시크릿 스캐너(gitleaks) 오탐, 인스턴스 교체 시 코드 수정 필요라는 실질적인 비용이 있었고 실제로 그 비용을 #83에서 치른 뒤 IMDS 방식으로 정리한 것으로 보입니다.

배운 점 및 개선점

셸 스크립트에서 set -euo pipefail 같은 안전장치는 있으면 당연하고 없으면 티가 안 나다가, 정작 필요한 순간(중간 실패)에 문제를 일으킨다는 걸 다시 확인한 계기였습니다. CD처럼 실패가 곧 서비스 장애로 이어지는 스크립트는 처음부터 이 옵션들을 기본값으로 깔고 가는 게 나아 보입니다.

백업 모니터링 쪽에서는 "성공을 어떻게 확인할 것인가"라는 질문에 dead man's switch라는 답을 얻었는데, 이 패턴은 백업뿐 아니라 다른 주기적 배치 작업(정기 리포트, 크론 기반 동기화 등)에도 그대로 적용할 수 있을 것 같습니다. 다음 단계로는 실제로 Absence Alarm을 OCI 콘솔에 설정해서 알림 채널(슬랙 등)과 연결하는 작업이 남아있을 것으로 보이고, prod/stage 각각의 알람 임계값(26시간)이 실제 cron 주기와 잘 맞는지도 운영하면서 확인이 필요할 듯합니다.