Files
AriWeb-Suite/deploy/agent-install.sh
T
zzabisagent-labandClaude Fable 5.1 2b31543a86 HAL 0.4.1: 서버 등록 TLS 수정·"이미 등록된 서버" 안내, HAL 배포가 HAL 서버의 Node Agent·HawkEye 를 함께 설치해 부트스트랩을 닫음
- hal/tls.mjs: 공개 주소 TLS 신뢰 확인(시스템 CA → 실패면 체인·공개키 핀), 신뢰 앵커(체인 자체 서명 / <data>/tls/hal-ca.pem), 10분 캐시, 핀 설치 명령(curl -k --pinnedpubkey … --hal-pin). 공개 경로 GET /agent/v1/hal-ca.pem · /agent/v1/tls, CLI tls-probe
- deploy/agent-install.sh: --hal-pin(핀으로 CA 받기)·--ca·--insecure, curl --cacert / Node NODE_EXTRA_CA_CERTS / register --ca / 유닛 Environment, 연결 실패 원인·조치 한 줄, 이미 등록된 서버 안내
- agent/halca.mjs + agent/index.mjs·config.mjs: caFile 런타임 적용(tls.setDefaultCACertificates), fetch 실패를 읽을 수 있는 한 줄로(종료 1), 이미 등록(종료 2, --force), HAL 거부(종료 3), heartbeat observability → hawkeye
- core.registerAgent: 같은 서버의 두 번째 토큰 409 server_already_registered / 쓴 토큰 409 token_used("이미 등록된 서버"), 거부 토큰 미소비, 재등록 허용(replace: 기존 Agent 해지, 감사 agent.replace), 마이그레이션 hal_0011_token_replace, API/CLI --replace --json(재인증), ops.js 재등록 확인·핀 안내
- deploy/hal-host-agent.sh(server-create --profile hal → server-token --json → agent-install loopback → server-approve), bootstrap-hal.sh 6/7 단계(--no-agent), CLI server-create·server-approve, SERVER_PROFILES.hal
- core.deploymentStatus/selfHealth.deployment, 배너 "부트스트랩 완료 · 기능 단위 서버 배포 대기: …" / "진행 중", 배포 릴리스 화면 문구, heartbeat canonicalModuleName(0.3.17 잔여 결함)
- 개발 지시 v0.4.1(Oz)·VERSION_HISTORY 0.4.1·README·docs/HAL-AGENT §6·RUNBOOK §11·AI 스킬, 도입 모듈 변경 없음(Webmaker 1.9.0·AriMail 1.1.0)
- test/hal-041.test.mjs RED → GREEN 7/7(실제 자체 서명 TLS 픽스처·별도 프로세스 CLI), 기존 배너·프로필·버전 테스트 갱신, 전체 32파일 통과, 로컬 E2E 보고서 2026-11-06

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 08:40:09 +00:00

68 lines
9.7 KiB
Bash
Executable File

