- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동) - 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/) - 기획팀 폴더 requests/ 생성 - plans/ → dev/dev_plans/ 이름 변경 - README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용) - resources.md 신규 (노션 링크용, assets/brochure 이관 예정) - CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동 - 전체 참조 경로 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1275 lines
35 KiB
Markdown
1275 lines
35 KiB
Markdown
# 11. 서버 설치 가이드
|
||
|
||
[목차로 돌아가기](./README.md)
|
||
|
||
---
|
||
|
||
## [운영] 설치 순서
|
||
|
||
| 순서 | 항목 | 의존성 |
|
||
|------|------|--------|
|
||
| ① | OS 기본 셋팅 (UFW, 스왑, 타임존) | - |
|
||
| ② | MySQL 8.4 | ① |
|
||
| ③ | Redis 7.x | ① |
|
||
| ④ | Nginx + Certbot | ① |
|
||
| ⑤ | PHP 8.4 + Composer | ① |
|
||
| ⑥ | Supervisor (Queue Worker) | ⑤ |
|
||
| ⑦ | Laravel API 배포 (api, api-stage, mng) | ②③⑤ |
|
||
| ⑧ | Sales 배포 | ⑤ |
|
||
| ⑨ | Node.js 22 + PM2 (react, react-stage) | ① |
|
||
| ⑩ | SSL 인증서 (Let's Encrypt) | ④ |
|
||
| ⑪ | node_exporter | ① |
|
||
| ⑫ | fail2ban | ① |
|
||
| ⑬ | 최종 점검 | 전체 |
|
||
|
||
---
|
||
|
||
### ① OS 기본 셋팅
|
||
|
||
```bash
|
||
# 시스템 업데이트
|
||
sudo apt update && sudo apt upgrade -y
|
||
|
||
# 기본 패키지
|
||
sudo apt install -y curl wget git unzip vim htop net-tools
|
||
|
||
# 타임존
|
||
sudo timedatectl set-timezone Asia/Seoul
|
||
|
||
# 스왑 4GB 설정
|
||
sudo fallocate -l 4G /swapfile
|
||
sudo chmod 600 /swapfile
|
||
sudo mkswap /swapfile
|
||
sudo swapon /swapfile
|
||
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
|
||
|
||
# 스왑 최적화
|
||
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
|
||
sudo sysctl -p
|
||
|
||
# UFW 방화벽
|
||
sudo ufw default deny incoming
|
||
sudo ufw default allow outgoing
|
||
sudo ufw allow 22/tcp # SSH
|
||
sudo ufw allow 80/tcp # HTTP
|
||
sudo ufw allow 443/tcp # HTTPS
|
||
sudo ufw allow from 110.10.147.46 to any port 9100 # node_exporter (CI/CD만)
|
||
sudo ufw allow from 110.10.147.46 to any port 3306 # MySQL (CI/CD 백업용)
|
||
sudo ufw enable
|
||
|
||
# webservice 사용자 그룹 생성
|
||
sudo groupadd -f webservice
|
||
sudo usermod -aG webservice hskwon
|
||
sudo usermod -aG webservice www-data
|
||
sudo mkdir -p /home/webservice
|
||
sudo chown hskwon:webservice /home/webservice
|
||
sudo chmod 2775 /home/webservice # setgid
|
||
```
|
||
|
||
### ② MySQL 8.4
|
||
|
||
```bash
|
||
# mysql-apt-config deb로 repo 등록
|
||
sudo wget https://dev.mysql.com/get/mysql-apt-config_0.8.33-1_all.deb
|
||
sudo DEBIAN_FRONTEND=noninteractive dpkg -i mysql-apt-config_0.8.33-1_all.deb
|
||
|
||
# GPG 키 만료 시 — Ubuntu keyserver에서 갱신
|
||
sudo gpg --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
|
||
sudo gpg --export B7B3B788A8D3785C | sudo tee /usr/share/keyrings/mysql-apt-config.gpg > /dev/null
|
||
|
||
# 설치
|
||
sudo apt update
|
||
sudo apt install -y mysql-server
|
||
```
|
||
|
||
**성능 튜닝** (`/etc/mysql/mysql.conf.d/sam-tuning.cnf`):
|
||
|
||
```ini
|
||
[mysqld]
|
||
innodb_buffer_pool_size = 2048M
|
||
innodb_log_file_size = 512M
|
||
innodb_flush_log_at_trx_commit = 2
|
||
max_connections = 100
|
||
character-set-server = utf8mb4
|
||
collation-server = utf8mb4_unicode_ci
|
||
slow_query_log = 1
|
||
slow_query_log_file = /var/log/mysql/slow.log
|
||
long_query_time = 2
|
||
validate_password.policy = LOW
|
||
```
|
||
|
||
**DB 및 사용자:**
|
||
|
||
```sql
|
||
-- 데이터베이스 (4개)
|
||
CREATE DATABASE sam CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
CREATE DATABASE sam_stage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
CREATE DATABASE codebridge CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
|
||
-- 앱 사용자
|
||
CREATE USER 'codebridge'@'localhost' IDENTIFIED BY '<비밀번호>';
|
||
GRANT ALL PRIVILEGES ON sam.* TO 'codebridge'@'localhost';
|
||
GRANT ALL PRIVILEGES ON sam_stage.* TO 'codebridge'@'localhost';
|
||
GRANT ALL PRIVILEGES ON sam_stat.* TO 'codebridge'@'localhost';
|
||
GRANT ALL PRIVILEGES ON codebridge.* TO 'codebridge'@'localhost';
|
||
|
||
-- 관리자 (auth_socket)
|
||
CREATE USER 'hskwon'@'localhost' IDENTIFIED WITH auth_socket;
|
||
GRANT ALL PRIVILEGES ON *.* TO 'hskwon'@'localhost' WITH GRANT OPTION;
|
||
|
||
-- CI/CD 서버 백업용
|
||
CREATE USER 'sam_backup'@'110.10.147.46' IDENTIFIED BY '<백업용_비밀번호>';
|
||
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam.* TO 'sam_backup'@'110.10.147.46';
|
||
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam_stat.* TO 'sam_backup'@'110.10.147.46';
|
||
|
||
FLUSH PRIVILEGES;
|
||
```
|
||
|
||
### ③ Redis 7.x
|
||
|
||
```bash
|
||
sudo apt install -y redis-server
|
||
|
||
# /etc/redis/redis.conf 설정:
|
||
# bind 127.0.0.1 ::1
|
||
# maxmemory 512mb
|
||
# maxmemory-policy allkeys-lru
|
||
# supervised systemd
|
||
|
||
sudo systemctl enable redis-server
|
||
sudo systemctl restart redis-server
|
||
redis-cli ping # → PONG
|
||
```
|
||
|
||
### ④ Nginx + Certbot
|
||
|
||
```bash
|
||
sudo apt install -y nginx certbot python3-certbot-nginx
|
||
```
|
||
|
||
**보안 스니펫** (`/etc/nginx/snippets/security.conf`):
|
||
|
||
```nginx
|
||
# 숨김 파일 차단 (.env, .git 등)
|
||
location ~ /\. {
|
||
deny all;
|
||
access_log off;
|
||
log_not_found off;
|
||
}
|
||
|
||
# 환경설정/백업/로그 파일 차단
|
||
location ~* \.(env|ini|log|conf|bak|sql)$ {
|
||
deny all;
|
||
access_log off;
|
||
log_not_found off;
|
||
}
|
||
|
||
# Composer, Node 패키지 등 민감 파일 차단
|
||
location ~* /(composer\.(json|lock)|package\.json|yarn\.lock|phpunit\.xml|artisan|server\.php)$ {
|
||
deny all;
|
||
access_log off;
|
||
log_not_found off;
|
||
}
|
||
```
|
||
|
||
**기본 설정** (`/etc/nginx/nginx.conf`):
|
||
|
||
```nginx
|
||
worker_processes auto;
|
||
events {
|
||
worker_connections 1024;
|
||
}
|
||
http {
|
||
keepalive_timeout 65;
|
||
client_max_body_size 50M;
|
||
gzip on;
|
||
gzip_types text/plain application/json application/javascript text/css;
|
||
}
|
||
```
|
||
|
||
### ⑤ PHP 8.4 + Composer
|
||
|
||
```bash
|
||
sudo add-apt-repository ppa:ondrej/php -y
|
||
sudo apt update
|
||
|
||
sudo apt install -y \
|
||
php8.4-fpm php8.4-mysql php8.4-mbstring php8.4-xml \
|
||
php8.4-curl php8.4-zip php8.4-gd php8.4-bcmath \
|
||
php8.4-intl php8.4-redis php8.4-opcache php8.4-soap
|
||
|
||
curl -sS https://getcomposer.org/installer | php
|
||
sudo mv composer.phar /usr/local/bin/composer
|
||
```
|
||
|
||
**PHP-FPM Pool 설정 (4개):**
|
||
|
||
| Pool | 설정 파일 | 소켓 | max_children |
|
||
|------|----------|------|-------------|
|
||
| api | /etc/php/8.4/fpm/pool.d/api.conf | php8.4-fpm-api.sock | 10 |
|
||
| admin | /etc/php/8.4/fpm/pool.d/admin.conf | php8.4-fpm-admin.sock | 5 |
|
||
| sales | /etc/php/8.4/fpm/pool.d/sales.conf | php8.4-fpm-sales.sock | 3 |
|
||
| api-stage | /etc/php/8.4/fpm/pool.d/api-stage.conf | php8.4-fpm-api-stage.sock | 3 |
|
||
|
||
**Pool 설정 템플릿 (api.conf 예시):**
|
||
|
||
```ini
|
||
[api]
|
||
user = www-data
|
||
group = webservice
|
||
listen = /run/php/php8.4-fpm-api.sock
|
||
listen.owner = www-data
|
||
listen.group = www-data
|
||
pm = dynamic
|
||
pm.max_children = 10
|
||
pm.start_servers = 4
|
||
pm.min_spare_servers = 2
|
||
pm.max_spare_servers = 6
|
||
pm.max_requests = 500
|
||
php_admin_value[memory_limit] = 128M
|
||
php_admin_value[upload_max_filesize] = 50M
|
||
php_admin_value[post_max_size] = 50M
|
||
php_admin_value[display_errors] = Off
|
||
```
|
||
|
||
```bash
|
||
# 기본 pool 제거, 분리된 pool 사용
|
||
sudo rm /etc/php/8.4/fpm/pool.d/www.conf
|
||
sudo systemctl restart php8.4-fpm
|
||
```
|
||
|
||
### ⑥ Supervisor (Queue Worker)
|
||
|
||
```bash
|
||
sudo apt install -y supervisor
|
||
|
||
sudo tee /etc/supervisor/conf.d/sam-queue.conf > /dev/null << 'EOF'
|
||
[program:sam-queue-worker]
|
||
process_name=%(program_name)s_%(process_num)02d
|
||
command=php /home/webservice/api/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
|
||
autostart=true
|
||
autorestart=true
|
||
stopasgroup=true
|
||
killasgroup=true
|
||
user=www-data
|
||
numprocs=2
|
||
redirect_stderr=true
|
||
stdout_logfile=/home/webservice/api/shared/storage/logs/queue-worker.log
|
||
stopwaitsecs=3600
|
||
EOF
|
||
|
||
sudo supervisorctl reread
|
||
sudo supervisorctl update
|
||
```
|
||
|
||
### ⑦ Laravel 배포 (API / API-Stage / MNG)
|
||
|
||
**디렉토리 구조 생성:**
|
||
|
||
```bash
|
||
# API 운영
|
||
sudo mkdir -p /home/webservice/api/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}}
|
||
sudo chown -R hskwon:webservice /home/webservice/api
|
||
sudo chmod -R 2775 /home/webservice/api
|
||
|
||
# API Stage
|
||
sudo mkdir -p /home/webservice/api-stage/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}}
|
||
sudo chown -R hskwon:webservice /home/webservice/api-stage
|
||
sudo chmod -R 2775 /home/webservice/api-stage
|
||
|
||
# MNG (Admin)
|
||
sudo mkdir -p /home/webservice/mng/{releases,shared/storage/{app/public,framework/{cache,sessions,views},logs}}
|
||
sudo chown -R hskwon:webservice /home/webservice/mng
|
||
sudo chmod -R 2775 /home/webservice/mng
|
||
```
|
||
|
||
**초기 배포 절차:**
|
||
|
||
```bash
|
||
# shared 심링크 연결
|
||
ln -sfn /home/webservice/api/shared/storage /home/webservice/api/current/storage
|
||
ln -sfn /home/webservice/api/shared/.env /home/webservice/api/current/.env
|
||
|
||
# 의존성 설치 + 최적화
|
||
cd /home/webservice/api/current
|
||
composer install --no-dev --optimize-autoloader
|
||
php artisan config:cache
|
||
php artisan route:cache
|
||
php artisan view:cache
|
||
php artisan migrate --force
|
||
```
|
||
|
||
### ⑧ Sales 배포
|
||
|
||
```bash
|
||
sudo mkdir -p /home/webservice/sales
|
||
sudo chown -R hskwon:webservice /home/webservice/sales
|
||
|
||
cd /home/webservice
|
||
git clone <repo_url> sales
|
||
cp /home/webservice/sales/.env.example /home/webservice/sales/.env
|
||
chmod 600 /home/webservice/sales/.env
|
||
chmod 775 /home/webservice/sales/uploads
|
||
```
|
||
|
||
### ⑨ Node.js 22 + PM2
|
||
|
||
```bash
|
||
# Node.js 22 LTS
|
||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||
sudo apt install -y nodejs
|
||
|
||
# PM2 설치
|
||
sudo npm install -g pm2
|
||
|
||
# 운영 + Stage 디렉토리
|
||
sudo mkdir -p /home/webservice/react/{releases,shared}
|
||
sudo mkdir -p /home/webservice/react-stage/{releases,shared}
|
||
sudo chown -R hskwon:webservice /home/webservice/react /home/webservice/react-stage
|
||
```
|
||
|
||
**PM2 설정** (`/home/webservice/ecosystem.config.js`):
|
||
|
||
```javascript
|
||
module.exports = {
|
||
apps: [
|
||
{
|
||
name: 'sam-front',
|
||
cwd: '/home/webservice/react/current',
|
||
script: 'node_modules/next/dist/bin/next',
|
||
args: 'start -p 3000',
|
||
instances: 2,
|
||
exec_mode: 'cluster',
|
||
max_memory_restart: '300M',
|
||
env: {
|
||
NODE_ENV: 'production',
|
||
NODE_OPTIONS: '--max-old-space-size=256'
|
||
}
|
||
},
|
||
{
|
||
name: 'sam-front-stage',
|
||
cwd: '/home/webservice/react-stage/current',
|
||
script: 'node_modules/next/dist/bin/next',
|
||
args: 'start -p 3100',
|
||
instances: 1,
|
||
exec_mode: 'fork',
|
||
max_memory_restart: '512M',
|
||
env: {
|
||
NODE_ENV: 'production',
|
||
NODE_OPTIONS: '--max-old-space-size=384'
|
||
}
|
||
}
|
||
]
|
||
};
|
||
```
|
||
|
||
```bash
|
||
cd /home/webservice
|
||
pm2 start ecosystem.config.js
|
||
pm2 save
|
||
pm2 startup
|
||
```
|
||
|
||
### ⑩ SSL 인증서
|
||
|
||
```bash
|
||
# Nginx 사이트 활성화
|
||
sudo ln -s /etc/nginx/sites-available/sam.it.kr /etc/nginx/sites-enabled/
|
||
sudo ln -s /etc/nginx/sites-available/api.sam.it.kr /etc/nginx/sites-enabled/
|
||
sudo ln -s /etc/nginx/sites-available/mng.codebridge-x.com /etc/nginx/sites-enabled/
|
||
sudo ln -s /etc/nginx/sites-available/sales.codebridge-x.com /etc/nginx/sites-enabled/
|
||
sudo ln -s /etc/nginx/sites-available/codebridge-x.com /etc/nginx/sites-enabled/
|
||
sudo ln -s /etc/nginx/sites-available/stage.sam.it.kr /etc/nginx/sites-enabled/
|
||
sudo ln -s /etc/nginx/sites-available/stage-api.sam.it.kr /etc/nginx/sites-enabled/
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
|
||
# SSL 인증서 발급
|
||
sudo certbot --nginx -d sam.it.kr --email develop@codebridge-x.com
|
||
sudo certbot --nginx -d api.sam.it.kr --email develop@codebridge-x.com
|
||
sudo certbot --nginx -d stage.sam.it.kr --email develop@codebridge-x.com
|
||
sudo certbot --nginx -d stage-api.sam.it.kr --email develop@codebridge-x.com
|
||
sudo certbot --nginx -d mng.codebridge-x.com --email develop@codebridge-x.com
|
||
sudo certbot --nginx -d sales.codebridge-x.com --email develop@codebridge-x.com
|
||
sudo certbot --nginx -d codebridge-x.com -d www.codebridge-x.com --email develop@codebridge-x.com
|
||
|
||
# 자동 갱신 확인
|
||
sudo certbot renew --dry-run
|
||
```
|
||
|
||
### ⑪ node_exporter
|
||
|
||
```bash
|
||
cd /tmp
|
||
wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
|
||
tar xzf node_exporter-1.8.2.linux-amd64.tar.gz
|
||
sudo mv node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/
|
||
rm -rf node_exporter-1.8.2*
|
||
|
||
sudo tee /etc/systemd/system/node_exporter.service > /dev/null << 'EOF'
|
||
[Unit]
|
||
Description=Node Exporter
|
||
After=network.target
|
||
|
||
[Service]
|
||
User=nobody
|
||
ExecStart=/usr/local/bin/node_exporter
|
||
Restart=always
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable node_exporter
|
||
sudo systemctl start node_exporter
|
||
```
|
||
|
||
### ⑫ fail2ban
|
||
|
||
```bash
|
||
sudo apt install -y fail2ban
|
||
sudo systemctl enable fail2ban
|
||
sudo systemctl start fail2ban
|
||
```
|
||
|
||
---
|
||
|
||
## Nginx 사이트 설정 템플릿
|
||
|
||
### sam.it.kr (Next.js 운영)
|
||
|
||
```nginx
|
||
upstream nextjs_prod {
|
||
server 127.0.0.1:3000;
|
||
keepalive 32;
|
||
}
|
||
|
||
server {
|
||
listen 80;
|
||
server_name sam.it.kr;
|
||
|
||
access_log /var/log/nginx/sam.it.kr.access.log;
|
||
error_log /var/log/nginx/sam.it.kr.error.log;
|
||
|
||
location /_next/static/ {
|
||
alias /home/webservice/react/current/.next/static/;
|
||
expires 365d;
|
||
add_header Cache-Control "public, immutable";
|
||
}
|
||
|
||
location / {
|
||
proxy_pass http://nextjs_prod;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection 'upgrade';
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_cache_bypass $http_upgrade;
|
||
}
|
||
}
|
||
```
|
||
|
||
### api.sam.it.kr (Laravel API)
|
||
|
||
```nginx
|
||
server {
|
||
listen 80;
|
||
server_name api.sam.it.kr;
|
||
|
||
root /home/webservice/api/current/public;
|
||
index index.php;
|
||
|
||
access_log /var/log/nginx/api.sam.it.kr.access.log;
|
||
error_log /var/log/nginx/api.sam.it.kr.error.log;
|
||
|
||
include /etc/nginx/snippets/security.conf;
|
||
|
||
location / {
|
||
try_files $uri $uri/ /index.php?$query_string;
|
||
}
|
||
|
||
location ~ \.php$ {
|
||
fastcgi_pass unix:/run/php/php8.4-fpm-api.sock;
|
||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||
include fastcgi_params;
|
||
fastcgi_read_timeout 60;
|
||
}
|
||
|
||
client_max_body_size 50M;
|
||
}
|
||
```
|
||
|
||
### mng.codebridge-x.com (Laravel Admin)
|
||
|
||
```nginx
|
||
server {
|
||
listen 80;
|
||
server_name mng.codebridge-x.com;
|
||
|
||
root /home/webservice/mng/current/public;
|
||
index index.php;
|
||
|
||
access_log /var/log/nginx/mng.codebridge-x.com.access.log;
|
||
error_log /var/log/nginx/mng.codebridge-x.com.error.log;
|
||
|
||
include /etc/nginx/snippets/security.conf;
|
||
|
||
location / {
|
||
try_files $uri $uri/ /index.php?$query_string;
|
||
}
|
||
|
||
location ~ \.php$ {
|
||
fastcgi_pass unix:/run/php/php8.4-fpm-admin.sock;
|
||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||
include fastcgi_params;
|
||
fastcgi_read_timeout 60;
|
||
}
|
||
|
||
client_max_body_size 50M;
|
||
}
|
||
```
|
||
|
||
### sales.codebridge-x.com (Plain PHP)
|
||
|
||
```nginx
|
||
server {
|
||
listen 80;
|
||
server_name sales.codebridge-x.com;
|
||
|
||
root /home/webservice/sales;
|
||
index index.php index.html;
|
||
|
||
access_log /var/log/nginx/sales.codebridge-x.com.access.log;
|
||
error_log /var/log/nginx/sales.codebridge-x.com.error.log;
|
||
|
||
include /etc/nginx/snippets/security.conf;
|
||
|
||
location / {
|
||
try_files $uri $uri/ /index.php?$query_string;
|
||
}
|
||
|
||
location ~ \.php$ {
|
||
fastcgi_pass unix:/run/php/php8.4-fpm-sales.sock;
|
||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||
include fastcgi_params;
|
||
fastcgi_read_timeout 60;
|
||
}
|
||
|
||
# uploads PHP 실행 차단 (보안)
|
||
location ~* /uploads/.*\.php$ {
|
||
deny all;
|
||
}
|
||
|
||
client_max_body_size 50M;
|
||
}
|
||
```
|
||
|
||
### stage.sam.it.kr / stage-api.sam.it.kr
|
||
|
||
stage.sam.it.kr은 sam.it.kr과 동일 구조 (upstream 포트: 3100).
|
||
stage-api.sam.it.kr은 api.sam.it.kr과 동일 구조 (소켓: php8.4-fpm-api-stage.sock, root: api-stage).
|
||
|
||
---
|
||
|
||
## [CI/CD] 설치 순서
|
||
|
||
| 순서 | 항목 | 의존성 |
|
||
|------|------|--------|
|
||
| ① | OS 기본 셋팅 (UFW, 스왑, 타임존) | - |
|
||
| ② | MySQL 8.4 | ① |
|
||
| ③ | Java 21 (Jenkins 런타임) | ① |
|
||
| ④ | Gitea | ② |
|
||
| ⑤ | 개발서버 post-receive hook 설정 | ④ |
|
||
| ⑥ | Jenkins | ③ |
|
||
| ⑦ | Nginx + SSL | ④⑥ |
|
||
| ⑧ | Prometheus + node_exporter | ① |
|
||
| ⑨ | Grafana | ⑧ |
|
||
| ⑩ | Jenkins 파이프라인 + Webhook | ⑥⑦ |
|
||
| ⑪ | 백업 자동화 (운영 DB 원격 백업) | ② |
|
||
| ⑫ | fail2ban + 최종 점검 | 전체 |
|
||
|
||
---
|
||
|
||
### ① OS 기본 셋팅
|
||
|
||
운영서버와 동일. 차이점:
|
||
- UFW: 22, 80, 443만 허용 (9100, 3306 불필요)
|
||
- webservice 그룹 생성 (배포 스크립트용)
|
||
|
||
### ② MySQL 8.4
|
||
|
||
운영서버와 동일한 설치 방법. 튜닝 차이:
|
||
|
||
```ini
|
||
[mysqld]
|
||
innodb_buffer_pool_size = 1536M # 운영(2048M)보다 작음
|
||
innodb_redo_log_capacity = 536870912
|
||
innodb_flush_log_at_trx_commit = 2
|
||
max_connections = 50 # 운영(100)보다 작음
|
||
```
|
||
|
||
**DB 및 사용자:**
|
||
|
||
```sql
|
||
-- Gitea DB
|
||
CREATE DATABASE gitea CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
CREATE USER 'gitea'@'localhost' IDENTIFIED BY '<gitea_비밀번호>';
|
||
GRANT ALL PRIVILEGES ON gitea.* TO 'gitea'@'localhost';
|
||
|
||
-- 관리자
|
||
CREATE USER 'hskwon'@'localhost' IDENTIFIED WITH auth_socket;
|
||
GRANT ALL PRIVILEGES ON *.* TO 'hskwon'@'localhost' WITH GRANT OPTION;
|
||
|
||
FLUSH PRIVILEGES;
|
||
```
|
||
|
||
### ③ Java 21
|
||
|
||
```bash
|
||
sudo apt install -y openjdk-21-jre-headless
|
||
java -version
|
||
# openjdk version "21.0.x" 확인
|
||
|
||
# 여러 버전 설치 시 기본 Java 전환
|
||
sudo update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java
|
||
```
|
||
|
||
> **참고**: Java 17은 2026-03-31 Jenkins 지원 종료. Java 21 사용 필수.
|
||
|
||
### ④ Gitea
|
||
|
||
```bash
|
||
GITEA_VERSION="1.22.6"
|
||
wget -O /tmp/gitea https://dl.gitea.com/gitea/${GITEA_VERSION}/gitea-${GITEA_VERSION}-linux-amd64
|
||
sudo mv /tmp/gitea /usr/local/bin/gitea
|
||
sudo chmod +x /usr/local/bin/gitea
|
||
|
||
sudo adduser --system --group --disabled-password --shell /bin/bash --home /home/git git
|
||
|
||
sudo mkdir -p /var/lib/gitea/{custom,data,log}
|
||
sudo mkdir -p /etc/gitea
|
||
sudo chown -R git:git /var/lib/gitea
|
||
sudo chown git:git /etc/gitea
|
||
sudo chmod 750 /etc/gitea
|
||
```
|
||
|
||
**systemd 서비스:**
|
||
|
||
```ini
|
||
# /etc/systemd/system/gitea.service
|
||
[Unit]
|
||
Description=Gitea (Git with a cup of tea)
|
||
After=syslog.target network.target mysql.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=git
|
||
Group=git
|
||
WorkingDirectory=/var/lib/gitea/
|
||
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
|
||
Restart=always
|
||
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
|
||
RestartSec=10
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
**Gitea 설정** (`/etc/gitea/app.ini`):
|
||
|
||
```ini
|
||
[server]
|
||
DOMAIN = git.sam.it.kr
|
||
HTTP_PORT = 3000
|
||
ROOT_URL = https://git.sam.it.kr/
|
||
DISABLE_SSH = false
|
||
SSH_PORT = 22
|
||
LFS_START_SERVER = true
|
||
|
||
[database]
|
||
DB_TYPE = mysql
|
||
HOST = 127.0.0.1:3306
|
||
NAME = gitea
|
||
USER = gitea
|
||
PASSWD = <gitea_비밀번호>
|
||
CHARSET = utf8mb4
|
||
|
||
[repository]
|
||
ROOT = /var/lib/gitea/data/repositories
|
||
|
||
[log]
|
||
ROOT_PATH = /var/lib/gitea/log
|
||
MODE = file
|
||
LEVEL = info
|
||
|
||
[service]
|
||
DISABLE_REGISTRATION = true
|
||
REQUIRE_SIGNIN_VIEW = true
|
||
```
|
||
|
||
**초기 설정:**
|
||
1. https://git.sam.it.kr 웹 설치 마법사 완료
|
||
2. 관리자 계정 생성 (hskwon)
|
||
3. Organization "SamProject" 생성
|
||
4. 저장소 생성: sam-api, sam-manage, sam-react-prod, sam-sales, sam-landing
|
||
5. Jenkins Webhook용 API 토큰 생성
|
||
|
||
### ⑤ 개발서버 post-receive hook (선택적 브랜치 동기화)
|
||
|
||
**토큰 보안 파일 (개발서버):**
|
||
|
||
```bash
|
||
# /data/GIT/.cicd-env (chmod 600, owner: git)
|
||
CICD_GITEA_TOKEN=<토큰>
|
||
CICD_GITEA_USER=hskwon
|
||
CICD_GITEA_HOST=git.sam.it.kr
|
||
```
|
||
|
||
**hook 스크립트** (`/data/GIT/samproject/<repo>.git/hooks/post-receive.d/push-to-cicd`):
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
source /data/GIT/.cicd-env
|
||
LOGFILE=/home/webservice/logs/cicd_push_<repo>.log
|
||
CICD_REMOTE="https://${CICD_GITEA_USER}:${CICD_GITEA_TOKEN}@${CICD_GITEA_HOST}/SamProject/<repo>.git"
|
||
|
||
mkdir -p /home/webservice/logs
|
||
|
||
while read oldrev newrev refname; do
|
||
BRANCH=$(echo "$refname" | sed 's|refs/heads/||')
|
||
if [ "$BRANCH" = "<target_branch>" ]; then
|
||
echo "$(date '+%Y-%m-%d %H:%M:%S'): Pushing ${BRANCH} to CI/CD Gitea" >> $LOGFILE
|
||
git push $CICD_REMOTE ${BRANCH}:${BRANCH} >> $LOGFILE 2>&1
|
||
echo "$(date '+%Y-%m-%d %H:%M:%S'): Done (exit: $?)" >> $LOGFILE
|
||
fi
|
||
done
|
||
```
|
||
|
||
**동기화 요약:**
|
||
|
||
| 저장소 | hook 대상 브랜치 | 동작 |
|
||
|--------|-----------------|------|
|
||
| sam-react-prod | main, develop | CI/CD Gitea에 push |
|
||
| sam-api | main | CI/CD Gitea에 push |
|
||
| sam-manage | main | CI/CD Gitea에 push (2026-02-24 추가) |
|
||
| sam-sales | main | CI/CD Gitea에 push |
|
||
| sam-landing | main | CI/CD Gitea에 push |
|
||
|
||
### ⑥ Jenkins
|
||
|
||
```bash
|
||
# GPG 키 + APT Repository
|
||
sudo gpg --keyserver keyserver.ubuntu.com --recv-keys 7198F4B714ABFC68
|
||
sudo gpg --export 7198F4B714ABFC68 | sudo tee /usr/share/keyrings/jenkins-keyring.gpg > /dev/null
|
||
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.gpg]" \
|
||
https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
|
||
/etc/apt/sources.list.d/jenkins.list > /dev/null
|
||
|
||
sudo apt update
|
||
sudo apt install -y jenkins
|
||
|
||
# JVM 메모리 제한
|
||
sudo mkdir -p /etc/systemd/system/jenkins.service.d
|
||
sudo tee /etc/systemd/system/jenkins.service.d/override.conf > /dev/null << 'EOF'
|
||
[Service]
|
||
Environment="JAVA_OPTS=-Xmx2048m -Xms512m -Djava.awt.headless=true"
|
||
EOF
|
||
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable jenkins
|
||
sudo systemctl start jenkins
|
||
```
|
||
|
||
**필요 도구 설치:**
|
||
|
||
```bash
|
||
# Node.js 22 (react 빌드용)
|
||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||
sudo apt install -y nodejs
|
||
|
||
# PHP 8.4 + Composer (선택 — Laravel 테스트용)
|
||
sudo add-apt-repository ppa:ondrej/php -y
|
||
sudo apt update
|
||
sudo apt install -y php8.4-cli php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-mysql php8.4-bcmath
|
||
curl -sS https://getcomposer.org/installer | php
|
||
sudo mv composer.phar /usr/local/bin/composer
|
||
```
|
||
|
||
**SSH 키 설정:**
|
||
|
||
```bash
|
||
sudo -u jenkins ssh-keygen -t ed25519 -C "jenkins@sam-cicd" -f /var/lib/jenkins/.ssh/id_ed25519 -N ""
|
||
|
||
# 운영/개발 서버에 공개키 등록
|
||
ssh sam-prod "echo '$(cat /var/lib/jenkins/.ssh/id_ed25519.pub)' >> /home/hskwon/.ssh/authorized_keys"
|
||
ssh sam-dev "echo '$(cat /var/lib/jenkins/.ssh/id_ed25519.pub)' >> /home/hskwon/.ssh/authorized_keys"
|
||
|
||
# known_hosts 등록
|
||
sudo -u jenkins ssh-keyscan -H 211.117.60.189 >> /var/lib/jenkins/.ssh/known_hosts
|
||
sudo -u jenkins ssh-keyscan -H 114.203.209.83 >> /var/lib/jenkins/.ssh/known_hosts
|
||
```
|
||
|
||
**Jenkins Credentials:**
|
||
- `deploy-ssh-key`: SSH 키 (hskwon@운영/개발 서버 공용)
|
||
- `gitea-api-token`: Gitea API 토큰
|
||
|
||
**분산 빌드 설정 (Built-in Node 보안 격리):**
|
||
|
||
```bash
|
||
# 1. Built-in Node executor를 0으로 변경 (Jenkins 정지 상태에서)
|
||
sudo systemctl stop jenkins
|
||
sudo sed -i 's|<numExecutors>2</numExecutors>|<numExecutors>0</numExecutors>|' /var/lib/jenkins/config.xml
|
||
# Agent 포트 활성화 (0 = 랜덤 포트)
|
||
sudo sed -i 's|<slaveAgentPort>-1</slaveAgentPort>|<slaveAgentPort>0</slaveAgentPort>|' /var/lib/jenkins/config.xml
|
||
|
||
# 2. Agent workspace 디렉토리
|
||
sudo mkdir -p /var/lib/jenkins-agent/workspace
|
||
sudo chown -R jenkins:jenkins /var/lib/jenkins-agent
|
||
|
||
# 3. Agent 노드 설정
|
||
sudo mkdir -p /var/lib/jenkins/nodes/local-agent
|
||
# config.xml 생성 (JNLP WebSocket, executor 2, label: build)
|
||
sudo chown -R jenkins:jenkins /var/lib/jenkins/nodes/local-agent
|
||
|
||
# 4. Jenkins 시작 → Agent secret 확인 (UI 또는 Groovy 스크립트)
|
||
sudo systemctl start jenkins
|
||
|
||
# 5. Agent jar 다운로드
|
||
sudo curl -sL http://localhost:8080/jnlpJars/agent.jar -o /var/lib/jenkins-agent/agent.jar
|
||
sudo chown jenkins:jenkins /var/lib/jenkins-agent/agent.jar
|
||
|
||
# 6. Agent systemd 서비스
|
||
sudo tee /etc/systemd/system/jenkins-agent.service > /dev/null << 'AGENTEOF'
|
||
[Unit]
|
||
Description=Jenkins Build Agent
|
||
After=network.target jenkins.service
|
||
Wants=jenkins.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=jenkins
|
||
Group=jenkins
|
||
WorkingDirectory=/var/lib/jenkins-agent
|
||
ExecStart=/usr/bin/java -jar /var/lib/jenkins-agent/agent.jar \
|
||
-url http://localhost:8080/ \
|
||
-secret <AGENT_SECRET> \
|
||
-name local-agent \
|
||
-workDir /var/lib/jenkins-agent \
|
||
-webSocket
|
||
Restart=always
|
||
RestartSec=10
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
AGENTEOF
|
||
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable jenkins-agent
|
||
sudo systemctl start jenkins-agent
|
||
```
|
||
|
||
> **참고**: Agent secret은 Jenkins UI > Manage Jenkins > Nodes > local-agent에서 확인하거나,
|
||
> init.groovy.d 스크립트로 추출 가능.
|
||
|
||
### ⑦ Nginx + SSL (CI/CD)
|
||
|
||
**리버스 프록시 설정:**
|
||
|
||
```nginx
|
||
# /etc/nginx/sites-available/git.sam.it.kr
|
||
server {
|
||
listen 80;
|
||
server_name git.sam.it.kr;
|
||
client_max_body_size 500M;
|
||
proxy_request_buffering off;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:3000;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
}
|
||
|
||
# /etc/nginx/sites-available/ci.sam.it.kr
|
||
server {
|
||
listen 80;
|
||
server_name ci.sam.it.kr;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:8080;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_read_timeout 90;
|
||
proxy_buffering off;
|
||
}
|
||
}
|
||
|
||
# /etc/nginx/sites-available/monitor.sam.it.kr
|
||
server {
|
||
listen 80;
|
||
server_name monitor.sam.it.kr;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:3100;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
}
|
||
}
|
||
```
|
||
|
||
```bash
|
||
sudo ln -s /etc/nginx/sites-available/git.sam.it.kr /etc/nginx/sites-enabled/
|
||
sudo ln -s /etc/nginx/sites-available/ci.sam.it.kr /etc/nginx/sites-enabled/
|
||
sudo ln -s /etc/nginx/sites-available/monitor.sam.it.kr /etc/nginx/sites-enabled/
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
|
||
sudo certbot --nginx -d git.sam.it.kr
|
||
sudo certbot --nginx -d ci.sam.it.kr
|
||
sudo certbot --nginx -d monitor.sam.it.kr
|
||
```
|
||
|
||
### ⑧ Prometheus + node_exporter
|
||
|
||
```bash
|
||
# Prometheus
|
||
PROM_VERSION="2.51.0"
|
||
cd /tmp
|
||
wget https://github.com/prometheus/prometheus/releases/download/v${PROM_VERSION}/prometheus-${PROM_VERSION}.linux-amd64.tar.gz
|
||
tar xzf prometheus-${PROM_VERSION}.linux-amd64.tar.gz
|
||
sudo mv prometheus-${PROM_VERSION}.linux-amd64/prometheus /usr/local/bin/
|
||
sudo mv prometheus-${PROM_VERSION}.linux-amd64/promtool /usr/local/bin/
|
||
sudo mkdir -p /etc/prometheus /var/lib/prometheus
|
||
sudo mv prometheus-${PROM_VERSION}.linux-amd64/consoles /etc/prometheus/
|
||
sudo mv prometheus-${PROM_VERSION}.linux-amd64/console_libraries /etc/prometheus/
|
||
rm -rf prometheus-${PROM_VERSION}*
|
||
sudo useradd --no-create-home --shell /bin/false prometheus
|
||
sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus
|
||
```
|
||
|
||
**systemd 서비스:**
|
||
|
||
```ini
|
||
# /etc/systemd/system/prometheus.service
|
||
[Unit]
|
||
Description=Prometheus
|
||
After=network-online.target
|
||
|
||
[Service]
|
||
User=prometheus
|
||
Group=prometheus
|
||
Type=simple
|
||
ExecStart=/usr/local/bin/prometheus \
|
||
--config.file=/etc/prometheus/prometheus.yml \
|
||
--storage.tsdb.path=/var/lib/prometheus/ \
|
||
--storage.tsdb.retention.time=30d \
|
||
--web.listen-address=127.0.0.1:9090
|
||
Restart=always
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
node_exporter: 운영서버 설치와 동일.
|
||
|
||
```bash
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable prometheus node_exporter
|
||
sudo systemctl start prometheus node_exporter
|
||
```
|
||
|
||
### ⑨ Grafana
|
||
|
||
```bash
|
||
sudo mkdir -p /etc/apt/keyrings/
|
||
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null
|
||
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
|
||
sudo apt update
|
||
sudo apt install -y grafana
|
||
```
|
||
|
||
**설정** (`/etc/grafana/grafana.ini`):
|
||
|
||
```ini
|
||
[server]
|
||
http_port = 3100
|
||
domain = monitor.sam.it.kr
|
||
root_url = https://monitor.sam.it.kr/
|
||
|
||
[security]
|
||
admin_password = <grafana_비밀번호>
|
||
|
||
[users]
|
||
allow_sign_up = false
|
||
```
|
||
|
||
```bash
|
||
sudo systemctl enable grafana-server
|
||
sudo systemctl start grafana-server
|
||
```
|
||
|
||
**초기 설정:** Data Source: Prometheus (http://localhost:9090) → 대시보드 임포트: Node Exporter Full (ID: 1860)
|
||
|
||
### ⑪ 백업 자동화 (운영 DB 원격 백업)
|
||
|
||
CI/CD 서버에서 운영 서버의 MySQL DB를 매일 자동 백업합니다.
|
||
|
||
**1. 운영 서버 — 백업 사용자 생성 (운영 MySQL에서 실행):**
|
||
|
||
```sql
|
||
CREATE USER 'sam_backup'@'110.10.147.46' IDENTIFIED BY '<백업용_비밀번호>';
|
||
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam.* TO 'sam_backup'@'110.10.147.46';
|
||
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON sam_stat.* TO 'sam_backup'@'110.10.147.46';
|
||
FLUSH PRIVILEGES;
|
||
```
|
||
|
||
**2. CI/CD 서버 — MySQL 인증 파일:**
|
||
|
||
```bash
|
||
cat > /home/hskwon/.sam_backup.cnf << 'EOF'
|
||
[client]
|
||
user=sam_backup
|
||
password=<백업용_비밀번호>
|
||
EOF
|
||
chmod 600 /home/hskwon/.sam_backup.cnf
|
||
```
|
||
|
||
**3. CI/CD 서버 — 백업 스크립트:**
|
||
|
||
```bash
|
||
mkdir -p /home/hskwon/scripts /home/hskwon/backups/mysql
|
||
|
||
cat > /home/hskwon/scripts/backup-db.sh << 'SCRIPT'
|
||
#!/bin/bash
|
||
set -e
|
||
|
||
BACKUP_DIR="/home/hskwon/backups/mysql"
|
||
BACKUP_CNF="/home/hskwon/.sam_backup.cnf"
|
||
DATE=$(date +%Y%m%d_%H%M%S)
|
||
RETENTION_DAYS=14
|
||
|
||
mkdir -p $BACKUP_DIR
|
||
|
||
# Gitea DB 백업 (로컬, auth_socket)
|
||
mysqldump --single-transaction --routines --triggers gitea | gzip > $BACKUP_DIR/gitea_$DATE.sql.gz
|
||
|
||
# 운영 DB 원격 백업 (sam_backup 사용자)
|
||
if [ -f "$BACKUP_CNF" ]; then
|
||
mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam | gzip > $BACKUP_DIR/sam_production_$DATE.sql.gz
|
||
mysqldump --defaults-extra-file=$BACKUP_CNF -h 211.117.60.189 --single-transaction --routines --triggers --no-tablespaces sam_stat | gzip > $BACKUP_DIR/sam_stat_production_$DATE.sql.gz
|
||
echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea + sam + sam_stat)" >> $BACKUP_DIR/backup.log
|
||
else
|
||
echo "$(date '+%Y-%m-%d %H:%M:%S'): Backup completed (gitea only - $BACKUP_CNF not found)" >> $BACKUP_DIR/backup.log
|
||
fi
|
||
|
||
# 오래된 백업 삭제
|
||
find $BACKUP_DIR -name '*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||
SCRIPT
|
||
|
||
chmod +x /home/hskwon/scripts/backup-db.sh
|
||
```
|
||
|
||
**4. CI/CD 서버 — 크론탭 등록:**
|
||
|
||
```bash
|
||
# hskwon이 crontab 사용 가능해야 함
|
||
sudo sh -c "echo hskwon > /etc/cron.allow"
|
||
sudo chmod 644 /etc/cron.allow
|
||
|
||
# 크론 등록 (매일 새벽 3시)
|
||
(crontab -l 2>/dev/null; echo "# SAM DB 백업 (매일 새벽 3시)"; echo "0 3 * * * /home/hskwon/scripts/backup-db.sh >> /home/hskwon/backups/mysql/backup.log 2>&1") | crontab -
|
||
|
||
# 등록 확인
|
||
crontab -l
|
||
```
|
||
|
||
**5. 테스트:**
|
||
|
||
```bash
|
||
# 수동 실행
|
||
/home/hskwon/scripts/backup-db.sh
|
||
|
||
# 결과 확인
|
||
ls -lht /home/hskwon/backups/mysql/ | head -5
|
||
tail -3 /home/hskwon/backups/mysql/backup.log
|
||
```
|
||
|
||
> 상세 복원 절차 및 sam→sam_stage 동기화는 [백업/복구/재부팅](./10-backup-recovery.md) 참조.
|
||
|
||
### ⑫ fail2ban + 최종 점검
|
||
|
||
```bash
|
||
sudo apt install -y fail2ban
|
||
sudo systemctl enable fail2ban
|
||
sudo systemctl start fail2ban
|
||
```
|
||
|
||
**최종 점검:**
|
||
|
||
```bash
|
||
# 전체 서비스 상태
|
||
sudo systemctl status nginx jenkins jenkins-agent gitea mysql prometheus grafana-server node_exporter fail2ban
|
||
|
||
# 포트 확인
|
||
sudo ss -tlnp | grep -E '(80|443|3000|3100|8080|9090|9100|3306)'
|
||
|
||
# 웹 서비스
|
||
curl -sI https://ci.sam.it.kr | head -3
|
||
curl -sI https://git.sam.it.kr | head -3
|
||
curl -sI https://monitor.sam.it.kr | head -3
|
||
|
||
# 백업 크론 확인
|
||
crontab -l
|
||
|
||
# 자동 시작 등록 확인
|
||
for svc in nginx jenkins jenkins-agent gitea mysql prometheus grafana-server node_exporter fail2ban; do
|
||
echo -n "$svc: "; systemctl is-enabled $svc 2>/dev/null || echo "NOT FOUND"
|
||
done
|
||
```
|
||
|
||
---
|
||
|
||
## 보안 체크리스트
|
||
|
||
### [운영]
|
||
|
||
- [x] SSH 키 인증만 허용 (비밀번호 로그인 비활성화)
|
||
- [x] root SSH 로그인 비활성화
|
||
- [x] UFW 방화벽 활성화
|
||
- [x] MySQL root 원격 접근 차단 (auth_socket)
|
||
- [x] MySQL 앱 사용자 최소 권한 (codebridge)
|
||
- [x] .env 파일 권한 600 (api, admin, sales)
|
||
- [x] storage/ 디렉토리 권한 775
|
||
- [x] Nginx security.conf 스니펫 적용
|
||
- [x] PHP display_errors = Off (모든 pool)
|
||
- [x] Laravel APP_DEBUG=false, APP_ENV=production
|
||
- [x] Sales uploads/ PHP 실행 차단
|
||
- [x] Certbot 자동 갱신 (7/7 dry-run success)
|
||
- [x] fail2ban (SSH 브루트포스 방지)
|
||
- [x] Redis bind 127.0.0.1 (외부 접근 차단)
|
||
- [x] node_exporter CI/CD IP만 허용 (UFW)
|
||
|
||
### [CI/CD]
|
||
|
||
- [x] SSH 키 인증만 허용 (PasswordAuthentication no)
|
||
- [x] root SSH 로그인 비활성화 (PermitRootLogin no)
|
||
- [x] UFW 방화벽 활성화 (22, 80, 443만)
|
||
- [x] Jenkins 관리자 계정 변경 (hskwon)
|
||
- [x] Gitea 회원가입 비활성화 (DISABLE_REGISTRATION = true)
|
||
- [x] Grafana 익명 접근 비활성화 (allow_sign_up = false)
|
||
- [x] Prometheus 외부 접근 차단 (127.0.0.1:9090)
|
||
- [x] MySQL root 원격 접근 차단 (auth_socket)
|
||
- [x] fail2ban (sshd jail)
|
||
- [x] Certbot 자동 갱신
|
||
- [x] Jenkins SSH 키 ed25519 + Credential 등록
|
||
- [x] Webhook Secret 설정 (Gitea → Jenkins)
|
||
- [x] post-receive hook 토큰 보안 (600 권한)
|
||
|
||
---
|
||
|
||
## 개발서버 비교 (참고)
|
||
|
||
| 항목 | 개발서버 | 운영서버 |
|
||
|------|----------|---------|
|
||
| OS | Ubuntu 24.04.2 | Ubuntu 24.04 (kernel 6.8.0-100) |
|
||
| CPU/RAM | 2C / 3.8GB (스왑 없음) | 2C / 8GB + 스왑 4GB |
|
||
| PHP | 8.4.15 (+ 5.6, 7.3) | 8.4.18 |
|
||
| MySQL | **8.4.8** | **8.4.8** |
|
||
| Node.js | 22.17.1 | 22.17.1 |
|
||
| Nginx | 1.24.0 | 1.24.0 |
|
||
| Redis | - | 7.0.15 (512mb) |
|
||
| PHP-FPM | 단일 www pool | 4개 분리 (api/admin/sales/stage) |
|
||
| PM2 | fork ×1 (:3001) | cluster ×2 (:3000) + fork ×1 (:3100) |
|
||
| Supervisor | - | queue worker ×2 |
|
||
| UFW | **비활성** | 활성 |
|
||
| fail2ban | - | ✅ |
|
||
|
||
---
|
||
|
||
## [개발] PM2 설정
|
||
|
||
개발서버는 ecosystem.config.js 없이 PM2 CLI로 직접 관리합니다.
|
||
|
||
```bash
|
||
# 실행 (포트 3001, Gitea가 3000 사용)
|
||
cd /home/webservice/react && pm2 start npm --name sam-react -- start -- -p 3001
|
||
|
||
# 재부팅 자동 시작 등록
|
||
pm2 save
|
||
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u hskwon --hp /home/hskwon
|
||
```
|
||
|
||
| 이름 | 모드 | 포트 | 비고 |
|
||
|------|------|------|------|
|
||
| sam-react | fork | 3001 | Gitea가 3000 사용, Jenkins 배포 시 자동 restart |
|
||
|
||
---
|
||
|
||
## [개발] MySQL 8.0 → 8.4 업그레이드 절차
|
||
|
||
Ubuntu 24.04 APT 기본은 MySQL 8.0입니다. 8.4로 업그레이드하는 절차:
|
||
|
||
### 사전 준비
|
||
|
||
```bash
|
||
# 1. DB 백업
|
||
DB_PASS=$(grep DB_PASSWORD /home/webservice/mng/.env | head -1 | cut -d= -f2)
|
||
for db in sam chandj sam_stat; do
|
||
mysqldump -ucodebridge -p$DB_PASS --no-tablespaces --skip-triggers --skip-routines $db | gzip > /tmp/${db}_backup.sql.gz
|
||
done
|
||
|
||
# 2. 인증 방식 변환 (mysql_native_password → caching_sha2_password)
|
||
# 8.4에서 mysql_native_password가 deprecated
|
||
mysql -u debian-sys-maint -p'<debian-sys-maint_비밀번호>' -e "
|
||
ALTER USER 'codebridge'@'localhost' IDENTIFIED WITH caching_sha2_password BY '<비밀번호>';
|
||
ALTER USER 'chandj'@'localhost' IDENTIFIED WITH caching_sha2_password BY '<비밀번호>';
|
||
FLUSH PRIVILEGES;"
|
||
```
|
||
|
||
> debian-sys-maint 비밀번호: `/etc/mysql/debian.cnf` 참조
|
||
|
||
### 업그레이드 실행
|
||
|
||
```bash
|
||
# 3. MySQL 중지
|
||
sudo systemctl stop mysql
|
||
|
||
# 4. MySQL APT 레포 추가
|
||
wget -q https://dev.mysql.com/get/mysql-apt-config_0.8.33-1_all.deb -O /tmp/mysql-apt-config.deb
|
||
sudo DEBIAN_FRONTEND=noninteractive dpkg -i /tmp/mysql-apt-config.deb
|
||
|
||
# 5. 레포를 8.4-lts로 변경
|
||
sudo sed -i 's/mysql-8.0/mysql-8.4-lts/g' /etc/apt/sources.list.d/mysql.list
|
||
sudo apt-get update
|
||
|
||
# 6. 업그레이드 (기존 설정 유지)
|
||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -o Dpkg::Options::="--force-confold" mysql-server mysql-client
|
||
|
||
# 7. 시작 및 확인
|
||
sudo systemctl start mysql
|
||
mysql --version # → 8.4.x 확인
|
||
```
|
||
|
||
### GPG 키 만료 시
|
||
|
||
MySQL APT 레포의 GPG 키가 만료된 경우:
|
||
|
||
```bash
|
||
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
|
||
# 또는 allow-insecure 임시 허용 후 설치
|
||
```
|