MeetBack 2026.08.21 7 min read

서버 수보다 경계를 먼저 정한 MeetBack 인프라 설계

Swarm, MySQL, NFS, 모니터링, VyOS와 외부 진입 경로를 왜 분리했는지, 각 이중화 구성이 실제로 보호하는 범위를 정리했다.

프로젝트 기간: 2026-08-18 ~ 2026-09-02
글 범위: 11개 서버 역할과 계층별 인프라 설계 의사결정

MeetBack 인프라를 처음 구상할 때 “서버를 몇 대 쓸 것인가”보다 먼저 정한 것은 각 계층이 무엇을 책임질 것인가였다. 단순히 많은 VM을 나열하면 복잡해 보일 수는 있지만, 장애가 발생했을 때 어떤 서버를 살려야 하고 어떤 데이터를 보호해야 하는지 설명하기 어렵다.

나는 서비스 실행, 데이터베이스, 파일 저장소, 관측, 외부 진입, 관리 경로를 서로 다른 책임으로 나눴다. 그 결과 전체 환경은 11개의 역할로 구성됐다.

Docker Swarm
  ├─ Manager 3
  └─ Worker 2

Database
  ├─ MySQL Primary 1
  └─ MySQL Read Only Replica 1

Storage
  ├─ NFS #1: application files
  └─ NFS #2: database backup and service logs

Operations
  ├─ Monitoring server 1
  └─ VyOS router 1

관리자는 일반 서비스 트래픽과 분리된 Tailscale 경로로 각 서버에 접근하도록 했다.

왜 Swarm Manager를 3대로 구성했는가

Swarm manager는 cluster 상태를 Raft 합의로 관리한다. 2대로 구성하면 한 대가 내려갔을 때 과반수를 유지하지 못한다. 3대라면 한 대가 실패해도 나머지 2대로 quorum을 유지할 수 있다.

반면 애플리케이션 실행은 worker에 집중하는 방향으로 잡았다. manager에도 task를 실행할 수 있지만, cluster 제어 역할과 application workload를 완전히 뒤섞고 싶지 않았다. 제한된 장비 환경이라 예외적인 배치는 가능하게 두더라도 placement의 기본 원칙은 분리하는 것으로 정했다.

여기서 manager 3대가 모든 장애를 해결해 주는 것은 아니다. 애플리케이션 데이터, DB, NFS, 외부 진입은 각각 별도의 장애 지점을 가진다. 나는 “Swarm이 있으니 고가용성”이라고 묶지 않고 계층마다 실제 보장 범위를 따로 적었다.

왜 MySQL을 Primary/Replica로 선택했는가

처음에는 자동 failover가 가능한 구성을 고민했다. 하지만 두 DB 서버만 있는 상황에서 자동 승격을 성급하게 넣으면 network partition과 source 장애를 구분하기 어렵고 split-brain 위험이 생긴다. 별도 quorum member와 router, 운영 절차까지 함께 설계해야 한다.

학원 프로젝트의 범위와 기간을 고려해 MySQL 8의 GTID 비동기 Primary/Replica 구조를 선택했다. Replica는 평소 읽기 전용으로 보호하고, Primary 장애 시 복제 상태와 transaction 위치를 확인한 뒤 수동으로 승격한다. 자동화 수준은 낮지만 왜 승격하는지 사람이 확인할 수 있고, 장애 시 데이터 정합성을 우선할 수 있다.

InnoDB Cluster 같은 선택지도 검토했지만, 이번 목표에 필요한 수준보다 운영 요소가 커질 수 있다고 판단했다. 기능 이름을 추가하는 것보다 Primary/Replica의 복제 상태, backup, 수동 failover와 원복 절차를 이해하고 검증하는 데 집중했다.

왜 DB망을 따로 분리했는가

초기에는 모든 서버가 하나의 서비스망에 있었다. 이 구조는 구축하기 쉽지만 application server가 사용하는 DB traffic과 일반 server traffic, 관리 traffic의 경계가 흐려진다.

나는 DB 서버에 별도 network interface를 추가하고 private DB network로 옮겼다. 기존 서비스망 NIC는 최종적으로 연결을 제거했다. application worker와 monitoring server가 DB에 접근할 때는 VyOS router를 통과해야 한다.

