← 모든 글

신규 환경 1시간 셋업 스크립트의 진화

결제·정산 서버에서 타임존 하나가 실제 정산 오류로 이어진 경험을 바탕으로, HEDVION이 노션 체크리스트에서 멱등성 셸 스크립트·4모듈 구조로 진화한 전 과정을 공유한다.

결제·정산 서버에서 설정 실수가 더 비싼 이유

일반 웹 서비스라면 서버 설정 실수의 결과는 "페이지 응답 느림" 혹은 "간헐적 502" 수준으로 끝날 때가 많다. 결제·정산 시스템에서는 이야기가 전혀 다르다. 우리가 직접 겪은 케이스를 공유하면, 새로 올린 정산 배치 서버의 타임존이 UTC로 남아 있었던 적이 있다. 정산 기준 시각이 KST 00:00이어야 하는데 실제 크론잡은 UTC 기준으로 실행돼 KST 09:00에 돌았다. 당일 오전 9시 이전 거래가 전날 정산분에 누락됐고, 이를 파악한 건 파트너사의 연락을 받은 뒤였다. 수동 대사(reconciliation)에 추가로 반나절이 소요됐다.

이건 실력 문제가 아니다. 체크리스트 어딘가에 "타임존 설정" 항목이 있었고, 담당자가 한 번 빠뜨렸을 뿐이다. 그리고 빠뜨릴 수 있는 환경은 언제든 다시 빠뜨린다. 결제·정산 시스템에서 환경 일관성은 선택지가 아니라 정확성의 전제 조건이다. 우리가 셋업 자동화에 시간을 투자한 이유는 속도 때문이 아니라, 실수 비용이 너무 크기 때문이다.

1단계: 텍스트 체크리스트가 실패하는 지점

노션 체크리스트 시절, 신규 서버 셋업에 평균 75분이 걸렸다. 실제 명령어 실행 시간은 약 20분이었고, 나머지 55분은 체크리스트를 읽고, 이해하고, 어디까지 했는지 확인하고, 실패한 단계의 원인을 찾는 데 쓰였다. 더 큰 문제는 "완료" 표시가 된 항목 중 실제로 미완성이거나 잘못 적용된 경우가 두 번에 한 번꼴로 발생했다는 것이다. 발견하는 시점은 배포 직전이거나, 최악의 경우 서비스 중이었다.

체크리스트의 근본적 한계는 실행 주체가 사람이라는 데 있다. 사람은 맥락을 해석하면서 읽기 때문에, "왜 이 단계가 필요한지" 모르면 중요도를 스스로 판단한다. fail2ban 설정은 "어차피 내부망이니까" 스킵하고, 스왑은 "RAM이 충분하니까" 넘긴다. 그 판단이 3개월 뒤 새벽 2시의 장애로 돌아온다. 결제 서버에서 새벽 2시 장애는 단순한 밤샘 작업이 아니라 정산 마감일과 겹칠 수 있는 상황이다.

2단계: 셸 스크립트 — 속도는 얻었고 멱등성은 잃었다

셸 스크립트로 전환하자 수치는 확연히 달라졌다. 75분이 12분으로 줄었다. set -e로 첫 실패에서 즉시 중단하고, 표준 출력에 단계별 로그가 남으니 어디서 멈췄는지 명확했다. 초기 형태로서는 분명한 진전이었다.

그러나 결제 서버는 "새로 올리는 것"만 있지 않다. 기존 서버에 보안 패치를 추가하거나 설정값을 일괄 변경할 때 스크립트를 재실행하면, nginx 설정이 덮어씌워지고 이미 생성된 사용자 계정에 useradd가 에러를 냈다. set -e 때문에 스크립트 전체가 중단되고, 어디까지 적용됐는지 다시 확인해야 했다. 어떤 경우엔 수동 작업보다 상황이 복잡해지기도 했다. 결론은 하나다: 새 서버 전용 도구로만 쓸 수 있는 스크립트는 진짜 인프라 도구가 아니다.

3단계: 멱등성 — 정산 서버 6대 동시 변경으로 검증하다

멱등성(idempotency) 구현은 패턴 자체는 단순하다. 각 단계 앞에 조건 체크를 일관되게 넣는 것이다.

# 커맨드 존재 여부로 설치 판단
if ! command -v nginx &>/dev/null; then
  apt-get install -y nginx
fi

# 타임존 상태 비교 후 적용
CURRENT_TZ=$(timedatectl show --property=Timezone --value)
if [ "$CURRENT_TZ" != "Asia/Seoul" ]; then
  timedatectl set-timezone Asia/Seoul
fi

# fail2ban maxretry 값 직접 비교 후 변경
CURRENT_RETRY=$(grep "^maxretry" /etc/fail2ban/jail.local | awk -F= '{print $2}' | tr -d ' ')
if [ "$CURRENT_RETRY" != "3" ]; then
  sed -i 's/^maxretry = .*/maxretry = 3/' /etc/fail2ban/jail.local
  systemctl reload fail2ban
fi

