Files
AriWeb-Suite/docs/ARCHITECTURE.md

16 KiB

Ariweb Suite v0.1.1 — 아키텍처와 개발 규약

이 문서는 Ariweb Suite 의 모든 구성요소가 따르는 공통 규약이다. 하위 시스템을 나누어 개발할 때 이 문서가 인터페이스 계약이다. 개발 지시 원문은 Ariweb-Suitle_Dev-Order_v0.1.1.md 이며, 절 번호(예: §13.3)는 그 문서를 가리킨다.

1. 구성요소와 프로세스

프로세스 진입점 포트(127.0.0.1) 역할
ariweb-hal hal/index.mjs 8900 HAL 제어 평면: 릴리스 등록부, 서버·Node Agent 등록부, 내구성 작업 엔진, 모듈 배포·갱신·되돌리기, 운영자 콘솔(hal.ariweb.net/ops), 공개 상태 페이지(/status), 긴급 복구, 플랫폼 재해복구
ariweb-agent agent/index.mjs (없음, HAL 에 long-poll) 각 관리 서버의 Node Agent: 일회용 등록 토큰으로 HAL 에 등록, heartbeat·지표(CPU·메모리·디스크·inode·I/O·네트워크·시계 차이), HAL 이 내린 작업(모듈 설치·갱신·되돌리기·건강 확인·사이트 배포·백업·복원·이전) 실행. 외부 고객 서버에서는 External Pull Agent 모드(사이트 배포만)
ariweb-portal portal/index.mjs 8910 고객 포털(my.ariweb.net): 회원·사용자 그룹·상품·주문·사이트·배포·도메인·메일·백업·복원·이전·소유권·사용량·알림·감사. Webmaker·AriMail 로 가는 통합 진입점
webmaker modules/webmaker/server.js 3080 도입 모듈(WebHosting_AI_agent 1.7.3). 사이트 제작·AI·도메인·DNS·TLS·Gitea·배포. 포털 SSO 로 로그인 없이 진입
webmaker-setup modules/webmaker/setup-server.js 8795 Webmaker 초기 설정 페이지(기존 그대로)
arimail modules/arimail/server/index.mjs 8790 + 25/110/143/465/587/993/995 도입 모듈(AriMail 1.0.11). 웹메일·IMAP·POP3·SMTP·사용량 API
nginx — 80/443 모든 공개 진입. vhost 는 HAL/Agent 가 템플릿으로 생성

