Files
arasystem/ops/lib/common.sh
T
ttipo 73babeaa2c feat(docker): 13단계 운영 기반 — 백업 · 감시 · 관리자 단단히 · 휴지통 30일 · 메일 프로그램 자동 설정
전 직원 전환 전에 서버가 스스로 지키고 되살릴 수 있게 했다.
- 관리자 단단히(setup): 관리자 역할에서 impersonate 를 빼고 관리자 허용 IP 를 앱 네트워크로 — 관리자 비밀번호로 바깥 993 · JMAP 에서 직원 메일함에 대리 로그인되던 길을 막았다.
  운영에서 처음 걸 때는 되돌릴 복구 관리자가 있을 때만 건다(관리자 잠김 방지).
- 휴지통 · 스팸 30일 정리를 KST 03:00 으로 명시. imapsync 로 옮긴 메일도 옮긴 때부터 센다(임시 엔진 실측).
- restic 백업(호스트 systemd 03:30 — 메일 볼륨 warm → 엔진 멈춤 9초 안팎 → final, pg_dump, 업로드, env) · 복원(새 서버 차례를 빈 클론에서 실측) · 15분 점검 · 부팅 되살리기, Healthchecks 하트비트.
- 메일 프로그램 자동 설정: frontend nginx 의 autoconfig · autodiscover 고정 응답(엔진 것은 POP3 를 알리고 입력을 되돌려 쓴다).
- 모든 서비스 docker 로그 보존, DB 이미지 판 · digest 고정.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:44:44 +09:00

142 lines
7.1 KiB
Bash

