Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,71 @@ PR 머지 → main push → [CI: build-and-test(필수)] → [Deploy 워크플
5. **첫 태그 파일**: `printf 'INNOLIVE_TAG=<현재 배포 커밋SHA>\n' > /opt/innolive/deploy/current_tag`
6. 구 preflight 우회(`/etc/innolive/preflight-off.env`)는 유닛 교체와 함께 제거한다.

## 런타임 로그 보관 (#268)

journald는 용량 상한에 닿으면 오래된 로그부터 지운다. `log-archive.sh`가 `innolive-server`의
전체 런타임 로그를 **매일 00:10**(`innolive-log-archive.timer`)과 **배포 재시작 직전**
(`apply-release.sh`)에 보관 디스크로 옮긴다.

- 파일: `/srv/innolive-logs/innolive-server-YYYY-MM-DD.log.zst` (서버 현지 날짜, 하루 한 파일, root:adm 640)
- 없는 날은 채우고 어제·오늘은 다시 쓴다. 첫 실행은 journald에 남은 가장 오래된 날부터 채운다.
- 스트림 키·토큰·비밀번호는 가린다. 사용자 IP(ICE 후보)는 장애 조사용으로 남는다.
- `/srv/innolive-logs`가 마운트 지점이 아니면 쓰지 않는다(루트 디스크에 잘못 쌓이는 것 방지).
- 배포 중 보관이 실패해도 배포는 진행한다. 사유는 `/opt/innolive/deploy/logs/`의 상세 로그에만 남는다.

### 보관 디스크 준비 (1회, root)
미사용 디스크(sda)의 빈 공간에 파티션을 만든다. **먼저 확인 명령으로 데이터가 없는지 본다.**
```bash
sudo sgdisk -p /dev/sda # 확인만: 1번(Microsoft reserved, 16MiB)만 있어야 한다
sudo wipefs -n /dev/sda # 확인만: gpt/PMBR 외 서명이 없어야 한다
sudo sgdisk -n 2:0:0 -t 2:8300 -c 2:innolive-logs /dev/sda
sudo partprobe /dev/sda
sudo mkfs.ext4 -L innolive-logs /dev/sda2
sudo mkdir -p /srv/innolive-logs
echo 'LABEL=innolive-logs /srv/innolive-logs ext4 defaults,noatime,nofail 0 2' | sudo tee -a /etc/fstab
sudo systemctl daemon-reload # fstab 변경을 systemd에 알린다(RequiresMountsFor가 이 마운트를 본다)
sudo mount /srv/innolive-logs
sudo chown root:adm /srv/innolive-logs && sudo chmod 750 /srv/innolive-logs
```
`nofail`이라 디스크가 고장 나도 부팅은 멈추지 않는다(그동안 보관만 실패한다).

### 설치 (PR 머지 후, root)
1) 로컬에서 파일 네 개를 서버로 올린다.
```bash
ssh -p <SSH 포트> <계정>@<서버> 'mkdir -p ~/log-archive-install'
scp -P <SSH 포트> deploy/log-archive.sh deploy/apply-release.sh \
deploy/innolive-log-archive.service deploy/innolive-log-archive.timer <계정>@<서버>:~/log-archive-install/
```
2) 서버에서 설치한다.
```bash
cd ~/log-archive-install
sudo install -o root -g root -m 755 log-archive.sh /opt/innolive/deploy/log-archive.sh
sudo install -o root -g root -m 755 -b apply-release.sh /opt/innolive/deploy/apply-release.sh
sudo install -o root -g root -m 644 innolive-log-archive.service innolive-log-archive.timer /etc/systemd/system/
sudo systemctl daemon-reload
```
3) 설치 전에 미리 떠 둔 보관본(`~hkit/innolive-log-archive-preinstall/`, 2026-09-23 생성)이 있으면
**첫 실행 전에** 옮긴다. journald가 그사이 오래된 로그를 지웠어도 그 날짜가 보존된다.
기존 파일은 덮어쓰지 않고(`--update=none`), 이후 스크립트는 없는 날만 채우고 어제·오늘만 다시 쓴다.
```bash
sudo cp --update=none ~hkit/innolive-log-archive-preinstall/*.log.zst /srv/innolive-logs/
sudo chgrp adm /srv/innolive-logs/*.log.zst && sudo chmod 640 /srv/innolive-logs/*.log.zst
```
4) 첫 실행 후 매일 타이머를 켠다.
```bash
sudo systemctl start innolive-log-archive.service # 첫 실행: 과거 로그 채우기
sudo systemctl enable --now innolive-log-archive.timer
```

### 확인·열람
```bash
systemctl list-timers innolive-log-archive.timer
journalctl -u innolive-log-archive -n 5
ls -l /srv/innolive-logs
zstdcat /srv/innolive-logs/innolive-server-2026-09-21.log.zst | less
zstdgrep 'peer_connection_failed' /srv/innolive-logs/innolive-server-*.log.zst
```

## GitHub 설정

- Secrets (Actions): `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`(Read&Write),
Expand Down
4 changes: 4 additions & 0 deletions deploy/apply-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ docker login docker.io -u "${DOCKERHUB_USER}" --password-stdin \
docker pull "${INNOLIVE_IMAGE}:${TAG}" >>"${DETAIL_LOG}" 2>&1 || fail "docker pull failed"
echo "PULL OK"

