블로그를 Ubuntu 16.04에서 10년 동안 운영하다가 FreeBSD로 마이그레이션함

3 hours ago 4
  • Ubuntu 16.04 LTS 기반 DigitalOcean VPS는 지원 종료와 보안 부담이 있었지만, 종료 시점까지 1491일 업타임을 유지함
  • 새 서버는 독일 Hetzner VPS와 FreeBSD 14.3으로 옮겼고, 기존 월 $13 서버보다 강한 사양을 월 6유로 미만에 사용함
  • Jails와 Bastille로 사이트별 격리 환경을 만들고, Caddy Jail이 SSL과 리버스 프록시를 맡아 각 nginx Jail로 전달함
  • 부하 테스트에서 새 FreeBSD 서버는 10,000 동시 연결을 위해 kern.ipc.somaxconn을 조정한 뒤 요청 처리량이 3~11배 높게 나옴
  • 전환에는 네트워킹과 FreeBSD 설정 학습이 필요했지만, 중앙화된 설정과 문서 품질 덕분에 예상보다 구성하기 쉬웠음

마이그레이션 배경

  • 기존 블로그는 DigitalOcean VPS에서 10년 넘게 운영됐고, 뉴욕 데이터센터의 Ubuntu 16.04 LTS를 사용했음
    • Ubuntu 16.04 LTS는 최소 5년 전부터 지원이 끝난 상태였고, apt 패키지 저장소 업데이트를 받을 수 없었음
    • 오래된 시스템은 보안상 불리하며, 과거 별도 WordPress 블로그가 오래된 VPS에서 카지노·도박 링크 삽입 공격을 겪은 적이 있음
  • 기존 서버는 블로그 외에도 몇 개의 사이트를 함께 제공했지만 대부분 정적 사이트였음
    • 가장 인기 있는 블로그도 보통 월 몇천 페이지뷰 수준이었고, Hacker News에서 몇몇 글이 바이럴된 경우를 제외하면 트래픽은 많지 않았음
    • 서버는 nginx/1.10.3로 정적 파일을 제공했고, 사이트별 설정은 /etc/nginx/sites-available에 있었음
    • 블로그는 Hugo로 생성됐고, 기존 배포 절차는 로컬 작성 → 저장소 커밋·푸시 → 서버 SSH 접속 → 저장소 pull → hugo 실행이었음
  • 기존 VPS는 초기에 테스트와 프로그래밍 용도로도 쓰여 오래된 소프트웨어가 많이 남아 있었음
    • 그래도 정상적으로 동작했고, 종료 시점의 업타임은 1491일로 약 4년 동안 재부팅 없이 운영됨
  • 새 서버는 독일의 Hetzner VPS로 옮겼고, 기존보다 사양이 높으면서 월 비용은 절반 이하였음
    • 기존 DigitalOcean 서버는 RAM 2GB, vCPU 1개, 디스크 50GB, 월 트래픽 2TB, 월 $13였음
    • Hetzner의 가장 저렴한 서버도 기존보다 메모리와 CPU가 두 배였고, 저장공간은 약간 적지만 트래픽은 10배였음
    • 최종 선택한 Hetzner 구성은 월 6유로 미만의 더 강한 사양이었음

FreeBSD를 선택한 이유

  • FreeBSD는 새로운 시스템을 실제 운영 환경에서 시험해보고 싶어 선택됨
    • 통합 설계, 안정성, 보안, Jails가 장점으로 꼽힘
  • Jails는 FreeBSD에 25년 넘게 포함된 가상화·컨테이너화 기능임
    • 호스트 시스템에 접근하지 못하는 “미니 시스템”을 샌드박스처럼 실행할 수 있음
    • Docker 같은 컨테이너 솔루션은 프로그램 패키징에 더 적합하고 일시적·불변적인 성격을 갖는 반면, Jails는 같은 커널을 공유하는 서브시스템이나 미니 VM에 가깝게 다뤄짐
  • ZFS도 서버용 파일시스템으로 매력적인 선택지였음
    • 데이터 무결성과 스냅샷 기능이 있고, Linux 쪽의 Btrfs와 유사한 면이 있음
    • ZFS는 Btrfs보다 훨씬 성숙한 것으로 평가됐고, 자주 스냅샷을 남기면 VPS 제공자의 유료 스냅샷·백업 시스템에 덜 의존할 수 있음
  • 목표 구조는 사이트마다 하나의 Jail을 두고, 각 Jail 안에 필요한 빌드 도구와 nginx를 넣는 방식이었음
    • 메인 웹 서버용 Jail은 외부와 연결되는 리버스 프록시를 맡음
    • 특정 Jail이 침해되면 해당 Jail을 삭제하고 새로 만들 수 있는 구조를 의도함

