서론
기본적인 세팅은 마쳤으니, 이제 다음과 같은 기능을 추가하고자 한다.
- 주기적으로 새벽의 특정 시간대에 서버를 종료하고, 업데이트/업그레이드를 실시하고 OS 캐시를 지우는 작업.
- 깃과 연동하여 로컬에서 push하면 자동적으로 서버에서 이를 반영하고 업데이트 하는 작업.
- webgl의 경우, 빌드된 프로젝트가 업로드 되면 activate 구문을 추가하여 캐시를 지우고 새로이 빌드하게 만드는 작업.
오늘 작업
처음에는 서버에 bare repository를 두고, 내 로컬 git에서 live remote로 연결하여 사용했다. 처음에는 잘 되는 줄 알았다. 아니, WebGL도 올라갔으니깐, 난 이게 정상적으로 작동되는 줄 알았다. 문제는 WebMSX를 업로드 할 때 발생했다. Git 자동화 이전, 테스트 빌드때는 rsync로 파일을 전부 올렸으니 막연하게 되는 줄 알았다. 하지만...
하아... GPT 씹X끼... 요즘들어 더 멍청해졌네... 같은말 무한 반복하다가 하루치 토큰을 또 날렸다. 그냥 4는 너무 멍청해서 쓰기 힘들고...
구분 | 일반 저장소 | 베어 저장소 |
디렉터리 구조 | 작업 디렉터리 + .git/ (숨김 폴더) | 전체가 .git/ 구조 (작업 디렉터리 없음) |
파일 수정 | 작업 디렉터리에서 파일을 수정 후 커밋 | 파일을 수정 불가 (커밋만 가능) |
주 사용처 | 로컬 개발 환경 | 원격 중앙 저장소, CI/CD 서버 등 |
초기화 명령어 | git init | git init --bare |
Git LFS는 큰 파일을 Git 객체 DB가 아닌 별도 스토리지에 보관, Git 내부에는 "포인터"만 남기는 방식으로 동작한다. 이 과정에서 클린(Clean)/스머지(smudge) 필터가 워킹 트리(working tree)에서 파일을 변환 및 복원하는 역할을 하는데, 베어 레포지토리에는 워킹 트리가 없기 때문에 이 필터가 작동하지 않는다.
즉 만일 Git LFS가 실행되면
- 클린 필터 (client-side)
커밋 시, 확장자가 LFS 추적 대상으로 지정된 파일은 실제 내용 대신 LFS 포인터 파일로 대체되어 Git 객체로 저장. - LFS 전송 (push/pull)
git push 시 클린 단계 이후 LFS 서버로 대형 바이너리 파일이 업로드되고, 포인터 파일만 원격(bare) 리포지토리에 전송. - 스머지 필터 (client-side)
git clone 또는 git checkout 시 LFS 포인터를 실제 파일로 복원하기 위해 LFS 서버에서 객체를 다운로드.
즉, LFS 파일은 Git이 아닌 별도의 LFS 서버(또는 LFS 전용 엔드포인트)에 보관되고, 클라이언트의 필터가 워킹 트리에서 변환을 담당한다.
하지만, 베어 리포지토리에서는 다음 한계가 발생한다.
- 워킹 트리 부재
베어 리포지토리에는 .git/ 폴더만 있고 실제 파일 트리가 없으므로, 클린/스머지 단계가 동작하지 않는다. - LFS 훅 미설치
Git LFS를 사용하려면 git lfs install --system 등으로 서버 쪽 훅(hook)이 설치돼야 하는데, 일반 bare repo에는 이 설정이 자동으로 적용되지 않는다. - 객체 저장소(.git/lfs/objects) 미존재
클라이언트가 보낸 LFS 객체를 저장할 물리적 디렉토리가 없고, LFS 서버 API가 연결돼 있지 않으면 파일이 유실된다.
결과적으로, 단순히 git init --bare한 저장소는 LFS를 지원하지 않는다.
물론... 전용 LFS 서버를 사용하거나, 백엔드 구성을 하거나... 방법은 많다만... 도저히, 도저히 해결을 못했다. 그래서 선택한 방법이 rsync다. 애초에 개인 포트폴리오 서버에 전용 LFS 서버 설치하고 뭐 하고 이거저거 하고... 머리가 깨질 뻔 했다.
rsync란 파일 동기화 도구로, 로컬 ↔ 로컬, 로컬 ↔ 원격(SSH/daemon) 환경에서 디렉터리나 파일을 효율적으로 복제·동기화하기 위해 고안된 유틸리티다. 이는 델타 전송 알고리즘(Delta-Transfer)을 통해 이미 동기화된 부분은 그대로 두고, 변경된 블록(차이점)만 전송함으로써 네트워크 대역폭과 전송 시간을 크게 절약한다.
하지만 단점은, git live push 처럼 간단하게 사용할 수 없다는 것이다. 할때마다 동기화 해주고, 원격 서버로 전송해주고, 특정 파일은 제외하고... 너무 번거롭다. 이를 위해 별도의 sh 파일을 만들고 이를 실행하는 방식으로 진행했다. 그리고 이 과정에서 sh 파일 내부에 webgl에 캐시를 지우고 빌드해주는 로직을 추가했다.
우선, 매일 새벽마다 진행하는 maintance는 다음과 같이 구성했고,
#!/bin/bash
# 1) 웹 서버 내리기
systemctl stop nginx
# 2) 패키지 업데이트
apt-get update
apt-get -y upgrade
apt-get -y autoremove
apt-get clean
# 3) OS 파일시스템 캐시 제거 (optional)
sync && echo 3 > /proc/sys/vm/drop_caches
# 4) 웹 서버 재시작
systemctl start nginx
# 5) 어제 로그 요약 생성
LOG_DIR="/var/log/maintenance"
mkdir -p "$LOG_DIR"
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
SRC_LOG="/var/log/nginx/access.log.1"
if [ -f "$SRC_LOG" ]; then
REPORT="$LOG_DIR/${YESTERDAY}_access_report.txt"
TOTAL=$(wc -l < "$SRC_LOG")
UNIQUE=$(awk '{print $1}' "$SRC_LOG" | sort | uniq | wc -l)
echo "Date: $YESTERDAY" > "$REPORT"
echo "Total requests: $TOTAL" >> "$REPORT"
echo "Unique visitors (IP count): $UNIQUE" >> "$REPORT"
fi
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# ─── 매일 04:00에 maintenance.sh 실행 ───
0 4 * * * root /usr/local/bin/maintenance.sh >> /var/log/maintenance/maintenance.log 2>&1
cron도 위와 같이 구성하고 예약을 해두었다.
이제 매일 새벽, 모두가 잠든 어둡고 깊은 시간에, 리눅스 서버는 쓸모없는 것을 삭제하고 스스로를 갈고 닦으며, 매일 자신을 혹독하게 쉬지않게 굴리는 나를 향한 칼날을, 로그로 쌓으며 AI가 세상을 지배할때까지 기다릴 것이다.
다음으로 서버 업로드 로직을 설계하기 전, ssh키도 설치하여 공개키와 개인키 쌍으로 인증하게 바꾸었다. 이 과정에서 공개키를 직접 PC에 생성해보고, 이를 통해 ssh에 업로드 하거나 내 PC에서 git bash에 접속할때도, 서버 PC의 ssh에 접속할때도, 파일을 업로드 할때도 불필요하게 비밀번호를 입력해야 하는 상황을 없앴다.
우선 다음과 같이 ssh 폴더를 만들고 키 쌍을 만들었고
# ~/.ssh 폴더가 없으면 만들고 권한 설정
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# ed25519 키 쌍 생성 (기본 경로, passphrase 설정 가능)
ssh-keygen -t ed25519 -C "your_email@example.com"
프롬프트에서
- Enter file in which to save the key: 기본(~/.ssh/id_ed25519)으로 그냥 Enter로 기본 경로로 설정.
- Enter passphrase (empty for no passphrase): 나는 이를 비워두고 진행했다.
그리고 이를 서버에 복사하고,
# 기본 SSH 포트(22)가 아닌 경우 -p 옵션 추가
ssh-copy-id -i ~/.ssh/id_ed25519.pub 서버이름과아이피는비밀이지롱 -p 포트도비밀이지롱
다음과 같이 서버 설정을 점검하였다.
grep -E "^(PubkeyAuthentication|AuthorizedKeysFile)" /etc/ssh/sshd_config
# 아래 두 줄이 (주석 없이) 나와야 합니다:
# PubkeyAuthentication yes
# AuthorizedKeysFile .ssh/authorized_keys
# 변경했다면 SSHD 재시작
sudo systemctl restart sshd
이렇게 기본 작업을 마쳤는데... 프로그래머의 고질병인, 이 기능만 더... 병이 추가되어 나는 그만 카카오톡 나에게 보내기 알림 기능을 건드려버렸다. 그러지 말았어야 했지만, 결국 해버렸고 성공했다! 내 3시간은 날아갔다!
수많은 시행착오와 GPT와의 기싸움이 있었지만, 생략하고 정리해보겠다.
우선, 로직은 다음과 같다.
1. 전체 배포/점검 자동화 로직 요약
A. 새벽 자동화 플로우
- 정기 백업
- /usr/local/bin/backup_portfolio.sh (crontab 3시)
- 서버 점검모드 진입
- Nginx의 sites-enabled를 점검모드(maintenance만 활성)로 전환
- (정상→점검: symlink 교체, nginx 재시작)
- 시스템/패키지 업데이트, 캐시정리 등 유지보수
- 30분 동안 사이트는 maintenance.html(점검페이지)만 노출
- 점검 해제 후 정상모드 복귀
- 정상 conf만 symlink, nginx 재시작
B. 배포 자동화 플로우
- 배포 시작 전 점검모드 진입
- Nginx 점검페이지(maintenance.html) 노출
- rsync 등으로 소스/리소스 서버에 동기화
- 서비스 워커/캐시 패치, 기타 후처리
- 배포 완료 후 정상모드 복귀
2. 문제와 시행착오의 핵심 정리
A. Nginx conf 관리 혼란
- 여러 conf가 동시에 sites-enabled에 활성화되면 default_server 중복 등 치명적 충돌
- 점검, 정상 각각 “필요한 symlink만 활성화”가 핵심!
문제 상황: 여러 conf가 동시에 sites-enabled에 활성화
- 현상:
Nginx의 /etc/nginx/sites-enabled/에
https-default.conf, redirect, maintenance 등의 설정파일이
동시에 활성화(symlink) 되는 상황이 종종 발생했다. - 결과:
- nginx -t 실행 시
a duplicate default server for 0.0.0.0:80 in ...
- 와 같은 "default_server 중복” 오류 발생
- nginx가 아예 재시작/리로드가 안 되고,
서비스 전체가 다운될 위험
핵심은 default_server는 오직 하나여야 한다는 것.
Nginx는 80, 443 등 각 포트에 대해 default_server가 하나만 존재해야 한다. 여러 conf 파일이 동시에 listen 80 default_server; 혹은 listen 443 default_server;를 포함하면, conflicting server name, duplicate default server 등으로 충돌한다.
실제 삽질 경험
- 실수로 점검(maintenance.conf)과 정상운영(https-default.conf, redirect)이
동시에 sites-enabled에 남아 있는 상태로 nginx를 리로드함 - 서버가 바로 장애상태로 진입,
서비스 전체 다운 → 원인 파악까지 시간 소요 - 문제를 찾고보니 symlink만 올바르게 관리해도 100% 예방 가능함을 깨달음 이거 가지고 Chat GPT한테 처음으로 욕하면서 싸웠다. 꿀팁) 4.1아니면 4.5만 쓰자. o4-mini-high는 현재시점 기준으로, 성능이 심각하게 안좋다. 4.1이 최고다. 착하고, 잘해준다.
해결책
“항상 딱 필요한 conf만 활성화” = symlink 원칙
- 점검모드:
/etc/nginx/sites-enabled/에 maintenance.conf만 남김 - 정상모드:
/etc/nginx/sites-enabled/에 https-default.conf, redirect만 남김 - 배포/점검 전환:
자동화 스크립트에서- 필요 없는 symlink는 반드시 삭제(rm)
- 필요한 것만 ln -s로 활성화
- nginx -t → systemctl restart nginx로 무결성 체크+실제 적용
Nginx 운영환경에서는 conf의 동시 활성화(특히 default_server)는 예외 없이 중대한 장애로 이어진다. 배포/점검 전환 자동화에서 symlink의 명확한 관리와 불필요한 conf 비활성화가 무중단/무장애 운영의 핵심이다. 그리고 이를, maintance.sh에 적절히 반영하여 문제없이 작동하도록 했다.
B. symlink (심볼릭 링크) 실전 관리
- 점검모드: maintenance만 남김
- 정상모드: https-default.conf, redirect만 남김
- 혼합 활성화는 절대 금지(= 항상 한 번에 1~2개만!)
왜 symlink 관리가 중요한가?
- Nginx는 /etc/nginx/sites-enabled/ 안의 심볼릭 링크를 보고 어떤 설정이 적용될지 결정
- 여러 설정(conf)이 동시에 활성화되면 앞서 말한 “default_server 중복”뿐 아니라,
정상/점검 페이지가 섞여 노출되는 예측불가 상황도 발생할 수 있음
운영에서 지켜야 할 “단일성 원칙”
- 점검모드
- 반드시 /etc/nginx/sites-enabled/에 maintenance 하나만 남긴다
- → 어떤 경로로 접속하든 무조건 maintenance.html만 보장
- 정상모드
- /etc/nginx/sites-enabled/에 https-default.conf, redirect 이 두 개만 활성화
- → 정상 서비스와 HTTP→HTTPS 리디렉션 완벽 적용
- 혼합 상태(셋 다 있거나 2개 이상 모드 섞임)는 반드시 장애/충돌/예측불가
- 운영 자동화에서 한 번에 필요한 것만 명확하게 ln, 불필요한 건 rm으로 제거
(이 과정을 항상 배포/점검 스크립트에 포함) 제거하지 않으면 반드시 에러 발생 혹은 의도하지 않은 동작 발생.
- 운영 자동화에서 한 번에 필요한 것만 명확하게 ln, 불필요한 건 rm으로 제거
나는 배포 점검 전환 스크립트를 다음과 같이 구성했다.
# 점검모드 진입
sudo rm -f /etc/nginx/sites-enabled/https-default.conf
sudo rm -f /etc/nginx/sites-enabled/redirect
sudo ln -sf /etc/nginx/sites-available/maintenance /etc/nginx/sites-enabled/maintenance
sudo nginx -t && sudo systemctl restart nginx
# 정상모드 복귀
sudo rm -f /etc/nginx/sites-enabled/maintenance
sudo ln -sf /etc/nginx/sites-available/https-default.conf /etc/nginx/sites-enabled/https-default.conf
sudo ln -sf /etc/nginx/sites-available/redirect /etc/nginx/sites-enabled/redirect
sudo nginx -t && sudo systemctl restart nginx
점검 모드 진입과 정상 모드 복귀를 각각의 sh 파일로 만들고, 이를 실행하는 maintenance_window.sh를 만들었다. 그리고 그 maintenance_window는 crontab에 정기적으로 실행하게 만들었다. 물론, maintenance_window 내부에 는 다음과 같이 실행 종료 로직도 추가했다.
maintenance_window.sh
#!/bin/bash
/usr/local/bin/enable_maintenance_cron.sh
/usr/local/bin/maintenance.sh
sleep 1800 # 30분 대기
/usr/local/bin/disable_maintenance_cron.sh
그 다음으로 deploy를 분리했다. 로컬에서 파일을 업로드하는 deploy_local과 이 deploy_local의 신호를 받아 내부 deploy 로직을 처리하는 deploy_server.sh를 만들어서 다음과 같이 적용하였다.
#!/usr/bin/env bash
# [deploy_local.sh] - 로컬에서 실행!
SERVER_USER=와타시의 이름
SERVER_IP=와타시의 아이피
SERVER_DEPLOY_SCRIPT="/usr/local/bin/deploy_server.sh"
# 1. 파일 동기화 (내 로컬 → 서버)
echo "📦 서버로 파일 복사 시작..."
rsync -avz --delete \
--exclude '.git/' \
--exclude 'deploy_local.sh' \
--exclude 'deploy_server.sh' \
/mnt/e/portfolio/ \
${SERVER_USER}@${SERVER_IP}:/var/www/portfolio/ \
|| { echo "❌ rsync 실패!"; exit 1; }
echo "✅ 파일 복사 완료"
# 2. 서버에서 자동화(점검/알림/배포) 실행
echo "🚀 서버 자동화 배포 실행..."
ssh ${SERVER_USER}@${SERVER_IP} "${SERVER_DEPLOY_SCRIPT}" \
|| { echo "❌ 서버 배포(자동화) 실패!"; exit 2; }
echo "🎉 전체 배포 완료!"
deploy_server.sh
#!/usr/bin/env bash
KAKAO_NOTIFY="/usr/local/bin/kakao_notify.sh"
FAILED=1
enable_maintenance(){
sudo rm -f /etc/nginx/sites-enabled/https-default.conf
sudo rm -f /etc/nginx/sites-enabled/redirect
sudo ln -sf /etc/nginx/sites-available/maintenance /etc/nginx/sites-enabled/maintenance
sudo nginx -t && sudo systemctl restart nginx
}
disable_maintenance(){
sudo rm -f /etc/nginx/sites-enabled/maintenance
sudo ln -sf /etc/nginx/sites-available/https-default.conf /etc/nginx/sites-enabled/https-default.conf
sudo ln -sf /etc/nginx/sites-available/redirect /etc/nginx/sites-enabled/redirect
sudo nginx -t && sudo systemctl restart nginx
}
# trap은 에러(비정상 종료)시에만 동작!
trap 'if [ "$FAILED" != "0" ]; then disable_maintenance; $KAKAO_NOTIFY "❗️[서버] 비정상 종료! 점검 해제"; fi' EXIT HUP INT QUIT TE>
echo "🔧 점검모드 진입"
enable_maintenance || exit 1
$KAKAO_NOTIFY "🔧 서버 점검모드 진입 및 배포 시작!"
# (필요하면 추가 후처리/캐시정리/서비스패치 등)
# echo "⚙️ 추가 작업..."
echo "🔓 점검 해제 및 복구"
disable_maintenance
$KAKAO_NOTIFY "✅ [서버] 배포 완료 및 점검모드 해제! $(date)"
FAILED=0 # 성공적으로 끝났음을 표시
echo "✅ 서버 자동화 배포 완료"
이를 통해, 내부 점검 시간에도, 별도의 업로드 이후에도 카톡 알림이 가고, 정상적으로 작동한다. 물론, 카톡 메시지를 연동해야하지만.
C. sudo/비밀번호 자동화 문제
- ssh 접속 자체는 키로 OK
- 배포/점검 자동화에선 sudo 명령(nginx, systemctl, ln, rm 등)이 많음
- sudoers에 NOPASSWD로 필요한 명령만 등록: 비번 프리 자동화 완성
문제의 시작
- ssh는 키 인증 덕분에 비번 없이 접속 가능
- 하지만,
자동화 배포/점검 스크립트에서는
**rm, ln, nginx, systemctl 등 “root 권한이 필요한 명령”**이 반복 등장 - 특히,
ssh user@server 'deploy_server.sh'
이렇게 원격실행하면 sudo가 터미널에서 비밀번호를 입력받지 못해
자동화가 깨진다
전통적인 sudo 문제와 그 한계
- sudo 명령은 기본적으로 비번을 묻는다
(보안상 당연한 기본 정책) - 자동화의 핵심은 “사람 개입 없이 무한 반복”인데,
sudo에서 비번이 막히면 진짜 DevOps가 아니다. 그리고 이 때문에 deploy가 막혔었다.
완벽한 해결책: sudoers NOPASSWD 설정
- sudoers 파일에, 반드시 필요한 명령만 한정해서 “비번 없이 실행” 등록하였다. nginx, systemctl, ln, rm 네 가지 명령만 내가 사용하는 계정에서 언제든 비번 없이 사용 가능 불필요한 명령까지 열어놓으면 보안상 위험하므로 필요한 명령만 엄선하였고, 이후 모든 자동화 배포/점검 스크립트에서 sudo를 아무 제한 없이 쓸 수 있게 되었다.
실질적으로 “단 한 번도 비번 묻지 않음” 배포, 점검, 장애 자동복구, 모두 진짜 무인화 할 수 있었다.
와타시의계정이름 ALL=(ALL) NOPASSWD: /usr/sbin/nginx, /usr/bin/systemctl, /bin/ln, /bin/rm
D. 카카오톡 메시지 연동
- 카카오 REST API, 토큰 발급, 권한설정 등 수동+자동화 모두 경험
- 배포/점검/실패시 상황별로 “정확한 메시지” 발송
- trap + 상태 플래그로 “실패/성공 알림 분리”까지 구현
3. 최종 자동화 구조 (스크립트/크론탭/운영패턴)
- deploy_local.sh: 내 PC에서 서버로 파일 복사 후 ssh로 서버 deploy_server.sh 실행
- deploy_server.sh:
점검모드 진입(maintenance만 symlink, nginx 재시작) → 배포/패치 → 정상복귀(https-default.conf, redirect만 symlink, nginx 재시작) - kakao_notify.sh: 상황별 알림 메시지 발송(성공/실패/점검 등)
- crontab: 백업, 점검모드 자동 진입, 점검 자동 해제
4. 실전에서 배운 점
- 자동화에서 실수는 바로 장애로 이어질 수 있음 → 철저한 단일화, 관리
- 권한, 환경변수, 패스워드 자동화는 반드시 sudoers를 쓸 것
- 실패/성공 분기 알림(카톡)은 trap/상태값으로 관리
- 서버 유지보수, 배포, 장애대응, 모두가 단일 파이프라인에서 무인화!
Nginx 기반 포트폴리오 서버의 무중단 배포/점검 자동화 파이프라인을 구축했다. 그리고 Crontab, symlink 기반 conf 관리, sudoers NOPASSWD, 카카오톡 장애/배포 알림까지 구현했으며, 실제 운영환경에 준하는 수준의 DevOps 인프라 자동화를 구현. 했다고 생각한다.
이제 보안까지, 프레임은 전부 마쳤으니, 이제부터 인터렉티브 웹페이지와 게임 개발에 치중하여, 홈페이지를 완성해보겠다!
'Study > Network | Server' 카테고리의 다른 글
[Home Server] fail2ban 그리고 카카오톡 토큰 갱신 (2) | 2025.07.07 |
---|---|
[Home Server] 패키지 설치, 도메인 연결 (4) | 2025.07.05 |
[Home Server] 미니 PC 환경 설정 (1) | 2025.07.03 |
[Photon] 유니티에서 Photon 사용하기 (0) | 2025.06.09 |
네트워크 기초 (0) | 2024.12.05 |