2026-10-26 11:58:15 +00:00
#!/bin/bash
# deploy/bootstrap-hal.sh — HAL 최초 설치(부트스트랩, Dev-Order §32.2). 최초 설치에서만 허용되는 작업:
# 1) 승인된 main 릴리스의 고정 artifact 선택 2) HAL 저장 기능·비밀정보 보관소·인증서 기반·접근 경로 준비 3) HAL 설치 4) 첫 관리 서버 Node Agent 등록 토큰 발급
# 그 뒤 모든 모듈(포털·Webmaker·AriMail·Agent 갱신 등)은 HAL 작업으로만 설치한다. 이 스크립트는 HAL 데이터에 부트스트랩이 완료된 흔적이 있으면 실행을 거부한다.
#
# 사용(대상 서버에서, sudo 가능한 일반 계정으로):
2026-10-27 03:04:43 +00:00
# bash deploy/bootstrap-hal.sh --dist <dist/<version> 디렉터리(manifest.json·artifact)> --host hal.ariweb.net --cert /etc/letsencrypt/live/wh-wild-ariweb.net [--portal-host my.ariweb.net | none] [--app-dir /srv/ariweb/hal]
# --portal-host none : 포털 접근 경로(nginx my.ariweb.net → 127.0.0.1:8910)를 만들지 않는다 — HAL 만 두는 호스트(예: hal21.ariweb.net)용.
2026-11-06 08:40:09 +00:00
# [0.4.1] 이 스크립트는 HAL 과 함께 이 서버의 Node Agent·HawkEye 를 설치·등록·승인해 부트스트랩을 닫는다(deploy/hal-host-agent.sh, 요구 2) — "부트스트랩 미완료" 상태를 남기지 않는다.
# Web·Mail·Backup·Portal 은 설치하지 않는다(기능 단위 서버로 HAL 작업 배포). --no-agent 로 끌 수 있다(이전 방식: 운영자가 첫 Agent 를 직접 설치·승인).
# 출력: 운영자 최초 설정 토큰(1회). 비밀값은 그 밖에 출력하지 않는다.
2026-10-26 11:58:15 +00:00
set -euo pipefail
2026-11-06 08:40:09 +00:00
DIST = "" ; HOST = "hal.ariweb.net" ; CERT = "/etc/letsencrypt/live/wh-wild-ariweb.net" ; APP_DIR = "/srv/ariweb/hal" ; PORTAL_HOST = "my.ariweb.net" ; ACME = "/var/www/letsencrypt" ; USER_NAME = " $( id -un) " ; WITH_AGENT = 1
while [ $# -gt 0 ] ; do case " $1 " in --dist) DIST = " $2 " ; shift 2;; --host) HOST = " $2 " ; shift 2;; --cert) CERT = " $2 " ; shift 2;; --app-dir) APP_DIR = " $2 " ; shift 2;; --portal-host) PORTAL_HOST = " $2 " ; shift 2;; --acme) ACME = " $2 " ; shift 2;; --no-agent) WITH_AGENT = 0; shift;; *) echo "알 수 없는 옵션 $1 " >& 2; exit 1;; esac ; done
2026-10-26 11:58:15 +00:00
[ -n " $DIST " ] && [ -f " $DIST /manifest.json" ] || { echo "--dist <manifest.json 이 있는 디렉터리> 가 필요합니다" >& 2; exit 1; }
NODE_BIN = " $( command -v node || true ) " ; [ -n " $NODE_BIN " ] || { echo "node 22.13+ 가 필요합니다" >& 2; exit 1; }
MAJOR = " $( $NODE_BIN --version | sed -E 's/^v([0-9]+).*/\1/' ) " ; [ " $MAJOR " -ge 22 ] || { echo "Node 22.13+ 필요: $( $NODE_BIN --version) " >& 2; exit 1; }
sudo -n true 2>/dev/null || { echo "비밀번호 없는 sudo 가 필요합니다" >& 2; exit 1; }
VERSION = " $( $NODE_BIN -e "console.log(JSON.parse(require('fs').readFileSync(' $DIST /manifest.json','utf8')).version)" ) "
COMMIT = " $( $NODE_BIN -e "console.log(JSON.parse(require('fs').readFileSync(' $DIST /manifest.json','utf8')).mainCommit)" ) "
DIRTY = " $( $NODE_BIN -e "const m=JSON.parse(require('fs').readFileSync(' $DIST /manifest.json','utf8'));console.log(m.checks&&m.checks.dirty?'1':'0')" ) "
[ " $DIRTY " = "0" ] || { echo "main 에 병합되지 않은(dirty) 빌드는 부트스트랩에 쓸 수 없습니다" >& 2; exit 1; }
ART = " $DIST /hal- $VERSION .tar.gz" ; [ -f " $ART " ] || { echo "artifact 없음: $ART " >& 2; exit 1; }
SUM = " $( sha256sum " $ART " | cut -d' ' -f1) " ; WANT = " $( $NODE_BIN -e "console.log(JSON.parse(require('fs').readFileSync(' $DIST /manifest.json','utf8')).artifacts.hal.sha256)" ) "
[ " $SUM " = " $WANT " ] || { echo "artifact sha256 불일치" >& 2; exit 1; }
2026-11-06 08:40:09 +00:00
echo "== 1/7 릴리스 $VERSION (main $COMMIT , hal artifact sha256 ${ SUM : 0 : 16 } …)"
2026-10-26 11:58:15 +00:00
DATA = " $APP_DIR /data"
if [ -f " $DATA /hal.sqlite" ] && $NODE_BIN -e "const {DatabaseSync}=require('node:sqlite');const d=new DatabaseSync(' $DATA /hal.sqlite',{readOnly:true});const r=d.prepare('SELECT completed_at FROM bootstrap_state WHERE id=1').get();process.exit(r&&r.completed_at?0:1)" 2>/dev/null; then
echo "이미 부트스트랩이 완료된 HAL 데이터( $DATA )가 있습니다 — 이후 설치·갱신은 HAL 작업(운영자 콘솔 › 배포 릴리스)으로만 합니다" >& 2; exit 2
fi
2026-11-06 08:40:09 +00:00
echo "== 2/7 디렉터리·보관소·접근 경로 준비"
2026-10-26 11:58:15 +00:00
sudo -n mkdir -p " $APP_DIR /releases/ $VERSION " " $DATA " " $ACME " /srv/ariweb /var/www/ariweb-sites; sudo -n chown -R " $USER_NAME : $USER_NAME " " $APP_DIR " /srv/ariweb /var/www/ariweb-sites; chmod 700 " $DATA "
tar -xzf " $ART " -C " $APP_DIR /releases/ $VERSION " ; ln -sfn " $APP_DIR /releases/ $VERSION " " $APP_DIR /current"
cd " $APP_DIR /current"
export ARIWEB_HAL_DATA = " $DATA " ; $NODE_BIN -e "import('./lib/vault.mjs').then(m=>{m.openVault(' $DATA ');console.log(' 보관소 준비(vault.key 0600)')})"
$NODE_BIN hal/index.mjs bootstrap-release --version " $VERSION " --commit " $COMMIT " --sha256 " $SUM "
2026-10-26 12:02:07 +00:00
$NODE_BIN hal/index.mjs register-release --manifest " $DIST /manifest.json" --approve --by bootstrap # hal artifact 에는 scripts/ 가 없으므로 HAL CLI 로 등록·artifact 복사
2026-10-26 11:58:15 +00:00
$NODE_BIN -e "const fs=require('fs');const f=' $DATA /config.json';const v=fs.existsSync(f)?JSON.parse(fs.readFileSync(f,'utf8')).values:{};v.publicUrl='https:// $HOST ';v.portalUrl=v.portalUrl||'http://127.0.0.1:8910';fs.writeFileSync(f,JSON.stringify({version:1,updatedAt:new Date().toISOString(),updatedBy:'bootstrap',values:v},null,1),{mode:0o600})"
2026-11-06 08:40:09 +00:00
echo "== 3/7 systemd 유닛 ariweb-hal"
2026-10-26 11:58:15 +00:00
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" deploy/units/ariweb-hal.service.tmpl | sudo -n tee /etc/systemd/system/ariweb-hal.service >/dev/null
sudo -n install -m 440 deploy/sudoers/ariweb-agent /etc/sudoers.d/ariweb-agent; sudo -n visudo -c >/dev/null
sudo -n systemctl daemon-reload; sudo -n systemctl enable --now ariweb-hal >/dev/null; sleep 2; systemctl is-active --quiet ariweb-hal || { echo "ariweb-hal 기동 실패 — journalctl -u ariweb-hal" >& 2; exit 1; }
2026-11-06 08:40:09 +00:00
echo "== 4/7 nginx $HOST $( [ " $PORTAL_HOST " != none ] && echo " · $PORTAL_HOST " ) (인증서 $CERT )"
2026-10-26 13:43:51 +00:00
sudo -n test -f " $CERT /fullchain.pem" || { echo "인증서 $CERT /fullchain.pem 이 없습니다" >& 2; exit 1; } # /etc/letsencrypt/live 는 root 만 읽을 수 있어 sudo 로 확인한다(실제 환경 결함 수정)
2026-10-26 11:58:15 +00:00
sed -e "s#{{HAL_HOST}}# $HOST #g" -e "s#{{CERT_DIR}}# $CERT #g" -e "s#{{PORT}}#8900#g" -e "s#{{ACME_ROOT}}# $ACME #g" deploy/nginx/ariweb-hal.conf.tmpl | sudo -n tee /etc/nginx/sites-available/ariweb-hal.conf >/dev/null
2026-10-26 13:38:45 +00:00
sudo -n ln -sf /etc/nginx/sites-available/ariweb-hal.conf /etc/nginx/sites-enabled/ariweb-hal.conf; sudo -n cp deploy/nginx/awstats.conf /etc/nginx/conf.d/awstats.conf
# 포털 접근 경로(my.ariweb.net → 127.0.0.1:8910)도 여기서 준비한다 — 포털 자체는 HAL 작업으로 설치되지만 nginx 접근 경로는 "접근 경로 준비"(§32.2 2단계)에 속한다(실제 환경 결함 수정: 이전에는 어디서도 만들지 않았다).
2026-10-27 03:04:43 +00:00
# --portal-host none 이면 포털 접근 경로를 만들지 않는다(HAL 만 두는 호스트 — 포털은 다른 호스트에서 운영).
if [ " $PORTAL_HOST " != "none" ] ; then
sed -e "s#{{PORTAL_HOST}}# $PORTAL_HOST #g" -e "s#{{CERT_DIR}}# $CERT #g" -e "s#{{PORT}}#8910#g" -e "s#{{ACME_ROOT}}# $ACME #g" deploy/nginx/ariweb-portal.conf.tmpl | sudo -n tee /etc/nginx/sites-available/ariweb-portal.conf >/dev/null
sudo -n ln -sf /etc/nginx/sites-available/ariweb-portal.conf /etc/nginx/sites-enabled/ariweb-portal.conf
fi
2026-10-26 13:38:45 +00:00
sudo -n nginx -t && sudo -n systemctl reload nginx
2026-11-06 08:40:09 +00:00
echo "== 5/7 HAL 자체 건강 확인"
2026-10-26 11:58:15 +00:00
curl -fsS http://127.0.0.1:8900/api/health | $NODE_BIN -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{const h=JSON.parse(s);console.log(' HAL',h.version,'db',h.db,'jobs',h.jobs,'bootstrap',JSON.stringify(h.bootstrap))})"
curl -fsS --resolve " $HOST :443:127.0.0.1" "https:// $HOST /api/health" >/dev/null && echo " https:// $HOST /api/health OK (로컬 resolve)" || echo " 경고: https:// $HOST 확인 실패 — DNS A 레코드( $HOST → 이 서버)와 인증서를 확인하세요"
2026-11-06 08:40:09 +00:00
# [0.4.1] HAL 공개 주소 TLS 신뢰 상태(사설 CA·자체 서명이면 설치 명령에 핀이 붙고 /agent/v1/hal-ca.pem 이 CA 를 준다)
$NODE_BIN hal/index.mjs tls-probe | $NODE_BIN -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{const t=JSON.parse(s);console.log(' 공개 주소 TLS:',t.trusted===true?'시스템 CA 로 신뢰됨':t.trusted===false?'신뢰되지 않음('+t.reason+') — 다른 서버의 설치 명령에 공개키 핀이 붙습니다(신뢰 앵커 '+t.caCount+'개)':'확인 불가('+(t.error||t.reason)+')')})" || true
if [ " $WITH_AGENT " = "1" ] ; then
echo "== 6/7 이 서버의 Node Agent·HawkEye 설치·등록·승인(부트스트랩 닫기)"
bash deploy/hal-host-agent.sh --hal-data " $DATA " --hal-local http://127.0.0.1:8900 --name " $HOST " --by bootstrap
else
echo "== 6/7 (--no-agent) 이 서버의 Node Agent 는 설치하지 않습니다 — 부트스트랩은 첫 Agent 승인 때 닫힙니다"
fi
echo "== 7/7 최초 운영자 설정 토큰"
2026-10-26 11:58:15 +00:00
$NODE_BIN hal/index.mjs setup-token
cat <<MSG
다음 순서:
1) 위 설정 토큰으로 첫 운영자 계정을 만듭니다(POST https://$HOST/ops/api/setup). 운영자 콘솔은 허용 네트워크(사설망/VPN) 또는 접근권(node hal/index.mjs grant --login <id>)이 있어야 열립니다.
2026-11-06 08:40:09 +00:00
2) $([ "$WITH_AGENT" = "1" ] && echo "부트스트랩은 완료되었습니다(이 서버의 Node Agent·HawkEye 가 함께 설치·승인됨). 운영자 콘솔 › 배포 릴리스의 \"역할별 통합 배포(새 서버)\" 로 기능 단위 서버(Webmaker·AriMail·백업·포털)를 추가해 배포합니다." || echo "운영자 콘솔 › 서버에서 이 서버(역할 control)를 만들고 등록 토큰을 발급해 첫 Node Agent 를 설치합니다. 첫 Agent 가 승인되면 부트스트랩이 닫힙니다.")
다른 서버의 설치 명령은 운영자 콘솔이 만들어 줍니다 — HAL 인증서가 사설 CA 면 명령에 공개키 핀(--hal-pin)이 붙습니다.
2026-10-26 11:58:15 +00:00
3) 포털 연동 키: node hal/index.mjs link-portal (출력 값을 포털 보관소에 넣습니다)
MSG