FreeBSD 설치와 Bastille 도입

  • Hetzner의 VM 생성 화면에는 기본 OS 이미지가 제한적이어서 BSD가 바로 보이지 않았음
    • FreeBSD 공식 YouTube 채널의 Hetzner 설치 영상을 참고함
    • Hetzner는 FreeBSD ISO 이미지를 제공하지만, VM 생성 후 콘솔의 ISO Images 탭에서 마운트해야 했음
    • 설치에는 FreeBSD 14.3 ISO가 사용됐고, 공식 영상의 설치 흐름을 따라 시스템을 구성함
  • Bastille은 Jails 관리를 쉽게 하기 위해 선택됨
    • 수동 Jail 생성에 필요한 여러 단계를 bastille 명령으로 처리할 수 있음
    • 예시 명령은 bastille list, bastille create, bastille console임
    • 설치와 활성화는 Bastille 시작 문서 기준으로 진행됨
pkg install bastille sysrc bastille_enable="YES"

네트워크와 리버스 프록시 구조

  • 전체 스택은 Caddy Jail 하나가 모든 도메인과 SSL 인증서를 처리하고, 사이트별 Jail로 트래픽을 리버스 프록시하는 구조임
    • 각 사이트 Jail은 사이트를 빌드하고 제공하는 데 필요한 도구만 포함함
    • 같은 네트워크 안의 여러 가상 머신과 비슷한 구조로 볼 수 있음
  • 내부 가상 네트워크 인터페이스는 bastille0로 만들었음
sudo sysrc cloned_interfaces+="lo1" sudo sysrc ifconfig_lo1_name="bastille0" sudo service netif cloneup sudo sysrc ifconfig_bastille0="inet 10.0.0.1 netmask 255.255.255.0"
  • 루프백 인터페이스를 복제해 bastille0라는 이름을 붙이고 10.0.0.1/24 네트워크를 할당함
  • Jail들은 이 네트워크 인터페이스에서 동작함
  • 외부 HTTP·HTTPS 요청은 PF(Packet Filter) 규칙으로 Caddy Jail에 전달됨
    • /etc/pf.conf에는 외부 인터페이스 vtnet0, 내부 인터페이스 bastille0, tailscale1이 설정됐음
    • 80, 443 포트 트래픽은 Caddy 서버가 될 10.0.0.5로 리다이렉트됨
ext_if = "vtnet0" int_if = "bastille0" vpn_if = "tailscale1" set skip on $int_if set skip on $vpn_if nat on $ext_if from 10.0.0.0/24 to any -> ($ext_if) rdr pass on $ext_if proto tcp from any to any port {80, 443} -> 10.0.0.5 block all pass out quick on $ext_if keep state
  • PF와 게이트웨이는 다음 명령으로 활성화됨
sysrc pf_enable="YES" service pf start sysrc gateway_enable="YES"

Caddy와 사이트별 Jail 구성

  • 기존 서버는 nginx를 오래 사용했지만, 새 구성에서는 SSL 인증서 관리를 자동화하기 위해 Caddy를 선택함
    • 기존 nginx 환경에서는 certbot으로 인증서를 주기적으로 갱신해야 했고, 갱신을 놓친 일이 여러 번 있었음
  • Caddy Jail을 만들기 전에 FreeBSD 릴리스를 Bastille에 부트스트랩함
bastille bootstrap 14.3-RELEASE
  • Caddy Jail은 10.0.0.5 IP로 생성됨
bastille create caddy 14.3-RELEASE 10.0.0.5 bastille0 bastille start caddy
  • Jail 이름은 caddy, 릴리스는 14.3-RELEASE, 인터페이스는 bastille0임
  • bastille list로 실행 상태를 확인하고, bastille console caddy로 Jail 내부 셸에 들어갈 수 있음
  • Caddy 설치와 서비스 활성화는 Jail 내부에서 진행됨
pkg install caddy sysrc caddy_enable="YES" service caddy start
  • Caddy 설정 파일은 Jail 내부의 /usr/local/etc/caddy/Caddyfile에 있음
    • 호스트에서 설정 파일을 관리하려면 호스트 디렉터리를 Jail 안으로 마운트할 수 있음
    • 예시에서는 nullfs와 읽기 전용 ro 옵션으로 Caddy가 설정을 변경하지 못하게 마운트함
bastille mount caddy /usr/local/etc/my-caddy-config /usr/local/etc/caddy nullfs ro 0 0