# ── 재시작 전 런타임 로그 보관 ──────────────────────────────────
# 실패해도 배포는 진행한다 — 원본은 journald에 남는다. 출력은 상세 로그에만 쓴다.
/opt/innolive/deploy/log-archive.sh >>"${DETAIL_LOG}" 2>&1 || detail "log archive failed"

# ── 태그 교체(이전 태그는 롤백용으로 백업) ───────────────────────
if [[ -f "${TAG_FILE}" ]]; then
cp "${TAG_FILE}" "${TAG_FILE}.prev"
Expand Down
14 changes: 14 additions & 0 deletions deploy/innolive-log-archive.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 서버 런타임 로그를 날짜별 파일로 보관한다(log-archive.sh). innolive-log-archive.timer가 매일 실행한다.
# /etc/systemd/system/innolive-log-archive.service 로 설치한다.

[Unit]
Description=Archive InnoLive server runtime logs to dated files
# 보관 디스크(/srv/innolive-logs)가 마운트된 뒤에만 돈다. 없으면 스크립트가 쓰지 않고 실패한다.
RequiresMountsFor=/srv/innolive-logs

[Service]
Type=oneshot
ExecStart=/opt/innolive/deploy/log-archive.sh
# 방송 중에도 돌 수 있으므로 CPU·디스크 우선순위를 가장 낮춘다.
Nice=19
IOSchedulingClass=idle
12 changes: 12 additions & 0 deletions deploy/innolive-log-archive.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 매일 00:10에 전날 로그를 마감해 보관한다. /etc/systemd/system/innolive-log-archive.timer 로 설치한다.

[Unit]
Description=Daily InnoLive runtime log archive

[Timer]
OnCalendar=*-*-* 00:10:00
# 서버가 꺼져 있어 놓친 실행은 켜진 뒤 한 번 돈다.
Persistent=true

[Install]
WantedBy=timers.target
59 changes: 59 additions & 0 deletions deploy/log-archive.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# 서버 런타임 로그(journald의 innolive-server)를 날짜별 파일로 보관한다.
# 사용법: log-archive.sh (root. 매일 innolive-log-archive.timer와 배포 직전 apply-release.sh가 호출)
#
# journald는 용량 상한에 닿으면 오래된 로그부터 지우므로, 보관 디스크에 하루 한 파일
# (innolive-server-YYYY-MM-DD.log.zst, 서버 현지 날짜)로 옮겨 둔다.
# - 아직 파일이 없는 날은 채우고, 어제와 오늘은 매번 다시 쓴다(멱등). 첫 실행은
# journald에 남은 가장 오래된 날부터 채운다.
# - 스트림 키·토큰·비밀번호는 가려서 쓴다. 사용자 IP(ICE 후보)는 장애 조사에 필요해 남긴다.
# - 출력은 요약 한 줄뿐이다. 로그 본문을 stdout으로 내보내지 않는다(배포 출력 계약).
set -euo pipefail

ARCHIVE_DIR="${INNOLIVE_LOG_ARCHIVE_DIR:-/srv/innolive-logs}"
UNIT="innolive-server"

# 보관 디스크가 빠진 상태에서 루트 디스크의 빈 마운트 지점에 쓰는 사고를 막는다.
if ! mountpoint -q "${ARCHIVE_DIR}"; then
echo "log-archive: ${ARCHIVE_DIR} is not a mount point; nothing written" >&2
exit 1
fi

mask() {
sed -E \
-e 's#(rtmps?://[^ "]*/)[^ "/]+#\1***MASKED***#g' \
-e 's#((stream_?key|access_?token|refresh_?token|owner_?token|client_?secret|secret|password|authorization)"?[=:] ?"?)[A-Za-z0-9._~+/=-]{16,}#\1***MASKED***#Ig' \
-e 's#(Bearer )[A-Za-z0-9._~+/=-]+#\1***MASKED***#g'
}

# head가 먼저 끝나 journalctl이 SIGPIPE를 받는 것은 정상이다 — 값은 아래에서 검증한다.
first_day="$(journalctl -u "${UNIT}" -o short-iso --no-pager 2>/dev/null | head -1 | cut -c1-10 || true)"
if [[ ! "${first_day}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "log-archive: no journal entries for ${UNIT}" >&2
exit 0
fi

# 중간에 실패하면 쓰다 만 임시 파일을 지운다.
tmp=""
trap 'rm -f "${tmp}"' EXIT

today="$(date +%F)"
yesterday="$(date -d yesterday +%F)"
written=0
day="${first_day}"
while [[ ! "${day}" > "${today}" ]]; do
next="$(date -d "${day} +1 day" +%F)"
out="${ARCHIVE_DIR}/${UNIT}-${day}.log.zst"
if [[ ! -e "${out}" || ! "${day}" < "${yesterday}" ]]; then
tmp="$(mktemp "${ARCHIVE_DIR}/.${UNIT}-${day}.XXXXXX")"
journalctl -q -u "${UNIT}" -o short-iso --no-pager --since "${day} 00:00:00" --until "${next} 00:00:00" \
| mask | zstd -q -19 -f -o "${tmp}"
chgrp adm "${tmp}"
chmod 640 "${tmp}"
mv -f "${tmp}" "${out}"
written=$((written + 1))
fi
day="${next}"
done

echo "log-archive: ${written} day file(s) written to ${ARCHIVE_DIR} (${first_day}..${today})"
Loading