Files
sam-docs/deploys/cicd-server-setup.md
권혁성 a3ef921a4f docs: 운영/CI/CD 서버 셋팅 가이드 추가
- 운영서버(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>
2026-02-24 10:02:48 +09:00

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):

  1. https://ci.sam.it.kr 접속

  2. 초기 비밀번호 입력

  3. 추천 플러그인 설치 + 추가 플러그인:

    • Gitea Plugin (Gitea webhook 연동)
    • SSH Agent Plugin (운영/개발서버 SSH 배포)
    • Pipeline (Jenkinsfile 지원)
    • Blue Ocean (모던 UI, 선택)
  4. 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
  1. 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):

  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: <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 설정을 위해)