- Express.js 기반 백엔드 API 서버 - MariaDB, Redis, phpMyAdmin Docker 환경 - Device 관리 기본 CRUD 구현 - Mac Mini M4 Pro 전용 설정 및 배포 스크립트 - 자동화된 설치 및 배포 시스템 - 완전한 문서화 및 실행 가이드
1665 lines
50 KiB
Markdown
1665 lines
50 KiB
Markdown
# 홈 관리 시스템 DB 구축 계획서
|
|
|
|
## 🎯 프로젝트 개요
|
|
|
|
### Git 저장소
|
|
- **Repository**: https://git.hyungi.net/hyungi/myhome-server.git
|
|
- **Gitea Server**: git.hyungi.net (DS1525+ 호스팅)
|
|
|
|
### 목표
|
|
- 홈 IoT 기기 모니터링 및 관리
|
|
- 전력 소비, 네트워크 트래픽 추적
|
|
- 시스템 리소스 모니터링
|
|
- 향후 개인 서비스 확장 기반 마련
|
|
|
|
### 기술 스택
|
|
```
|
|
Backend: Express.js (Node.js)
|
|
Database: MariaDB 11.x
|
|
Admin Tool: phpMyAdmin
|
|
Architecture: MVC + Service Layer
|
|
Code Style: 모듈형 구조 (500자 이하 함수)
|
|
```
|
|
|
|
---
|
|
|
|
## 🗄️ 데이터베이스 설계
|
|
|
|
### 데이터베이스 생성
|
|
```sql
|
|
CREATE DATABASE home_management
|
|
CHARACTER SET utf8mb4
|
|
COLLATE utf8mb4_unicode_ci;
|
|
|
|
CREATE USER 'homeuser'@'localhost' IDENTIFIED BY 'secure_password';
|
|
GRANT ALL PRIVILEGES ON home_management.* TO 'homeuser'@'localhost';
|
|
FLUSH PRIVILEGES;
|
|
```
|
|
|
|
### 테이블 설계
|
|
|
|
#### 1. 디바이스 관리 (devices)
|
|
```sql
|
|
CREATE TABLE devices (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
device_id VARCHAR(50) UNIQUE NOT NULL,
|
|
name VARCHAR(100) NOT NULL,
|
|
device_type ENUM('server', 'nas', 'router', 'smart_plug', 'other') NOT NULL,
|
|
location VARCHAR(50),
|
|
ip_address VARCHAR(45),
|
|
mac_address VARCHAR(17),
|
|
power_rating_watts INT,
|
|
monitoring_enabled BOOLEAN DEFAULT TRUE,
|
|
metadata JSON,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_device_id (device_id),
|
|
INDEX idx_type_enabled (device_type, monitoring_enabled)
|
|
) ENGINE=InnoDB;
|
|
```
|
|
|
|
#### 2. 전력 소비 데이터 (power_consumption)
|
|
```sql
|
|
CREATE TABLE power_consumption (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
device_id VARCHAR(50) NOT NULL,
|
|
timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
|
|
watts DECIMAL(8,2) NOT NULL,
|
|
voltage DECIMAL(6,2),
|
|
current DECIMAL(6,3),
|
|
kwh_total DECIMAL(10,4),
|
|
metadata JSON,
|
|
INDEX idx_device_time (device_id, timestamp),
|
|
INDEX idx_timestamp (timestamp),
|
|
INDEX idx_watts (watts),
|
|
FOREIGN KEY (device_id) REFERENCES devices(device_id) ON DELETE CASCADE
|
|
) ENGINE=InnoDB
|
|
PARTITION BY RANGE (UNIX_TIMESTAMP(timestamp)) (
|
|
PARTITION p_2025_q1 VALUES LESS THAN (UNIX_TIMESTAMP('2025-04-01')),
|
|
PARTITION p_2025_q2 VALUES LESS THAN (UNIX_TIMESTAMP('2025-07-01')),
|
|
PARTITION p_2025_q3 VALUES LESS THAN (UNIX_TIMESTAMP('2025-10-01')),
|
|
PARTITION p_2025_q4 VALUES LESS THAN (UNIX_TIMESTAMP('2026-01-01')),
|
|
PARTITION p_future VALUES LESS THAN MAXVALUE
|
|
);
|
|
```
|
|
|
|
#### 3. 네트워크 트래픽 (network_traffic)
|
|
```sql
|
|
CREATE TABLE network_traffic (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
device_mac VARCHAR(17) NOT NULL,
|
|
device_name VARCHAR(100),
|
|
timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
|
|
bytes_in BIGINT UNSIGNED NOT NULL,
|
|
bytes_out BIGINT UNSIGNED NOT NULL,
|
|
packets_in INT UNSIGNED,
|
|
packets_out INT UNSIGNED,
|
|
connection_count INT UNSIGNED,
|
|
metadata JSON,
|
|
INDEX idx_mac_time (device_mac, timestamp),
|
|
INDEX idx_timestamp (timestamp)
|
|
) ENGINE=InnoDB;
|
|
```
|
|
|
|
#### 4. 시스템 리소스 (system_resources)
|
|
```sql
|
|
CREATE TABLE system_resources (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
server_name VARCHAR(50) NOT NULL,
|
|
timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
|
|
cpu_percent DECIMAL(5,2),
|
|
memory_used_gb DECIMAL(6,2),
|
|
memory_total_gb DECIMAL(6,2),
|
|
disk_used_gb DECIMAL(8,2),
|
|
disk_total_gb DECIMAL(8,2),
|
|
network_io JSON,
|
|
temperature DECIMAL(4,1),
|
|
load_average JSON,
|
|
processes_count INT,
|
|
uptime_seconds BIGINT,
|
|
INDEX idx_server_time (server_name, timestamp)
|
|
) ENGINE=InnoDB;
|
|
```
|
|
|
|
#### 5. 서비스 상태 (service_status)
|
|
```sql
|
|
CREATE TABLE service_status (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
service_name VARCHAR(50) NOT NULL,
|
|
server_name VARCHAR(50) NOT NULL,
|
|
timestamp TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
|
|
status ENUM('online', 'offline', 'degraded', 'maintenance') NOT NULL,
|
|
response_time_ms INT UNSIGNED,
|
|
cpu_usage DECIMAL(5,2),
|
|
memory_usage_mb INT,
|
|
error_message TEXT,
|
|
metadata JSON,
|
|
INDEX idx_service_time (service_name, timestamp),
|
|
INDEX idx_server_service (server_name, service_name),
|
|
INDEX idx_status (status, timestamp)
|
|
) ENGINE=InnoDB;
|
|
```
|
|
|
|
#### 6. 사용자 관리 (users)
|
|
```sql
|
|
CREATE TABLE users (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
username VARCHAR(50) UNIQUE NOT NULL,
|
|
email VARCHAR(100) UNIQUE,
|
|
password_hash VARCHAR(255),
|
|
full_name VARCHAR(100),
|
|
role ENUM('admin', 'family', 'guest') DEFAULT 'family',
|
|
preferences JSON,
|
|
last_login TIMESTAMP NULL,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_username (username),
|
|
INDEX idx_email (email),
|
|
INDEX idx_role (role)
|
|
) ENGINE=InnoDB;
|
|
```
|
|
|
|
#### 7. 알림 규칙 (alert_rules)
|
|
```sql
|
|
CREATE TABLE alert_rules (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
condition_type ENUM('threshold', 'change', 'pattern', 'custom') NOT NULL,
|
|
target_table VARCHAR(50) NOT NULL,
|
|
target_field VARCHAR(50) NOT NULL,
|
|
operator ENUM('>', '<', '>=', '<=', '=', '!=', 'contains') NOT NULL,
|
|
threshold_value DECIMAL(15,4),
|
|
time_window_minutes INT DEFAULT 5,
|
|
severity ENUM('info', 'warning', 'critical') DEFAULT 'warning',
|
|
notification_methods JSON,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
cooldown_minutes INT DEFAULT 60,
|
|
last_triggered TIMESTAMP NULL,
|
|
created_by INT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (created_by) REFERENCES users(id),
|
|
INDEX idx_active (is_active),
|
|
INDEX idx_target (target_table, target_field)
|
|
) ENGINE=InnoDB;
|
|
```
|
|
|
|
#### 8. 알림 로그 (alert_logs)
|
|
```sql
|
|
CREATE TABLE alert_logs (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
alert_rule_id INT NOT NULL,
|
|
triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
trigger_value DECIMAL(15,4),
|
|
message TEXT,
|
|
severity ENUM('info', 'warning', 'critical') NOT NULL,
|
|
notification_sent BOOLEAN DEFAULT FALSE,
|
|
resolved_at TIMESTAMP NULL,
|
|
metadata JSON,
|
|
FOREIGN KEY (alert_rule_id) REFERENCES alert_rules(id) ON DELETE CASCADE,
|
|
INDEX idx_rule_time (alert_rule_id, triggered_at),
|
|
INDEX idx_severity (severity, triggered_at)
|
|
) ENGINE=InnoDB;
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 Express.js 프로젝트 구조
|
|
|
|
### 폴더 구조
|
|
```
|
|
home-management-api/
|
|
├── src/
|
|
│ ├── config/
|
|
│ │ ├── database.js # DB 연결 설정
|
|
│ │ ├── redis.js # Redis 캐시 설정
|
|
│ │ └── environment.js # 환경변수 관리
|
|
│ ├── models/
|
|
│ │ ├── index.js # 모델 인덱스
|
|
│ │ ├── Device.js # 디바이스 모델
|
|
│ │ ├── PowerConsumption.js # 전력 소비 모델
|
|
│ │ ├── NetworkTraffic.js # 네트워크 트래픽 모델
|
|
│ │ ├── SystemResource.js # 시스템 리소스 모델
|
|
│ │ ├── ServiceStatus.js # 서비스 상태 모델
|
|
│ │ ├── User.js # 사용자 모델
|
|
│ │ ├── AlertRule.js # 알림 규칙 모델
|
|
│ │ └── AlertLog.js # 알림 로그 모델
|
|
│ ├── controllers/
|
|
│ │ ├── deviceController.js # 디바이스 CRUD
|
|
│ │ ├── powerController.js # 전력 데이터 처리
|
|
│ │ ├── networkController.js # 네트워크 데이터 처리
|
|
│ │ ├── systemController.js # 시스템 모니터링
|
|
│ │ ├── dashboardController.js # 대시보드 데이터
|
|
│ │ ├── authController.js # 인증/권한
|
|
│ │ └── alertController.js # 알림 관리
|
|
│ ├── services/
|
|
│ │ ├── deviceService.js # 디바이스 비즈니스 로직
|
|
│ │ ├── powerService.js # 전력 분석 서비스
|
|
│ │ ├── networkService.js # 네트워크 분석 서비스
|
|
│ │ ├── statisticsService.js # 통계 계산 서비스
|
|
│ │ ├── alertService.js # 알림 처리 서비스
|
|
│ │ ├── cacheService.js # 캐시 관리 서비스
|
|
│ │ └── schedulerService.js # 스케줄 작업 서비스
|
|
│ ├── routes/
|
|
│ │ ├── index.js # 라우터 인덱스
|
|
│ │ ├── devices.js # 디바이스 라우터
|
|
│ │ ├── power.js # 전력 라우터
|
|
│ │ ├── network.js # 네트워크 라우터
|
|
│ │ ├── system.js # 시스템 라우터
|
|
│ │ ├── dashboard.js # 대시보드 라우터
|
|
│ │ ├── auth.js # 인증 라우터
|
|
│ │ └── alerts.js # 알림 라우터
|
|
│ ├── middleware/
|
|
│ │ ├── auth.js # 인증 미들웨어
|
|
│ │ ├── validation.js # 데이터 검증
|
|
│ │ ├── rateLimit.js # 요청 제한
|
|
│ │ ├── errorHandler.js # 에러 처리
|
|
│ │ ├── logger.js # 로깅 미들웨어
|
|
│ │ └── cors.js # CORS 설정
|
|
│ ├── utils/
|
|
│ │ ├── logger.js # 로깅 유틸
|
|
│ │ ├── validation.js # 검증 함수들
|
|
│ │ ├── dateUtils.js # 날짜 처리 유틸
|
|
│ │ ├── mathUtils.js # 수학 계산 유틸
|
|
│ │ ├── encryption.js # 암호화 유틸
|
|
│ │ └── responseUtils.js # 응답 포매팅
|
|
│ ├── collectors/
|
|
│ │ ├── powerCollector.js # 전력 데이터 수집
|
|
│ │ ├── networkCollector.js # 네트워크 데이터 수집
|
|
│ │ ├── systemCollector.js # 시스템 데이터 수집
|
|
│ │ └── serviceCollector.js # 서비스 상태 수집
|
|
│ └── app.js # Express 앱 설정
|
|
├── tests/
|
|
│ ├── unit/ # 단위 테스트
|
|
│ ├── integration/ # 통합 테스트
|
|
│ └── fixtures/ # 테스트 데이터
|
|
├── docs/
|
|
│ ├── api/ # API 문서
|
|
│ └── database/ # DB 스키마 문서
|
|
├── scripts/
|
|
│ ├── setup-db.sql # 초기 DB 설정
|
|
│ ├── seed-data.sql # 샘플 데이터
|
|
│ └── migrate.js # 마이그레이션 스크립트
|
|
├── docker-compose.yml # Docker 구성
|
|
├── package.json
|
|
├── .env.example
|
|
└── README.md
|
|
```
|
|
|
|
---
|
|
|
|
## 📦 Package.json 구성
|
|
|
|
### 주요 의존성
|
|
```json
|
|
{
|
|
"name": "home-management-api",
|
|
"version": "1.0.0",
|
|
"description": "홈 관리 시스템 백엔드 API",
|
|
"main": "src/app.js",
|
|
"scripts": {
|
|
"start": "node src/app.js",
|
|
"dev": "nodemon src/app.js",
|
|
"test": "jest",
|
|
"test:watch": "jest --watch",
|
|
"db:migrate": "node scripts/migrate.js",
|
|
"db:seed": "node scripts/seed.js",
|
|
"lint": "eslint src/",
|
|
"lint:fix": "eslint src/ --fix"
|
|
},
|
|
"dependencies": {
|
|
"express": "^4.18.2",
|
|
"mysql2": "^3.6.0",
|
|
"sequelize": "^6.32.1",
|
|
"redis": "^4.6.7",
|
|
"bcryptjs": "^2.4.3",
|
|
"jsonwebtoken": "^9.0.2",
|
|
"joi": "^17.9.2",
|
|
"helmet": "^7.0.0",
|
|
"cors": "^2.8.5",
|
|
"compression": "^1.7.4",
|
|
"express-rate-limit": "^6.8.1",
|
|
"winston": "^3.10.0",
|
|
"node-cron": "^3.0.2",
|
|
"dotenv": "^16.3.1",
|
|
"express-validator": "^7.0.1",
|
|
"moment": "^2.29.4"
|
|
},
|
|
"devDependencies": {
|
|
"nodemon": "^3.0.1",
|
|
"jest": "^29.6.1",
|
|
"supertest": "^6.3.3",
|
|
"eslint": "^8.45.0",
|
|
"prettier": "^3.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🐳 Docker Compose 구성
|
|
|
|
### docker-compose.yml
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
mariadb:
|
|
image: mariadb:11-jammy
|
|
container_name: home_mariadb
|
|
environment:
|
|
MYSQL_ROOT_PASSWORD: root_password
|
|
MYSQL_DATABASE: home_management
|
|
MYSQL_USER: homeuser
|
|
MYSQL_PASSWORD: home_password
|
|
ports:
|
|
- "3306:3306"
|
|
volumes:
|
|
- mariadb_data:/var/lib/mysql
|
|
- ./scripts/setup-db.sql:/docker-entrypoint-initdb.d/setup.sql
|
|
- ./config/mariadb.cnf:/etc/mysql/conf.d/custom.cnf
|
|
command: >
|
|
--character-set-server=utf8mb4
|
|
--collation-server=utf8mb4_unicode_ci
|
|
--innodb-buffer-pool-size=2G
|
|
--innodb-log-file-size=256M
|
|
--max-connections=200
|
|
--query-cache-size=256M
|
|
restart: unless-stopped
|
|
networks:
|
|
- home_network
|
|
|
|
phpmyadmin:
|
|
image: phpmyadmin/phpmyadmin:latest
|
|
container_name: home_phpmyadmin
|
|
environment:
|
|
PMA_HOST: mariadb
|
|
PMA_PORT: 3306
|
|
PMA_USER: homeuser
|
|
PMA_PASSWORD: home_password
|
|
UPLOAD_LIMIT: 2G
|
|
MEMORY_LIMIT: 512M
|
|
ports:
|
|
- "8080:80"
|
|
depends_on:
|
|
- mariadb
|
|
restart: unless-stopped
|
|
networks:
|
|
- home_network
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
container_name: home_redis
|
|
ports:
|
|
- "6379:6379"
|
|
volumes:
|
|
- redis_data:/data
|
|
- ./config/redis.conf:/usr/local/etc/redis/redis.conf
|
|
command: redis-server /usr/local/etc/redis/redis.conf
|
|
restart: unless-stopped
|
|
networks:
|
|
- home_network
|
|
|
|
api:
|
|
build: .
|
|
container_name: home_api
|
|
environment:
|
|
NODE_ENV: development
|
|
DB_HOST: mariadb
|
|
DB_PORT: 3306
|
|
DB_NAME: home_management
|
|
DB_USER: homeuser
|
|
DB_PASSWORD: home_password
|
|
REDIS_HOST: redis
|
|
REDIS_PORT: 6379
|
|
JWT_SECRET: your-jwt-secret-key
|
|
API_PORT: 3000
|
|
ports:
|
|
- "3000:3000"
|
|
volumes:
|
|
- ./src:/app/src
|
|
- ./logs:/app/logs
|
|
depends_on:
|
|
- mariadb
|
|
- redis
|
|
restart: unless-stopped
|
|
networks:
|
|
- home_network
|
|
|
|
volumes:
|
|
mariadb_data:
|
|
redis_data:
|
|
|
|
networks:
|
|
home_network:
|
|
driver: bridge
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 환경 설정
|
|
|
|
### .env.example
|
|
```env
|
|
# 서버 설정
|
|
NODE_ENV=development
|
|
API_PORT=3000
|
|
API_HOST=0.0.0.0
|
|
|
|
# 데이터베이스 설정
|
|
DB_HOST=localhost
|
|
DB_PORT=3306
|
|
DB_NAME=home_management
|
|
DB_USER=homeuser
|
|
DB_PASSWORD=home_password
|
|
DB_POOL_MIN=5
|
|
DB_POOL_MAX=20
|
|
DB_TIMEOUT=30000
|
|
|
|
# Redis 설정
|
|
REDIS_HOST=localhost
|
|
REDIS_PORT=6379
|
|
REDIS_PASSWORD=
|
|
REDIS_DB=0
|
|
REDIS_CACHE_TTL=3600
|
|
|
|
# JWT 설정
|
|
JWT_SECRET=your-super-secret-jwt-key
|
|
JWT_EXPIRES_IN=24h
|
|
JWT_REFRESH_EXPIRES_IN=7d
|
|
|
|
# 로깅 설정
|
|
LOG_LEVEL=info
|
|
LOG_FILE_PATH=./logs/app.log
|
|
LOG_MAX_SIZE=10m
|
|
LOG_MAX_FILES=5
|
|
|
|
# 알림 설정
|
|
SMTP_HOST=smtp.gmail.com
|
|
SMTP_PORT=587
|
|
SMTP_USER=your-email@gmail.com
|
|
SMTP_PASSWORD=your-app-password
|
|
EMAIL_FROM=Home Management System <noreply@home.local>
|
|
|
|
# 데이터 수집 설정
|
|
POWER_COLLECTION_INTERVAL=300000 # 5분
|
|
NETWORK_COLLECTION_INTERVAL=60000 # 1분
|
|
SYSTEM_COLLECTION_INTERVAL=30000 # 30초
|
|
|
|
# API 제한 설정
|
|
RATE_LIMIT_WINDOW=900000 # 15분
|
|
RATE_LIMIT_MAX_REQUESTS=100
|
|
|
|
# 보안 설정
|
|
BCRYPT_ROUNDS=12
|
|
CORS_ORIGIN=http://localhost:3001,https://yourdomain.com
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 MariaDB 최적화 설정
|
|
|
|
### config/mariadb.cnf
|
|
```ini
|
|
[mariadb]
|
|
# 기본 설정
|
|
character-set-server = utf8mb4
|
|
collation-server = utf8mb4_unicode_ci
|
|
init-connect = 'SET NAMES utf8mb4'
|
|
|
|
# 메모리 최적화 (Mac Mini M4 Pro 64GB 기준)
|
|
innodb_buffer_pool_size = 4G
|
|
innodb_log_buffer_size = 64M
|
|
innodb_log_file_size = 512M
|
|
key_buffer_size = 256M
|
|
sort_buffer_size = 4M
|
|
read_buffer_size = 2M
|
|
read_rnd_buffer_size = 8M
|
|
thread_cache_size = 50
|
|
table_open_cache = 4000
|
|
|
|
# 연결 설정
|
|
max_connections = 200
|
|
max_user_connections = 180
|
|
wait_timeout = 600
|
|
interactive_timeout = 600
|
|
|
|
# 쿼리 캐시
|
|
query_cache_type = 1
|
|
query_cache_size = 256M
|
|
query_cache_limit = 2M
|
|
|
|
# InnoDB 최적화
|
|
innodb_flush_log_at_trx_commit = 2
|
|
innodb_flush_method = O_DIRECT
|
|
innodb_file_per_table = 1
|
|
innodb_io_capacity = 2000
|
|
innodb_io_capacity_max = 4000
|
|
innodb_read_io_threads = 4
|
|
innodb_write_io_threads = 4
|
|
|
|
# 시계열 데이터 최적화
|
|
innodb_compression_default = ON
|
|
innodb_page_compression = ON
|
|
innodb_adaptive_hash_index = ON
|
|
|
|
# 로깅
|
|
general_log = OFF
|
|
slow_query_log = ON
|
|
slow_query_log_file = /var/log/mysql/slow.log
|
|
long_query_time = 2
|
|
|
|
# 바이너리 로그 (백업/복제용)
|
|
log_bin = mysql-bin
|
|
binlog_format = ROW
|
|
expire_logs_days = 7
|
|
max_binlog_size = 100M
|
|
```
|
|
|
|
---
|
|
|
|
## 🔍 초기 데이터 및 인덱스
|
|
|
|
### scripts/setup-db.sql
|
|
```sql
|
|
-- 기본 관리자 사용자 생성
|
|
INSERT INTO users (username, email, password_hash, full_name, role) VALUES
|
|
('admin', 'admin@home.local', '$2b$12$encrypted_password_hash', '시스템 관리자', 'admin'),
|
|
('family', 'family@home.local', '$2b$12$encrypted_password_hash', '가족 사용자', 'family');
|
|
|
|
-- 기본 디바이스 등록
|
|
INSERT INTO devices (device_id, name, device_type, location, monitoring_enabled) VALUES
|
|
('mac_mini_m4', 'Mac Mini M4 Pro', 'server', '서재', TRUE),
|
|
('ds1525plus', 'Synology DS1525+', 'nas', '서재', TRUE),
|
|
('rt6600ax', 'Synology RT6600ax', 'router', '거실', TRUE);
|
|
|
|
-- 기본 알림 규칙
|
|
INSERT INTO alert_rules (name, description, condition_type, target_table, target_field, operator, threshold_value, severity, notification_methods, created_by) VALUES
|
|
('높은 CPU 사용률', 'CPU 사용률이 80% 이상일 때 알림', 'threshold', 'system_resources', 'cpu_percent', '>', 80.0, 'warning', '["email"]', 1),
|
|
('높은 전력 소비', '전력 소비가 평소보다 50% 이상 증가했을 때', 'change', 'power_consumption', 'watts', '>', 50.0, 'warning', '["email"]', 1),
|
|
('서비스 다운', '서비스가 오프라인 상태일 때', 'threshold', 'service_status', 'status', '=', 'offline', 'critical', '["email", "push"]', 1);
|
|
|
|
-- 파티션 관리 프로시저
|
|
DELIMITER //
|
|
CREATE PROCEDURE CreateMonthlyPartitions()
|
|
BEGIN
|
|
DECLARE partition_date DATE;
|
|
DECLARE partition_name VARCHAR(20);
|
|
DECLARE partition_value BIGINT;
|
|
|
|
SET partition_date = DATE_ADD(NOW(), INTERVAL 3 MONTH);
|
|
SET partition_name = CONCAT('p_', DATE_FORMAT(partition_date, '%Y_%m'));
|
|
SET partition_value = UNIX_TIMESTAMP(DATE_ADD(partition_date, INTERVAL 1 MONTH));
|
|
|
|
SET @sql = CONCAT(
|
|
'ALTER TABLE power_consumption ADD PARTITION (',
|
|
'PARTITION ', partition_name,
|
|
' VALUES LESS THAN (', partition_value, '))'
|
|
);
|
|
|
|
PREPARE stmt FROM @sql;
|
|
EXECUTE stmt;
|
|
DEALLOCATE PREPARE stmt;
|
|
END //
|
|
DELIMITER ;
|
|
|
|
-- 월별 파티션 자동 생성 이벤트
|
|
CREATE EVENT monthly_partition_maintenance
|
|
ON SCHEDULE EVERY 1 MONTH
|
|
STARTS CURRENT_TIMESTAMP
|
|
DO CALL CreateMonthlyPartitions();
|
|
|
|
-- 성능 모니터링용 뷰
|
|
CREATE VIEW device_power_summary AS
|
|
SELECT
|
|
d.device_id,
|
|
d.name,
|
|
d.device_type,
|
|
AVG(pc.watts) as avg_watts,
|
|
MAX(pc.watts) as max_watts,
|
|
COUNT(*) as reading_count,
|
|
MAX(pc.timestamp) as last_reading
|
|
FROM devices d
|
|
LEFT JOIN power_consumption pc ON d.device_id = pc.device_id
|
|
WHERE d.monitoring_enabled = TRUE
|
|
GROUP BY d.device_id, d.name, d.device_type;
|
|
|
|
CREATE VIEW system_health_summary AS
|
|
SELECT
|
|
server_name,
|
|
AVG(cpu_percent) as avg_cpu,
|
|
AVG(memory_used_gb / memory_total_gb * 100) as avg_memory_pct,
|
|
AVG(disk_used_gb / disk_total_gb * 100) as avg_disk_pct,
|
|
MAX(timestamp) as last_update
|
|
FROM system_resources
|
|
WHERE timestamp >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
|
|
GROUP BY server_name;
|
|
```
|
|
|
|
---
|
|
|
|
## 🔄 CI/CD 파이프라인 구성
|
|
|
|
### 개발 환경 구성
|
|
```
|
|
MacBook Pro M3 Pro (개발/빌드)
|
|
Mac Mini M4 Pro (개발/빌드)
|
|
↓ git push
|
|
Gitea Server (git.hyungi.net - DS1525+ Container)
|
|
↓ webhook
|
|
개발 머신 (MacBook Pro / Mac Mini)
|
|
↓ docker build & test
|
|
Synology DS1525+ (프로덕션 배포)
|
|
```
|
|
|
|
#### 개발 환경 특징
|
|
- **MacBook Pro M3 Pro**: 이동성이 필요한 개발 작업
|
|
- **Mac Mini M4 Pro**: 고정된 개발 환경, 장시간 빌드/테스트
|
|
- **공통 환경**: 동일한 ARM64 아키텍처로 일관된 빌드 환경
|
|
- **유연한 작업**: 두 머신 모두에서 완전한 개발 사이클 가능
|
|
|
|
### CI/CD 워크플로우
|
|
|
|
#### 1. Gitea Actions 설정 (.gitea/workflows/deploy.yml)
|
|
```yaml
|
|
name: Home Management CI/CD
|
|
|
|
on:
|
|
push:
|
|
branches: [ main, develop ]
|
|
pull_request:
|
|
branches: [ main ]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: self-hosted
|
|
container:
|
|
image: node:18-alpine
|
|
options: --network host
|
|
|
|
services:
|
|
mariadb:
|
|
image: mariadb:11-jammy
|
|
env:
|
|
MARIADB_ROOT_PASSWORD: test_password
|
|
MARIADB_DATABASE: home_management_test
|
|
MARIADB_USER: testuser
|
|
MARIADB_PASSWORD: test_password
|
|
ports:
|
|
- 3307:3306
|
|
options: >-
|
|
--health-cmd="mariadb-admin ping -h localhost"
|
|
--health-interval=10s
|
|
--health-timeout=5s
|
|
--health-retries=5
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Cache Node modules
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.npm
|
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-node-
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Wait for MariaDB
|
|
run: |
|
|
for i in {1..30}; do
|
|
if nc -z localhost 3307; then break; fi
|
|
echo "Waiting for MariaDB... ($i/30)"
|
|
sleep 2
|
|
done
|
|
|
|
- name: Run database migrations
|
|
run: npm run db:migrate
|
|
env:
|
|
NODE_ENV: test
|
|
DB_HOST: localhost
|
|
DB_PORT: 3307
|
|
DB_NAME: home_management_test
|
|
DB_USER: testuser
|
|
DB_PASSWORD: test_password
|
|
|
|
- name: Run unit tests
|
|
run: npm run test:unit
|
|
env:
|
|
NODE_ENV: test
|
|
DB_HOST: localhost
|
|
DB_PORT: 3307
|
|
|
|
- name: Run integration tests
|
|
run: npm run test:integration
|
|
env:
|
|
NODE_ENV: test
|
|
DB_HOST: localhost
|
|
DB_PORT: 3307
|
|
|
|
- name: Generate coverage report
|
|
run: npm run test:coverage
|
|
|
|
- name: Lint check
|
|
run: npm run lint
|
|
|
|
- name: Security audit
|
|
run: npm audit --audit-level high
|
|
|
|
build:
|
|
needs: test
|
|
runs-on: self-hosted
|
|
if: gitea.ref == 'refs/heads/main'
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set build info
|
|
run: |
|
|
echo "BUILD_TIME=$(date -Iseconds)" >> $GITHUB_ENV
|
|
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
|
echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
|
|
|
|
- name: Build Docker image
|
|
run: |
|
|
docker build \
|
|
--build-arg BUILD_TIME=$BUILD_TIME \
|
|
--build-arg COMMIT_HASH=$COMMIT_HASH \
|
|
--build-arg BRANCH_NAME=$BRANCH_NAME \
|
|
-t home-management-api:$COMMIT_HASH \
|
|
-t home-management-api:latest .
|
|
|
|
- name: Run container security scan
|
|
run: |
|
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
|
aquasec/trivy image home-management-api:latest
|
|
|
|
- name: Save Docker image
|
|
run: |
|
|
docker save home-management-api:latest | gzip > home-management-api.tar.gz
|
|
|
|
- name: Record deployment start
|
|
run: |
|
|
curl -X POST http://localhost:3000/api/deployments/start \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"deployment_id": "${{ gitea.run_id }}",
|
|
"commit_hash": "${{ env.COMMIT_HASH }}",
|
|
"branch": "${{ env.BRANCH_NAME }}",
|
|
"stage": "build"
|
|
}'
|
|
|
|
- name: Deploy to NAS
|
|
run: |
|
|
scp -o StrictHostKeyChecking=no \
|
|
home-management-api.tar.gz \
|
|
admin@ds1525plus:/volume1/docker/images/
|
|
|
|
ssh -o StrictHostKeyChecking=no admin@ds1525plus \
|
|
"cd /volume1/docker && ./deploy-api.sh $COMMIT_HASH ${{ gitea.run_id }}"
|
|
|
|
- name: Record deployment result
|
|
if: always()
|
|
run: |
|
|
STATUS=${{ job.status == 'success' && 'success' || 'failed' }}
|
|
curl -X POST http://localhost:3000/api/deployments/complete \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"deployment_id": "${{ gitea.run_id }}",
|
|
"status": "'$STATUS'",
|
|
"stage": "deploy"
|
|
}'
|
|
```
|
|
|
|
#### 2. Gitea Webhook 설정
|
|
```bash
|
|
# Gitea 서버에서 webhook 설정
|
|
# git.hyungi.net > myhome-server > Settings > Webhooks > Add Webhook
|
|
|
|
Payload URL: http://developer-machine:9000/webhook # MacBook Pro 또는 Mac Mini IP
|
|
Content Type: application/json
|
|
Secret: your-webhook-secret
|
|
Events: Push events, Pull request events
|
|
|
|
# 개발 머신에서 webhook 리스너 설정 (webhook.js)
|
|
const express = require('express');
|
|
const crypto = require('crypto');
|
|
const { execSync } = require('child_process');
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
|
|
const GITEA_ACTIONS_PATH = '/Users/admin/gitea-actions';
|
|
|
|
function verifySignature(payload, signature) {
|
|
const computedSignature = crypto
|
|
.createHmac('sha256', WEBHOOK_SECRET)
|
|
.update(payload)
|
|
.digest('hex');
|
|
return crypto.timingSafeEqual(
|
|
Buffer.from(`sha256=${computedSignature}`),
|
|
Buffer.from(signature)
|
|
);
|
|
}
|
|
|
|
app.post('/webhook', (req, res) => {
|
|
const signature = req.headers['x-gitea-signature'];
|
|
const payload = JSON.stringify(req.body);
|
|
|
|
if (!verifySignature(payload, signature)) {
|
|
return res.status(401).send('Unauthorized');
|
|
}
|
|
|
|
const { repository, ref, commits } = req.body;
|
|
|
|
if (ref === 'refs/heads/main' || ref === 'refs/heads/develop') {
|
|
console.log(`Received push to ${ref} in ${repository.full_name}`);
|
|
|
|
try {
|
|
// Gitea Actions Runner 트리거
|
|
execSync(`cd ${GITEA_ACTIONS_PATH} && ./act_runner exec`,
|
|
{ stdio: 'inherit' });
|
|
res.status(200).send('Workflow triggered');
|
|
} catch (error) {
|
|
console.error('Failed to trigger workflow:', error);
|
|
res.status(500).send('Workflow trigger failed');
|
|
}
|
|
} else {
|
|
res.status(200).send('No action needed');
|
|
}
|
|
});
|
|
|
|
app.listen(9000, () => {
|
|
console.log('Webhook server listening on port 9000');
|
|
});
|
|
```
|
|
|
|
#### 2. DS1525+ 배포 스크립트 (/volume1/docker/deploy-api.sh)
|
|
```bash
|
|
#!/bin/bash
|
|
|
|
# 배포 설정
|
|
IMAGE_NAME="home-management-api"
|
|
CONTAINER_NAME="home-api"
|
|
BACKUP_DIR="/volume1/backups/deployments"
|
|
LOG_FILE="/volume1/logs/deploy.log"
|
|
|
|
echo "$(date): Starting deployment..." >> $LOG_FILE
|
|
|
|
# 1. 기존 컨테이너 백업
|
|
if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then
|
|
echo "Creating backup of current deployment..." >> $LOG_FILE
|
|
docker commit $CONTAINER_NAME $IMAGE_NAME:backup-$(date +%Y%m%d-%H%M%S)
|
|
docker stop $CONTAINER_NAME
|
|
docker rm $CONTAINER_NAME
|
|
fi
|
|
|
|
# 2. 새 이미지 로드
|
|
echo "Loading new Docker image..." >> $LOG_FILE
|
|
gunzip -c /volume1/docker/images/home-management-api.tar.gz | docker load
|
|
|
|
# 3. 데이터베이스 마이그레이션 체크
|
|
echo "Checking database migrations..." >> $LOG_FILE
|
|
docker run --rm --network=host -e NODE_ENV=production $IMAGE_NAME:latest npm run db:migrate:check
|
|
|
|
# 4. 새 컨테이너 시작
|
|
echo "Starting new container..." >> $LOG_FILE
|
|
docker run -d \
|
|
--name $CONTAINER_NAME \
|
|
--network=host \
|
|
--restart=unless-stopped \
|
|
-v /volume1/docker/config/api.env:/app/.env \
|
|
-v /volume1/logs/api:/app/logs \
|
|
-v /volume1/data/uploads:/app/uploads \
|
|
$IMAGE_NAME:latest
|
|
|
|
# 5. 헬스체크
|
|
echo "Performing health check..." >> $LOG_FILE
|
|
sleep 10
|
|
for i in {1..30}; do
|
|
if curl -f http://localhost:3000/health > /dev/null 2>&1; then
|
|
echo "$(date): Deployment successful!" >> $LOG_FILE
|
|
# 이전 백업 이미지 정리 (7일 이상 된 것)
|
|
docker images | grep "$IMAGE_NAME:backup" | awk '{print $2}' | tail -n +8 | xargs -I {} docker rmi $IMAGE_NAME:{}
|
|
exit 0
|
|
fi
|
|
echo "Waiting for service to start... ($i/30)" >> $LOG_FILE
|
|
sleep 2
|
|
done
|
|
|
|
echo "$(date): Deployment failed! Rolling back..." >> $LOG_FILE
|
|
# 롤백 로직
|
|
LATEST_BACKUP=$(docker images | grep "$IMAGE_NAME:backup" | head -1 | awk '{print $2}')
|
|
if [ ! -z "$LATEST_BACKUP" ]; then
|
|
docker stop $CONTAINER_NAME 2>/dev/null
|
|
docker rm $CONTAINER_NAME 2>/dev/null
|
|
docker run -d --name $CONTAINER_NAME --network=host --restart=unless-stopped $IMAGE_NAME:backup-$LATEST_BACKUP
|
|
fi
|
|
exit 1
|
|
```
|
|
|
|
#### 3. Gitea Actions Runner 설정 (개발 머신)
|
|
```bash
|
|
# Gitea Actions Runner 설치 및 설정 (MacBook Pro / Mac Mini 공통)
|
|
cd /Users/$(whoami)
|
|
wget https://gitea.com/gitea/act_runner/releases/download/v0.2.6/act_runner-0.2.6-darwin-arm64
|
|
chmod +x act_runner-0.2.6-darwin-arm64
|
|
sudo mv act_runner-0.2.6-darwin-arm64 /usr/local/bin/act_runner
|
|
|
|
# Runner 등록 (Gitea에서 토큰 생성 후)
|
|
act_runner register \
|
|
--instance https://git.hyungi.net \
|
|
--token YOUR_RUNNER_TOKEN \
|
|
--name $(hostname)-runner \
|
|
--labels self-hosted,macOS,ARM64
|
|
|
|
# 설정 파일 생성 (.runner 파일이 생성됨)
|
|
# 데몬으로 실행하기 위한 LaunchDaemon 설정
|
|
sudo tee /Library/LaunchDaemons/com.gitea.act_runner.plist << EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>com.gitea.act_runner</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/usr/local/bin/act_runner</string>
|
|
<string>daemon</string>
|
|
<string>--config</string>
|
|
<string>/Users/$(whoami)/.runner</string>
|
|
</array>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
<key>StandardOutPath</key>
|
|
<string>/usr/local/var/log/act_runner.log</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>/usr/local/var/log/act_runner.error.log</string>
|
|
<key>WorkingDirectory</key>
|
|
<string>/Users/$(whoami)</string>
|
|
<key>UserName</key>
|
|
<string>$(whoami)</string>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
|
|
# 서비스 시작
|
|
sudo launchctl load /Library/LaunchDaemons/com.gitea.act_runner.plist
|
|
sudo launchctl start com.gitea.act_runner
|
|
```
|
|
|
|
#### 4. DS1525+ 배포 스크립트 업데이트
|
|
```bash
|
|
#!/bin/bash
|
|
# /volume1/docker/deploy-api.sh
|
|
|
|
# 배포 설정
|
|
IMAGE_NAME="home-management-api"
|
|
CONTAINER_NAME="home-api"
|
|
BACKUP_DIR="/volume1/backups/deployments"
|
|
LOG_FILE="/volume1/logs/deploy.log"
|
|
COMMIT_HASH=$1
|
|
DEPLOYMENT_ID=$2
|
|
|
|
echo "$(date): Starting deployment $DEPLOYMENT_ID (commit: $COMMIT_HASH)..." >> $LOG_FILE
|
|
|
|
# 배포 상태를 API에 보고
|
|
function report_status() {
|
|
local status=$1
|
|
local message=$2
|
|
curl -s -X POST http://localhost:3000/api/deployments/update \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"deployment_id\": \"$DEPLOYMENT_ID\",
|
|
\"status\": \"$status\",
|
|
\"message\": \"$message\",
|
|
\"timestamp\": \"$(date -Iseconds)\"
|
|
}" || true
|
|
}
|
|
|
|
# 1. 현재 컨테이너 상태 확인 및 백업
|
|
if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then
|
|
echo "Creating backup of current deployment..." >> $LOG_FILE
|
|
BACKUP_TAG="backup-$(date +%Y%m%d-%H%M%S)"
|
|
docker commit $CONTAINER_NAME $IMAGE_NAME:$BACKUP_TAG
|
|
report_status "backing_up" "Creating backup: $BACKUP_TAG"
|
|
|
|
# Graceful shutdown
|
|
docker exec $CONTAINER_NAME npm run graceful-shutdown 2>/dev/null || true
|
|
sleep 5
|
|
docker stop $CONTAINER_NAME
|
|
docker rm $CONTAINER_NAME
|
|
fi
|
|
|
|
# 2. 새 이미지 로드
|
|
echo "Loading new Docker image..." >> $LOG_FILE
|
|
report_status "loading_image" "Loading Docker image"
|
|
if ! gunzip -c /volume1/docker/images/home-management-api.tar.gz | docker load; then
|
|
echo "Failed to load Docker image" >> $LOG_FILE
|
|
report_status "failed" "Failed to load Docker image"
|
|
exit 1
|
|
fi
|
|
|
|
# 3. 데이터베이스 마이그레이션 체크
|
|
echo "Checking database migrations..." >> $LOG_FILE
|
|
report_status "migrating" "Running database migrations"
|
|
if ! docker run --rm --network=host \
|
|
-v /volume1/docker/config/api.env:/app/.env \
|
|
$IMAGE_NAME:latest npm run db:migrate; then
|
|
echo "Database migration failed" >> $LOG_FILE
|
|
report_status "failed" "Database migration failed"
|
|
exit 1
|
|
fi
|
|
|
|
# 4. 새 컨테이너 시작
|
|
echo "Starting new container..." >> $LOG_FILE
|
|
report_status "starting" "Starting new container"
|
|
docker run -d \
|
|
--name $CONTAINER_NAME \
|
|
--network=host \
|
|
--restart=unless-stopped \
|
|
-v /volume1/docker/config/api.env:/app/.env \
|
|
-v /volume1/logs/api:/app/logs \
|
|
-v /volume1/data/uploads:/app/uploads \
|
|
-e COMMIT_HASH=$COMMIT_HASH \
|
|
-e DEPLOYMENT_ID=$DEPLOYMENT_ID \
|
|
$IMAGE_NAME:latest
|
|
|
|
if [ $? -ne 0 ]; then
|
|
echo "Failed to start container" >> $LOG_FILE
|
|
report_status "failed" "Failed to start container"
|
|
exit 1
|
|
fi
|
|
|
|
# 5. 헬스체크 및 성능 테스트
|
|
echo "Performing health check..." >> $LOG_FILE
|
|
report_status "health_check" "Performing health check"
|
|
sleep 15
|
|
|
|
for i in {1..30}; do
|
|
if curl -f -s http://localhost:3000/health > /dev/null 2>&1; then
|
|
# 기본 기능 테스트
|
|
if curl -f -s http://localhost:3000/api/devices > /dev/null 2>&1; then
|
|
echo "$(date): Deployment successful!" >> $LOG_FILE
|
|
report_status "success" "Deployment completed successfully"
|
|
|
|
# 성능 벤치마크 실행
|
|
/volume1/docker/scripts/performance-test.sh $DEPLOYMENT_ID &
|
|
|
|
# 오래된 백업 이미지 정리 (7개 이상)
|
|
docker images | grep "$IMAGE_NAME:backup" | tail -n +8 | \
|
|
awk '{print $1":"$2}' | xargs -r docker rmi
|
|
|
|
exit 0
|
|
fi
|
|
fi
|
|
echo "Waiting for service to start... ($i/30)" >> $LOG_FILE
|
|
sleep 2
|
|
done
|
|
|
|
# 6. 실패시 롤백
|
|
echo "$(date): Deployment failed! Rolling back..." >> $LOG_FILE
|
|
report_status "rolling_back" "Deployment failed, rolling back"
|
|
|
|
docker stop $CONTAINER_NAME 2>/dev/null || true
|
|
docker rm $CONTAINER_NAME 2>/dev/null || true
|
|
|
|
# 최신 백업으로 롤백
|
|
LATEST_BACKUP=$(docker images | grep "$IMAGE_NAME:backup" | head -1 | awk '{print $2}')
|
|
if [ ! -z "$LATEST_BACKUP" ]; then
|
|
docker run -d \
|
|
--name $CONTAINER_NAME \
|
|
--network=host \
|
|
--restart=unless-stopped \
|
|
-v /volume1/docker/config/api.env:/app/.env \
|
|
-v /volume1/logs/api:/app/logs \
|
|
-v /volume1/data/uploads:/app/uploads \
|
|
$IMAGE_NAME:backup-$LATEST_BACKUP
|
|
|
|
report_status "rolled_back" "Rolled back to: backup-$LATEST_BACKUP"
|
|
else
|
|
report_status "failed" "Rollback failed - no backup available"
|
|
fi
|
|
|
|
exit 1
|
|
```
|
|
|
|
#### 4. Docker 컨테이너 모니터링 (DS1525+)
|
|
```bash
|
|
# /volume1/docker/monitor-containers.sh
|
|
#!/bin/bash
|
|
|
|
SERVICES=("home-api" "home-mariadb" "home-redis" "home-phpmyadmin")
|
|
WEBHOOK_URL="http://developer-machine:3000/api/alerts/webhook"
|
|
|
|
for service in "${SERVICES[@]}"; do
|
|
if ! docker ps --format "table {{.Names}}" | grep -q "^${service}$"; then
|
|
# 서비스 다운 알림
|
|
curl -X POST $WEBHOOK_URL \
|
|
-H "Content-Type: application/json" \
|
|
-d "{
|
|
\"service\": \"$service\",
|
|
\"status\": \"down\",
|
|
\"timestamp\": \"$(date -Iseconds)\",
|
|
\"server\": \"ds1525plus\"
|
|
}"
|
|
|
|
# 자동 재시작 시도
|
|
echo "$(date): Attempting to restart $service..." >> /volume1/logs/monitor.log
|
|
docker start $service
|
|
fi
|
|
done
|
|
```
|
|
|
|
### Gitea 연동 최적화
|
|
|
|
#### 1. Gitea 서버 설정 (DS1525+ 컨테이너)
|
|
```yaml
|
|
# docker-compose.yml에 추가
|
|
gitea:
|
|
image: gitea/gitea:1.21
|
|
container_name: home_gitea
|
|
environment:
|
|
- USER_UID=1000
|
|
- USER_GID=1000
|
|
- GITEA__database__DB_TYPE=mysql
|
|
- GITEA__database__HOST=mariadb:3306
|
|
- GITEA__database__NAME=gitea
|
|
- GITEA__database__USER=gitea
|
|
- GITEA__database__PASSWD=gitea_password
|
|
- GITEA__server__DOMAIN=git.hyungi.net
|
|
- GITEA__server__HTTP_PORT=3000
|
|
- GITEA__server__ROOT_URL=https://git.hyungi.net
|
|
- GITEA__actions__ENABLED=true
|
|
- GITEA__actions__DEFAULT_ACTIONS_URL=https://github.com
|
|
restart: unless-stopped
|
|
networks:
|
|
- home_network
|
|
volumes:
|
|
- gitea_data:/data
|
|
- /etc/timezone:/etc/timezone:ro
|
|
- /etc/localtime:/etc/localtime:ro
|
|
ports:
|
|
- "3000:3000"
|
|
- "222:22"
|
|
depends_on:
|
|
- mariadb
|
|
```
|
|
|
|
#### 2. 배포 상태 추적 API
|
|
```javascript
|
|
// routes/deployments.js
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const DeploymentService = require('../services/deploymentService');
|
|
|
|
// 배포 시작 기록
|
|
router.post('/start', async (req, res) => {
|
|
try {
|
|
const { deployment_id, commit_hash, branch, stage } = req.body;
|
|
|
|
const logId = await DeploymentService.recordDeployment({
|
|
id: deployment_id,
|
|
stage: stage || 'build',
|
|
commit: commit_hash,
|
|
branch: branch,
|
|
user: req.user?.username || 'system'
|
|
});
|
|
|
|
res.json({ log_id: logId, status: 'recorded' });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 배포 상태 업데이트
|
|
router.post('/update', async (req, res) => {
|
|
try {
|
|
const { deployment_id, status, message } = req.body;
|
|
|
|
await DeploymentService.updateDeploymentStatus(
|
|
deployment_id, status, message
|
|
);
|
|
|
|
// 실시간 알림 (WebSocket)
|
|
req.app.get('io').emit('deployment_update', {
|
|
deployment_id,
|
|
status,
|
|
message,
|
|
timestamp: new Date()
|
|
});
|
|
|
|
res.json({ status: 'updated' });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 배포 완료 처리
|
|
router.post('/complete', async (req, res) => {
|
|
try {
|
|
const { deployment_id, status, stage } = req.body;
|
|
|
|
await DeploymentService.completeDeployment(
|
|
deployment_id, status, stage
|
|
);
|
|
|
|
if (status === 'success') {
|
|
// 성공시 현재 버전 업데이트
|
|
await DeploymentService.updateActiveVersion(deployment_id);
|
|
}
|
|
|
|
res.json({ status: 'completed' });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// 배포 히스토리 조회
|
|
router.get('/history', async (req, res) => {
|
|
try {
|
|
const { limit = 20, offset = 0 } = req.query;
|
|
const history = await DeploymentService.getDeploymentHistory(
|
|
parseInt(limit), parseInt(offset)
|
|
);
|
|
res.json(history);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|
|
```
|
|
|
|
#### 3. 브랜치별 배포 전략
|
|
```yaml
|
|
# .gitea/workflows/deploy.yml 추가 구성
|
|
on:
|
|
push:
|
|
branches: [ main, develop, feature/* ]
|
|
pull_request:
|
|
branches: [ main, develop ]
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: self-hosted
|
|
# 모든 브랜치에서 테스트 실행
|
|
|
|
build-develop:
|
|
needs: test
|
|
runs-on: self-hosted
|
|
if: gitea.ref == 'refs/heads/develop'
|
|
steps:
|
|
- name: Deploy to Staging
|
|
run: |
|
|
docker build -t home-management-api:develop .
|
|
# 스테이징 환경 배포 (포트 3001)
|
|
ssh admin@ds1525plus \
|
|
"cd /volume1/docker && ./deploy-staging.sh develop"
|
|
|
|
build-main:
|
|
needs: test
|
|
runs-on: self-hosted
|
|
if: gitea.ref == 'refs/heads/main'
|
|
steps:
|
|
- name: Deploy to Production
|
|
run: |
|
|
docker build -t home-management-api:latest .
|
|
# 프로덕션 배포 (포트 3000)
|
|
ssh admin@ds1525plus \
|
|
"cd /volume1/docker && ./deploy-api.sh latest ${{ gitea.run_id }}"
|
|
|
|
feature-test:
|
|
needs: test
|
|
runs-on: self-hosted
|
|
if: startsWith(gitea.ref, 'refs/heads/feature/')
|
|
steps:
|
|
- name: Feature Branch Tests
|
|
run: |
|
|
# 추가 테스트만 수행, 배포 안함
|
|
npm run test:e2e
|
|
npm run test:security
|
|
```
|
|
|
|
#### 4. Gitea Actions 캐시 최적화
|
|
```yaml
|
|
# 캐시 설정으로 빌드 시간 단축
|
|
steps:
|
|
- name: Cache Node modules
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: ~/.npm
|
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-node-
|
|
|
|
- name: Cache Docker layers
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: /tmp/.buildx-cache
|
|
key: ${{ runner.os }}-buildx-${{ gitea.sha }}
|
|
restore-keys: |
|
|
${{ runner.os }}-buildx-
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 환경 설정
|
|
|
|
#### 1. 개발 환경 (MacBook Pro / Mac Mini)
|
|
```env
|
|
# .env.development
|
|
NODE_ENV=development
|
|
DB_HOST=ds1525plus # NAS의 개발용 DB 연결
|
|
DB_PORT=3306
|
|
API_PORT=3000
|
|
CORS_ORIGIN=http://localhost:3001
|
|
LOG_LEVEL=debug
|
|
REDIS_HOST=ds1525plus
|
|
```
|
|
|
|
#### 2. 로컬 개발 환경 (개발 머신)
|
|
```env
|
|
# .env.local
|
|
NODE_ENV=development
|
|
DB_HOST=localhost # 로컬 Docker DB
|
|
DB_PORT=3306
|
|
API_PORT=3000
|
|
CORS_ORIGIN=http://localhost:3001
|
|
LOG_LEVEL=debug
|
|
REDIS_HOST=localhost
|
|
```
|
|
|
|
#### 3. 테스트 환경 (Mac Mini CI)
|
|
```env
|
|
# .env.test
|
|
NODE_ENV=test
|
|
DB_HOST=localhost
|
|
DB_PORT=3307
|
|
DB_NAME=home_management_test
|
|
API_PORT=3001
|
|
REDIS_HOST=localhost
|
|
REDIS_DB=1
|
|
```
|
|
|
|
#### 4. 프로덕션 환경 (DS1525+)
|
|
```env
|
|
# /volume1/docker/config/api.env
|
|
NODE_ENV=production
|
|
DB_HOST=localhost
|
|
DB_PORT=3306
|
|
DB_NAME=home_management
|
|
API_PORT=3000
|
|
CORS_ORIGIN=https://home.yourdomain.com
|
|
LOG_LEVEL=info
|
|
REDIS_HOST=localhost
|
|
|
|
# 보안 설정
|
|
JWT_SECRET=production-jwt-secret-key
|
|
BCRYPT_ROUNDS=14
|
|
|
|
# 프로덕션 최적화
|
|
NODE_OPTIONS=--max-old-space-size=2048
|
|
PM2_INSTANCES=2
|
|
```
|
|
|
|
### 데이터베이스 마이그레이션 관리
|
|
|
|
#### 1. 마이그레이션 스크립트 구조
|
|
```
|
|
migrations/
|
|
├── 001_initial_schema.sql
|
|
├── 002_add_alert_system.sql
|
|
├── 003_add_user_preferences.sql
|
|
├── 004_optimize_indexes.sql
|
|
└── 005_add_ci_cd_logs.sql
|
|
```
|
|
|
|
#### 2. 마이그레이션 실행기 (scripts/migrate.js)
|
|
```javascript
|
|
// 500자 이하로 간단하게 구성
|
|
const mysql = require('mysql2/promise');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
|
|
async function runMigrations() {
|
|
const connection = await mysql.createConnection({
|
|
host: process.env.DB_HOST,
|
|
user: process.env.DB_USER,
|
|
password: process.env.DB_PASSWORD,
|
|
database: process.env.DB_NAME,
|
|
multipleStatements: true
|
|
});
|
|
|
|
// 마이그레이션 테이블 생성
|
|
await connection.execute(`
|
|
CREATE TABLE IF NOT EXISTS migrations (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
filename VARCHAR(255) UNIQUE NOT NULL,
|
|
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`);
|
|
|
|
const migrationsDir = path.join(__dirname, '../migrations');
|
|
const files = await fs.readdir(migrationsDir);
|
|
|
|
for (const file of files.sort()) {
|
|
if (!file.endsWith('.sql')) continue;
|
|
|
|
const [rows] = await connection.execute(
|
|
'SELECT id FROM migrations WHERE filename = ?', [file]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
console.log(`Running migration: ${file}`);
|
|
const sql = await fs.readFile(path.join(migrationsDir, file), 'utf8');
|
|
await connection.execute(sql);
|
|
await connection.execute(
|
|
'INSERT INTO migrations (filename) VALUES (?)', [file]
|
|
);
|
|
}
|
|
}
|
|
|
|
await connection.end();
|
|
}
|
|
|
|
module.exports = { runMigrations };
|
|
```
|
|
|
|
### 로그 및 모니터링 통합
|
|
|
|
#### 1. 배포 로그 수집 테이블
|
|
```sql
|
|
-- 005_add_ci_cd_logs.sql
|
|
CREATE TABLE deployment_logs (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
deployment_id VARCHAR(100) NOT NULL,
|
|
stage ENUM('test', 'build', 'deploy', 'rollback') NOT NULL,
|
|
status ENUM('started', 'success', 'failed') NOT NULL,
|
|
commit_hash VARCHAR(40),
|
|
branch_name VARCHAR(100),
|
|
deployed_by VARCHAR(100),
|
|
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
end_time TIMESTAMP NULL,
|
|
duration_seconds INT GENERATED ALWAYS AS (
|
|
CASE WHEN end_time IS NOT NULL
|
|
THEN TIMESTAMPDIFF(SECOND, start_time, end_time)
|
|
ELSE NULL END
|
|
) STORED,
|
|
error_message TEXT,
|
|
metadata JSON,
|
|
INDEX idx_deployment_stage (deployment_id, stage),
|
|
INDEX idx_status_time (status, start_time)
|
|
) ENGINE=InnoDB;
|
|
|
|
CREATE TABLE service_deployments (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
service_name VARCHAR(50) NOT NULL,
|
|
version VARCHAR(50) NOT NULL,
|
|
commit_hash VARCHAR(40) NOT NULL,
|
|
deployed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
deployed_by VARCHAR(100),
|
|
rollback_version VARCHAR(50) NULL,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
performance_baseline JSON, -- 배포 후 성능 지표
|
|
INDEX idx_service_version (service_name, version),
|
|
INDEX idx_active (is_active, deployed_at)
|
|
) ENGINE=InnoDB;
|
|
```
|
|
|
|
#### 2. 배포 성능 모니터링
|
|
```javascript
|
|
// services/deploymentService.js
|
|
class DeploymentService {
|
|
async recordDeployment(deploymentData) {
|
|
// 배포 기록 저장 (500자 이하)
|
|
const deployment = await this.db.query(`
|
|
INSERT INTO deployment_logs
|
|
(deployment_id, stage, status, commit_hash, branch_name, deployed_by)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`, [deploymentData.id, deploymentData.stage, 'started',
|
|
deploymentData.commit, deploymentData.branch, deploymentData.user]);
|
|
|
|
return deployment.insertId;
|
|
}
|
|
|
|
async updateDeploymentStatus(logId, status, errorMessage = null) {
|
|
// 배포 상태 업데이트
|
|
await this.db.query(`
|
|
UPDATE deployment_logs
|
|
SET status = ?, end_time = CURRENT_TIMESTAMP, error_message = ?
|
|
WHERE id = ?
|
|
`, [status, errorMessage, logId]);
|
|
}
|
|
|
|
async getDeploymentHistory(limit = 50) {
|
|
// 최근 배포 이력 조회
|
|
const [rows] = await this.db.query(`
|
|
SELECT * FROM deployment_logs
|
|
ORDER BY start_time DESC LIMIT ?
|
|
`, [limit]);
|
|
|
|
return rows;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 성능 벤치마크 자동화
|
|
|
|
#### 1. 배포 후 성능 테스트
|
|
```bash
|
|
# scripts/performance-test.sh
|
|
#!/bin/bash
|
|
|
|
API_URL="http://localhost:3000"
|
|
RESULTS_FILE="/tmp/perf-results.json"
|
|
|
|
echo "Running performance tests after deployment..."
|
|
|
|
# API 응답 시간 테스트
|
|
ab -n 1000 -c 10 -g /tmp/ab-results.tsv $API_URL/api/devices/ > /tmp/ab-output.txt
|
|
|
|
# 메모리 사용량 체크
|
|
MEMORY_USAGE=$(docker stats --no-stream --format "table {{.Container}}\t{{.MemUsage}}" | grep home-api)
|
|
|
|
# 결과를 JSON으로 저장
|
|
echo "{
|
|
\"timestamp\": \"$(date -Iseconds)\",
|
|
\"memory_usage\": \"$MEMORY_USAGE\",
|
|
\"ab_results\": \"$(tail -5 /tmp/ab-output.txt)\"
|
|
}" > $RESULTS_FILE
|
|
|
|
# 성능 저하 감지시 알림
|
|
if [ $(awk '/Time per request/ {print $4}' /tmp/ab-output.txt | head -1 | cut -d. -f1) -gt 100 ]; then
|
|
curl -X POST http://developer-machine:3000/api/alerts/webhook \
|
|
-d '{"type":"performance_degradation","details":"'$(cat $RESULTS_FILE)'"}'
|
|
fi
|
|
```
|
|
|
|
---
|
|
|
|
## 🚀 개발 시작 체크리스트
|
|
|
|
### 1. 환경 준비
|
|
- [ ] Node.js 18+ 설치
|
|
- [ ] Docker & Docker Compose 설치
|
|
- [ ] Git 저장소 클론: `git clone https://git.hyungi.net/hyungi/myhome-server.git`
|
|
- [ ] 프로젝트 폴더 구조 생성
|
|
|
|
### 2. 데이터베이스 설정
|
|
- [ ] `docker-compose up -d mariadb` 실행
|
|
- [ ] phpMyAdmin 접속 확인 (localhost:8080)
|
|
- [ ] 테이블 생성 및 초기 데이터 삽입
|
|
- [ ] 인덱스 및 파티션 설정 확인
|
|
|
|
### 3. 백엔드 개발 순서
|
|
1. **기본 설정**: Express 앱, DB 연결, 미들웨어 설정
|
|
2. **모델 개발**: Sequelize 모델 정의 (devices → power_consumption → network_traffic)
|
|
3. **컨트롤러 개발**: 기본 CRUD 작업 (GET, POST, PUT, DELETE)
|
|
4. **서비스 계층**: 비즈니스 로직 분리 (통계, 분석, 집계)
|
|
5. **라우터 연결**: API 엔드포인트 구성
|
|
6. **데이터 수집기**: 실제 하드웨어 연동 모듈
|
|
7. **알림 시스템**: 실시간 모니터링 및 알림
|
|
8. **테스트 작성**: 단위 테스트 및 통합 테스트
|
|
|
|
### 4. 성능 최적화
|
|
- [ ] Redis 캐싱 구현
|
|
- [ ] DB 쿼리 최적화
|
|
- [ ] API 응답 시간 모니터링
|
|
- [ ] 메모리 사용량 추적
|
|
|
|
### 5. CI/CD 파이프라인 구축
|
|
- [ ] Gitea Actions 활성화 및 설정
|
|
- [ ] 개발 머신(MacBook Pro/Mac Mini)에 Gitea Actions Runner 설치
|
|
- [ ] DS1525+에 Docker 환경 및 Gitea 서버 구성
|
|
- [ ] 배포 스크립트 작성 및 테스트 (프로덕션/스테이징)
|
|
- [ ] 데이터베이스 마이그레이션 자동화
|
|
- [ ] 브랜치별 배포 전략 설정 (main→production, develop→staging)
|
|
- [ ] 성능 모니터링 및 배포 알림 설정
|
|
- [ ] Webhook 서버 구성 (개발 머신)
|
|
|
|
### 6. 보안 설정
|
|
- [ ] JWT 인증 구현
|
|
- [ ] API 속도 제한 설정
|
|
- [ ] HTTPS 인증서 구성 (Let's Encrypt)
|
|
- [ ] 입력 데이터 검증 강화
|
|
- [ ] SSH Key 기반 서버 간 통신 설정
|
|
- [ ] Gitea 보안 설정 (2FA, 브랜치 보호)
|
|
|
|
### 7. 모니터링 및 알림
|
|
- [ ] 배포 성공/실패 실시간 알림 (WebSocket)
|
|
- [ ] 서비스 상태 모니터링 대시보드
|
|
- [ ] 성능 저하 감지 및 자동 알림
|
|
- [ ] 로그 중앙화 (Mac Mini → DS1525+)
|
|
- [ ] 배포 메트릭 수집 및 분석
|
|
|
|
## 🎯 Gitea 기반 CI/CD 장점
|
|
|
|
### 완전 프라이빗 환경
|
|
- **내부 네트워크**: 모든 CI/CD 프로세스가 홈 네트워크 내에서 실행
|
|
- **데이터 보안**: 소스코드와 빌드 아티팩트가 외부로 유출되지 않음
|
|
- **비용 효율**: 외부 CI/CD 서비스 비용 절약
|
|
|
|
### 하드웨어 최적화
|
|
- **MacBook Pro**: 모바일 개발 환경, 외부 작업 가능
|
|
- **Mac Mini**: 고정 개발 환경, 안정적인 빌드 서버 역할
|
|
- **공통 ARM64**: 두 머신 모두 네이티브 빌드 환경
|
|
- **DS1525+**: 안정적인 Git 서버 및 프로덕션 환경
|
|
- **통합 관리**: 홈 관리 시스템과 개발 도구 통합
|
|
|
|
### 확장성
|
|
- **멀티 브랜치**: feature/develop/main 브랜치별 배포 전략
|
|
- **스테이징**: develop 브랜치로 스테이징 환경 자동 배포
|
|
- **롤백**: 실패시 자동 롤백 및 알림
|
|
|
|
이제 완전히 프라이빗한 환경에서 기업급 CI/CD 파이프라인을 구축할 수 있습니다. Gitea 서버로 완전한 DevOps 환경을 홈에서 운영하는 것이 가능하겠네요!
|
|
|
|
이 계획서를 따라 단계별로 구현하면 확장성과 유지보수성을 갖춘 견고한 홈 관리 시스템과 함께 전문적인 CI/CD 파이프라인을 구축할 수 있습니다. |