이 패턴이 실제로 효과를 발휘한 건 정산 배치 서버 6대에 동시에 fail2ban maxretry를 5→3으로 낮춰야 했을 때다. 각 서버에 SSH로 접속해 수동 변경하는 대신, 업데이트된 security.sh를 배포하고 실행했다. 이미 적용된 설정은 [SKIPPED], 변경이 필요한 항목만 [APPLIED]로 로그에 남았다. 전체 작업 8분, 검증 2분. 여섯 대의 결과가 동일했다. 이 버전부터는 기존 서버에 누락된 설정을 추가할 때도 동일한 스크립트를 쓸 수 있게 됐다.

현재 운영 구조: 4모듈 분리와 역할별 실행

지금 우리 스크립트는 4개 모듈로 나뉜다.

  • base.sh: apt 업데이트, 타임존 Asia/Seoul 강제 고정, RAM×1.5 기준 스왑 자동 계산, NTP 동기화 확인 및 로그 기록
  • security.sh: UFW 규칙(결제 서버는 443·80·22만 개방, 정산 배치 서버는 내부 IP 대역만), fail2ban(maxretry=3, bantime=1h), SSH 키 전용 인증 강제
  • nginx.sh: 리버스 프록시 기본 설정, Let's Encrypt 자동 갱신 cron 등록, 업스트림 health check 설정
  • docker.sh: Docker CE 설치, Compose v2 플러그인, 로그 드라이버(json-file, max-size=50m)

전체 실행은 ./setup.sh --all, 특정 모듈만 적용할 때는 ./setup.sh --security 플래그를 넘긴다. 각 모듈은 독립 실행 가능하고, 실행 결과는 setup-YYYYMMDD.log로 남긴다. 로그는 각 단계가 [APPLIED]·[SKIPPED]·[FAILED] 중 어느 상태인지 명시한다. 이 구조에서 핵심 가치는 스크립트가 인프라의 살아있는 명세서가 된다는 점이다. "우리 서버 구성이 어떻게 돼 있나요?"라는 질문에 문서 대신 스크립트를 건네면, 실행해보는 것으로 바로 답이 나온다. 1시간이 걸리던 것이 자동 실행 12분, 검증 포함 30분 이내로 줄었고, 더 중요한 건 팀원 누가 하든 결과가 같다는 것이다.

Ansible이냐 셸 스크립트냐 — 우리가 지금 선택한 이유

이 시점에서 자주 받는 질문이 있다. "그냥 Ansible 쓰면 되지 않나요?" 맞는 말이다. Ansible은 멱등성을 언어 수준에서 보장하고, 다수 서버 동시 적용·드라이런(--check)·롤백 등 우리가 셸로 직접 구현한 것들을 기본 제공한다.

우리가 셸 스크립트를 유지하는 이유는 두 가지다. 첫째, 팀 전원이 수정할 수 있다. Ansible의 YAML 문법과 모듈 체계를 모르는 팀원도 셸 스크립트에 if문 하나는 추가할 수 있다. 인프라 코드에 진입 장벽이 낮을수록 유지보수가 실제로 일어난다. 둘째, 외부 의존성이 없다. 컨트롤 노드가 필요 없고, 새 서버에서 curl로 스크립트를 받아 실행하면 끝난다. 그 대신 Ansible 전환 기준은 명확히 정해뒀다: 동시에 관리할 서버가 20대를 넘거나, 서버 역할이 3종류 이상으로 세분화될 때다. 지금 수준에서 Ansible 도입은 도구 복잡도가 문제 복잡도를 초과한다.

지금 바로 적용 가능한 실행 시사점

① 타임존을 스크립트 첫 줄에서 강제로 고정하라. 결제·정산 서버라면 timedatectl set-timezone Asia/Seoul 바로 다음 줄에 date 출력을 로그에 남겨라. 확인이 눈에 보여야 빠뜨리지 않는다.

② 멱등성 없는 스크립트는 '새 서버 전용'임을 README 첫 줄에 명시하라. "기존 서버에 재실행 시 nginx 설정이 덮어씌워질 수 있음"을 써두는 것만으로 사고를 예방할 수 있다.

③ 로그를 [APPLIED] / [SKIPPED] / [FAILED] 세 가지로 구분하라. stdout을 그냥 흘려보내면 성공인지 스킵인지 구분이 안 된다. 세 가지 상태를 명시하면 검토 시간이 절반으로 줄고, 나중에 로그를 찾아볼 때 grep FAILED로 바로 이상 여부를 확인할 수 있다.

④ 스크립트를 git으로 관리하고 변경 이유를 커밋 메시지에 남겨라. "왜 fail2ban maxretry를 5에서 3으로 바꿨나"가 git log에 있어야 6개월 뒤 되돌리거나 감사(audit) 요청에 답할 수 있다. 변경 의도 없는 diff는 절반짜리 기록이다.

⑤ 설정 스크립트와 검증 스크립트를 분리하라. verify.sh를 별도로 만들어 타임존·열린 포트·서비스 상태·스왑 크기를 순서대로 확인하고 [PASS] / [FAIL]로 출력하라. 셋업 완료 후 체크리스트 없이도 "다 됐다"를 확인할 수 있고, CI 파이프라인에 붙이면 새 서버 프로비저닝 자동 검증까지 연결된다.

* 위 링크는 인프런 affiliate 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

📚 추천 강의
한 입 크기로 잘라먹는 바이브코딩 (with Claude Code)
Claude Code로 바이브코딩, 개발자라면 꼭 들어야 할 필수 강의
강의 보러가기 →

* 위 추천 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.