HAL 제어 평면(릴리스·서버·Agent·작업·모듈 배포·운영자 계정·긴급 복구·재해복구·공개 상태), Node Agent, 배포 스크립트·유닛·nginx 템플릿, 릴리스 도구, 포털 HAL 클라이언트 (§28·§29·§32~§34)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zzabisagent-lab
2026-10-26 11:58:15 +00:00
co-authored by Claude Fable 5.1
parent 403d0fb845
commit 6b5a014041
49 changed files with 2584 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/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]
# 하는 일: 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)"
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;; *) echo "알 수 없는 옵션 $1" >&2; exit 1;; esac; done
[ -n "$HAL" ] || { echo "--hal 이 필요합니다" >&2; exit 1; }
HAL="${HAL%/}"; USER_NAME="$(id -un)"
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)"
echo "== HAL 확인 $HAL"; HEALTH="$(curl -fsS "$HAL/api/health")"; 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" "$APP_DIR/data" /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 -fsS -H "X-Registration-Token: $TOKEN" "$HAL/agent/v1/bootstrap-artifact?module=agent&version=$VERSION" -o /tmp/ariweb-agent-$$.tgz; tar -xzf /tmp/ariweb-agent-$$.tgz -C "$APP_DIR/releases/$VERSION"; rm -f /tmp/ariweb-agent-$$.tgz; else echo "코드가 없고 토큰도 없습니다" >&2; exit 1; fi
fi
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 "== 등록"; if [ "$EXTERNAL" = "1" ]; then $NODE_BIN agent/index.mjs register --hal "$HAL" --token "$TOKEN" --external --site "$SITE" --label "$LABEL"; else $NODE_BIN agent/index.mjs register --hal "$HAL" --token "$TOKEN" --role "$ROLE" --label "$LABEL"; fi; else echo "== 이미 등록됨($(cat "$APP_DIR/data/agent.json" | $NODE_BIN -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>console.log(JSON.parse(s).id))")) — 코드·유닛만 갱신"; fi
echo "== systemd 유닛"
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}}#$([ "$EXTERNAL" = "1" ] && echo "Environment=ARIWEB_AGENT_EXTERNAL=1" || echo "")#" 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; }
+57
View File
@@ -0,0 +1,57 @@
#!/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 가능한 일반 계정으로):
# 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] [--app-dir /srv/ariweb/hal]
# 출력: 운영자 최초 설정 토큰(1회)과 첫 Agent 설치 명령. 비밀값은 그 밖에 출력하지 않는다.
set -euo pipefail
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)"
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;; *) echo "알 수 없는 옵션 $1" >&2; exit 1;; esac; done
[ -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; }
echo "== 1/6 릴리스 $VERSION (main $COMMIT, hal artifact sha256 ${SUM:0:16}…)"
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
echo "== 2/6 디렉터리·보관소·접근 경로 준비"
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"
$NODE_BIN scripts/release.mjs register --hal-data "$DATA" --dist "$DIST" --approve --by bootstrap
$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})"
echo "== 3/6 systemd 유닛 ariweb-hal"
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; }
echo "== 4/6 nginx $HOST (인증서 $CERT)"
[ -f "$CERT/fullchain.pem" ] || { echo "인증서 $CERT/fullchain.pem 이 없습니다" >&2; exit 1; }
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
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; sudo -n nginx -t && sudo -n systemctl reload nginx
echo "== 5/6 HAL 자체 건강 확인"
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 → 이 서버)와 인증서를 확인하세요"
echo "== 6/6 최초 운영자 설정 토큰과 첫 Agent"
$NODE_BIN hal/index.mjs setup-token
cat <<MSG
다음 순서:
1) 위 설정 토큰으로 첫 운영자 계정을 만듭니다(POST https://$HOST/ops/api/setup). 운영자 콘솔은 허용 네트워크(사설망/VPN) 또는 접근권(node hal/index.mjs grant --login <id>)이 있어야 열립니다.
2) 운영자 콘솔 › 서버에서 이 서버(역할 control/web)를 만들고 등록 토큰을 발급해 첫 Node Agent 를 설치합니다:
curl -fsSL https://$HOST/agent/v1/install.sh | bash -s -- --hal https://$HOST --token <토큰> --role control
첫 Agent 가 승인되면 부트스트랩이 닫히고 수동 설치가 잠깁니다. 이후 포털·Webmaker·AriMail 은 HAL 작업으로 배포합니다.
3) 포털 연동 키: node hal/index.mjs link-portal (출력 값을 포털 보관소에 넣습니다)
MSG
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
# deploy/hal-restore.sh — 플랫폼 재해복구(§34): HAL 복구 사본(dr snapshot tar.gz)으로 HAL 데이터를 복원한다. 운영자 콘솔 › 플랫폼 복구에서 "복원 시작" 승인과 기존 제어 위치 중지 확인 뒤에만 실행한다.
# sudo bash deploy/hal-restore.sh <snapshot.tar.gz> <sha256> [--data /srv/ariweb/hal/data] [--vault-key <hex 32바이트>]
# vault.key 는 사본에 들어 있지 않으므로 운영자가 보관한 키를 --vault-key 또는 ARIWEB_VAULT_KEY 로 준다(없으면 기존 데이터 디렉터리의 키를 유지).
set -euo pipefail
SNAP="${1:?snapshot}"; SUM="${2:?sha256}"; shift 2; DATA="/srv/ariweb/hal/data"; VKEY="${ARIWEB_VAULT_KEY:-}"
while [ $# -gt 0 ]; do case "$1" in --data) DATA="$2"; shift 2;; --vault-key) VKEY="$2"; shift 2;; *) shift;; esac; done
[ "$(sha256sum "$SNAP" | cut -d' ' -f1)" = "$SUM" ] || { echo "복구 사본 sha256 불일치" >&2; exit 1; }
systemctl is-active --quiet ariweb-hal && { echo "ariweb-hal 이 실행 중입니다 — 기존 제어 위치를 먼저 중지하세요(systemctl stop ariweb-hal)" >&2; exit 2; }
STAMP="$(date +%Y%m%d-%H%M%S)"; mkdir -p "$DATA"; [ -f "$DATA/hal.sqlite" ] && cp -a "$DATA" "$DATA.pre-restore-$STAMP" && echo "기존 데이터를 $DATA.pre-restore-$STAMP 로 보존"
WORK="$(mktemp -d)"; tar -xzf "$SNAP" -C "$WORK"; [ -f "$WORK/hal.sqlite" ] || { echo "사본에 hal.sqlite 가 없습니다" >&2; exit 1; }
install -m 600 "$WORK/hal.sqlite" "$DATA/hal.sqlite"; rm -f "$DATA/hal.sqlite-wal" "$DATA/hal.sqlite-shm"; [ -f "$WORK/vault.json" ] && install -m 600 "$WORK/vault.json" "$DATA/vault.json"; [ -f "$WORK/config.json" ] && install -m 600 "$WORK/config.json" "$DATA/config.json"
[ -n "$VKEY" ] && printf '%s' "$VKEY" > "$DATA/vault.key" && chmod 600 "$DATA/vault.key"; [ -f "$DATA/vault.key" ] || echo "경고: vault.key 가 없습니다 — 보관소 값을 읽을 수 없으니 --vault-key 로 넣으세요" >&2
OWNER="$(stat -c %U "$(dirname "$DATA")")"; chown -R "$OWNER:$OWNER" "$DATA"; rm -rf "$WORK"
echo "복원 완료($SNAP). 새 HAL 기동: systemctl start ariweb-hal (다른 인스턴스의 임대가 남아 있으면 기동이 거부됩니다 → 그 HAL 이 정말 중지되었는지 확인한 뒤 node hal/index.mjs --takeover 로 1회 기동)"
echo "기동 뒤 운영자 콘솔 › 플랫폼 복구 › 복원 완료 확인(운영자 로그인·HAL 제어·HTTPS·고객 서비스 외부 확인)을 기록하세요"
+31
View File
@@ -0,0 +1,31 @@
# hal.ariweb.net — HAL(제어 평면). 운영자 콘솔 /ops 는 HAL 이 네트워크 관문(허용 CIDR 또는 접근권 쿠키)을 적용한다.
# Agent 는 /agent/v1 로, 포털은 /portal/v1 로 서명된 요청을 보낸다. 큰 전송(/agent/v1/transfers)은 본문 제한을 넉넉히 둔다.
server {
listen 80;
listen [::]:80;
server_name {{HAL_HOST}};
location ^~ /.well-known/acme-challenge/ { root {{ACME_ROOT}}; default_type text/plain; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name {{HAL_HOST}};
server_tokens off;
client_max_body_size 0;
ssl_certificate {{CERT_DIR}}/fullchain.pem;
ssl_certificate_key {{CERT_DIR}}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
proxy_pass http://127.0.0.1:{{PORT}};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_request_buffering off;
proxy_buffering off;
proxy_read_timeout 120s;
}
}
+29
View File
@@ -0,0 +1,29 @@
# my.ariweb.net — 고객 포털
server {
listen 80;
listen [::]:80;
server_name {{PORTAL_HOST}};
location ^~ /.well-known/acme-challenge/ { root {{ACME_ROOT}}; default_type text/plain; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name {{PORTAL_HOST}};
server_tokens off;
client_max_body_size 200m;
ssl_certificate {{CERT_DIR}}/fullchain.pem;
ssl_certificate_key {{CERT_DIR}}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://127.0.0.1:{{PORT}};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 300s;
}
}
+3
View File
@@ -0,0 +1,3 @@
# deploy/nginx/awstats.conf — /etc/nginx/conf.d/awstats.conf : Ariweb 관리형 사이트 접속 통계 로그 형식(§13.1).
# Agent(site.usage)가 사이트별 로그(aw-<siteId>.access.log)를 읽어 요청 수·수신/송신 바이트·오류율·응답 시간을 집계한다. IP 는 기록하지 않는다(개인정보 최소화).
log_format awstats '$time_iso8601|$host|$request_length|$bytes_sent|$status|$request_time|$uri';
+7
View File
@@ -0,0 +1,7 @@
server {
listen 80;
listen [::]:80;
server_name {{OLD_NAMES}};
location ^~ /.well-known/acme-challenge/ { root {{ACME_ROOT}}; default_type text/plain; }
return 301 https://{{PRIMARY}}$request_uri;
}
+7
View File
@@ -0,0 +1,7 @@
access_log {{LOG}} awstats;
error_page 503 /__aw_maint;
location = /__aw_maint { internal; default_type text/html; add_header Cache-Control "no-store" always; return 503 '{{MAINT_HTML}}'; }
if (-f {{SITE_DIR}}/.maintenance) { return 503; }
if (-f {{SITE_DIR}}/.limited) { rewrite ^ /__aw_limited last; }
location = /__aw_limited { internal; default_type text/html; add_header Cache-Control "no-store" always; return 503 '{{LIMIT_HTML}}'; }
location / { proxy_pass http://127.0.0.1:{{PORT}}; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; }
+11
View File
@@ -0,0 +1,11 @@
root {{ROOT}};
index index.html;
access_log {{LOG}} awstats;
error_page 503 /__aw_maint;
location = /__aw_maint { internal; default_type text/html; add_header Cache-Control "no-store" always; return 503 '{{MAINT_HTML}}'; }
if (-f {{SITE_DIR}}/.maintenance) { return 503; }
if (-f {{SITE_DIR}}/.limited) { rewrite ^ /__aw_limited last; }
location = /__aw_limited { internal; default_type text/html; add_header Cache-Control "no-store" always; return 503 '{{LIMIT_HTML}}'; }
location ~ /\. { return 404; }
location / { try_files $uri $uri/ /index.html =404; add_header Cache-Control "no-cache" always; }
location /uploads/ { alias {{SITE_DIR}}/data/uploads/; }
+12
View File
@@ -0,0 +1,12 @@
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name {{SERVER_NAMES}};
server_tokens off;
client_max_body_size 64m;
ssl_certificate {{CERT_DIR}}/fullchain.pem;
ssl_certificate_key {{CERT_DIR}}/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
{{BODY}}
}
+11
View File
@@ -0,0 +1,11 @@
# Ariweb 관리형 사이트 vhost — Agent 가 만든다(site.deploy/domain.vhost). 손으로 고치지 마세요(다음 배포 때 덮어씁니다).
# 사이트 {{SITE_ID}} · 도메인 {{DOMAIN_LIST}} · 생성 {{GENERATED_AT}}
server {
listen 80;
listen [::]:80;
server_name {{SERVER_NAMES}};
server_tokens off;
location ^~ /.well-known/acme-challenge/ { root {{ACME_ROOT}}; default_type text/plain; }
{{HTTP_BODY}}
}
{{HTTPS_SERVER}}
+9
View File
@@ -0,0 +1,9 @@
# /etc/sudoers.d/ariweb-agent — Node Agent(계정 master)가 비밀번호 없이 쓸 수 있는 최소 명령(§28·§32). 그 밖의 sudo 는 허용하지 않는다.
# 설치: sudo install -m 440 deploy/sudoers/ariweb-agent /etc/sudoers.d/ariweb-agent && sudo visudo -c
Defaults:master !requiretty
master ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload, /usr/bin/systemctl start *, /usr/bin/systemctl stop *, /usr/bin/systemctl restart *, /usr/bin/systemctl reload *, /usr/bin/systemctl enable *, /usr/bin/systemctl disable *, /usr/bin/systemctl is-active *
master ALL=(root) NOPASSWD: /usr/sbin/nginx -t, /usr/bin/certbot *, /snap/bin/certbot *
master ALL=(root) NOPASSWD: /usr/bin/tee /etc/nginx/sites-available/aw-*, /usr/bin/tee /etc/nginx/sites-enabled/aw-*, /usr/bin/tee /etc/nginx/conf.d/awstats.conf, /usr/bin/tee /etc/nginx/sites-available/ariweb-*, /usr/bin/tee /etc/systemd/system/ariweb-*, /usr/bin/tee /etc/systemd/system/webmaker.service, /usr/bin/tee /etc/systemd/system/webmaker-setup.service
master ALL=(root) NOPASSWD: /usr/bin/ln -sf /etc/nginx/sites-available/aw-* /etc/nginx/sites-enabled/*, /usr/bin/ln -sf /etc/nginx/sites-available/ariweb-* /etc/nginx/sites-enabled/*, /usr/bin/rm -f /etc/nginx/sites-enabled/aw-*, /usr/bin/rm -f /etc/nginx/sites-available/aw-*, /usr/bin/rm -f /etc/systemd/system/ariweb-site-*
master ALL=(root) NOPASSWD: /usr/bin/mkdir -p /etc/nginx/*, /usr/bin/mkdir -p /var/www/*, /usr/bin/chown master\:master /var/www/*, /usr/bin/chmod * /etc/nginx/sites-available/aw-*, /usr/bin/chmod * /etc/systemd/system/ariweb-*, /usr/bin/chmod * /etc/nginx/conf.d/awstats.conf
master ALL=(root) NOPASSWD: /usr/bin/true
+31
View File
@@ -0,0 +1,31 @@
# AriMail(도입 모듈) — 원본 deploy/systemd/mail-gateway.service.in 과 같은 내용. 서버에 이미 있는 유닛(arihakdang-mail/arimail/heart-mail)은 그대로 두고 current 심링크만 바꾸므로 보통 쓰이지 않는다.
[Unit]
Description={{UNIT}} — AriMail gateway ({{VERSION}})
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{USER}}
Group={{USER}}
WorkingDirectory={{APP_DIR}}/current
EnvironmentFile={{ENV_FILE}}
Environment=NODE_ENV=production
Environment=HOST=127.0.0.1
Environment=PORT=8790
ExecStart={{NODE}} server/index.mjs
Restart=always
RestartSec=3
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths={{APP_DIR}}/data
StandardOutput=journal
StandardError=journal
SyslogIdentifier={{UNIT}}
[Install]
WantedBy=multi-user.target
+24
View File
@@ -0,0 +1,24 @@
[Unit]
Description=Ariweb Node Agent ({{VERSION}})
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{USER}}
Group={{USER}}
WorkingDirectory={{APP_DIR}}/current
Environment=NODE_ENV=production
Environment=ARIWEB_AGENT_DATA={{APP_DIR}}/data
Environment=NODE_OPTIONS=--no-warnings
Environment=PATH=/usr/local/bin:/usr/bin:/bin:/snap/bin
{{ENV_LINES}}
ExecStart={{NODE}} agent/index.mjs run
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ariweb-agent
[Install]
WantedBy=multi-user.target
+29
View File
@@ -0,0 +1,29 @@
[Unit]
Description=Ariweb HAL — 제어 평면 ({{VERSION}})
Documentation=https://source.ibit.kr/OZ-Group/AriWeb-Suite
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{USER}}
Group={{USER}}
WorkingDirectory={{APP_DIR}}/current
Environment=NODE_ENV=production
Environment=HOST=127.0.0.1
Environment=PORT=8900
Environment=ARIWEB_HAL_DATA={{APP_DIR}}/data
Environment=NODE_OPTIONS=--no-warnings
ExecStart={{NODE}} hal/index.mjs
Restart=always
RestartSec=3
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths={{APP_DIR}}
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ariweb-hal
[Install]
WantedBy=multi-user.target
+27
View File
@@ -0,0 +1,27 @@
[Unit]
Description=Ariweb Portal — 고객 포털 ({{VERSION}})
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{USER}}
Group={{USER}}
WorkingDirectory={{APP_DIR}}/current
Environment=NODE_ENV=production
Environment=HOST=127.0.0.1
Environment=PORT=8910
Environment=ARIWEB_PORTAL_DATA={{APP_DIR}}/data
Environment=NODE_OPTIONS=--no-warnings
{{ENV_LINES}}
ExecStart={{NODE}} portal/index.mjs
Restart=always
RestartSec=3
NoNewPrivileges=yes
PrivateTmp=yes
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ariweb-portal
[Install]
WantedBy=multi-user.target
+25
View File
@@ -0,0 +1,25 @@
# 관리형 동적(Node) 사이트 실행 유닛 — Agent 가 site.deploy(runtime=node) 때 만든다. 사이트 번들의 ariweb.json { "start": "node server.js", "port": N } 을 따른다.
[Unit]
Description=Ariweb site {{SITE_ID}}
After=network-online.target
[Service]
Type=simple
User={{USER}}
WorkingDirectory={{SITE_DIR}}/current
Environment=NODE_ENV=production
Environment=PORT={{PORT}}
Environment=ARIWEB_SITE_DATA={{SITE_DIR}}/data
ExecStart={{NODE}} {{START}}
Restart=always
RestartSec=3
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths={{SITE_DIR}}/data
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ariweb-site-{{SITE_ID}}
[Install]
WantedBy=multi-user.target
+25
View File
@@ -0,0 +1,25 @@
# Webmaker(도입 모듈 WebHosting_AI_agent) — 원본 deploy/webhosting-agent.service 를 바탕으로 HAL 배치 레이아웃(releases/current)에 맞춘 유닛.
# 데이터·포트·경로 환경 변수(DATA_ROOT·DATA_DIR·SITES_ROOT·STATS_LOG·PUBLIC_BASE_URL 등)는 서버에 있던 기존 유닛의 Environment 줄({{ENV_LINES}})을 그대로 보존한다.
[Unit]
Description=AI Webmaker ({{VERSION}}, Ariweb Suite managed)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{USER}}
Group={{USER}}
WorkingDirectory={{APP_DIR}}/current
Environment=NODE_ENV=production
Environment=HOST=127.0.0.1
Environment=PORT=3080
{{ENV_LINES}}
ExecStart={{NODE}} server.js
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal
SyslogIdentifier=webmaker
[Install]
WantedBy=multi-user.target