MySQL 역시 DB private address 하나에만 listen하도록 했다. 두 주소에 동시에 listen하면 연결은 편해지지만 망분리의 의미가 약해진다. 운영체제 firewall은 전체 구성이 끝난 뒤 통신 matrix를 기준으로 적용하기로 했지만, 그 전에도 MySQL account의 Host 제한과 TLS 요구 조건으로 접근 범위를 줄였다.

왜 NFS를 두 역할로 나눴는가

사용자가 올린 이미지·첨부파일과 DB backup은 중요도와 access pattern이 다르다. 하나의 export에 모두 넣으면 application file 사용량이 급증했을 때 backup 공간까지 영향을 받을 수 있고, 권한도 지나치게 넓어진다.

그래서 NFS #1은 application data, NFS #2는 database backup과 service log로 역할을 나눴다. 두 서버 모두 hardware RAID5 logical disk 위에 LVM과 ext4를 구성했다.

MySQL data directory는 NFS로 옮기지 않았다. network filesystem 장애나 latency가 DB 실행 경로에 직접 들어오는 것을 피하기 위해 DB data는 local disk에 두고 dump 결과만 NFS에 보관했다.

왜 Monitoring을 별도 서버로 뒀는가

장애가 난 대상과 그 대상을 관측하는 도구가 같은 서버에 있으면 함께 사라질 수 있다. 나는 Prometheus, Grafana, Alertmanager, Loki, Alloy를 별도 monitoring server에 배치했다. node_exporter, mysqld_exporter, cAdvisor를 통해 node, DB, container를 수집한다.

이 구조도 완전한 monitoring HA는 아니다. monitoring server 자체는 단일 장애점이다. 그러나 application node와 분리했기 때문에 worker 하나가 내려가도 나머지 환경에서 그 상태를 확인할 수 있고, 프로젝트 범위 안에서 장애 원인을 관찰하기에는 충분했다.

외부 진입은 기본 경로와 예비 경로로 나눴다

기본 경로는 Cloudflare Tunnel로 정했다. 내부에서 outbound connector를 만들기 때문에 public inbound port를 항상 열 필요가 없고, Swarm global service로 여러 connector를 실행할 수 있다.

NGINX Proxy Manager는 public IP와 NAT가 가능할 때만 사용하는 예비 경로로 뒀다. DNS-01 wildcard certificate는 미리 준비했지만, 현재 public port forwarding을 사용할 수 없으므로 외부 NPM E2E를 완료했다고 표현하지 않는다.

기본: Client → Cloudflare → Tunnel → Swarm Service
예비: Client → Cloudflare → Public NAT → NPM → Swarm Service

NPM을 Tunnel의 필수 중간 계층으로 넣으면 한 component와 hop이 더 늘고, NPM 장애가 기본 경로까지 막을 수 있다. 그래서 두 경로의 책임을 분리했다.

설계 단계에서 발견한 가장 큰 오해

구축 초기에 가장 자주 경계한 것은 “이중화”라는 단어가 모든 계층에서 같은 뜻처럼 쓰이는 일이었다.

  • Manager 3대는 Swarm control plane quorum을 위한 구성이다.
  • Application replica는 process와 node 장애에 대응한다.
  • MySQL Replica는 data 복제본이지만 자동 failover를 의미하지 않는다.
  • RAID5는 disk 장애 허용이지 backup이 아니다.
  • NFS 서버 2대는 역할 분리이지 같은 data의 자동 mirror가 아니다.
  • Cloudflare connector 5개는 Tunnel 연결점 분산이지 origin application의 정상 상태를 보장하지 않는다.

이 구분을 하지 않으면 한 기술의 성공을 전체 서비스의 고가용성으로 과장하게 된다. 나는 각 문서에 “구성 완료”, “검증 완료”, “미검증”, “보류”를 나눠 기록했다.

남은 한계

현재 architecture의 뼈대는 완성됐지만 실제 application stack은 개발 완료를 기다리고 있다. application이 배포돼야 NFS UID/GID, Docker Secret, replica 수, placement, rolling update와 rollback을 최종 검증할 수 있다. Redis도 공통 module은 구현됐지만 service 배포와 업무 기능 연동은 남아 있다.

이번 설계에서 내가 세운 기준은 단순했다. 구성요소가 많아 보이는 것보다, 왜 존재하며 어디까지 보호하는지 내가 설명할 수 있어야 한다. 다음 글부터는 이 설계를 실제 서버에 적용하면서 발생한 문제를 계층별로 기록해 보려고 한다.