- 운영서버(211.117.60.189) 전체 설치 완료 문서화 - OS, MySQL 8.4.8, Redis 7.0.15, Nginx 1.24.0, PHP 8.4.18 - 7개 도메인 SSL (develop@codebridge-x.com), PM2 cluster - Supervisor queue worker, node_exporter, 보안 설정 - CI/CD 서버(110.10.147.46) 셋팅 가이드 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
42 KiB
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) |
배포관리자 운영 배포 워크플로우
# 배포관리자 로컬에서 (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 기본 셋팅 ✅
# 시스템 업데이트
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 시작 실패 — 사용하지 않음
# 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):
[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 쓰기를 차단하므로 반드시 주석 유지
sudo systemctl restart mysql
DB 및 사용자:
-- 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';
-- 관리자 (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)에서 실행:
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 추가:
[mysqld]
server-id = 1
log-bin = /var/log/mysql/mysql-bin
binlog-do-db = sam_production
binlog-do-db = chandj
CI/CD서버(Slave)에서 실행:
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 Redis 섹션 참조
④ Java (Jenkins 의존) ✅
설치 완료: OpenJDK 17.0.18
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 토큰 생성 완료
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 서비스 등록:
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):
[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
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/<repo>.git/hooks/post-receive.d/push-to-cicd
토큰 보안 (환경변수 파일):
# /data/GIT/.cicd-env (chmod 600, owner: git)
CICD_GITEA_TOKEN=<토큰>
CICD_GITEA_USER=hskwon
CICD_GITEA_HOST=git.sam.it.kr
hook 스크립트 구조 (공통 패턴):
#!/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
참고: 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 설치 완료
# 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에 필요한 도구 설치:
# 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):
-
https://ci.sam.it.kr접속 -
초기 비밀번호 입력
-
추천 플러그인 설치 + 추가 플러그인:
- Gitea Plugin (Gitea webhook 연동)
- SSH Agent Plugin (운영/개발서버 SSH 배포)
- Pipeline (Jenkinsfile 지원)
- Blue Ocean (모던 UI, 선택)
-
Jenkins SSH 키 설정:
# 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
- 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)
sudo apt install -y nginx certbot python3-certbot-nginx
Gitea 리버스 프록시 (/etc/nginx/sites-available/git.sam.it.kr):
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):
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):
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";
}
}
# 사이트 활성화 + 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
# 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):
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'
# 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)
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):
[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
sudo systemctl enable grafana-server
sudo systemctl start grafana-server
Grafana 초기 설정 (웹 UI):
- Data Source: Prometheus →
http://localhost:9090 - 대시보드 임포트: 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: <webhook_secret>
- Events: Push events
파이프라인: Laravel API (api/)
Jenkinsfile (api/Jenkinsfile):
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):
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)
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)
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)
# /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
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. 최종 점검
# 서비스 상태
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. 보안 체크리스트
- SSH 키 인증만 허용 (PasswordAuthentication no)
- root SSH 로그인 비활성화 (PermitRootLogin no)
- UFW 방화벽 활성화 (22, 80, 443만 허용)
- Jenkins 관리자 계정 변경 (hskwon)
- Jenkins CSRF 보호 활성화
- Gitea 회원가입 비활성화 (DISABLE_REGISTRATION = true)
- Grafana 익명 접근 비활성화 (allow_sign_up = false)
- Prometheus 외부 접근 차단 (127.0.0.1:9090 바인딩)
- MySQL root 원격 접근 차단 (auth_socket 인증)
- fail2ban 설치 (sshd jail 활성)
- Certbot 자동 갱신 (certbot.timer 활성)
- Jenkins SSH 키 ed25519 생성 + Credential 등록
- Webhook Secret 설정 (Gitea → Jenkins)
- 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. 결정 필요 사항
Gitea 이전 여부→ 선택적 브랜치 동기화 (post-receive hook)브랜치 전략→ main(수동/운영), stage(자동/Stage), develop(react만 자동)도메인 확정→ git.sam.it.kr, ci.sam.it.kr, monitor.sam.it.kr (SSL 발급 완료)- Jenkins 테스트 실행 여부: CI에서 phpunit/lint 실행 vs 배포만
- 알림 채널: Slack, 이메일, 카카오톡 등
- 개발서버 Gitea bare repo 경로 확인 (hook 설정을 위해)