#!/bin/bash
# deploy/agent-install.sh — 관리 서버 또는 고객 외부 서버에 Node Agent 설치·등록(§28.2·§10.2). HAL 이 /agent/v1/install.sh 로 제공한다.
# curl -fsSL https://hal.ariweb.net/agent/v1/install.sh | bash -s -- --hal https://hal.ariweb.net --token <일회용 토큰> [--role web|mail|backup|control] [--external --site <siteId>] [--app-dir /srv/ariweb/agent] [--label <이름>]
# [0.4.1] HAL 인증서가 사설 CA·자체 서명이면(개발 지시 v0.4.1 요구 1) HAL 이 만든 설치 명령에 공개키 핀이 붙는다:
# curl -fsSL -k --pinnedpubkey 'sha256//…' https://hal/agent/v1/install.sh | bash -s -- --hal https://hal --token … --hal-pin 'sha256//…'
# --hal-pin : 그 핀으로 HAL 의 신뢰 앵커(/agent/v1/hal-ca.pem)를 받아 <app-dir>/data/hal-ca.pem 에 두고, 이후 모든 curl(--cacert)·Node(NODE_EXTRA_CA_CERTS·agent.json caFile) 호출의 CA 로 쓴다.
# --ca <pem> : 운영자가 가진 CA 파일을 직접 지정(핀 대신). --insecure : 검증 없이 진행(실험실 전용, 경고 출력).
# 하는 일: node 22+ 확인(없으면 NodeSource 로 설치), agent artifact 를 HAL 에서 받아 <app-dir>/releases/<ver> 에 풀고 current 연결, 등록(토큰 1회), systemd 유닛 ariweb-agent 설치·시작, sudoers 최소 규칙(관리형만).
# 멱등: 다시 실행하면 이미 등록된 Agent 는 재등록하지 않고("이미 등록된 서버" 안내) 코드·유닛만 맞춘다. 토큰 외의 비밀은 출력하지 않는다.
set -euo pipefail
HAL=""; TOKEN=""; ROLE="web"; EXTERNAL=0; SITE=""; APP_DIR="/srv/ariweb/agent"; LABEL="$(hostname)"; HAL_PIN=""; CA_FILE=""; INSECURE=0
while [ $# -gt 0 ]; do case "$1" in --hal) HAL="$2"; shift 2;; --token) TOKEN="$2"; shift 2;; --role) ROLE="$2"; shift 2;; --external) EXTERNAL=1; shift;; --site) SITE="$2"; shift 2;; --app-dir) APP_DIR="$2"; shift 2;; --label) LABEL="$2"; shift 2;; --hal-pin) HAL_PIN="$2"; shift 2;; --ca) CA_FILE="$2"; shift 2;; --insecure) INSECURE=1; shift;; *) echo "알 수 없는 옵션 $1" >&2; exit 1;; esac; done
[ -n "$HAL" ] || { echo "--hal 이 필요합니다" >&2; exit 1; }
HAL="${HAL%/}"; USER_NAME="$(id -un)"; SCHEME="${HAL%%://*}"
if ! command -v node >/dev/null 2>&1 || [ "$(node --version | sed -E 's/^v([0-9]+).*/\1/')" -lt 22 ]; then echo "== node 22 설치"; curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -n bash - >/dev/null && sudo -n apt-get install -y -q nodejs >/dev/null; fi
NODE_BIN="$(command -v node)"
# 데이터 디렉터리를 먼저 만든다 — HAL CA 파일(hal-ca.pem)을 둘 곳. (0.4.1: CA 는 HAL 확인보다 먼저 있어야 한다)
sudo -n mkdir -p "$APP_DIR/data"; sudo -n chown "$USER_NAME:$USER_NAME" "$APP_DIR" "$APP_DIR/data"; chmod 700 "$APP_DIR/data"
# ---- [0.4.1] HAL 인증서 신뢰: curl 은 --cacert, Node 는 NODE_EXTRA_CA_CERTS(+ agent.json caFile) 로 같은 CA 를 쓴다
CURL=(curl -fsS); CA=""
if [ "$SCHEME" = "https" ]; then
if [ -n "$CA_FILE" ]; then [ -f "$CA_FILE" ] || { echo "--ca 파일이 없습니다: $CA_FILE" >&2; exit 1; }; cp "$CA_FILE" "$APP_DIR/data/hal-ca.pem"; CA="$APP_DIR/data/hal-ca.pem"
elif [ -n "$HAL_PIN" ]; then
echo "== HAL CA 받기(공개키 핀 확인: ${HAL_PIN:0:20}…)"
# -k 는 CA 검증만 끈다. --pinnedpubkey 는 서버 공개키가 핀과 다르면 자료를 받기 전에 끊는다(중간자 방지). 핀은 HAL 운영자 콘솔이 설치 명령에 넣어 준 값이다.
if ! curl -fsS -k --pinnedpubkey "$HAL_PIN" "$HAL/agent/v1/hal-ca.pem" -o "$APP_DIR/data/hal-ca.pem.tmp"; then rm -f "$APP_DIR/data/hal-ca.pem.tmp"; echo "HAL CA 를 받지 못했습니다 — 핀이 HAL 의 공개키와 다르거나(명령을 새로 발급받으세요) HAL 이 CA 를 제공하지 않습니다(HAL 서버의 <data>/tls/hal-ca.pem 또는 --ca 로 지정)" >&2; exit 1; fi
grep -q "BEGIN CERTIFICATE" "$APP_DIR/data/hal-ca.pem.tmp" || { rm -f "$APP_DIR/data/hal-ca.pem.tmp"; echo "받은 CA 파일에 인증서가 없습니다" >&2; exit 1; }
mv "$APP_DIR/data/hal-ca.pem.tmp" "$APP_DIR/data/hal-ca.pem"; CA="$APP_DIR/data/hal-ca.pem"
elif [ -f "$APP_DIR/data/hal-ca.pem" ]; then CA="$APP_DIR/data/hal-ca.pem"; fi # 이전 실행이 받아 둔 CA(재실행·갱신)
if [ -n "$CA" ]; then chmod 644 "$CA"; CURL+=(--cacert "$CA"); export NODE_EXTRA_CA_CERTS="$CA"; echo "== HAL CA 사용: $CA"; fi
if [ "$INSECURE" = "1" ]; then CURL+=(-k); export NODE_TLS_REJECT_UNAUTHORIZED=0; echo "경고: --insecure — HAL 인증서를 검증하지 않습니다(실험실 전용)" >&2; fi
fi
echo "== HAL 확인 $HAL"
if ! HEALTH="$("${CURL[@]}" "$HAL/api/health" 2>"/tmp/ariweb-agent-$$.err")"; then
ERR="$(cat "/tmp/ariweb-agent-$$.err" 2>/dev/null || true)"; rm -f "/tmp/ariweb-agent-$$.err"
echo "HAL($HAL) 에 연결할 수 없습니다: ${ERR:-원인 불명}" >&2
case "$ERR" in *certificate*|*SSL*|*ssl*) echo "인증서 문제입니다 — HAL 운영자 콘솔이 만든 설치 명령(--hal-pin 포함)을 그대로 쓰거나, HAL 의 CA 파일을 --ca <PEM> 으로 지정하세요" >&2;; esac
exit 1
fi
rm -f "/tmp/ariweb-agent-$$.err"
VERSION="$(echo "$HEALTH" | $NODE_BIN -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>console.log(JSON.parse(s).bootstrap.releaseVersion||JSON.parse(s).version))")"
sudo -n mkdir -p "$APP_DIR/releases/$VERSION" /var/www/ariweb-sites /srv/ariweb/backups; sudo -n chown -R "$USER_NAME:$USER_NAME" "$APP_DIR" /var/www/ariweb-sites /srv/ariweb/backups; chmod 700 "$APP_DIR/data"
if [ ! -f "$APP_DIR/releases/$VERSION/agent/index.mjs" ]; then
echo "== agent artifact $VERSION 내려받기"
# 등록 전에는 서명을 만들 수 없으므로 부트스트랩 전용 공개 경로가 아니라 등록 토큰으로 1회 받는다: HAL 의 install.sh 는 토큰과 함께 artifact 를 함께 보낸다(아래 등록 뒤 재시도 경로).
if [ -n "$TOKEN" ]; then "${CURL[@]}" -H "X-Registration-Token: $TOKEN" "$HAL/agent/v1/bootstrap-artifact?module=agent&version=$VERSION" -o /tmp/ariweb-agent-$$.tgz || { echo "agent artifact 를 받지 못했습니다 — 토큰이 만료·사용되었거나(이미 등록된 서버면 재설치가 필요 없습니다) HAL 에 릴리스 $VERSION 의 agent artifact 가 없습니다" >&2; exit 1; }; tar -xzf /tmp/ariweb-agent-$$.tgz -C "$APP_DIR/releases/$VERSION"; rm -f /tmp/ariweb-agent-$$.tgz; else echo "코드가 없고 토큰도 없습니다" >&2; exit 1; fi
fi
# 실행 계정이 다른 도입 모듈 설치(deploy/module-as-user.sh)를 위해 코드 트리는 다른 계정도 읽을 수 있어야 한다 — 호출자의 umask 와 무관하게 맞춘다(실제 환경 결함 수정)
chmod 755 "$APP_DIR" "$APP_DIR/releases"; chmod -R a+rX "$APP_DIR/releases/$VERSION"
ln -sfn "$APP_DIR/releases/$VERSION" "$APP_DIR/current"; cd "$APP_DIR/current"
export ARIWEB_AGENT_DATA="$APP_DIR/data"
if [ ! -f "$APP_DIR/data/agent.json" ]; then
[ -n "$TOKEN" ] || { echo "등록되지 않았고 --token 도 없습니다" >&2; exit 1; }; echo "== 등록"
CA_OPT=(); [ -n "$CA" ] && CA_OPT=(--ca "$CA")
if [ "$EXTERNAL" = "1" ]; then $NODE_BIN agent/index.mjs register --hal "$HAL" --token "$TOKEN" --external --site "$SITE" --label "$LABEL" "${CA_OPT[@]}"; else $NODE_BIN agent/index.mjs register --hal "$HAL" --token "$TOKEN" --role "$ROLE" --label "$LABEL" "${CA_OPT[@]}"; fi || { RC=$?; [ "$RC" = "3" ] && echo "이 서버는 HAL 에 이미 등록되어 있습니다 — 재설치가 필요 없습니다. 새 Agent 로 바꾸려면 운영자 콘솔에서 \"재등록 허용\" 토큰을 발급받으세요" >&2 || echo "등록에 실패했습니다 — 위 메시지를 확인하세요" >&2; exit 1; }
else
echo "== 이미 등록된 서버($(cat "$APP_DIR/data/agent.json" | $NODE_BIN -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{const j=JSON.parse(s);console.log('Agent '+j.id+' → '+j.halUrl+(j.registeredAt?', 등록 '+j.registeredAt:''))})")) — 재등록하지 않고 코드·유닛만 갱신합니다. 새 Agent 로 바꾸려면 운영자 콘솔에서 \"재등록 허용\" 토큰을 받고 $APP_DIR/data/agent.json 을 지운 뒤 다시 실행하세요"
# 0.4.1: 이전 설치가 CA 를 몰랐고 지금 CA 가 생겼으면 agent.json 에 caFile 을 기록한다(Agent 가 기동 때 적용)
if [ -n "$CA" ]; then $NODE_BIN -e "const fs=require('fs');const f='$APP_DIR/data/agent.json';const j=JSON.parse(fs.readFileSync(f,'utf8'));if(j.caFile!=='$CA'){j.caFile='$CA';fs.writeFileSync(f+'.tmp',JSON.stringify(j,null,1),{mode:0o600});fs.renameSync(f+'.tmp',f);console.log(' agent.json caFile 갱신')}"; fi
fi
echo "== systemd 유닛"
ENV_LINES=""; [ "$EXTERNAL" = "1" ] && ENV_LINES="Environment=ARIWEB_AGENT_EXTERNAL=1"
[ -n "$CA" ] && ENV_LINES="${ENV_LINES:+$ENV_LINES\\n}Environment=NODE_EXTRA_CA_CERTS=$CA" # Agent 서비스도 같은 CA 로 HAL 을 믿는다(자기 갱신 때 Environment 줄은 보존된다)
sed -e "s#{{APP_DIR}}#$APP_DIR#g" -e "s#{{USER}}#$USER_NAME#g" -e "s#{{NODE}}#$NODE_BIN#g" -e "s#{{VERSION}}#$VERSION#g" -e "s#{{ENV_LINES}}#$ENV_LINES#" deploy/units/ariweb-agent.service.tmpl | sudo -n tee /etc/systemd/system/ariweb-agent.service >/dev/null
if [ "$EXTERNAL" = "0" ]; then sudo -n install -m 440 deploy/sudoers/ariweb-agent /etc/sudoers.d/ariweb-agent && sudo -n visudo -c >/dev/null; sudo -n cp deploy/nginx/awstats.conf /etc/nginx/conf.d/awstats.conf 2>/dev/null && (sudo -n nginx -t >/dev/null 2>&1 && sudo -n systemctl reload nginx || true); fi
sudo -n systemctl daemon-reload; sudo -n systemctl enable --now ariweb-agent >/dev/null; sudo -n systemctl restart ariweb-agent; sleep 2
systemctl is-active --quiet ariweb-agent && echo "== 완료: ariweb-agent 실행 중. HAL 운영자 콘솔에서 서버를 승인하면 배치가 가능합니다" || { echo "ariweb-agent 기동 실패 — journalctl -u ariweb-agent" >&2; exit 1; }