generated from ttipo/project-base
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>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# 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}"
|
||||
}
|
||||
Reference in New Issue
Block a user