# 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 <파일> — 마지막 정의의 값(없으면 빈 글, 종료 코드 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}" }