Add pharmacy auto-registration and infrastructure improvements
- Auto-generate pharmacy_code (P001~P999) when creating new pharmacy - Add new pharmacy fields: owner info, institution code/type, API port - Change Headplane port mapping: 3000 → 3001 to avoid conflicts - Add code-server setup script for development environment - Add LXC Caddy setup documentation - Update .gitignore to exclude farmq-admin submodule 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f739916737
commit
8d27461f76
5
.gitignore
vendored
5
.gitignore
vendored
@ -158,4 +158,7 @@ venv.bak/
|
|||||||
dmypy.json
|
dmypy.json
|
||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# Submodules managed separately
|
||||||
|
farmq-admin/
|
||||||
@ -35,7 +35,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./headplane-config:/etc/headplane
|
- ./headplane-config:/etc/headplane
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000" # Headplane Web UI
|
- "3001:3000" # Headplane Web UI (외부:3001, 내부:3000)
|
||||||
depends_on:
|
depends_on:
|
||||||
- headscale
|
- headscale
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
128
docs/code-server.sh
Normal file
128
docs/code-server.sh
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# setup-code-server.sh
|
||||||
|
# - code-server 미설치 시 자동 설치
|
||||||
|
# - 최초 1회 실행해 ~/.config/code-server/config.yaml 생성
|
||||||
|
# - config.yaml을 0.0.0.0:<PORT> + 지정 비밀번호로 갱신
|
||||||
|
# - 기존에 떠있는 code-server(수동/비-systemd) 프로세스 정리
|
||||||
|
# - systemd 미사용: nohup으로 백그라운드 실행
|
||||||
|
#
|
||||||
|
# 환경변수:
|
||||||
|
# PORT=8080 # 바인드 포트 (기본 8080)
|
||||||
|
# PASSWORD= # 비밀번호(무인 실행용)
|
||||||
|
# SKIP_CONFIRM=0/1 # 비밀번호 확인 입력 생략
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PORT="${PORT:-8080}"
|
||||||
|
CONFIG_DIR="${HOME}/.config/code-server"
|
||||||
|
CONFIG_FILE="${CONFIG_DIR}/config.yaml"
|
||||||
|
LOG_FILE="${HOME}/code-server.log"
|
||||||
|
|
||||||
|
say() { echo -e "$@"; }
|
||||||
|
die() { echo -e "❌ $@" >&2; exit 1; }
|
||||||
|
|
||||||
|
# 0) 필수 도구 준비 (curl/timeout/pgrep 등)
|
||||||
|
if ! command -v curl >/dev/null 2>&1 || ! command -v timeout >/dev/null 2>&1; then
|
||||||
|
say "📦 필요 패키지 설치 중 (curl, coreutils, procps 등)..."
|
||||||
|
if command -v apt >/dev/null 2>&1; then
|
||||||
|
apt update -y >/dev/null 2>&1 || true
|
||||||
|
apt install -y curl ca-certificates coreutils procps >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
die "apt 환경이 아닙니다. curl/timeout/pgrep가 필요합니다."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1) code-server 설치 확인 및 자동 설치
|
||||||
|
if ! command -v code-server >/dev/null 2>&1; then
|
||||||
|
say "📦 code-server 미설치 상태 → 설치 진행..."
|
||||||
|
bash <(curl -fsSL https://code-server.dev/install.sh)
|
||||||
|
command -v code-server >/dev/null 2>&1 || die "code-server 설치 실패"
|
||||||
|
say "✅ code-server 설치 완료"
|
||||||
|
else
|
||||||
|
say "✅ code-server 이미 설치됨"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2) config.yaml 생성 (없으면 최초 1회 3~5초 실행)
|
||||||
|
if [ ! -f "${CONFIG_FILE}" ]; then
|
||||||
|
say "📝 config.yaml 이 없어 최초 1회 실행으로 생성합니다..."
|
||||||
|
mkdir -p "${CONFIG_DIR}"
|
||||||
|
timeout 5s code-server >/dev/null 2>&1 || true
|
||||||
|
[ -f "${CONFIG_FILE}" ] || die "config.yaml 생성 실패"
|
||||||
|
say "✅ 기본 config.yaml 생성됨: ${CONFIG_FILE}"
|
||||||
|
else
|
||||||
|
say "ℹ️ 기존 config.yaml 감지: ${CONFIG_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3) 비밀번호 입력/확정
|
||||||
|
if [ "${PASSWORD-}" = "" ]; then
|
||||||
|
read -rsp "🔐 code-server 접속 비밀번호 입력: " PASS; echo
|
||||||
|
if [ "${SKIP_CONFIRM-0}" != "1" ]; then
|
||||||
|
read -rsp "🔐 비밀번호 확인 입력: " PASS2; echo
|
||||||
|
[ "$PASS" = "$PASS2" ] || die "비밀번호 불일치"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
PASS="$PASSWORD"
|
||||||
|
fi
|
||||||
|
[ -n "$PASS" ] || die "비밀번호는 비어 있을 수 없습니다."
|
||||||
|
|
||||||
|
# 4) 기존 파일 백업 후 config.yaml 갱신
|
||||||
|
ts="$(date +%Y%m%d%H%M%S)"
|
||||||
|
if [ -f "${CONFIG_FILE}" ]; then
|
||||||
|
cp -a "${CONFIG_FILE}" "${CONFIG_FILE}.bak.${ts}"
|
||||||
|
say "🗂 백업 생성: ${CONFIG_FILE}.bak.${ts}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "${CONFIG_FILE}" <<EOF
|
||||||
|
bind-addr: 0.0.0.0:${PORT}
|
||||||
|
auth: password
|
||||||
|
password: ${PASS}
|
||||||
|
cert: false
|
||||||
|
EOF
|
||||||
|
|
||||||
|
say "✅ config.yaml 갱신됨 (bind-addr=0.0.0.0:${PORT})"
|
||||||
|
|
||||||
|
# 5) systemd로 떠있다면 중지(원하시는 게 '비-systemd' 운영이므로)
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
if systemctl is-active --quiet "code-server@${USER}"; then
|
||||||
|
say "⏹ systemd 서비스(code-server@${USER}) 중지"
|
||||||
|
systemctl stop "code-server@${USER}" || true
|
||||||
|
fi
|
||||||
|
if [ "$EUID" -eq 0 ] && systemctl is-active --quiet "code-server@root"; then
|
||||||
|
say "⏹ systemd 서비스(code-server@root}) 중지"
|
||||||
|
systemctl stop "code-server@root" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6) 수동/기존 실행 프로세스 정리 (부모/자식 순서 종료)
|
||||||
|
say "🧹 기존 code-server 수동 프로세스 정리..."
|
||||||
|
# 부모 엔트리(메인/entry) TERM
|
||||||
|
pids="$(pgrep -f "/usr/lib/code-server/lib/node /usr/lib/code-server($|/out/node/entry)" || true)"
|
||||||
|
if [ -n "${pids}" ]; then
|
||||||
|
for p in $pids; do
|
||||||
|
pkill -TERM -P "$p" 2>/dev/null || true
|
||||||
|
kill -TERM "$p" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
# 남아있으면 KILL
|
||||||
|
pids="$(pgrep -f "/usr/lib/code-server/lib/node /usr/lib/code-server($|/out/node/entry)" || true)"
|
||||||
|
[ -n "${pids}" ] && kill -9 $pids 2>/dev/null || true
|
||||||
|
|
||||||
|
# 보조 호스트/터미널 프로세스 잔여물 정리(있어도 없어도 무방)
|
||||||
|
pkill -f "vscode/out/bootstrap-fork --type=ptyHost" 2>/dev/null || true
|
||||||
|
pkill -f "vscode/out/bootstrap-fork --type=extensionHost" 2>/dev/null || true
|
||||||
|
pkill -f "shellIntegration-bash.sh" 2>/dev/null || true
|
||||||
|
|
||||||
|
# 7) 비-systemd 백그라운드 실행
|
||||||
|
say "🚀 code-server 일반 실행(nohup 백그라운드) 시작..."
|
||||||
|
nohup code-server > "${LOG_FILE}" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
disown || true
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
say "✅ 실행됨 (PID: ${pid})"
|
||||||
|
say "📄 로그 보기: tail -f ${LOG_FILE}"
|
||||||
|
say "🌐 접속 URL: http://<서버IP>:${PORT}"
|
||||||
|
say "🔑 비밀번호: (방금 설정한 값)"
|
||||||
|
say "🔒 보안 권장: 역프록시(Caddy/Nginx) + HTTPS 사용 시 config는 127.0.0.1로 바꾸세요."
|
||||||
@ -415,23 +415,53 @@ def create_app(config_name=None):
|
|||||||
# FARMQ 데이터베이스에 약국 생성
|
# FARMQ 데이터베이스에 약국 생성
|
||||||
farmq_session = get_farmq_session()
|
farmq_session = get_farmq_session()
|
||||||
try:
|
try:
|
||||||
|
# pharmacy_code 자동 생성 (P001~P999)
|
||||||
|
last_pharmacy = farmq_session.query(PharmacyInfo)\
|
||||||
|
.filter(PharmacyInfo.pharmacy_code.like('P%'))\
|
||||||
|
.order_by(PharmacyInfo.pharmacy_code.desc())\
|
||||||
|
.first()
|
||||||
|
|
||||||
|
if last_pharmacy and last_pharmacy.pharmacy_code:
|
||||||
|
try:
|
||||||
|
last_num = int(last_pharmacy.pharmacy_code[1:])
|
||||||
|
new_num = last_num + 1
|
||||||
|
except:
|
||||||
|
new_num = 1
|
||||||
|
else:
|
||||||
|
new_num = 1
|
||||||
|
|
||||||
|
pharmacy_code = f"P{new_num:03d}" # P001, P002, ...
|
||||||
|
|
||||||
new_pharmacy = PharmacyInfo(
|
new_pharmacy = PharmacyInfo(
|
||||||
|
pharmacy_code=pharmacy_code,
|
||||||
pharmacy_name=pharmacy_name,
|
pharmacy_name=pharmacy_name,
|
||||||
business_number=data.get('business_number', '').strip(),
|
business_number=data.get('business_number', '').strip(),
|
||||||
manager_name=data.get('manager_name', '').strip(),
|
manager_name=data.get('manager_name', '').strip(),
|
||||||
phone=data.get('phone', '').strip(),
|
phone=data.get('phone', '').strip(),
|
||||||
address=data.get('address', '').strip(),
|
address=data.get('address', '').strip(),
|
||||||
|
|
||||||
|
# 신규 필드
|
||||||
|
owner_name=data.get('owner_name', '').strip(),
|
||||||
|
owner_license=data.get('owner_license', '').strip(),
|
||||||
|
owner_phone=data.get('owner_phone', '').strip(),
|
||||||
|
owner_email=data.get('owner_email', '').strip(),
|
||||||
|
institution_code=data.get('institution_code', '').strip() or None,
|
||||||
|
institution_type=data.get('institution_type', '').strip() or None,
|
||||||
|
api_port=data.get('api_port', 8082),
|
||||||
|
|
||||||
|
# 기존 필드
|
||||||
proxmox_host=data.get('proxmox_host', '').strip(),
|
proxmox_host=data.get('proxmox_host', '').strip(),
|
||||||
headscale_user_name=data.get('headscale_user_name', '').strip(),
|
headscale_user_name=data.get('headscale_user_name', '').strip(),
|
||||||
status='active'
|
status='active'
|
||||||
)
|
)
|
||||||
|
|
||||||
farmq_session.add(new_pharmacy)
|
farmq_session.add(new_pharmacy)
|
||||||
farmq_session.commit()
|
farmq_session.commit()
|
||||||
|
farmq_session.refresh(new_pharmacy)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': f'약국 "{pharmacy_name}"가 성공적으로 생성되었습니다.',
|
'message': f'약국 "{pharmacy_name}" (코드: {pharmacy_code}) 생성 완료',
|
||||||
'pharmacy': new_pharmacy.to_dict()
|
'pharmacy': new_pharmacy.to_dict()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -440,6 +470,8 @@ def create_app(config_name=None):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 약국 생성 오류: {e}")
|
print(f"❌ 약국 생성 오류: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'서버 오류: {str(e)}'
|
'error': f'서버 오류: {str(e)}'
|
||||||
|
|||||||
@ -42,31 +42,51 @@ class JSONType(TypeDecorator):
|
|||||||
class PharmacyInfo(FarmqBase):
|
class PharmacyInfo(FarmqBase):
|
||||||
"""약국 정보 테이블 - Headscale과 독립적"""
|
"""약국 정보 테이블 - Headscale과 독립적"""
|
||||||
__tablename__ = 'pharmacies'
|
__tablename__ = 'pharmacies'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
|
# 약국 코드 (핵심 식별자) - P001~P999
|
||||||
|
pharmacy_code = Column(String(10), unique=True)
|
||||||
|
|
||||||
# Headscale 연결 정보 (느슨한 결합)
|
# Headscale 연결 정보 (느슨한 결합)
|
||||||
headscale_user_name = Column(String(255)) # users.name 참조 (외래키 제약조건 없음)
|
headscale_user_name = Column(String(255)) # users.name 참조 (외래키 제약조건 없음)
|
||||||
headscale_user_id = Column(Integer) # users.id 참조 (외래키 제약조건 없음)
|
headscale_user_id = Column(Integer) # users.id 참조 (외래키 제약조건 없음)
|
||||||
|
|
||||||
# 약국 기본 정보
|
# 약국 기본 정보
|
||||||
pharmacy_name = Column(String(255), nullable=False)
|
pharmacy_name = Column(String(255), nullable=False)
|
||||||
business_number = Column(String(20))
|
business_number = Column(String(20))
|
||||||
manager_name = Column(String(100))
|
manager_name = Column(String(100)) # deprecated - use owner_name
|
||||||
phone = Column(String(20))
|
phone = Column(String(20))
|
||||||
address = Column(Text)
|
address = Column(Text)
|
||||||
|
|
||||||
# 기술적 정보
|
# 대표자 정보 (신규)
|
||||||
|
owner_name = Column(String(100))
|
||||||
|
owner_license = Column(String(50))
|
||||||
|
owner_phone = Column(String(20))
|
||||||
|
owner_email = Column(String(100))
|
||||||
|
|
||||||
|
# 요양기관 정보 (신규)
|
||||||
|
institution_code = Column(String(20))
|
||||||
|
institution_type = Column(String(20))
|
||||||
|
|
||||||
|
# 운영 정보 (신규)
|
||||||
|
opening_date = Column(DateTime)
|
||||||
|
business_hours = Column(Text)
|
||||||
|
|
||||||
|
# API 포트 (신규)
|
||||||
|
api_port = Column(Integer, default=8082)
|
||||||
|
|
||||||
|
# 기술적 정보 (deprecated - pharmacy_servers로 이동)
|
||||||
proxmox_host = Column(String(255))
|
proxmox_host = Column(String(255))
|
||||||
proxmox_username = Column(String(100))
|
proxmox_username = Column(String(100))
|
||||||
proxmox_api_token = Column(Text) # 암호화 권장
|
proxmox_api_token = Column(Text) # 암호화 권장
|
||||||
tailscale_ip = Column(String(45)) # IPv4/IPv6 지원
|
tailscale_ip = Column(String(45)) # IPv4/IPv6 지원 (deprecated)
|
||||||
|
|
||||||
# 상태 관리
|
# 상태 관리
|
||||||
status = Column(String(20), default='active') # active, inactive, maintenance
|
status = Column(String(20), default='active') # active, inactive, maintenance
|
||||||
last_sync = Column(DateTime) # 마지막 동기화 시간
|
last_sync = Column(DateTime) # 마지막 동기화 시간
|
||||||
notes = Column(Text) # 관리 메모
|
notes = Column(Text) # 관리 메모
|
||||||
|
|
||||||
# 타임스탬프
|
# 타임스탬프
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
@ -78,6 +98,7 @@ class PharmacyInfo(FarmqBase):
|
|||||||
"""딕셔너리로 변환"""
|
"""딕셔너리로 변환"""
|
||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
|
'pharmacy_code': self.pharmacy_code,
|
||||||
'headscale_user_name': self.headscale_user_name,
|
'headscale_user_name': self.headscale_user_name,
|
||||||
'headscale_user_id': self.headscale_user_id,
|
'headscale_user_id': self.headscale_user_id,
|
||||||
'pharmacy_name': self.pharmacy_name,
|
'pharmacy_name': self.pharmacy_name,
|
||||||
@ -85,6 +106,15 @@ class PharmacyInfo(FarmqBase):
|
|||||||
'manager_name': self.manager_name,
|
'manager_name': self.manager_name,
|
||||||
'phone': self.phone,
|
'phone': self.phone,
|
||||||
'address': self.address,
|
'address': self.address,
|
||||||
|
'owner_name': self.owner_name,
|
||||||
|
'owner_license': self.owner_license,
|
||||||
|
'owner_phone': self.owner_phone,
|
||||||
|
'owner_email': self.owner_email,
|
||||||
|
'institution_code': self.institution_code,
|
||||||
|
'institution_type': self.institution_type,
|
||||||
|
'opening_date': self.opening_date.isoformat() if self.opening_date else None,
|
||||||
|
'business_hours': self.business_hours,
|
||||||
|
'api_port': self.api_port,
|
||||||
'proxmox_host': self.proxmox_host,
|
'proxmox_host': self.proxmox_host,
|
||||||
'tailscale_ip': self.tailscale_ip,
|
'tailscale_ip': self.tailscale_ip,
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
@ -179,6 +209,97 @@ class MachineProfile(FarmqBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PharmacyServer(FarmqBase):
|
||||||
|
"""약국 서버 테이블 - 약국과 서버 분리"""
|
||||||
|
__tablename__ = 'pharmacy_servers'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
|
# 약국 연결
|
||||||
|
pharmacy_id = Column(Integer, nullable=False)
|
||||||
|
pharmacy_code = Column(String(10), nullable=False)
|
||||||
|
|
||||||
|
# Headscale 노드 연결
|
||||||
|
headscale_node_id = Column(Integer, unique=True)
|
||||||
|
headscale_user_id = Column(Integer)
|
||||||
|
|
||||||
|
# 네트워크 정보
|
||||||
|
vpn_ip = Column(String(45), nullable=False)
|
||||||
|
api_port = Column(Integer, default=8082)
|
||||||
|
is_online = Column(Boolean, default=False)
|
||||||
|
last_seen_at = Column(DateTime)
|
||||||
|
|
||||||
|
# 서버 역할
|
||||||
|
server_role = Column(String(20), default='primary') # primary, backup, test
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
# 하드웨어 정보
|
||||||
|
hostname = Column(String(255))
|
||||||
|
machine_name = Column(String(255))
|
||||||
|
serial_number = Column(String(100))
|
||||||
|
|
||||||
|
cpu_model = Column(String(255))
|
||||||
|
cpu_cores = Column(Integer)
|
||||||
|
cpu_threads = Column(Integer)
|
||||||
|
ram_gb = Column(Integer)
|
||||||
|
storage_type = Column(String(50))
|
||||||
|
storage_gb = Column(Integer)
|
||||||
|
gpu_model = Column(String(255))
|
||||||
|
gpu_memory_gb = Column(Integer)
|
||||||
|
network_interfaces = Column(JSONType)
|
||||||
|
|
||||||
|
os_type = Column(String(50))
|
||||||
|
os_version = Column(String(100))
|
||||||
|
tailscale_version = Column(String(50))
|
||||||
|
installed_software = Column(JSONType)
|
||||||
|
|
||||||
|
# 관리 정보
|
||||||
|
status = Column(String(20), default='active')
|
||||||
|
location = Column(String(255))
|
||||||
|
purchase_date = Column(DateTime)
|
||||||
|
warranty_expires = Column(DateTime)
|
||||||
|
last_maintenance = Column(DateTime)
|
||||||
|
|
||||||
|
# 베이스라인 메트릭
|
||||||
|
baseline_cpu_temp = Column(Float)
|
||||||
|
baseline_cpu_usage = Column(Float)
|
||||||
|
baseline_memory_usage = Column(Float)
|
||||||
|
|
||||||
|
# 메타데이터
|
||||||
|
notes = Column(Text)
|
||||||
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
|
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<PharmacyServer(id={self.id}, pharmacy_code='{self.pharmacy_code}', vpn_ip='{self.vpn_ip}')>"
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'pharmacy_id': self.pharmacy_id,
|
||||||
|
'pharmacy_code': self.pharmacy_code,
|
||||||
|
'headscale_node_id': self.headscale_node_id,
|
||||||
|
'vpn_ip': self.vpn_ip,
|
||||||
|
'api_port': self.api_port,
|
||||||
|
'is_online': self.is_online,
|
||||||
|
'last_seen_at': self.last_seen_at.isoformat() if self.last_seen_at else None,
|
||||||
|
'server_role': self.server_role,
|
||||||
|
'is_active': self.is_active,
|
||||||
|
'hostname': self.hostname,
|
||||||
|
'machine_name': self.machine_name,
|
||||||
|
'cpu_model': self.cpu_model,
|
||||||
|
'cpu_cores': self.cpu_cores,
|
||||||
|
'ram_gb': self.ram_gb,
|
||||||
|
'storage_gb': self.storage_gb,
|
||||||
|
'os_type': self.os_type,
|
||||||
|
'os_version': self.os_version,
|
||||||
|
'status': self.status,
|
||||||
|
'location': self.location,
|
||||||
|
'created_at': self.created_at.isoformat(),
|
||||||
|
'updated_at': self.updated_at.isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MonitoringMetrics(FarmqBase):
|
class MonitoringMetrics(FarmqBase):
|
||||||
"""실시간 모니터링 메트릭스 - 시계열 데이터"""
|
"""실시간 모니터링 메트릭스 - 시계열 데이터"""
|
||||||
__tablename__ = 'monitoring_metrics'
|
__tablename__ = 'monitoring_metrics'
|
||||||
|
|||||||
388
setup_doc/LXC_Caddysetup.md
Normal file
388
setup_doc/LXC_Caddysetup.md
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
# LXC Caddy에서 호스트 Tailscale 네트워크 연결 가이드
|
||||||
|
|
||||||
|
## 📅 작성일
|
||||||
|
2025년 9월 22일
|
||||||
|
|
||||||
|
## 🎯 개요
|
||||||
|
호스트에 Tailscale이 설치된 환경에서 LXC 컨테이너의 Caddy가 Tailscale 네트워크에 접근할 수 있도록 설정하는 완전한 가이드입니다.
|
||||||
|
|
||||||
|
## 🏗️ 시스템 구성
|
||||||
|
|
||||||
|
### 환경 정보
|
||||||
|
- **호스트**: Proxmox VE (Tailscale 설치됨)
|
||||||
|
- **LXC 컨테이너**: Caddy 웹서버 (ID: 103)
|
||||||
|
- **목표**: LXC Caddy에서 Tailscale 네트워크의 서버들에 리버스 프록시
|
||||||
|
|
||||||
|
### 네트워크 구조
|
||||||
|
```
|
||||||
|
인터넷 → 도메인(*.pharmq.kr) → Caddy(LXC 103) → 호스트(라우팅) → Tailscale 네트워크
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ 전제 조건
|
||||||
|
|
||||||
|
### 1. 호스트 Tailscale 설치 확인
|
||||||
|
```bash
|
||||||
|
# 호스트에서 Tailscale 상태 확인
|
||||||
|
tailscale status
|
||||||
|
|
||||||
|
# 출력 예시:
|
||||||
|
# 100.64.0.3 pve-p2 myuser linux -
|
||||||
|
# 100.64.0.6 pve-hp myuser linux -
|
||||||
|
# 100.64.0.11 pve-p1 myuser linux -
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. LXC 컨테이너 정보 확인
|
||||||
|
```bash
|
||||||
|
# LXC 네트워크 정보 확인
|
||||||
|
pct exec 103 -- ip addr show eth0
|
||||||
|
|
||||||
|
# 출력 예시:
|
||||||
|
# inet 192.168.0.19/24 brd 192.168.0.255 scope global dynamic eth0
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 설정 단계
|
||||||
|
|
||||||
|
### 1단계: 호스트에서 IP 포워딩 활성화
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# IP 포워딩 활성화 (임시)
|
||||||
|
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||||
|
|
||||||
|
# IP 포워딩 영구 활성화
|
||||||
|
echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
|
||||||
|
sysctl -p
|
||||||
|
```
|
||||||
|
|
||||||
|
**확인:**
|
||||||
|
```bash
|
||||||
|
cat /proc/sys/net/ipv4/ip_forward
|
||||||
|
# 출력: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2단계: LXC에서 Tailscale 네트워크로 라우팅 추가
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# LXC에 라우팅 규칙 추가 (임시)
|
||||||
|
pct exec 103 -- ip route add 100.64.0.0/10 via 192.168.0.200
|
||||||
|
|
||||||
|
# 라우팅 확인
|
||||||
|
pct exec 103 -- ip route | grep 100.64
|
||||||
|
# 출력: 100.64.0.0/10 via 192.168.0.200 dev eth0
|
||||||
|
```
|
||||||
|
|
||||||
|
**영구 라우팅 설정:**
|
||||||
|
```bash
|
||||||
|
# LXC 내부에서 설정
|
||||||
|
pct exec 103 -- bash -c 'echo "100.64.0.0/10 via 192.168.0.200" >> /etc/systemd/network/10-eth0.network'
|
||||||
|
|
||||||
|
# 또는 /etc/network/interfaces 사용 (Debian 기반)
|
||||||
|
pct exec 103 -- bash -c 'echo "up ip route add 100.64.0.0/10 via 192.168.0.200" >> /etc/network/interfaces'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3단계: iptables MASQUERADE 설정 (핵심!)
|
||||||
|
|
||||||
|
**⚠️ 중요: 이 단계가 없으면 일부 Tailscale 노드 연결이 실패할 수 있습니다.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# LXC에서 Tailscale 네트워크로의 트래픽에 MASQUERADE 적용
|
||||||
|
iptables -t nat -A POSTROUTING -s 192.168.0.19 -d 100.64.0.0/10 -j MASQUERADE
|
||||||
|
|
||||||
|
# MASQUERADE 규칙 확인
|
||||||
|
iptables -t nat -L POSTROUTING -v -n | grep 100.64
|
||||||
|
```
|
||||||
|
|
||||||
|
**영구 iptables 설정:**
|
||||||
|
```bash
|
||||||
|
# iptables-persistent 설치 (Debian/Ubuntu)
|
||||||
|
apt-get install iptables-persistent
|
||||||
|
|
||||||
|
# 현재 규칙 저장
|
||||||
|
iptables-save > /etc/iptables/rules.v4
|
||||||
|
|
||||||
|
# 또는 systemd 서비스로 영구화
|
||||||
|
cat > /etc/systemd/system/lxc-tailscale-nat.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=LXC Tailscale NAT Rules
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/sbin/iptables -t nat -A POSTROUTING -s 192.168.0.19 -d 100.64.0.0/10 -j MASQUERADE
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl enable lxc-tailscale-nat.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4단계: 연결 테스트
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# LXC에서 Tailscale 네트워크 ping 테스트
|
||||||
|
pct exec 103 -- ping -c 2 100.64.0.3
|
||||||
|
|
||||||
|
# 출력 예시:
|
||||||
|
# PING 100.64.0.3 (100.64.0.3) 56(84) bytes of data.
|
||||||
|
# 64 bytes from 100.64.0.3: icmp_seq=1 ttl=64 time=0.038 ms
|
||||||
|
# 64 bytes from 100.64.0.3: icmp_seq=2 ttl=64 time=0.047 ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5단계: Caddyfile 설정
|
||||||
|
|
||||||
|
**호스트 기준 Tailscale IP 매핑 확인:**
|
||||||
|
```bash
|
||||||
|
# 호스트에서 Tailscale 노드 목록 확인
|
||||||
|
tailscale status
|
||||||
|
|
||||||
|
# 예시 출력:
|
||||||
|
# 100.64.0.3 pve-p2 myuser linux -
|
||||||
|
# 100.64.0.6 pve-hp myuser linux -
|
||||||
|
# 100.64.0.11 pve-p1 myuser linux -
|
||||||
|
# 100.64.0.12 pve7 myuser linux -
|
||||||
|
```
|
||||||
|
|
||||||
|
**Caddyfile 설정 예시:**
|
||||||
|
```caddyfile
|
||||||
|
{
|
||||||
|
email admin@pharmq.kr
|
||||||
|
acme_dns cloudflare YOUR_CLOUDFLARE_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
# 와일드카드 인증서
|
||||||
|
*.pharmq.kr {
|
||||||
|
tls {
|
||||||
|
dns cloudflare YOUR_CLOUDFLARE_TOKEN
|
||||||
|
}
|
||||||
|
respond "Wildcard domain: {host} - SSL ready!" 200
|
||||||
|
}
|
||||||
|
|
||||||
|
# PVE 노드들 (호스트 기준 Tailscale 네트워크)
|
||||||
|
p2.pharmq.kr {
|
||||||
|
reverse_proxy https://100.64.0.3:8006 {
|
||||||
|
transport http {
|
||||||
|
tls_insecure_skip_verify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tls {
|
||||||
|
dns cloudflare YOUR_CLOUDFLARE_TOKEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hp.pharmq.kr {
|
||||||
|
reverse_proxy https://100.64.0.6:8006 {
|
||||||
|
transport http {
|
||||||
|
tls_insecure_skip_verify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tls {
|
||||||
|
dns cloudflare YOUR_CLOUDFLARE_TOKEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p1.pharmq.kr {
|
||||||
|
reverse_proxy https://100.64.0.11:8006 {
|
||||||
|
transport http {
|
||||||
|
tls_insecure_skip_verify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tls {
|
||||||
|
dns cloudflare YOUR_CLOUDFLARE_TOKEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6단계: Caddy 설정 적용
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Caddyfile 문법 검증
|
||||||
|
pct exec 103 -- caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
# Caddy 설정 다시 로드
|
||||||
|
pct exec 103 -- systemctl reload caddy
|
||||||
|
|
||||||
|
# Caddy 상태 확인
|
||||||
|
pct exec 103 -- systemctl status caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7단계: 최종 테스트
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 외부에서 도메인 접근 테스트
|
||||||
|
curl -I https://p2.pharmq.kr
|
||||||
|
|
||||||
|
# 성공 예시 응답:
|
||||||
|
# HTTP/2 501
|
||||||
|
# server: pve-api-daemon/3.0
|
||||||
|
# via: 1.1 Caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 문제 해결
|
||||||
|
|
||||||
|
### 문제 1: LXC에서 Tailscale IP 접근 불가
|
||||||
|
|
||||||
|
**증상:**
|
||||||
|
```bash
|
||||||
|
pct exec 103 -- ping 100.64.0.3
|
||||||
|
# PING 100.64.0.3 (100.64.0.3) 56(84) bytes of data.
|
||||||
|
# --- 100.64.0.3 ping statistics ---
|
||||||
|
# 2 packets transmitted, 0 received, 100% packet loss
|
||||||
|
```
|
||||||
|
|
||||||
|
**해결책:**
|
||||||
|
```bash
|
||||||
|
# 1. 호스트 IP 포워딩 확인
|
||||||
|
cat /proc/sys/net/ipv4/ip_forward
|
||||||
|
# 0이면 활성화 필요
|
||||||
|
|
||||||
|
# 2. LXC 라우팅 규칙 확인
|
||||||
|
pct exec 103 -- ip route | grep 100.64
|
||||||
|
# 없으면 라우팅 규칙 추가 필요
|
||||||
|
|
||||||
|
# 3. 호스트 Tailscale 상태 확인
|
||||||
|
tailscale status
|
||||||
|
# 대상 노드가 활성 상태인지 확인
|
||||||
|
```
|
||||||
|
|
||||||
|
### 문제 2: Caddy에서 502 Bad Gateway
|
||||||
|
|
||||||
|
**증상:**
|
||||||
|
```bash
|
||||||
|
curl -I https://p2.pharmq.kr
|
||||||
|
# HTTP/2 502
|
||||||
|
```
|
||||||
|
|
||||||
|
**해결책:**
|
||||||
|
```bash
|
||||||
|
# 1. LXC에서 직접 연결 테스트
|
||||||
|
pct exec 103 -- curl -I --connect-timeout 5 https://100.64.0.3:8006
|
||||||
|
|
||||||
|
# 2. Caddy 로그 확인
|
||||||
|
pct exec 103 -- journalctl -u caddy --since "1 minute ago"
|
||||||
|
|
||||||
|
# 3. 대상 서버 응답 확인
|
||||||
|
curl -I --connect-timeout 5 https://100.64.0.3:8006
|
||||||
|
```
|
||||||
|
|
||||||
|
### 문제 3: SSL 인증서 오류
|
||||||
|
|
||||||
|
**증상:**
|
||||||
|
```
|
||||||
|
transport http {
|
||||||
|
dial_timeout 10s
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**해결책:**
|
||||||
|
```caddyfile
|
||||||
|
# HTTPS 백엔드의 경우 SSL 검증 무시 추가
|
||||||
|
reverse_proxy https://100.64.0.3:8006 {
|
||||||
|
transport http {
|
||||||
|
tls_insecure_skip_verify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 체크리스트
|
||||||
|
|
||||||
|
### 설정 전 확인사항
|
||||||
|
- [ ] 호스트에 Tailscale 설치 및 활성화됨
|
||||||
|
- [ ] LXC 컨테이너 네트워크 설정 확인
|
||||||
|
- [ ] 대상 Tailscale 노드들이 활성 상태
|
||||||
|
|
||||||
|
### 설정 단계 체크리스트
|
||||||
|
- [ ] 호스트 IP 포워딩 활성화
|
||||||
|
- [ ] LXC에서 Tailscale 네트워크 라우팅 추가
|
||||||
|
- [ ] LXC에서 Tailscale IP로 ping 성공
|
||||||
|
- [ ] Caddyfile에 올바른 Tailscale IP 설정
|
||||||
|
- [ ] HTTPS 백엔드에 `tls_insecure_skip_verify` 추가
|
||||||
|
- [ ] Caddy 설정 검증 및 리로드
|
||||||
|
- [ ] 외부에서 도메인 접근 테스트 성공
|
||||||
|
|
||||||
|
## 🎯 핵심 포인트
|
||||||
|
|
||||||
|
### 1. LXC에 Tailscale 설치 불필요
|
||||||
|
- **잘못된 접근**: LXC에 Tailscale 직접 설치
|
||||||
|
- **올바른 접근**: 호스트 Tailscale을 통한 라우팅
|
||||||
|
|
||||||
|
### 2. 네트워크 라우팅이 핵심
|
||||||
|
```bash
|
||||||
|
# 이 명령어가 모든 것을 해결합니다
|
||||||
|
pct exec 103 -- ip route add 100.64.0.0/10 via 192.168.0.200
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 호스트 기준 IP 사용
|
||||||
|
- LXC 내부 Tailscale 상태가 아닌 **호스트 Tailscale 상태** 기준으로 IP 설정
|
||||||
|
|
||||||
|
### 4. HTTPS 백엔드 처리
|
||||||
|
```caddyfile
|
||||||
|
# PVE와 같은 HTTPS 백엔드의 경우 필수
|
||||||
|
transport http {
|
||||||
|
tls_insecure_skip_verify
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 확장 가능성
|
||||||
|
|
||||||
|
### 다른 서비스 추가
|
||||||
|
```caddyfile
|
||||||
|
# 새로운 Tailscale 노드 추가 예시
|
||||||
|
newservice.pharmq.kr {
|
||||||
|
reverse_proxy http://100.64.0.X:PORT
|
||||||
|
tls {
|
||||||
|
dns cloudflare YOUR_TOKEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 자동화 스크립트
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# setup-lxc-tailscale-routing.sh
|
||||||
|
|
||||||
|
LXC_ID="103"
|
||||||
|
HOST_IP="192.168.0.200"
|
||||||
|
TAILSCALE_NETWORK="100.64.0.0/10"
|
||||||
|
|
||||||
|
# IP 포워딩 활성화
|
||||||
|
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||||
|
|
||||||
|
# LXC 라우팅 추가
|
||||||
|
pct exec $LXC_ID -- ip route add $TAILSCALE_NETWORK via $HOST_IP
|
||||||
|
|
||||||
|
echo "✅ LXC Tailscale 라우팅 설정 완료"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 유지보수
|
||||||
|
|
||||||
|
### 정기 점검 항목
|
||||||
|
1. **Tailscale 연결 상태**: `tailscale status`
|
||||||
|
2. **LXC 라우팅 상태**: `pct exec 103 -- ip route | grep 100.64`
|
||||||
|
3. **Caddy 도메인 목록**: `pct exec 103 -- journalctl -u caddy | grep domains`
|
||||||
|
|
||||||
|
### 재부팅 후 복구
|
||||||
|
```bash
|
||||||
|
# 호스트 재부팅 후 실행할 명령어들 (3단계 모두 필수!)
|
||||||
|
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||||
|
pct exec 103 -- ip route add 100.64.0.0/10 via 192.168.0.200
|
||||||
|
iptables -t nat -A POSTROUTING -s 192.168.0.19 -d 100.64.0.0/10 -j MASQUERADE
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎉 결론
|
||||||
|
|
||||||
|
이 방법을 사용하면:
|
||||||
|
- ✅ **LXC에 Tailscale 설치 불필요**
|
||||||
|
- ✅ **간단한 네트워크 라우팅으로 해결**
|
||||||
|
- ✅ **호스트 Tailscale 리소스 효율적 활용**
|
||||||
|
- ✅ **SSL 인증서 자동 관리**
|
||||||
|
- ✅ **확장성 및 유지보수성 우수**
|
||||||
|
|
||||||
|
**핵심 원리**: 호스트가 이미 Tailscale 네트워크에 연결되어 있다면, LXC는 **라우팅 + MASQUERADE**를 통해 호스트를 경유하여 접근할 수 있습니다!
|
||||||
|
|
||||||
|
**⚠️ 중요 교훈**: MASQUERADE 없이는 일부 Tailscale 노드 연결이 실패할 수 있습니다. 반드시 3단계 모두 필요합니다!
|
||||||
|
|
||||||
|
---
|
||||||
|
**작성자**: Claude Code Assistant
|
||||||
|
**파일 위치**: `/srv/pq_setup/LXC_Caddy_with_Host_Tailscale_Setup_Guide.md`
|
||||||
|
**최종 업데이트**: 2025년 9월 22일
|
||||||
Loading…
Reference in New Issue
Block a user