# 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 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 ''; 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 = 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/.git/hooks/post-receive.d/push-to-cicd`): ```bash #!/bin/bash source /data/GIT/.cicd-env LOGFILE=/home/webservice/logs/cicd_push_.log CICD_REMOTE="https://${CICD_GITEA_USER}:${CICD_GITEA_TOKEN}@${CICD_GITEA_HOST}/SamProject/.git" mkdir -p /home/webservice/logs while read oldrev newrev refname; do BRANCH=$(echo "$refname" | sed 's|refs/heads/||') if [ "$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|2|0|' /var/lib/jenkins/config.xml # Agent 포트 활성화 (0 = 랜덤 포트) sudo sed -i 's|-1|0|' /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 \ -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 = [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'' -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 임시 허용 후 설치 ```