세 신규 프로세스(hal·agent·portal)는 같은 저장소의 lib/ 를 공유하고 외부 npm 의존성이 없다(Node 22.13+, node:sqlite). 도입 모듈(modules/*)은 원본 저장소의 구조를 그대로 두고, 변경은 IMPORTED_SOURCES.md 에 파일 단위로 기록한다.

2. 저장소 레이아웃

lib/            공통 라이브러리(ESM). db·http·session·status·jobs·quota·notify·audit·kst·redact·i18n
hal/            HAL 서버(api, ops 콘솔 UI, 릴리스·서버·작업·복구)
agent/          Node Agent(등록·heartbeat·작업 실행기·모듈 설치기·External Pull 모드)
portal/         고객 포털(api, 화면, 도메인 로직) — portal/<영역>/*.mjs
modules/webmaker/   도입: WebHosting_AI_agent @3b10859 (CommonJS)
modules/arimail/    도입: AriMail @a07f69f (ESM)
catalog/        출시 검증용 버전 고정 상품 Catalog(JSON) — 운영자가 HAL/포털 화면에서 관리하는 ProductVersion 의 시드
deploy/         설치 스크립트·systemd 유닛·nginx 템플릿·부트스트랩(HAL 최초 설치)
scripts/        import-sources(도입 소스 복사·manifest), release(릴리스 artifact 생성·검사)
test/           회귀 테스트(node:test 없이 lib/test.mjs 의 작은 러너). `npm test` 가 전부 실행
docs/           이 문서, 운영 문서, 검증 기록
VERSION_HISTORY.md, IMPORTED_SOURCES.md, README.md

3. 공통 라이브러리(lib/) 계약

  • lib/db.mjs — openDb(file, migrations[]): node:sqlite 를 열고(WAL, foreign_keys) migrations 배열({id, sql})을 순서대로 적용한다. 반환 { db, get(sql, ...p), all(sql, ...p), run(sql, ...p), tx(fn) }. 모든 저장은 SQLite 이며 JSON 열은 json_* 접미사.
  • lib/http.mjs — createApp() → { route(method, pattern, handler), static(prefix, dir), listen(port, host) }. 핸들러 시그니처 (ctx) => body|Response. ctx = { req, res, method, path, params, query, body(json), cookies, ip, user, set(status, headers), json(obj, status), html(str), redirect(url), error(status, code, message) }. 오류는 { ok:false, error:'<code>', message } JSON. 모든 응답 Cache-Control: no-store(정적 제외). 신뢰 프록시(nginx) 의 X-Real-IP 만 믿는다.
  • lib/session.mjs — 서명 쿠키가 아니라 DB 세션(기기·IP·UA·발급·만료·마지막 사용, 개별/전체 종료 §3.2). createSession(db, {userId, kind:'customer'|'operator', ...}), resolve(db, token), revoke, revokeAll. CSRF 는 세션에 묶인 HMAC 토큰(X-CSRF 헤더).
  • lib/status.mjs — §2.2 사용자용 작업 상태 어휘와 전이 규칙. STATUS = { NOT_READY:'준비 전', NEEDS_SETUP:'설정 필요', AWAITING_APPROVAL:'승인 대기', AWAITING_PAYMENT:'결제 대기', QUEUED:'처리 대기', RUNNING:'진행 중', AWAITING_EXTERNAL:'외부 작업 대기', VERIFYING:'검증 중', PAUSED:'일시 중지', DONE:'완료', FAILED:'실패', REVERTING:'되돌리는 중', REVERTED:'되돌림 완료', NEEDS_USER:'사용자 확인 필요' }. 코드(영문 키)로 저장하고 화면에서 한국어로 바꾼다. 서비스 상태(SERVICE_STATE)·작업 상태(STATUS)·외부 건강 상태(HEALTH)는 별도 필드로 둔다(§2.2 "합치지 않는다").
  • lib/jobs.mjs — 내구성 작업 엔진. 작업은 jobs 표에 {id, kind, subject(json), steps(json: [{id,title,state,startedAt,endedAt,detail}]), status, attempt, maxAttempts, nextRunAt, lockedBy, lockedAt, result(json), error, idempotencyKey, createdBy, groupId} 로 저장된다. 실행기는 registerKind(kind, { run(job, ctx), revert?(job, ctx), steps:[...] }) 로 종류를 등록하고, ctx.step(id, fn) 으로 단계를 기록한다(단계마다 저장 → 프로세스 재시작 뒤 이어서 실행 §29). 같은 idempotencyKey 의 작업은 하나만 만든다(§36). 일시 오류는 retryable 표시로 지수 백오프 재시도(횟수·다음 시각 표시 §2.2). ctx.waitExternal(reason) 은 AWAITING_EXTERNAL 로 둔다.
  • lib/quota.mjs — §13 보장량·예약. 자원 종류 RESOURCE = sites | web_disk | web_out | mail_storage | mail_msgs | mailboxes | mail_services | source | backup. 표 entitlements(그룹별 구매 총량, 상품정책 버전 참조), allocations(서비스별 배정량), reservations(진행 중 작업 예약: {id, groupId, resource, amount, state: available|reserved|in_use|returning|returned, jobId, expiresAt}). reserve() 는 단일 트랜잭션에서 구매총량 - (배정 + 예약) 을 검사해 동시 요청 중 하나만 성공(§13.3, 검증 27). commit()→in_use, release()→returned. warnLevel(used, limit) → 0|70|80|90|95|100.
  • lib/kst.mjs — KST 월 경계(monthStart(at), nextMonthStart(at)), 고정 시각 입력 가능(테스트).
  • lib/redact.mjs — 비밀로 보이는 키·값 가리기. 감사 기록·알림·작업 결과·로그 모두 이 함수를 거친다(§37).
  • lib/notify.mjs — 알림 센터(§24.1): 분류 category, 사건 묶음 incidentKey(같은 원인은 하나로 갱신), 필수 알림(mandatory), 외부 이메일 발송 훅(mailer, AriMail/Resend 는 포털 설정). 링크는 조치 화면으로.
  • lib/audit.mjs — 감사 기록(§24.2): {at, actorType: user|operator|system, actorId, groupId, subjectType, subjectId, action, outcome, detail(redacted), immutable}. 권한·소유권·배포·해지·복원·이전·결제 승인은 immutable=1(삭제 불가).
  • lib/i18n.mjs — 한국어 문구 사전(lang/ko.json). 화면 문구는 코드에 박지 않고 여기서 읽는다.
  • lib/test.mjs — 작은 테스트 러너: test(name, fn), eq, ok, rejects, 임시 DB 생성.

4. 데이터 모델(포털 SQLite, portal/schema.mjs)

핵심 표(열은 요지만, 상세는 스키마 파일):

  • users(id, email, email_verified_at, display_name, pass_hash(scrypt), pass_salt, mfa_json, failed_logins, lock_until, created_at) · email_tokens(verify/reset, 만료·사용 완료) · sessions
  • groups(id, name, kind: personal|team, billing_contact, tech_contact, owner_user_id) · memberships(group_id, user_id, role: owner|admin|member|billing|viewer, alias user-01, status) · invites(email, role, token_hash, expires_at, accepted_at)
  • site_permissions(site_id, user_id, perms_json: view|edit|deploy|domain_mail|billing|admin)
  • product_versions(id, product_id, version, name, audience, billing_cycle, price, currency, tax_rate, limits_json, options_json, warn_levels_json [70,80,90,95], approval: manual, effective_from, published) — 코드에 가격을 고정하지 않는다(§6.1, §27.2)
  • orders(id, group_id, product_version_id, items_json, amount, tax, total, currency, status: awaiting_payment|awaiting_approval|approved|rejected|expired|cancelled, payment_ref, due_at, approved_by, idempotency_key) · entitlements(group_id, resource, amount, source_order_id, starts_at, ends_at)
  • sites(id, group_id, name, purpose, webmaker_project_id, lifecycle: draft|building|deploying|live|paused|cancel_scheduled|readonly_retention|pending_delete|deleted, deploy_mode: managed|external|null, primary_domain, site_count_state: none|reserved|in_use|returning, server_id, current_version, health, settings_json)
  • deployments(id, site_id, version_ref(저장본), mode, target(server/external agent), status(job), verify_state: verified|needs_domain|public, result_json)
  • domains(id, group_id, name, purpose: web|mail|both, site_id, mail_service_id, ownership_state, dns_state, cert_state, mail_dns_state, last_checked_at, is_primary)
  • mail_services(id, group_id, kind: site_bound|standalone, site_id, domain, arimail_server_id, status, allocations…) · mailboxes(mail_service_id, address, status, quota_bytes, …) — 실제 계정·메일 데이터는 AriMail 이 가지며 포털은 상태·배정만 둔다
  • backup_options(site_id, product_version_id, schedule_json, retention_json, status, first_backup_due_at) · backups(id, site_id, created_at, scope_json, verify_state, expires_at, location) · restores(id, site_id, backup_id, status, cost, approval…)
  • relocations(id, subject_type, subject_id, from_server, to_server, kind: auto_static|approved|customer_paid, status, boundaries_json)
  • ownership_transfers(id, from_group, to_group, scope_json, status: requested|accepted|reviewing|approved|completed|rejected|expired|disputed, expires_at)
  • jobs, reservations, allocations, notifications, audit_log, usage_samples(site/mail 별 측정값, measured_at)

5. HAL ↔ Node Agent 프로토콜(docs/HAL-AGENT.md 에 상세)

  • 등록: 운영자가 HAL 에서 서버(역할·위치) 생성 → 일회용 등록 토큰(10분, 1회) 발급 → 서버에서 ariweb-agent register --hal https://hal.ariweb.net --token … → Agent 가 키쌍 생성, 공개키 등록, HAL 이 Agent 자격(agent_id + 비밀, 응답에 1회) 발급 → 운영자 승인 후 배치 가능.
  • 인증: 모든 Agent 요청은 Authorization: Bearer <agent secret> + 요청 서명(HMAC: method|path|ts|body-sha256). 시계 차이 ±5분.
  • heartbeat: POST /agent/v1/heartbeat 30초마다 {version, modules:{name:{version,active,health}}, metrics:{cpu,mem,disk,inode,io,net,clockSkewMs}, jobs:{running}}.
  • 작업: Agent 가 GET /agent/v1/jobs/next?wait=25 로 long-poll → HAL 이 agent_jobs 에서 배정 → Agent 실행 → POST /agent/v1/jobs/<id>/progress|result. Agent 재시작 시 미완료 작업은 재개 가능한 단계부터 다시 하거나 NEEDS_USER 로 보고한다.
  • artifact: GET /agent/v1/releases/<ver>/artifact (tar.gz, sha256 검증). 모듈 설치 레이아웃 /srv/ariweb/<module>/releases/<ver> + current 심링크, 유닛 ariweb-<module>.service(도입 모듈은 기존 유닛 이름 webmaker·arimail 호환).

6. 포털 ↔ HAL

포털은 관리형 작업(사이트 배포·백업·복원·이전·사용량 수집)을 HAL API(/portal/v1/*, 서비스 키 + HMAC)로 요청하고 작업 ID 를 받는다. 포털 jobs 표의 작업이 HAL 작업을 참조(hal_job_id)하며 상태는 HAL 이 webhook(/internal/hal-events)으로 밀고, 포털도 주기 조회로 맞춘다(둘 중 빠른 쪽). HAL 이 응답하지 않으면 포털은 정보 확인 지연 을 표시하고 신규 증가를 허용하지 않는다(§13.4, §26.2).

7. 포털 ↔ Webmaker / AriMail

  • Webmaker SSO(§0.2): 포털이 사용자별 Webmaker 계정(aw-<groupId 앞 8>-<userId 앞 8>)을 만들어 두고, "제작 화면 열기" 때 포털 서명 티켓(/sso/webmaker?ticket=…)으로 Webmaker 의 /api/auth/sso (신규, 포털 공유 비밀로 검증)에서 세션 쿠키를 받아 들어간다. Webmaker 의 users.json 비밀번호는 무작위이며 사용자는 모른다(별도 로그인 없음). 현재 사용자 그룹 이름을 Webmaker 상단에 표시한다(X-Ariweb-Group 배너).
  • Webmaker Project ↔ Site: 포털 sites.webmaker_project_id ↔ Webmaker data/sites/<id>.json. Webmaker 프로젝트 삭제 API 에 포털 영향 확인 훅(beforeDelete → 포털 /internal/webmaker/project-impact)을 둔다.
  • AriMail: 관리자 API(계정 CRUD)·사용량 API(/api/usage)·전체 설정 API(도메인 추가)를 포털 portal/mail/arimail.mjs 어댑터가 호출한다. 비밀 키는 포털 vault(lib/vault.mjs, AES-256-GCM, 데이터 디렉터리의 vault.key)에만 둔다.

8. 보안 규칙(§37)

  1. 비밀값(비밀번호·토큰·API 키·개인키·복구 코드·메일 원문)은 소스·문서·로그·테스트·커밋·보고서에 쓰지 않는다. 시험용 값도 example·무작위 생성만.
  2. 모든 기록 경로(감사·알림·작업 결과·오류 메시지)는 redact() 를 거친다.
  3. 비밀값은 저장 후 다시 읽을 수 없고 재발급만 제공한다(일회용 표시 뒤 해시만 저장).
  4. 그룹 경계: 모든 조회 SQL 은 group_id 조건을 포함한다. 검색·자동완성도 같다.
  5. 운영자 콘솔은 별도 계정·별도 세션(kind='operator')·제한 경로(/ops)·허용 네트워크(사설망/VPN CIDR 또는 기기 귀속 단기 접근권) 없이는 로그인 화면도 열리지 않는다(§25).
  6. 고위험 작업은 최근 재인증(10분) + 3단계 확인(영향 범위 → 대상 식별정보 재입력 → 최종 실행·되돌리기 가능 여부)을 거친다.

9. 코딩 규약

  • ESM(.mjs), 외부 의존성 없음, Node 22.13+.
  • 모든 파일 머리에 모듈 목적 주석, 모든 함수·클래스에 목적·매개변수·반환·비자명한 논리 이유를 한국어로 적는다.
  • 화면 문구는 lang/ko.json(포털·HAL) 에 둔다. 상태는 코드로 저장하고 표시 때 번역한다.
  • 각 영역은 portal/<영역>/{model,api,ui}.mjs 로 나눈다. api.mjs 는 register(app, deps) 를 내보낸다.
  • 테스트는 test/<영역>.test.mjs, npm test 로 전부 실행. 외부 네트워크·실서버에 닿는 테스트는 ARIWEB_LIVE=1 일 때만.
  • 날짜·시각은 ISO 8601 UTC 로 저장하고 화면에서 KST 로 표시한다. 월간 사용량 경계는 KST.

10. 배포 레이아웃(관리 서버)

/srv/ariweb/
  hal/        releases/<ver>/ current -> …   data/(hal.sqlite, vault.key, artifacts/)
  portal/     releases/<ver>/ current        data/(portal.sqlite, vault.key)
  agent/      releases/<ver>/ current        data/(agent.json: id·공개키·HAL 주소; 비밀은 0600)
  webmaker/   (도입 모듈) releases/<ver>/ current   데이터는 기존 /data/webmaker 유지
  arimail/    (도입 모듈) releases/<ver>/ current   데이터는 기존 경로 유지
  backups/    관리형 사이트 백업 저장소(운영 서버와 다른 서버가 원칙)
systemd: ariweb-hal.service · ariweb-portal.service · ariweb-agent.service · webmaker.service · arimail(또는 기존 유닛명).service
nginx:   /etc/nginx/sites-enabled/ariweb-hal.conf (hal.ariweb.net) · ariweb-portal.conf (my.ariweb.net) · wh-<siteId>.conf (사이트)

HAL 최초 설치(부트스트랩, §32.2)만 deploy/bootstrap-hal.sh 로 수행한다. 그 뒤 모든 모듈은 HAL 작업으로만 설치·갱신한다.