# 0.1.1 배포·검증 절차(RUNBOOK) 대상: ariweb.net 호스트(`ssh hostinger`, 187.53.133.173, master 계정, sudo, Node 22.23, nginx, 와일드카드 인증서 `*.ariweb.net`). 원칙(§32): HAL 먼저, 그 뒤 모든 모듈은 HAL 작업으로만. 개별 서버에 임의 접속해 일반 배포하지 않는다. ## 0. 사전 조건(운영자 조치) - DNS(hosting.co.kr 패널): `hal.ariweb.net A 187.53.133.173`, `my.ariweb.net A 187.53.133.173`. 레코드 전에는 `curl --resolve` 로만 확인된다. - 검증용 메일 수신 주소(자동 응답 메일함) — 메일 활성화의 실제 왕복 확인에 필요(`PUT /internal/ops/mail/policy {verifier}`). ## 1. 릴리스 artifact(개발 PC, main 병합 뒤) ```bash git checkout main && git pull node scripts/release.mjs build # 전체 테스트 → dist/0.1.1/{hal,portal,agent,webmaker,arimail}-0.1.1.tar.gz + manifest.json scp -r dist/0.1.1 hostinger:/tmp/ariweb-dist-0.1.1 ``` ## 2. HAL 부트스트랩(최초 1회, 서버에서) ```bash ssh hostinger tar -xzf /tmp/ariweb-dist-0.1.1/hal-0.1.1.tar.gz -C /tmp/ariweb-hal-src # deploy/ 스크립트를 쓰기 위해 풀어 둔다 bash /tmp/ariweb-hal-src/deploy/bootstrap-hal.sh --dist /tmp/ariweb-dist-0.1.1 --host hal.ariweb.net --cert /etc/letsencrypt/live/wh-wild-ariweb.net ``` 출력된 **운영자 설정 토큰(1회)** 으로 첫 운영자를 만든다: `POST https://hal.ariweb.net/ops/api/setup {setupToken, login, password}` (허용 네트워크 밖이면 `node hal/index.mjs grant --login ` 로 접근권 URL 발급). 확인: `curl -s https://hal.ariweb.net/api/health` 의 `version`·`db`·`jobs`·`bootstrap.halRunningVersion`. DNS 레코드가 아직 없으면 같은 서버의 Agent·포털이 `https://hal.ariweb.net` 에 닿도록 `/etc/hosts` 에 `127.0.0.1 hal.ariweb.net my.ariweb.net` 을 임시로 둔다(cloud-init 이 `/etc/hosts` 를 관리하는 호스트는 재부팅 때 지워질 수 있다 — DNS 레코드가 생기면 필요 없다). 부트스트랩은 `my.ariweb.net` nginx 접근 경로(→ 127.0.0.1:8910)도 함께 만든다. HAL 만 두는 호스트(예: bear `hal21.ariweb.net`, 포털은 다른 호스트)는 `--portal-host none` 으로 포털 접근 경로를 만들지 않고, 인증서가 호스트별 certbot(webroot `/var/www/html`)이면 `--cert /etc/letsencrypt/live/<호스트> --acme /var/www/html` 을 준다(§7 참조). ## 3. 첫 Node Agent(같은 서버, 역할 control) 운영자 콘솔 › 서버 › 새 서버(역할 control, 위치 ariweb.net) → 등록 토큰(10분·1회) → ```bash curl -fsSL https://hal.ariweb.net/agent/v1/install.sh | bash -s -- --hal https://hal.ariweb.net --token <토큰> --role control ``` 운영자 승인 → `bootstrap.complete=true`, 수동 설치 잠김. 모듈 기술서는 서버 역할을 배포 조건으로 두지 않으므로 한 대(control)에 portal·webmaker·arimail 을 모두 둘 수 있다. 포털 연동 키: `node hal/index.mjs link-portal` 의 출력 값을 포털 보관소에 1회 입력한다. 포털 데이터 디렉터리(`/srv/ariweb/portal/data`)는 배포 전에 만들어 보관소를 먼저 채워도 된다(같은 `lib/vault.mjs`). 값을 화면·로그에 남기지 않으려면 서버 안에서 파이프로 바로 넣는다. ## 4. 모듈 배포(HAL 작업) 운영자 콘솔 › 배포 릴리스 › 0.1.1 승인 확인 → 모듈 배포 순서: `portal` → `webmaker` → `arimail` → (`agent` 갱신). 각 작업의 대상별 결과·실제 버전·건강 상태를 확인한다. HAL 자체 건강이 실패하면 나머지를 배포하지 않는다. - portal: `/srv/ariweb/portal`, 유닛 `ariweb-portal`, nginx `my.ariweb.net`(와일드카드 인증서). 포털 보관소에 `service:hal`(link-portal 출력), `sso:webmaker`, `arimail:*` 키 입력. - webmaker: 기존 `/srv/webmaker` + `/data/webmaker` 유지(HAL 모듈 설명자가 기존 유닛명·데이터 경로를 발견해 보존), 설정 `ariweb.portalUrl`·`ariweb.secret`. - arimail: 기존 `/srv/arihakdang-mail` 유지(실행 계정 `arihakdang` — Agent 가 `deploy/module-as-user.sh` 로 그 계정이 파일 작업을 하게 한다), 마이그레이션 `0009_ariweb_queue.sql` 적용(유닛의 `EnvironmentFile` 의 `DB_FILE`). 포털 › 운영자 › 메일 서버 등록(`POST /internal/ops/mail/servers`)에 setup-token(`/data/setup-token`)·usage-key(기본 도메인 설정의 `USAGE_API_KEY`)를 넣고, 고객 도메인의 `ARIWEB_SSO_SECRET` 은 포털이 서비스 생성 때 스스로 넣는다. - 도입 모듈의 실제 버전은 `REVISION.json` 의 Suite 버전(0.1.1)이고 상류 버전(1.7.3·1.0.11)은 `upstreamVersion` 으로 함께 보인다. ## 4-1. 실제 배포(2026-10-26)에서 확인한 운영 사실 - 서버가 한 대(역할 control)면 사이트 배포·백업 저장은 control 서버가 대체한다(`pickServer`). 백업을 같은 서버에 두려면 운영자 override(`/internal/ops/backup/options/:id/override-same-server`)가 필요하다. - 포털 메일 서버 등록(`POST /ops/api/portal/mail/servers`): `baseUrl https://webmail.ariweb.net`, `internalUrl http://127.0.0.1:8790`, `setupUrl https://mail.arihakdang.com:8791`(AriMail 전체 설정 API 는 기본 도메인 호스트에서만 응답하므로 기본 도메인 이름을 쓰고, 설정 포트가 loopback 전용이라 서버 `/etc/hosts` 에 `127.0.0.1 mail.arihakdang.com` 을 둔다), 비밀은 `setup-token`(`/data/setup-token`)·`usage-key`(없으면 `POST /api/setup/usage-key {action:'generate'}` 로 발급, 기본 도메인 Host 로). - 발신 메일: 포털 보관소 `mailer:resend-key`(Webmaker 보관소의 Resend 키 재사용), 설정 `mailer: resend`, `mailFrom no-reply@ariweb.net`. 검증 때는 `resendBase` 를 로컬 포획 서버로 돌려 링크를 꺼냈고, 끝난 뒤 되돌렸다. - 수정 artifact 를 같은 릴리스(0.1.1)로 다시 내려면 `node hal/index.mjs register-release --manifest --approve` 로 재등록(기존 버전은 artifact·커밋이 갱신된다)한 뒤 운영자 콘솔에서 `agent` → `hal` → 바뀐 모듈 순으로 배포한다. `bootstrap-release --commit` 으로 건강 응답의 main 커밋도 맞춘다. - 검증 데이터(`*@check.ariweb.net` 계정, `check-*/solo-*.ariweb.net` 도메인·메일 서비스)는 운영자 취소로 정리했고 보존 기간 뒤 삭제된다. ## 5. 검증(Release 0.1.1 완료 조건 34개) `docs/verification/` 의 로컬 E2E 결과와 실제 환경 확인 기록을 함께 둔다. 각 흐름은 버튼 성공이 아니라 최종 화면·외부 접속·실제 메일 왕복·실제 사용량·실제 배포 버전으로 판정한다. 충족하지 못한 흐름은 완료로 표시하지 않는다. ## 6. 되돌리기 - 모듈: 운영자 콘솔 › 모듈 › 되돌리기(이전 릴리스 디렉터리 유지, 3개 보관). - HAL: `deploy/hal-restore.sh` + 재해복구 절차(§34, 기존 제어 위치 중지 확인 뒤 `--takeover`). ## 7. HAL 전용 호스트 부트스트랩 — bear `hal21.ariweb.net` (2026-10-27) 대상: bear(`ssh bear`, 192.168.20.23 / 공인 121.152.149.45, master 계정, sudo NOPASSWD, Ubuntu 24.04, nginx 1.24, Node v24.22(엔진 요구 ≥22.13 충족), certbot 2.9 nginx 플러그인·`certbot.timer`). 기존 운영(Webmaker `webmaker.ibit.kr`·`hs-company.ariweb.net`, Gitea `source.ibit.kr`, Matrix, board/file/office/xmap, 기본 서버 `wh-default.conf`)은 건드리지 않는다. 이 호스트에는 **HAL 만** 둔다(포털·Webmaker·AriMail·Agent 는 다음 단계). ```bash # 개발 PC: main 이 깨끗한 상태(미추적 파일 없음)에서 빌드 — 전체 테스트 뒤 dist/0.1.1 (manifest.mainCommit = HEAD, dirty=false) node scripts/release.mjs build && node scripts/release.mjs verify --dist dist/0.1.1 scp -r dist/0.1.1 bear:ariweb-deploy-0.1.1/dist # bear: 호스트별 인증서(기존 인증서는 ibit.kr 7개뿐, ariweb.net 와일드카드 없음). 기본 서버가 /.well-known/acme-challenge/ 를 /var/www/html 로 서비스하므로 webroot 로 발급한다(nginx 설정을 건드리지 않음) sudo certbot certonly --webroot -w /var/www/html -d hal21.ariweb.net --non-interactive --keep-until-expiring sudo certbot reconfigure --cert-name hal21.ariweb.net --deploy-hook "systemctl reload nginx" # 갱신 뒤 reload(installer 가 없으므로) # bear: 부트스트랩(출력에 1회 설정 토큰이 있으므로 0600 파일로만 받는다) cd ~/ariweb-deploy-0.1.1 && mkdir -p -m 700 src && tar -xzf dist/hal-0.1.1.tar.gz -C src umask 077; bash src/deploy/bootstrap-hal.sh --dist ~/ariweb-deploy-0.1.1/dist --host hal21.ariweb.net \ --cert /etc/letsencrypt/live/hal21.ariweb.net --acme /var/www/html --portal-host none > bootstrap.log 2>&1 ``` - `--acme /var/www/html`: HAL vhost 의 acme 경로를 webroot 발급 경로와 같게 둔다(아니면 vhost 가 생긴 뒤 갱신 실패). - 만든 것: `/srv/ariweb/hal/{releases/0.1.1,current,data(0700)}`, `/var/www/ariweb-sites`, 유닛 `ariweb-hal`(enabled, 127.0.0.1:8900), nginx `sites-available/ariweb-hal.conf`(+enabled 링크), `conf.d/awstats.conf`(log_format awstats — 기존 wmstats 와 이름이 다름), `/etc/sudoers.d/ariweb-agent`(master 는 이미 NOPASSWD ALL 이라 추가 권한 없음). 포털 vhost 는 만들지 않았다. - 비밀값: 설정 토큰은 `/srv/ariweb/hal/data/setup.token`(0600)과 `~/ariweb-deploy-0.1.1/bootstrap.log`(0600)에만 있다. 첫 운영자 생성(`POST /ops/api/setup`)은 이 단계에서 하지 않았다 — `/ops` 는 허용 네트워크(기본 사설망 CIDR) 또는 접근권이 필요하다. - 알려진 사소한 결함: 5단계의 `https://` 자체 확인은 `nginx reload` 직후 바로 실행되어 새 vhost 가 아직 적용되기 전에 "unrecognized name" 경고가 날 수 있다(거짓 경고, 몇 초 뒤 정상). 다음 릴리스에서 reload 뒤 짧은 대기를 넣는다. 검증(2026-10-27 03:08~03:12 UTC, 모두 통과): `systemctl is-active/is-enabled ariweb-hal` active/enabled · 127.0.0.1:8900 리슨(MainPID) · `nginx -t` 성공 후 reload, 기존 nginx 설정 파일 12개 sha256 변화 없음 · `https://hal21.ariweb.net/api/health` 외부(LAN 밖 fetch·LAN NAT 양쪽)에서 200, 인증서 CN=hal21.ariweb.net(Let's Encrypt, 2027-01-25 만료), HTTP→HTTPS 301 · 건강 응답 version 0.1.1·mainCommit ab75b7d·artifactSha256 f574d875…·bootstrap.complete=false(첫 Agent 전) · `systemctl restart ariweb-hal` 뒤 재기동·건강 ok(NRestarts 0, enabled 로 재부팅 지속; 서버 재부팅은 기존 서비스 중단이라 하지 않음) · 기존 서비스 회귀: nginx/webmaker/webmaker-setup/webmaker-gitea-reverse-tunnel/docker/certbot.timer active, 기존 vhost 8개 응답 코드 배포 전과 동일 · `certbot reconfigure` 의 모의 갱신 성공, `renew_hook = systemctl reload nginx`. 되돌리기(HAL 만 제거, 기존 서비스 영향 없음): ```bash sudo systemctl disable --now ariweb-hal && sudo rm /etc/systemd/system/ariweb-hal.service && sudo systemctl daemon-reload sudo rm /etc/nginx/sites-enabled/ariweb-hal.conf /etc/nginx/sites-available/ariweb-hal.conf /etc/nginx/conf.d/awstats.conf && sudo nginx -t && sudo systemctl reload nginx sudo rm /etc/sudoers.d/ariweb-agent; sudo rm -rf /srv/ariweb /var/www/ariweb-sites # 데이터 포함 — 보관이 필요하면 /srv/ariweb/hal/data 먼저 복사 sudo certbot delete --cert-name hal21.ariweb.net # 인증서까지 지울 때만 ``` ## 8. 운영 Webmaker(webmaker.ariweb.net) ↔ HAL 연동 점검·dry-run (2026-10-27) 사실: `webmaker.ariweb.net` = hostinger, 이미 HAL 작업으로 설치된 Ariweb Suite Webmaker 모듈 0.1.1(상류 1.7.3)이며 hostinger HAL 의 `module_targets.webmaker` 에 등록돼 있다(교체 불필요). HAL 은 사이트·계정을 모델링하지 않으므로 "HAL 리소스" 는 모듈 등록이고, 프로젝트 연결은 포털 ↔ Webmaker 양쪽 등록부로만 이뤄진다. 기록: `docs/verification/webmaker-hal-link-2026-10-27.md`. ```bash # 1) 사전 스냅샷(hostinger, master, 사설 0700) — /srv/webmaker/backups/pre-hal-link-/ : data·sites tar, 원본 32,608 파일 sha256, conf, Gitea refs, HAL·포털 DB VACUUM INTO 사본, MANIFEST.sha256, ROLLBACK.md. 만든 뒤 임시 디렉터리에 풀어 체크섬 전수 대조(RESTORE_TEST_OK) # 2) 읽기 전용 inventory + dry-run (비밀값 없음, 충돌·누락이 있으면 종료 코드 2) node --no-warnings scripts/webmaker-inventory.mjs --webmaker-data /data/webmaker --sites-root /var/www/webmaker-sites --app-dir /srv/webmaker/current \ --portal-db /srv/ariweb/portal/data/portal.sqlite --hal-db /srv/ariweb/hal/data/hal.sqlite --nginx-enabled /etc/nginx/sites-enabled \ --gitea-refs --out <백업 디렉터리>/dry-run.json # 3) 사후 readback: (cd / && sha256sum --quiet -c <백업>/checksums-source.txt) · git fsck · ls-remote diff · systemctl show webmaker -p NRestarts · nginx -t · 외부 URL ``` 판정 뜻: `linked_consistent` 양쪽 등록부 일치 · `unlinked_native` Webmaker 고유 프로젝트(변경 대상 아님) · `link_*`/`duplicate`/`missing_project`/`gitea_ref`/`hal_*` 는 충돌·누락이라 적용 금지. 2026-10-27 결과: 프로젝트 10(일치 3·고유 7), 회원 12(SSO 3 일치·고유 9), 충돌 0·누락 0 → **적용 변경 없음**. 되돌리기는 스냅샷 디렉터리의 `ROLLBACK.md`. 남은 사람 결정: hal21(bear) 운영자 자격 증명으로 서버 등록·Agent 재등록 여부(제어 평면 이전은 §34 "하나의 HAL" 절차), 고유 회원 9명의 포털 계정 전환(사용자 자격 증명·동의 필요). ## 9. 기존 메일 서비스(AriMail) ↔ HAL 관리형 MailService/Node 가져오기 (2026-10-27) 사실(읽기 전용 조사): `https://mail.arihakdang.com/` = hostinger(187.53.133.173, 유닛 `arihakdang-mail`, `/srv/arihakdang-mail`, 실행 계정 arihakdang, Ariweb Suite arimail 모듈 0.1.1 = AriMail 1.0.11, 도메인 6개 멀티테넌트) · `https://webmail.ibit.kr/` = 115.68.194.192(`ssh jitsi`, 유닛 `arimail`, `/srv/arimail`, 실행 계정 arimail, 원본 AriMail 1.0.11 `a07f69f`, 도메인 1개). **서로 다른 호스트·다른 인스턴스**다. 두 호스트 모두 Haraka 비활성(25 포트 없음), 수신 MX 는 Resend(inbound-smtp.ap-northeast-1.amazonaws.com), 제출 587/465·IMAP 143/993·POP 110/995 는 AriMail 프로세스가 직접 연다. hostinger 에는 자체 HAL(`hal.ariweb.net`)과 거기에 등록된 Node Agent 가 이미 있다(bear `hal21` 과 별개 제어 평면). 원칙: 기존 송수신·계정·메일·DNS/MX·TLS·provider 는 그대로. HAL 에는 **등록·감시·통제용 등록부**(`mail_services` 도메인별 논리 리소스 + `servers` 노드)만 만든다. AriMail 재시작·DNS 변경·계정/DB 변형·리스너 교체 금지. ```bash # 1) 사전 사설 스냅샷(각 원본 호스트, root 0700/0600, git 밖) — /backups/pre-hal-onboard-/ : archive.tgz(데이터·설정·유닛·env·nginx·훅, DB 제외) · db.tgz(VACUUM INTO 사본 + integrity_check) · code.tgz(current 릴리스 트리) · meta.tgz · SHA256SUMS · RESTORE.md # bear HAL 도 같은 모양: /srv/ariweb/backups/hal-pre-mail-onboard-/ (hal.sqlite VACUUM 사본·vault·config·유닛·nginx·current 트리) # 2) 읽기 전용 discovery(원본 호스트, 파일 하나만 복사해 sudo 로 실행 — 데이터 디렉터리가 0700 이라서). 출력에 비밀값·메일 주소 없음 sudo node scripts/mail-discover.mjs --unit arihakdang-mail.service --app-dir /srv/arihakdang-mail --etc /etc/arihakdang-mail \ --vhost /etc/nginx/sites-available/mail.arihakdang.conf --hook /etc/letsencrypt/renewal-hooks/deploy/arihakdang-mail.sh --out discovery.json # 3) bear(HAL 데이터 소유자 master): 서버(노드) 만들기 + dry-run → 거부 0 이면 --apply. 같은 문서를 다시 적용하면 update(멱등) cd /srv/ariweb/hal/current && export ARIWEB_HAL_DATA=/srv/ariweb/hal/data node hal/index.mjs mail-import --discovery discovery.json --create-server --name "srv1953374 (mail.arihakdang.com)" --location hostinger --by hera # dry-run, 서버 id 출력 node hal/index.mjs mail-import --discovery discovery.json --server --apply --by hera node hal/index.mjs mail-services # readback: counts.active = 도메인 수, duplicates [], secretLeaks 0 # 4) (선택) Node Agent — Agent 가 없는 호스트만. 유닛 ariweb-agent(별도 프로세스, HAL 로의 outbound HMAC 만, 메일 포트·유닛 무관) node hal/index.mjs server-token --server --by hera # 1회 표시 토큰 → 원본 호스트에서 install.sh (--role mail) # 운영자 콘솔에서 서버 '승인' 은 사람 결정(첫 Agent 승인은 부트스트랩을 닫고 수동 설치를 잠근다 §32.2) ``` 되돌리기: `node hal/index.mjs mail-import-rollback --import `(그 적용이 만든 행 삭제·갱신 행 복원), 서버 행은 운영자 콘솔 서버 삭제(3단계 확인). Agent 는 `systemctl disable --now ariweb-agent` + `/srv/ariweb/agent` 제거(메일 서비스 영향 없음). 원본 메일 데이터 복원은 각 백업의 RESTORE.md(운영자 승인 뒤에만 — 메일 중단 동반). hostinger 주의: `arihakdang-mail` 은 hostinger HAL(`hal.ariweb.net`)의 `module_targets.arimail` 로도 관리된다. hal21 에 같은 호스트의 두 번째 Agent 를 설치하면 두 제어 평면이 같은 유닛에 배포할 수 있으므로(§34 "하나의 HAL") **설치하지 않고** 서버 행을 pending(Agent 미연결)으로 두었다 — 제어 평면 이전은 사람 결정. ## 10. 포털·Node Agent 제어 평면 이전 — hostinger HAL(hal.ariweb.net) → bear HAL21 (2026-10-27) 전제: 포털은 hostinger 에서 이미 active(신규 초기화·빈 DB 생성 금지), Agent 는 한 HAL 에만 연결(단일 제어 평면), 비밀값은 파이프로만 이동. 기록: `docs/verification/portal-hal21-link-2026-10-27.md`. ```bash # 0) 읽기 전용 preflight: DNS(공개·권한 resolver)·TLS·login·/api/health, 유닛 PID/NRestarts/cwd/REVISION/리스너, Agent 의 halUrl(agent.json 의 비밀 아닌 값), 포털 config 값·vault 이름, DB integrity/집계, 구 HAL·HAL21 의 servers/agents/jobs/module_targets # 1) 사설 백업(각 호스트, root 0700/0600, RESTORE.md 먼저): 포털 DB VACUUM 사본(+wal/shm 크기 기록)·vault·config·유닛·nginx·cert 메타·Agent data·구 HAL DB/config, HAL21 DB/config/release — SHA256SUMS·stage 추출 readback·복원 경로 존재 검증 # 2) HAL21(bear, HAL 데이터 소유자): main 릴리스 artifact 등록·승인, 포털 주소 설정(재시작 필요), current 전환 cd /srv/ariweb/hal/releases/0.1.1- && export ARIWEB_HAL_DATA=/srv/ariweb/hal/data node hal/index.mjs register-release --manifest /manifest.json --approve --by node hal/index.mjs bootstrap-release --version 0.1.1 --commit --sha256 node hal/index.mjs config-set --portalUrl https://my.ariweb.net --by --reason '…' # 포털이 다른 호스트일 때 ln -sfn $PWD /srv/ariweb/hal/current && sudo systemctl restart ariweb-hal && curl -s http://127.0.0.1:8900/api/health # 3) 포털 연동 키: 값을 화면에 두지 않고 host-to-host 파이프로 포털 vault 에 넣는다(service:hal 하나 — 포털은 양방향에 같은 이름을 쓴다) ssh bear 'cd /srv/ariweb/hal/current && ARIWEB_HAL_DATA=/srv/ariweb/hal/data node hal/index.mjs link-portal | sed -n "s/^ service:hal = //p"' \ | ssh hostinger 'node -e "let s=\"\";process.stdin.on(\"data\",c=>s+=c).on(\"end\",async()=>{const m=await import(\"/srv/ariweb/portal/current/lib/vault.mjs\");m.openVault(\"/srv/ariweb/portal/data\").set(\"service:hal\",s.trim(),{kind:\"service-key\"})})"' # 4) 포털 주소(비밀 아님): portal/config.mjs 의 set 으로 config.json 기록(0600, 원자적). 포털은 기동 때 읽으므로 다음 단계의 재배포(재시작)로 반영된다 ARIWEB_PORTAL_DATA=/srv/ariweb/portal/data node -e "import('/srv/ariweb/portal/current/portal/config.mjs').then(m=>m.loadConfig().set({publicUrl:'https://my.ariweb.net',halUrl:'https://hal21.ariweb.net'},'cli:'))" # 5) HAL21 작업으로 portal 재배포(같은 0.1.1 이면 Agent 가 현재 릴리스를 .prev-<시각> 으로 보존하고 health 실패 시 자동 복귀). 실행은 ariweb-hal 서비스가 맡는다 node hal/index.mjs module-deploy --module portal --version 0.1.1 --servers --by --reason '…' # → jobId node hal/index.mjs job --id # status done, children[].steps await.result.actualVersion/previousVersion/health # 6) readback: module_targets.portal target=actual=0.1.1 health ok · 포털의 실제 client+config+vault 로 /portal/v1/health·servers·jobs 계약 · bear nginx access log 의 포털 요청 200 · 포털 DB 집계·Webmaker 인벤토리 전후 비교 · 외부 URL/TLS · 유닛/NRestarts/journal ``` 주의: 포털 vault 교체와 포털 재시작 사이(수 초)에는 포털→HAL 호출이 인증 실패한다(화면은 '정보 확인 지연'). 서버 행의 role 이 `mail` 이면 `pickServer(web)` 이 null 이라 포털 사이트 배포 대상이 없다 — 운영자 콘솔에서 role 을 control 로 바꾼다(운영자 API 만 있음). 구 HAL 은 Agent 가 떠나면 server offline 으로 남는다; 중지·폐기는 사람 결정. ## 11. 0.4.1 — HAL 서버 자신의 Node Agent·HawkEye, 사설 CA 서버 등록 (2026-11-06) ### 11.1 기존 HAL 서버(0.4.0 이하로 부트스트랩)에 Agent 보완 — bear hal21 ``` cd /srv/ariweb/hal/current && bash deploy/hal-host-agent.sh --name hal21.ariweb.net --by ``` - 하는 일: 관리 서버 `hal21.ariweb.net`(역할 control, 프로필 `hal`) 생성 → 일회용 토큰(표시 안 함) → `agent-install.sh --hal http://127.0.0.1:8900 --role control`(agent artifact 는 HAL 릴리스 등록부에서, `/srv/ariweb/agent`, 유닛 `ariweb-agent`, sudoers 는 부트스트랩 때 둔 것과 같은 파일) → `server-approve`. 이미 `agent.json` 이 있으면 승인만 한다. - 확인: `curl -fsS http://127.0.0.1:8900/api/health`(bootstrap.complete·deployment.pending), 운영자 콘솔 서버 목록에 HAL 서버(역할 control, Agent 0.4.1, HawkEye 지표), 배너 "부트스트랩 완료 · 기능 단위 서버 배포 대기…"(기능 모듈이 다 배치된 운영 HAL 은 배너 없음). - 되돌리기: `sudo systemctl disable --now ariweb-agent && sudo rm /etc/systemd/system/ariweb-agent.service && sudo systemctl daemon-reload`, 운영자 콘솔에서 그 서버 삭제(Agent 해지). HAL 데이터는 서버·Agent 행만 추가된다. ### 11.2 새 HAL 부트스트랩(0.4.1) `bootstrap-hal.sh` 가 6/7 단계에서 11.1 을 스스로 수행한다. `--no-agent` 를 주면 이전처럼 첫 Agent 승인 때 부트스트랩이 닫힌다. ### 11.3 사설 CA·자체 서명 HAL 주소(예: hal.local.test)의 서버 등록 1. HAL 서버에서 상태 확인: `node hal/index.mjs tls-probe` → `trusted:false, reason:…, pin:"sha256//…", caCount:1`. `caCount:0` 이면(루트를 체인에 싣지 않는 사설 CA) `/srv/ariweb/hal/data/tls/hal-ca.pem` 에 루트 CA PEM 을 두고 HAL 을 재시작하거나 10분 기다린다(`GET /agent/v1/tls?refresh=1`). 2. 운영자 콘솔 › 서버 › 등록 토큰: 핀이 붙은 명령이 나온다. 대상 서버에서 그대로 실행한다(`-k --pinnedpubkey` 로 스크립트를 받고 `--hal-pin` 으로 CA 를 받는다). CA 는 `/srv/ariweb/agent/data/hal-ca.pem`, 유닛에 `NODE_EXTRA_CA_CERTS`, `agent.json` 에 `caFile`. 3. 이미 등록된 서버에서 같은 명령을 다시 실행하면 "이미 등록된 서버 … 코드·유닛만 갱신" 으로 끝난다. 새 Agent 로 바꾸려면 토큰 발급 때 "재등록 허용" 을 선택한다(재인증). 4. 0.4.0 Agent 가 이미 설치된 서버(인증서가 사설 CA 로 바뀐 경우): `curl -k --pinnedpubkey '<핀>' https:///agent/v1/hal-ca.pem -o /srv/ariweb/agent/data/hal-ca.pem` 후 유닛에 `Environment=NODE_EXTRA_CA_CERTS=/srv/ariweb/agent/data/hal-ca.pem` 을 더하고 재시작, 또는 HAL 작업으로 agent 0.4.1 배포 뒤 설치 명령을 다시 실행(`--hal-pin`)해 `caFile` 을 보완한다.