# CI/CD 서버 셋팅 가이드 > 작성일: 2026-02-23 | 최종 수정: 2026-02-24 > 상태: 설치 완료 (Jenkinsfile 작성 + 실제 배포 테스트 남음) --- ## 1. 서버 구성 개요 ### 인프라 구조 ``` ┌──────────────────────────────────────────────────────────────┐ │ CI/CD서버 (2 vCPU / 8GB) │ │ Ubuntu 24.04 / IDC 클라우드 │ │ IP: 110.10.147.46 │ │ │ │ ┌──────────┐ ┌───────────┐ ┌───────────────────────────┐ │ │ │ Nginx │ │ Certbot │ │ UFW (22,80,443) │ │ │ │ (Proxy) │ │ (SSL) │ │ │ │ │ └────┬─────┘ └───────────┘ └───────────────────────────┘ │ │ │ │ │ ┌────┴───────────────────────────────────────────────────┐ │ │ │ Virtual Hosts │ │ │ │ │ │ │ │ git.sam.it.kr ──────────→ Gitea (:3000) │ │ │ │ ci.sam.it.kr ───────────→ Jenkins (:8080) │ │ │ │ monitor.sam.it.kr ──────→ Grafana (:3100) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ │ │ Gitea │ │ Jenkins │ │ MySQL 8.4 │ │ │ │ (운영 Git) │ │ (CI/CD) │ │ (Gitea DB + 백업) │ │ │ └────────────┘ └────────────┘ └────────────────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ Prometheus │ │ Grafana │ │ │ │ (:9090) │ │ (:3100) │ │ │ └──────────────┘ └──────────────┘ │ └───────────────────────────────────────────────────────────────┘ ``` ### 도메인 매핑 | 도메인 | 서비스 | 포트 | SSL | |--------|--------|------|-----| | git.sam.it.kr | Gitea | 3000 | Let's Encrypt | | ci.sam.it.kr | Jenkins | 8080 | Let's Encrypt | | monitor.sam.it.kr | Grafana | 3100 | Let's Encrypt | ### Git 동기화 전략 **방침**: 개발서버 Gitea(origin) 유지 + CI/CD Gitea에 **선택적 브랜치 push** (post-receive hook) > Gitea Push Mirror는 전체 브랜치를 미러링하므로 사용하지 않음. > 대신 개발서버 Gitea의 **post-receive hook**으로 필요한 브랜치만 CI/CD Gitea에 push. ``` 개발자 로컬 │ git push origin (develop/stage/main) ▼ 개발서버 Gitea (114.203.209.83:3000) ← 모든 개발자의 origin │ ├─ develop push 시 │ ├─ api/mng/sales: 기존 post-update hook (개발서버 pull) ← 현행 유지 │ └─ react: hook → CI/CD Gitea push → Jenkins 빌드 → 개발서버 배포 │ ├─ stage push 시 │ ├─ react: hook → CI/CD Gitea push → Jenkins 빌드 → 운영서버 Stage 배포 │ └─ api: hook → CI/CD Gitea push → Jenkins → 운영서버 Stage pull │ └─ main push 시 (react/mng/api) └─ ❌ CI/CD Gitea에 자동 push 안함 → 배포관리자가 수동으로 CI/CD Gitea에 push → Jenkins 자동 배포 별도 처리: sales/www(landing): Push Mirror 또는 hook → CI/CD Gitea → Jenkins → 운영서버 pull ``` ### 브랜치별 배포 정책 상세 | 브랜치 | 저장소 | CI/CD Gitea 동기화 | Jenkins 배포 | 배포 대상 | |--------|--------|-------------------|-------------|----------| | **stage** | react | 자동 (hook) | 빌드 + rsync | 운영서버 Stage | | **stage** | api | 자동 (hook) | SSH pull | 운영서버 Stage | | **main** | react | 수동 (배포관리자) | 빌드 + rsync | 운영서버 Production | | **main** | mng | 수동 (배포관리자) | SSH deploy | 운영서버 Production | | **main** | api | 수동 (배포관리자) | SSH deploy | 운영서버 Production | | **main** | sales | 자동 (hook/mirror) | SSH pull | 운영서버 Production | | **main** | www | 자동 (hook/mirror) | SSH pull | 운영서버 Production | | **develop** | react | 자동 (hook) | 빌드 → 개발서버 배포 | 개발서버 | | **develop** | api | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | | **develop** | mng | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | | **develop** | sales | ❌ (현행 유지) | ❌ | 개발서버 (post-update hook) | ### 배포관리자 운영 배포 워크플로우 ```bash # 배포관리자 로컬에서 (react/mng/api 저장소) # CI/CD Gitea를 별도 remote로 등록 (1회) git remote add production https://git.sam.it.kr/SamProject/sam-api.git # 운영 배포 시: main 브랜치를 CI/CD Gitea에 push git push production main # → CI/CD Gitea webhook → Jenkins → 운영서버 자동 배포 ``` 현재 Git remote 현황 (개발서버): ``` sam-api: http://114.203.209.83:3000/SamProject/sam-api.git sam-manage: http://114.203.209.83:3000/SamProject/sam-manage.git sam-react-prod: http://114.203.209.83:3000/SamProject/sam-react-prod.git sam-sales: http://114.203.209.83:3000/SamProject/sam-sales.git sam-docs: http://114.203.209.83:3000/SamProject/sam-docs.git sam-design: http://114.203.209.83:3000/SamProject/sam-design.git sam-planning: http://114.203.209.83:3000/SamProject/sam-planning.git ``` --- ## 2. 메모리 배분 계획 (8GB) | 서비스 | 할당 | 설정 | 비고 | |--------|------|------|------| | Jenkins | ~2.0GB | -Xmx2048m | Java 기반, 빌드 시 메모리 소모 큼 | | MySQL 8.4 | ~1.5GB | innodb_buffer_pool_size=1536M | Gitea DB + 운영 백업 | | Gitea | ~0.5GB | - | Go 기반, 가벼움 | | Prometheus | ~0.5GB | --storage.tsdb.retention.time=30d | 메트릭 저장 30일 | | Grafana | ~0.3GB | - | 대시보드 시각화 | | Nginx | ~0.1GB | - | 리버스 프록시 | | node_exporter | ~0.01GB | - | 자체 모니터링 | | OS + 여유 | ~3.1GB | 스왑 4GB | 안전 마진 | | **합계** | **~8GB** | | **스왑 4GB 백업** | --- ## 3. 설치 순서 ### ① OS 기본 셋팅 ✅ ```bash # 시스템 업데이트 sudo apt update && sudo apt upgrade -y # 기본 패키지 sudo apt install -y curl wget git unzip vim htop net-tools software-properties-common # 타임존 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 enable # webservice 사용자 그룹 (배포 스크립트용) sudo groupadd -f webservice sudo usermod -aG webservice hskwon ``` ### ② MySQL 8.4 ✅ > **설치 완료**: MySQL 8.4.8 (GPG 키 만료 이슈로 Ubuntu keyserver 경유 설치) > **주의**: `validate_password.policy` 설정은 플러그인 미로딩 시 MySQL 시작 실패 — 사용하지 않음 ```bash # 1. 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 # 2. 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 # 3. 설치 sudo apt update sudo apt install -y mysql-server ``` **성능 튜닝** (`/etc/mysql/mysql.conf.d/sam-tuning.cnf`): ```ini [mysqld] innodb_buffer_pool_size = 1536M innodb_redo_log_capacity = 536870912 innodb_flush_log_at_trx_commit = 2 max_connections = 50 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 # Replication (Slave) — 운영서버 Master 설정 완료 후 활성화 # server-id = 2 # relay-log = /var/log/mysql/mysql-relay-bin # read-only = 1 ← Gitea 로컬 DB 쓰기를 차단하므로 반드시 주석 유지 ``` ```bash sudo systemctl restart mysql ``` **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'; -- 관리자 (hskwon) - auth_socket 인증 CREATE USER 'hskwon'@'localhost' IDENTIFIED WITH auth_socket; GRANT ALL PRIVILEGES ON *.* TO 'hskwon'@'localhost' WITH GRANT OPTION; FLUSH PRIVILEGES; ``` > **참고**: root는 auth_socket 인증 (비밀번호 없이 `sudo mysql`로 접근) ### ②-slave MySQL Replication 설정 (운영 DB 백업) > 운영서버 MySQL Master → CI/CD 서버 MySQL Slave **운영서버(Master)에서 실행:** ```sql CREATE USER 'repl_user'@'110.10.147.46' IDENTIFIED BY '<복제_비밀번호>'; GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'110.10.147.46'; FLUSH PRIVILEGES; SHOW MASTER STATUS; -- → File: mysql-bin.XXXXXX, Position: XXXX 기록 ``` **운영서버 my.cnf 추가:** ```ini [mysqld] server-id = 1 log-bin = /var/log/mysql/mysql-bin binlog-do-db = sam_production binlog-do-db = chandj ``` **CI/CD서버(Slave)에서 실행:** ```sql CHANGE REPLICATION SOURCE TO SOURCE_HOST='211.117.60.189', SOURCE_USER='repl_user', SOURCE_PASSWORD='<복제_비밀번호>', SOURCE_LOG_FILE='mysql-bin.XXXXXX', SOURCE_LOG_POS=XXXX; START REPLICA; SHOW REPLICA STATUS\G -- Slave_IO_Running: Yes, Slave_SQL_Running: Yes 확인 ``` ### ③ Redis → 운영서버에 설치 > **Redis는 운영서버(211.117.60.189)에 설치.** > 상세 설정: [production-server-setup.md](./production-server-setup.md) Redis 섹션 참조 ### ④ Java (Jenkins 의존) ✅ > **설치 완료**: OpenJDK 17.0.18 ```bash sudo apt install -y openjdk-17-jre-headless java -version ``` ### ⑤ Gitea ✅ > **설치 완료**: Gitea 1.22.6 > - 관리자: hskwon / kent@codebridge-x.com > - Organization: SamProject > - 저장소: sam-api, sam-manage, sam-react-prod, sam-sales, sam-landing > - 회원가입 비활성화, API 토큰 생성 완료 ```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 서비스 등록:** ```bash sudo tee /etc/systemd/system/gitea.service > /dev/null << 'EOF' [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 EOF sudo systemctl daemon-reload sudo systemctl enable gitea ``` **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 ``` ```bash sudo systemctl start gitea # 확인: curl http://localhost:3000 ``` **CI/CD Gitea 초기 설정:** ``` 1. https://git.sam.it.kr 웹 설치 마법사 완료 2. 관리자 계정 생성 3. Organization "SamProject" 생성 4. 빈 저장소 생성: - sam-api, sam-manage, sam-react-prod - sam-sales, sam-landing (www) 5. Jenkins Webhook용 API 토큰 생성 ``` ### ⑤-hook 개발서버 post-receive hook 설정 (선택적 브랜치 동기화) ✅ > **설치 완료**: 개발서버 Gitea bare repository에 post-receive hook 추가 > stage/develop(react만) push 시 CI/CD Gitea에 해당 브랜치만 push. > 기존 post-update hook (개발서버 pull)은 **그대로 유지**. **개발서버 Gitea bare repo 경로:** ``` /data/GIT/samproject/.git/hooks/post-receive.d/push-to-cicd ``` **토큰 보안 (환경변수 파일):** ```bash # /data/GIT/.cicd-env (chmod 600, owner: git) CICD_GITEA_TOKEN=<토큰> CICD_GITEA_USER=hskwon CICD_GITEA_HOST=git.sam.it.kr ``` **hook 스크립트 구조 (공통 패턴):** ```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 ``` > **참고**: Gitea는 `hooks/post-receive.d/` 디렉토리의 스크립트를 자동 실행. > 기존 post-update hook (pull 스크립트)과 별도로 동작. **동기화 요약:** | 저장소 | hook 대상 브랜치 | 동작 | |--------|-----------------|------| | sam-react-prod | stage, develop | CI/CD Gitea에 push | | sam-api | stage | CI/CD Gitea에 push | | sam-sales | main | CI/CD Gitea에 push | | sam-landing | main | CI/CD Gitea에 push | | sam-manage | ❌ 없음 | main만 사용, 배포관리자 수동 push | ### ⑥ Jenkins ✅ > **설치 완료**: Jenkins 2.541.2 > - 관리자: hskwon > - 플러그인: Gitea, SSH Agent, Pipeline Stage View, Workflow Aggregator, Blue Ocean, NodeJS > - Gitea 서버 연동 완료 (https://git.sam.it.kr) > - SSH Credential 등록: `deploy-ssh-key` (운영/개발 서버 모두 접속 확인) > - Gitea API Token Credential 등록: `gitea-api-token` > - Node.js 22.22.0 설치 완료 ```bash # Jenkins GPG 키 + APT Repository # 주의: 공식 2023 키가 유효하지 않을 수 있음 → Ubuntu keyserver에서 획득 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 # 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 # 초기 관리자 비밀번호 sudo cat /var/lib/jenkins/secrets/initialAdminPassword ``` **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 ``` **Jenkins 초기 설정 (웹 UI):** 1. `https://ci.sam.it.kr` 접속 2. 초기 비밀번호 입력 3. 추천 플러그인 설치 + 추가 플러그인: - **Gitea Plugin** (Gitea webhook 연동) - **SSH Agent Plugin** (운영/개발서버 SSH 배포) - **Pipeline** (Jenkinsfile 지원) - **Blue Ocean** (모던 UI, 선택) 4. Jenkins SSH 키 설정: ```bash # Jenkins 사용자로 SSH 키 생성 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" # 개발서버에도 공개키 등록 (develop react 빌드 배포용) ssh sam-dev "echo '$(cat /var/lib/jenkins/.ssh/id_ed25519.pub)' >> /home/hskwon/.ssh/authorized_keys" # SSH 호스트 키 등록 (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 ``` 5. Jenkins Credentials 설정 (CLI로 완료): - `deploy-ssh-key`: SSH 키 (hskwon@운영/개발 서버 공용) - `gitea-api-token`: Gitea API 토큰 ### ⑦ Nginx + Certbot ✅ > **설치 완료**: Nginx + Let's Encrypt SSL (git/ci/monitor.sam.it.kr) ```bash sudo apt install -y nginx certbot python3-certbot-nginx ``` **Gitea 리버스 프록시** (`/etc/nginx/sites-available/git.sam.it.kr`): ```nginx server { listen 80; server_name git.sam.it.kr; client_max_body_size 100M; 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; } } ``` **Jenkins 리버스 프록시** (`/etc/nginx/sites-available/ci.sam.it.kr`): ```nginx 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_set_header X-Forwarded-Port $server_port; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 90; proxy_buffering off; } } ``` **Grafana 리버스 프록시** (`/etc/nginx/sites-available/monitor.sam.it.kr`): ```nginx 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 # 사이트 활성화 + SSL 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 sudo certbot renew --dry-run ``` ### ⑧ Prometheus + node_exporter ✅ > **설치 완료**: Prometheus 2.51.0, node_exporter 1.8.2 ```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 ``` **Prometheus 설정** (`/etc/prometheus/prometheus.yml`): ```yaml global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'sam-prod' static_configs: - targets: ['211.117.60.189:9100'] labels: server: 'production' - job_name: 'sam-cicd' static_configs: - targets: ['localhost:9100'] labels: server: 'cicd' ``` ```bash # Prometheus systemd sudo tee /etc/systemd/system/prometheus.service > /dev/null << 'EOF' [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=:9090 Restart=always [Install] WantedBy=multi-user.target EOF # node_exporter 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 prometheus node_exporter sudo systemctl start prometheus node_exporter ``` ### ⑨ Grafana ✅ > **설치 완료**: Grafana (포트 3100) ```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 ``` **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 ``` **Grafana 초기 설정 (웹 UI):** 1. Data Source: Prometheus → `http://localhost:9090` 2. 대시보드 임포트: **Node Exporter Full** (ID: 1860) --- ## 4. 배포 파이프라인 설계 ### Webhook 설정 (CI/CD Gitea → Jenkins) 각 저장소에 Webhook 추가 (CI/CD Gitea 웹 UI): ``` Repository Settings → Webhooks → Add Webhook (Gitea) - URL: https://ci.sam.it.kr/gitea-webhook/post - Content Type: application/json - Secret: - Events: Push events ``` ### 파이프라인: Laravel API (api/) **Jenkinsfile** (`api/Jenkinsfile`): ```groovy pipeline { agent any environment { DEPLOY_USER = 'hskwon' APP_NAME = 'api' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') } stages { stage('Checkout') { steps { checkout scm } } // ── main → 운영서버 (배포관리자 수동 push 후 트리거) ── stage('Deploy Production') { when { branch 'main' } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ ssh ${DEPLOY_USER}@211.117.60.189 ' cd /home/webservice/api/releases && git clone --depth 1 --branch main https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} && ln -sfn /home/webservice/api/shared/storage /home/webservice/api/releases/${RELEASE_ID}/storage && ln -sfn /home/webservice/api/shared/.env /home/webservice/api/releases/${RELEASE_ID}/.env && cd /home/webservice/api/releases/${RELEASE_ID} && composer install --no-dev --optimize-autoloader --no-interaction && php artisan config:cache && php artisan route:cache && php artisan view:cache && php artisan migrate --force && ln -sfn /home/webservice/api/releases/${RELEASE_ID} /home/webservice/api/current && sudo systemctl reload php8.4-fpm && sudo supervisorctl restart sam-queue-worker:* && cd /home/webservice/api/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true ' """ } } } // ── stage → 운영서버 Stage ── stage('Deploy Stage') { when { branch 'stage' } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ ssh ${DEPLOY_USER}@211.117.60.189 ' cd /home/webservice/api-stage/releases && git clone --depth 1 --branch stage https://git.sam.it.kr/SamProject/sam-api.git ${RELEASE_ID} && ln -sfn /home/webservice/api-stage/shared/storage /home/webservice/api-stage/releases/${RELEASE_ID}/storage && ln -sfn /home/webservice/api-stage/shared/.env /home/webservice/api-stage/releases/${RELEASE_ID}/.env && cd /home/webservice/api-stage/releases/${RELEASE_ID} && composer install --no-dev --optimize-autoloader --no-interaction && php artisan config:cache && php artisan route:cache && php artisan view:cache && php artisan migrate --force && ln -sfn /home/webservice/api-stage/releases/${RELEASE_ID} /home/webservice/api-stage/current && sudo systemctl reload php8.4-fpm && cd /home/webservice/api-stage/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true ' """ } } } // develop → Jenkins 관여 안함 (기존 post-update hook 유지) } post { success { echo "✅ api 배포 완료 (${env.BRANCH_NAME})" } failure { echo "❌ api 배포 실패 (${env.BRANCH_NAME})" // 운영/Stage 실패 시 이전 릴리즈로 롤백 script { if (env.BRANCH_NAME in ['main', 'stage']) { def baseDir = env.BRANCH_NAME == 'main' ? '/home/webservice/api' : '/home/webservice/api-stage' sshagent(credentials: ['deploy-ssh-key']) { sh """ ssh ${DEPLOY_USER}@211.117.60.189 ' PREV=\$(ls -1dt ${baseDir}/releases/*/ | sed -n "2p" | xargs basename) && [ -n "\$PREV" ] && ln -sfn ${baseDir}/releases/\$PREV ${baseDir}/current && sudo systemctl reload php8.4-fpm ' """ } } } } } } ``` ### 파이프라인: Laravel Admin (mng/) > API와 동일 구조. 차이점: > - main만 (stage/develop 없음 — develop은 기존 post-update hook) > - Queue Worker 재시작 불필요 > - npm run build 추가 (Blade + Vite) ### 파이프라인: Next.js React (react/) **Jenkinsfile** (`react/Jenkinsfile`): ```groovy pipeline { agent any environment { DEPLOY_USER = 'hskwon' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') } stages { stage('Checkout') { steps { checkout scm } } stage('Build') { steps { sh 'npm ci && npm run build' } } // ── main → 운영서버 (배포관리자 수동 push 후 트리거) ── stage('Deploy Production') { when { branch 'main' } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}' rsync -az --delete \ .next/ package.json package-lock.json next.config.* public/ node_modules/ \ ${DEPLOY_USER}@211.117.60.189:/home/webservice/react/releases/${RELEASE_ID}/ ssh ${DEPLOY_USER}@211.117.60.189 ' ln -sfn /home/webservice/react/shared/.env.local /home/webservice/react/releases/${RELEASE_ID}/.env.local && ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && cd /home/webservice && pm2 reload sam-front && cd /home/webservice/react/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true ' """ } } } // ── stage → 운영서버 Stage ── stage('Deploy Stage') { when { branch 'stage' } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ ssh ${DEPLOY_USER}@211.117.60.189 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}' rsync -az --delete \ .next/ package.json package-lock.json next.config.* public/ node_modules/ \ ${DEPLOY_USER}@211.117.60.189:/home/webservice/react-stage/releases/${RELEASE_ID}/ ssh ${DEPLOY_USER}@211.117.60.189 ' ln -sfn /home/webservice/react-stage/shared/.env.local /home/webservice/react-stage/releases/${RELEASE_ID}/.env.local && ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current && cd /home/webservice && pm2 reload sam-front-stage && cd /home/webservice/react-stage/releases && ls -1dt */ | tail -n +3 | xargs rm -rf 2>/dev/null || true ' """ } } } // ── develop → 개발서버 (CI/CD에서 빌드 후 배포) ── stage('Deploy Development') { when { branch 'develop' } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ # 빌드 결과물을 개발서버로 전송 rsync -az --delete \ .next/ package.json package-lock.json next.config.* public/ node_modules/ \ ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ ssh ${DEPLOY_USER}@114.203.209.83 ' cd /home/webservice/react && pm2 restart sam-front ' """ } } } } post { success { echo "✅ react 배포 완료 (${env.BRANCH_NAME})" } failure { echo "❌ react 배포 실패 (${env.BRANCH_NAME})" } } } ``` ### 파이프라인: Sales (레거시 PHP) ```groovy pipeline { agent any environment { DEPLOY_USER = 'hskwon' } stages { // main → 운영서버 (hook 자동) stage('Deploy Production') { when { branch 'main' } steps { sshagent(credentials: ['deploy-ssh-key']) { sh "ssh ${DEPLOY_USER}@211.117.60.189 'cd /home/webservice/sales && git pull origin main'" } } } // develop → 개발서버는 기존 post-update hook 유지 } } ``` ### 파이프라인: Landing (www) ```groovy pipeline { agent any environment { DEPLOY_USER = 'hskwon' } stages { stage('Deploy Production') { when { branch 'main' } steps { sshagent(credentials: ['deploy-ssh-key']) { sh "ssh ${DEPLOY_USER}@211.117.60.189 'cd /home/webservice/landing && git pull origin main'" } } } } } ``` --- ## 5. 배포 흐름도 ``` 개발자 로컬 │ git push origin (develop / stage / main) ▼ ┌──────────────────────────────────────────────────────────────┐ │ 개발서버 Gitea (114.203.209.83:3000) ← 모든 개발자 origin │ │ │ │ post-receive hooks: │ │ │ │ ┌─ develop push ────────────────────────────────────────┐ │ │ │ react → hook: CI/CD Gitea push ──→ Jenkins 빌드 │ │ │ │ → 빌드 결과 rsync → 개발서버 배포 │ │ │ │ api → 기존 post-update hook (pull + migrate) │ │ │ │ mng → 기존 post-update hook (pull + build) │ │ │ │ sales → 기존 post-update hook (pull) │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ ┌─ stage push ──────────────────────────────────────────┐ │ │ │ react → hook: CI/CD Gitea push ──→ Jenkins 빌드 │ │ │ │ → rsync → 운영서버 Stage + PM2 reload │ │ │ │ api → hook: CI/CD Gitea push ──→ Jenkins │ │ │ │ → 운영서버 Stage Release + 심링크 │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ ┌─ main push (sales/www만 자동) ────────────────────────┐ │ │ │ sales → hook: CI/CD Gitea push ──→ Jenkins │ │ │ │ → 운영서버 pull │ │ │ │ www → hook: CI/CD Gitea push ──→ Jenkins │ │ │ │ → 운영서버 pull │ │ │ │ react/mng/api → ❌ 자동 push 안함 │ │ │ └───────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────────┘ ┌─ 운영 배포 (main - react/mng/api) ──────────────────────────┐ │ │ │ 배포관리자 로컬 │ │ │ git push production main (CI/CD Gitea remote) │ │ ▼ │ │ CI/CD Gitea (git.sam.it.kr) │ │ │ Webhook │ │ ▼ │ │ Jenkins → 운영서버 배포 │ │ react: CI/CD 빌드 → rsync → PM2 reload │ │ api: Release + 심링크 → PHP-FPM reload │ │ mng: Release + 심링크 → PHP-FPM reload │ │ │ └───────────────────────────────────────────────────────────────┘ ``` ### 환경별 배포 비교 | 항목 | 운영 (main) | Stage (stage) | 개발 (develop) | |------|------------|---------------|----------------| | **트리거** | 배포관리자 수동 push | 자동 (hook) | react만 자동 (hook), 나머지 기존 hook | | **react 전략** | CI/CD 빌드 → rsync | CI/CD 빌드 → rsync | CI/CD 빌드 → rsync | | **api 전략** | Release + 심링크 | Release + 심링크 | 기존 post-update (pull) | | **mng 전략** | Release + 심링크 | - | 기존 post-update (pull + build) | | **롤백** | 이전 릴리즈 심링크 | 이전 릴리즈 심링크 | git revert | | **릴리즈 보관** | 최근 5개 | 최근 3개 | - | --- ## 6. 백업 자동화 ### DB 일일 백업 (CI/CD 서버 crontab) ```bash # /home/hskwon/scripts/backup-db.sh #!/bin/bash set -e BACKUP_DIR="/home/hskwon/backups/mysql" DATE=$(date +%Y%m%d_%H%M%S) RETENTION_DAYS=14 mkdir -p $BACKUP_DIR mysqldump --single-transaction --routines --triggers \ sam_production > $BACKUP_DIR/sam_production_$DATE.sql mysqldump --single-transaction --routines --triggers \ chandj > $BACKUP_DIR/chandj_$DATE.sql gzip $BACKUP_DIR/sam_production_$DATE.sql gzip $BACKUP_DIR/chandj_$DATE.sql find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete echo "$(date): Backup completed" >> $BACKUP_DIR/backup.log ``` ```bash chmod +x /home/hskwon/scripts/backup-db.sh # crontab (매일 새벽 3시) (crontab -l 2>/dev/null; echo "0 3 * * * /home/hskwon/scripts/backup-db.sh") | crontab - ``` --- ## 7. 최종 점검 ```bash # 서비스 상태 sudo systemctl status nginx jenkins gitea mysql prometheus grafana-server node_exporter # 방화벽 / 메모리 / 디스크 / 포트 sudo ufw status verbose free -h df -h sudo ss -tlnp # 웹 접속 테스트 curl -sI https://ci.sam.it.kr curl -sI https://git.sam.it.kr curl -sI https://monitor.sam.it.kr # SSL sudo certbot certificates ``` --- ## 8. 보안 체크리스트 - [x] SSH 키 인증만 허용 (PasswordAuthentication no) - [x] root SSH 로그인 비활성화 (PermitRootLogin no) - [x] UFW 방화벽 활성화 (22, 80, 443만 허용) - [x] Jenkins 관리자 계정 변경 (hskwon) - [ ] Jenkins CSRF 보호 활성화 - [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 자동 갱신 (certbot.timer 활성) - [x] Jenkins SSH 키 ed25519 생성 + Credential 등록 - [x] Webhook Secret 설정 (Gitea → Jenkins) - [x] post-receive hook 토큰 보안 (/data/GIT/.cicd-env 파일 참조, 600 권한) --- ## 9. 설치 순서 요약 | 순서 | 항목 | 예상 시간 | 의존성 | |------|------|----------|--------| | ① | OS 기본 셋팅 | 15분 | - | | ② | MySQL 8.4 | 20분 | ① | | ②-s | MySQL Replication | 30분 | ② + 운영서버 MySQL | | ④ | Java 17 | 5분 | ① | | ⑤ | Gitea 설치 + 초기 설정 | 30분 | ② | | ⑤-h | 개발서버 post-receive hook 설정 | 30분 | ⑤ | | ⑥ | Jenkins | 20분 | ④ | | ⑦ | Nginx + SSL | 20분 | ⑤⑥ | | ⑧ | Prometheus + node_exporter | 15분 | ① | | ⑨ | Grafana | 15분 | ⑧ | | ⑩ | Jenkins 파이프라인 + Webhook 설정 | 1시간 | ⑥⑦ | | ⑪ | 백업 자동화 | 15분 | ②-s | | ⑫ | 최종 점검 + 보안 | 30분 | 전체 | **총 예상 시간: 5~6시간** --- ## 10. 결정 필요 사항 - [x] ~~Gitea 이전 여부~~ → 선택적 브랜치 동기화 (post-receive hook) - [x] ~~브랜치 전략~~ → main(수동/운영), stage(자동/Stage), develop(react만 자동) - [x] ~~도메인 확정~~ → git.sam.it.kr, ci.sam.it.kr, monitor.sam.it.kr (SSL 발급 완료) - [ ] **Jenkins 테스트 실행 여부**: CI에서 phpunit/lint 실행 vs 배포만 - [ ] **알림 채널**: Slack, 이메일, 카카오톡 등 - [ ] **개발서버 Gitea bare repo 경로 확인** (hook 설정을 위해)