# shellcheck shell=bash
# ==========================================
# 운영 스크립트 공용 함수 (백업 · 복원 · 15분 점검이 source 한다 — 이 파일만 따로 실행하지 않는다)
#
# 설정은 /etc/arasystem/ops.env(예시 ops/ops.env.example)와 저장소의 .env.production 에서 읽는다 — 셸로 실행(source)하지 않고 줄마다 읽는다
# (값에 $ · 따옴표 · 공백이 있어도 명령이 되지 않게). 비밀(비밀번호 · 키)은 출력 · 로그 · 알림 어디에도 싣지 않는다.
# ==========================================
# Git Bash(Windows)에서 시험할 때만 뜻이 있다 — docker 인자의 컨테이너 경로(/backup/…)를 Windows 경로로 바꾸지 않게. 리눅스에서는 아무 일도 하지 않는다
export MSYS_NO_PATHCONV=1
OPS_CONFIG=${ARASYSTEM_OPS_CONFIG:-/etc/arasystem/ops.env}
log() { printf '%s %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$*"; }
die() { log "[실패] $*" >&2; exit 1; }
trim() {
local value=$1
value=${value#"${value%%[![:space:]]*}"}
value=${value%"${value##*[![:space:]]}"}
printf '%s' "$value"
}
# KEY=값 한 줄의 값 — Node 의 --env-file 과 같게 읽는다(따옴표로 감싸지 않은 값은 # 부터 주석)
parse_env_value() {
local value
value=$(trim "$1")
case $value in
\"*\") value=${value#\"}; value=${value%\"} ;;
\'*\') value=${value#\'}; value=${value%\'} ;;
*) value=$(trim "${value%%#*}") ;;
esac
printf '%s' "$value"
}
# env_get <파일> <KEY> — 마지막 정의의 값(없으면 빈 글, 종료 코드 1). KEY 는 이 스크립트들이 정한 이름뿐이다(영문 대문자 · 숫자 · _)
env_get() {
local line
line=$(grep -E "^[[:space:]]*(export[[:space:]]+)?$2[[:space:]]*=" "$1" 2>/dev/null | tail -n 1) || true
[ -n "$line" ] || return 1
parse_env_value "${line#*=}"
}
# /etc/arasystem/ops.env 를 읽어 KEY 마다 환경 변수로 — 이미 셸에 있는 값은 덮어쓰지 않는다(시험에서 하나만 바꿔 돌릴 수 있게)
# load_config 저장소의 .env.production 이 있어야 한다 (백업 · 점검 · 볼륨 · DB 복원)
# load_config optional 없어도 된다 — 새 서버 복원의 첫 단계(restore.sh files · snapshots · check)는 바로 그 파일을 백업에서 꺼낸다.
# 그때 PROJECT_NAME(백업 스냅숏의 host 이름 · 볼륨 이름)은 ops.env 의 값이다
# PROJECT_NAME 이 ops.env 와 .env.production 에 둘 다 있고 다르면 멈춘다 — 다른 이름의 볼륨을 받거나 되살리지 않게
load_config() {
local app_env=${1:-required}
[ -r "$OPS_CONFIG" ] || die "설정 파일 $OPS_CONFIG 를 읽을 수 없습니다 — ops/ops.env.example 을 복사해 채운다 (mail/README.md \"백업\")"
local line key
while IFS= read -r line || [ -n "$line" ]; do
case $line in '' | '#'*) continue ;; esac
line=$(trim "$line")
[[ $line =~ ^(export[[:space:]]+)?([A-Z_][A-Z0-9_]*)[[:space:]]*= ]] || continue
key=${BASH_REMATCH[2]}
[ -n "${!key+x}" ] && continue
printf -v "$key" '%s' "$(parse_env_value "${line#*=}")"
export "${key?}"
done < "$OPS_CONFIG"
[ -n "${ARASYSTEM_DIR:-}" ] || die "$OPS_CONFIG 에 ARASYSTEM_DIR(저장소 폴더)이 없습니다"
APP_ENV_FILE=${APP_ENV_FILE:-$ARASYSTEM_DIR/.env.production}
local from_file=""
if [ -r "$APP_ENV_FILE" ]; then
from_file=$(env_get "$APP_ENV_FILE" PROJECT_NAME || true)
elif [ "$app_env" = optional ]; then
APP_ENV_FILE=""
else
die "$APP_ENV_FILE 을 읽을 수 없습니다 (ARASYSTEM_DIR 확인 — 새 서버 복원이면 restore.sh files 로 먼저 꺼낸다, mail/README.md \"복원\")"
fi
if [ -n "${PROJECT_NAME:-}" ] && [ -n "$from_file" ] && [ "$PROJECT_NAME" != "$from_file" ]; then
die "PROJECT_NAME 이 $OPS_CONFIG(${PROJECT_NAME})와 $APP_ENV_FILE(${from_file})에서 다릅니다 — 같게 맞춘다(볼륨 · 컨테이너 · 백업 스냅숏의 이름이 이 값이다)"
fi
PROJECT_NAME=${PROJECT_NAME:-$from_file}
[[ ${PROJECT_NAME:-} =~ ^[a-z0-9][a-z0-9_-]*$ ]] ||
die "PROJECT_NAME 을 알 수 없습니다 — $OPS_CONFIG 에 적거나(새 서버 복원 — 옛 서버의 값) 저장소에 .env.production 을 둔다"
export PROJECT_NAME APP_ENV_FILE
}
# 작업 폴더(상태 파일 · 잠금 · 표시 파일 · DB 덤프) — systemd 의 StateDirectory(/var/lib/arasystem-ops, root 만)
ops_work_dir() {
local dir=${BACKUP_WORK_DIR:-$(env_get "$OPS_CONFIG" BACKUP_WORK_DIR || true)}
printf '%s' "${dir:-/var/lib/arasystem-ops}"
}
# 백업과 복원 연습이 함께 돌지 않게 — 작업 폴더의 같은 잠금 파일. 잡지 못하면 1 (flock 이 없는 곳 — 시험용 Git Bash — 은 잠그지 않는다)
take_backup_lock() {
command -v flock >/dev/null 2>&1 || return 0
exec 9> "$1/backup.lock"
flock -n 9
}
# 예시 값(change-me)이 남은 설정을 거절한다 — 이름만 알린다(값은 출력하지 않는다)
require_set() {
local key
for key in "$@"; do
[ -n "${!key:-}" ] || die "$OPS_CONFIG 에 $key 가 비어 있습니다"
case ${!key} in *[Cc][Hh][Aa][Nn][Gg][Ee]-[Mm][Ee]* | *[Cc][Hh][Aa][Nn][Gg][Ee][Mm][Ee]*) die "$OPS_CONFIG 의 $key 가 예시 값 그대로입니다" ;; esac
done
}
# curl 설정 글(-K)의 따옴표 값 — \ · " 는 이스케이프, 줄바꿈 · 탭은 \n · \t (curl 이 따옴표 안에서 푼다)
curl_config_quote() {
local value=$1
value=${value//\\/\\\\}
value=${value//\"/\\\"}
value=${value//$'\r'/}
value=${value//$'\n'/\\n}
value=${value//$'\t'/\\t}
printf '%s' "$value"
}
# hc_ping <주소> [/fail|/start] [본문] — Healthchecks.io 에 알린다. 주소가 비면 아무것도 하지 않는다. 실패해도 스크립트를 멈추지 않는다.
# 주소 · 본문은 curl 의 설정 글로 표준 입력에 준다(-K -) — 명령줄(ps · /proc)에 ping 주소가 남지 않게 (주소를 아는 사람은 거짓 신호를 보낼 수 있다)
hc_ping() {
local url=$1 suffix=${2:-} body=${3:-}
[ -n "$url" ] || return 0
if command -v curl >/dev/null 2>&1; then
# 응답은 셸이 버린다(-o /dev/null 은 쓰지 않는다 — 위 MSYS_NO_PATHCONV 때문에 Windows 의 curl 이 /dev/null 을 열지 못한다)
printf 'url = "%s"\ndata-raw = "%s"\n' "$(curl_config_quote "${url%/}${suffix}")" "$(curl_config_quote "${body:0:9000}")" \
| curl -fsS -m 15 --retry 4 --retry-delay 5 -K - >/dev/null 2>&1 \
|| log "[주의] Healthchecks 에 알리지 못했습니다 (${suffix:-성공})"
else
log "[주의] curl 이 없어 Healthchecks 에 알리지 못했습니다"
fi
}
# 컨테이너가 떠 있으면 0 · 멈춰 있으면 1 · 도커에 닿지 못하거나 컨테이너가 없으면 2
container_running() {
local running
running=$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null) || return 2
[ "$running" = true ]
}
# 컨테이너 상태 한 낱말 — healthy · unhealthy · starting · running(healthcheck 없음) · exited · … · missing
container_state() {
local state
state=$(docker inspect -f '{{if .State.Running}}{{if .State.Health}}{{.State.Health.Status}}{{else}}running{{end}}{{else}}{{.State.Status}}{{end}}' "$1" 2>/dev/null) || state=""
printf '%s' "${state:-missing}"
}