사이트와 블로그 배포

  • 첫 번째 배포 대상은 es.cro.to였고, 사이트 저장소는 GitHub 저장소로 관리됨
    • 호스트의 /usr/local/www/escroto에 저장소를 두고, 사이트 Jail에는 해당 디렉터리를 읽기 전용으로 마운트함
    • 모든 사이트는 호스트의 /usr/local/www 아래에 두는 방식으로 정리됨
  • escroto Jail은 nginx Bastille 템플릿으로 만들었음
bastille bootstrap https://github.com/bastillebsd/templates bastille create escroto 14.3-RELEASE 10.0.0.11 bastille0 bastille template escroto www/nginx
  • IP는 10.0.0.11로 지정됨
  • nginx 기본 페이지는 FreeBSD 관례대로 /usr/local/www/nginx에서 제공됨
  • 호스트의 사이트 디렉터리는 Jail에 읽기 전용으로 마운트됨
bastille mount escroto /usr/local/www/escroto /usr/local/www/escroto nullfs ro 0 0
  • 저장소의 .git 디렉터리가 웹으로 제공되지 않도록 배포 스크립트를 사용함
rm -fr /usr/local/www/nginx/* cp -R /usr/local/www/escroto/* /usr/local/www/nginx/ rm -fr /usr/local/www/nginx/.git
  • 새 버전 배포는 호스트에서 저장소를 갱신한 뒤 Jail 안의 배포 스크립트를 실행하는 방식임
cd /usr/local/www/escroto git pull bastille cmd escroto /root/deploy.sh
  • Caddy 설정은 cro.to를 es.cro.to로 영구 리다이렉트하고, es.cro.to를 10.0.0.11로 프록시함
cro.to { redir https://es.cro.to{uri} permanent } es.cro.to { reverse_proxy 10.0.0.11 }
  • 블로그는 Hugo를 사용하며 GitHub 저장소로 관리됨
    • 저장소는 호스트의 /usr/local/www/blog에 클론됨
    • blog Jail은 escroto와 비슷하게 생성됐고, IP는 10.0.0.12로 지정됨
bastille create blog 14.3-RELEASE 10.0.0.12 bastille0 bastille template blog www/nginx bastille mount blog /usr/local/www/blog /usr/local/www/blog nullfs ro 0 0
  • Hugo는 blog Jail 안에 설치됨
bastille pkg blog update bastille pkg blog install gohugo
  • 블로그 배포 스크립트는 nginx 웹 루트를 비우고 Hugo 출력물을 /usr/local/www/nginx에 생성함
rm -fr /usr/local/www/nginx/* cd /usr/local/www/blog hugo -d /usr/local/www/nginx
  • DNS를 옮기기 전에는 기존 도메인 대신 crocidb.cro.to를 새 블로그 서버에 연결해 테스트함
crocidb.cro.to { reverse_proxy 10.0.0.12 }

벤치마크와 부하 테스트

  • DNS 레코드를 바꾸기 전에 기존 서버 crocidb.com과 새 서버 crocidb.cro.to의 부하 처리 능력을 비교함
    • 블로그 방문자는 주로 북미, 그다음 유럽과 남미에서 오기 때문에 새 독일 서버의 지연시간이 일부 사용자에게 약간 길어질 수 있음
    • 핵심 관심사는 정적 사이트 제공 속도와 큰 부하를 견디는 능력이었음
  • GTMetrix, Pingdom, WebPageTest 같은 무료 온라인 도구도 사용했지만, 두 서버의 차이는 대부분 지연시간 정도였음
  • HTTP 부하 벤치마크 도구로 wrkhey를 사용함
    • 두 도구는 대량의 동시 요청을 생성하고 요청 지연시간, 오류 응답, 초당 전송량 등을 수집함
  • 같은 Hetzner의 다른 VPS에서 wrk를 실행했을 때 새 서버가 크게 앞섰음
wrk -t4 -c100 -d30s --latency https://crocidb.com/
  • 옵션은 4스레드, 100개 동시 연결, 30초 실행이었음
  • 기존 서버는 평균 지연시간 89.63ms, 초당 요청 833.41, 전송량 8.29MB/s였음
  • 새 서버는 평균 지연시간 6.75ms, 초당 요청 12260.10, 전송량 130.80MB/s였음
  • 테스트 머신이 새 서버와 같은 데이터센터에 있어 공정한 비교는 아니었음
  • Proton VPN을 이용해 여러 지역에서 wrk를 돌리는 방식도 시도했지만 결과는 기대보다 낮았음
    • 기존 서버 평균은 초당 약 300 요청, 새 서버 평균은 초당 약 800 요청으로 기록됨
    • 최종적으로 일반 사용자용 VPN 대신 여러 지역의 실제 VPS를 만드는 방식으로 바뀜

Vultr VPS 기반 지역별 테스트

  • 서버가 올라간 DigitalOcean·Hetzner와 다른 인프라를 쓰기 위해 Vultr가 선택됨
    • 지역은 수동 작업 부담 때문에 London, São Paulo, Silicon Valley, Tokyo 네 곳으로 제한됨
    • 각 지역에 가장 저렴한 Fedora VM을 만들고 테스트를 실행함
  • 최종 테스트 도구는 hey가 더 적합하다고 판단됨
./hey_linux_amd64 -n 1000000 -c 10000 -t 10 -z 5m -h2 https://crocidb.com/ > crocidb.com.log ./hey_linux_amd64 -n 1000000 -c 10000 -t 10 -z 5m -h2 https://crocidb.cro.to/ > crocidb.cro.to.log
  • 설정은 총 1,000,000 요청, 동시 요청 10,000, 타임아웃 10초, 총 실행 시간 5분, HTTP/2 사용이었음
  • 현실적인 블로그 트래픽보다 훨씬 큰 부하였음
  • 첫 실행에서 새 FreeBSD 서버는 10,000개 동시 연결을 감당하지 못해 초반에 실패함
    • netstat -Lan으로 소켓 큐 크기를 확인하자 모두 128로 나타남
    • 기본값 kern.ipc.somaxconn이 128이어서 다음처럼 늘림
sysctl kern.ipc.somaxconn=16384
  • São Paulo 테스트에서 두 서버 모두 상당한 오류를 반환했지만, FreeBSD 서버는 기대한 1,000,000 요청에 대응했고 Ubuntu 서버는 20,000 요청도 반환하지 못했음
    • 기존 Ubuntu 서버는 전체 요청의 약 7%만 완료함
    • 새 FreeBSD 서버는 약 94%를 완료함
    • Tokyo에서는 새 서버의 성공률이 약간 낮았지만, 크게 우려할 수준은 아니라고 판단됨
  • 초당 요청 수 기준으로 새 서버는 기존 서버보다 최소 3배, 최대 11배 나았음
    • 지연시간 백분위에서는 새 서버가 약 90% 지점까지 더 선형적으로 증가해 예측 가능성이 더 높았음
    • 높은 부하에서도 전 세계 대부분 지역에서 블로그 메인 페이지 콘텐츠를 3.5초 미만에 받을 수 있는 결과였음
  • Tokyo 결과는 깊게 분석하지 않았음
    • hey의 요청 단계별 분석에서는 일본으로 향하는 트래픽이 더 느릴 가능성이 나타남
    • 두 번째 도메인의 DNS dial-up·lookup 값이 비정상적으로 낮아 보였고, CNAME 레코드 영향 가능성이 있었음
    • resp wait와 resp read 값도 이상하게 높았는데, 성공한 요청만 집계했기 때문에 기존 서버가 초기에 빠르게 응답하다가 이후 새 요청을 사실상 막았을 가능성이 있었음

최종 전환과 핵심 교훈

  • 벤치마크는 답하지 못한 부분이 많았지만, 결과에 만족해 DNS 레코드를 새 서버로 전환함
    • 이 블로그는 이후 FreeBSD 기반 Hetzner 서버에서 공식적으로 운영됨
  • FreeBSD로 사이트 호스팅 머신을 구성하는 일은 여러 시간의 실험, 수정, 빌드, 실패를 거쳤지만 예상보다 복잡하지 않았음
    • 요구 조건을 만족하는 웹 호스팅 서비스를 사용할 수도 있었고, 예시로 OpenBSD Amsterdam이 나옴
    • Proxmox로 컨테이너와 관리 대시보드를 사용할 수도 있었음
    • FreeBSD 쪽 대안으로 Sylve도 거론됨
    • 직접 구성한 경로는 많은 학습을 제공했기 때문에 만족스러운 선택이었음
  • 기존 Ubuntu 서버도 매우 견고했음
    • 10년 동안 사이트 부하를 잘 처리했고, 마지막 4년은 재부팅 없이 운영됨
    • 설정에 큰 노력을 들이지 않고도 안정적으로 동작함
  • FreeBSD 설정은 예상보다 쉬웠고, 시스템 설정을 한곳에 중앙화하는 방식과 온라인 문서 품질이 좋았음
  • 직접 블로그 호스팅 머신을 구성하려면 게임 개발자가 아는 범위를 넘어서는 네트워킹 지식이 필요했음
    • 다른 시스템을 배우는 과정이 즐거웠고, 다음에는 OpenBSD나 NetBSD를 시도할 수도 있음
    • 블로그 트래픽 대부분이 AI 시스템의 크롤링에서 온다는 점에서, 이 모든 작업의 실용성은 제한적이라고 마무리됨
Read Entire Article