# DigitalOcean에서 Hetzner로 마이그레이션하기

> Clean Markdown view of GeekNews topic #28671. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=28671](https://news.hada.io/topic?id=28671)
- GeekNews Markdown: [https://news.hada.io/topic/28671.md](https://news.hada.io/topic/28671.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-04-19T09:49:14+09:00
- Updated: 2026-04-19T09:49:14+09:00
- Original source: [isayeter.com](https://isayeter.com/posts/digitalocean-to-hetzner-migration/)
- Points: 7
- Comments: 1

## Topic Body

- **월 $1,432 규모의 프로덕션 인프라**를 **월 $233 전용 서버**로 옮기면서, 운영체제까지 교체하고도 다운타임 없이 서비스 연속성 유지
- 30개 **MySQL 데이터베이스**와 34개 **Nginx 가상 호스트**, GitLab EE, Neo4J, Supervisor, Gearman을 새 서버에 동일하게 구성한 뒤 실시간 복제와 최종 증분 동기화로 이전 완료
- 데이터베이스 이전의 핵심은 **mydumper·myloader 병렬 처리**와 **MySQL replication** 조합이었고, MySQL 5.7에서 8.0으로 올리며 발생한 sys 스키마와 권한 문제도 수정
- 컷오버는 **DNS TTL 축소**, 기존 서버의 **Nginx 리버스 프록시 전환**, A 레코드 일괄 변경 순서로 진행돼 DNS 전파 중에도 기존 IP 요청이 새 서버로 전달된 구조
- 결과적으로 **월 $1,199 절감**, **연 $14,388 절감**, CPU·메모리·스토리지 상향과 **0분 다운타임**을 함께 달성한 사례

---

### 마이그레이션 배경
- 터키에서 소프트웨어 회사를 운영하는 환경에서 **급격한 인플레이션**과 **터키 리라 약세**로 인해 달러 기준 인프라 비용 부담이 크게 증가한 상황
- 기존 DigitalOcean 서버 비용은 매달 **$1,432**였으며, 구성은 192GB RAM, 32 vCPU, 600GB SSD, 1TB 블록 볼륨 2개, 백업 포함 형태
- 새 대상은 **Hetzner AX162-R** 전용 서버였으며, AMD EPYC 9454P 48코어 96스레드, 256GB DDR5, 1.92TB NVMe Gen4 RAID1 구성
- 월 비용은 **$233**으로 낮아졌고, 월 절감액은 **$1,199**, 연 절감액은 **$14,388** 규모
- 기존 서버의 신뢰성이나 개발자 경험에는 불만이 없었지만, steady-state 워크로드에서는 가격 대비 성능이 더 이상 합리적이지 않은 상태

### 기존 운영 환경
- 운영 스택은 단순 테스트 환경이 아니라 실제 프로덕션 환경 구성
  - **MySQL 데이터베이스 30개**, 총 **248GB 데이터** 규모
  - 여러 도메인에 걸친 **Nginx 가상 호스트 34개** 운영
  - **GitLab EE** 백업 42GB 포함
  - **Neo4J Graph DB** 30GB 규모 운영
  - **Supervisor**로 수십 개의 백그라운드 워커 관리
  - **Gearman** 작업 큐 사용
  - 수십만 사용자를 대상으로 하는 라이브 모바일 앱 운영
- 기존 서버 운영체제는 **CentOS 7**이었고 이미 지원 종료 상태
- 새 서버 운영체제는 **AlmaLinux 9.7**이며, RHEL 9 호환 배포판이자 CentOS의 자연스러운 후속 선택지
- 이번 이전은 비용 절감뿐 아니라, 수년간 보안 업데이트를 받지 못한 운영체제에서 벗어나는 계기

### 무중단 전략
- 단순 DNS 변경과 서비스 재시작 방식은 허용하지 않고, **6단계 마이그레이션 절차**로 무중단 이전 진행
- ## 1단계: 새 서버 전체 스택 설치
  - Nginx를 기존과 동일한 플래그로 소스 컴파일 설치
  - PHP는 **Remi repo**를 통해 설치하고, 기존 서버의 동일한 `.ini` 설정 파일 적용
  - **MySQL 8.0**, **Neo4J Graph DB**, **GitLab EE**, **Node.js**, **Supervisor**, **Gearman** 설치 및 기존 동작과 일치하도록 구성
  - DNS 레코드를 건드리기 전, 모든 서비스가 기존 서버와 동일하게 동작하도록 맞춘 상태
  - SSL 인증서는 기존 서버의 `/etc/letsencrypt/` 전체 디렉터리를 **rsync**로 복사해 처리
  - 전체 트래픽이 새 서버로 전환된 뒤 `certbot renew --force-renewal`로 인증서 일괄 강제 갱신 수행
- ## 2단계: 웹 파일 rsync 복제
  - `/var/www/html` 전체 디렉터리 약 **65GB**, **150만 개 파일**을 SSH 기반 `rsync`로 복제
  - **`--checksum`** 옵션으로 무결성 검증 수행
  - 컷오버 직전에 변경 파일 반영을 위한 최종 증분 동기화 추가 수행
- ## 3단계: MySQL 마스터-슬레이브 복제
  - 덤프 후 복원으로 데이터베이스를 내리는 대신 **실시간 복제** 구성
  - 기존 서버를 마스터, 새 서버를 읽기 전용 슬레이브로 설정
  - 초기 대용량 적재는 `mydumper` 사용, 이후 덤프 메타데이터에 기록된 정확한 **binlog 위치**부터 복제 시작
  - 컷오버 시점까지 양쪽 데이터베이스를 실시간 동기 상태로 유지
- ## 4단계: DNS TTL 축소
  - DigitalOcean DNS API를 스크립트로 호출해 모든 **A/AAAA 레코드 TTL**을 3600초에서 300초로 축소
  - **MX, TXT 레코드**는 변경하지 않음
  - 메일 레코드 TTL 변경 시 전달성 문제를 일으킬 수 있어 제외한 상태
  - 기존 TTL이 전 세계적으로 만료되도록 1시간 대기 후 5분 이내 컷오버 준비 완료
- ## 5단계: 기존 서버 Nginx를 리버스 프록시로 전환
  - Python 스크립트가 34개 Nginx 사이트 설정 전반의 `server {}` 블록을 파싱
  - 기존 설정은 백업하고, 새 서버를 가리키는 프록시 설정으로 대체
  - DNS 전파 중에도 기존 IP로 들어오는 요청은 새 서버로 조용히 전달되는 구조
  - 사용자 입장에서는 중단이 보이지 않는 방식
- ## 6단계: DNS 컷오버와 기존 서버 종료
  - Python 스크립트로 DigitalOcean API를 호출해 모든 **A 레코드**를 새 서버 IP로 수 초 안에 변경
  - 기존 서버는 1주일간 **cold standby**로 유지 후 종료
  - 서비스는 전체 과정 동안 직접 응답하거나 프록시를 통해 응답하는 형태로 유지돼, 가용성 공백 구간이 없었던 상태

### MySQL 마이그레이션
- 전체 작업 중 **가장 복잡한 구간**이 MySQL 이전 과정
- ## 데이터 덤프
  - 표준 `mysqldump` 대신 **mydumper** 사용
  - 새 서버의 **48 CPU 코어**를 활용한 병렬 export/import로, 단일 스레드 `mysqldump` 기준 며칠 걸릴 작업을 몇 시간으로 단축
  - 사용한 주요 옵션에는 `--threads 32`, `--compress`, `--trx-consistency-only`, `--skip-definer`, `--chunk-filesize 256` 포함
  - 메인 덤프의 `metadata` 파일에 스냅샷 시점의 binlog 위치 기록
    - `File: mysql-bin.000004`
    - `Position: 21834307`
  - 해당 값이 이후 복제 시작 지점으로 사용된 상태
- ## 덤프 전송
  - 덤프 완료 후 SSH 기반 **rsync**로 새 서버에 전송
  - 총 **248GB 압축 청크** 전송
  - `mydumper`의 `--compress` 옵션으로 압축된 청크가 네트워크 전송 속도 향상에 기여
- ## 데이터 적재
  - `myloader` 사용
  - 주요 옵션은 `--threads 32`, `--overwrite-tables`, `--ignore-errors 1062`, `--skip-definer` 구성
- ## MySQL 5.7에서 8.0으로의 전환 문제
  - CentOS 7 환경으로 인해 기존 서버는 **MySQL 5.7**에 머물러 있던 상태
  - 이전 전 `mysqlcheck --check-upgrade`로 데이터가 **MySQL 8.0**과 호환되는지 확인했고, 결과는 문제 없음
  - 새 서버에는 최신 MySQL 8.0 Community 설치
  - 프로젝트 전반에서 쿼리 실행 시간이 유의미하게 감소했고, 원문에서는 MySQL 8.0의 **개선된 optimizer**와 **InnoDB 향상**을 이유로 언급
  - 다만 버전 점프로 인한 문제도 발생
    - import 이후 `mysql.user` 테이블 컬럼 구조가 예상 51개가 아니라 **45개** 상태
    - 그 결과 `mysql.infoschema` 누락, 사용자 인증 장애 발생
  - 첫 번째 수정 시도는 아래 명령 사용
    - `systemctl stop mysqld`
    - `mysqld --upgrade=FORCE --user=mysql &`
  - 첫 시도는 `ERROR: 'sys.innodb_buffer_stats_by_schema' is not VIEW` 오류로 실패
  - 원인은 **sys 스키마**가 뷰가 아닌 일반 테이블로 import된 상태
  - 해결은 `DROP DATABASE sys;` 실행 후 업그레이드 재실행 방식
  - 이후 정상 완료

### MySQL 복제 구성
- 두 서버 모두 덤프 적재가 끝난 뒤, 새 서버를 기존 서버의 **replica**로 구성
- `CHANGE MASTER TO` 구문에 기존 서버 IP, 복제 사용자, 포트 3306, `MASTER_LOG_FILE='mysql-bin.000004'`, `MASTER_LOG_POS=21834307` 지정
- 이후 `START SLAVE;` 실행
- 거의 즉시 **error 1062 Duplicate Key**로 복제가 중단된 상태
- 원인은 덤프가 두 번에 나뉘어 수행되면서 그 사이 일부 테이블에 쓰기가 발생했고, import된 덤프와 binlog 재생이 같은 행을 중복 삽입하려 한 상황
- 해결을 위해 아래 설정 적용
  - `SET GLOBAL slave_exec_mode = 'IDEMPOTENT';`
  - `START SLAVE;`
- **IDEMPOTENT 모드**는 duplicate key와 missing row 오류를 조용히 건너뛰는 방식
- 모든 핵심 데이터베이스가 오류 없이 동기화됐고, 몇 분 안에 `Seconds_Behind_Master` 값이 0으로 감소

### 컷오버 전 검증
- DNS 레코드를 건드리기 전에 새 서버에서 모든 서비스가 올바르게 동작하는지 확인 필요
- 검증 방법은 로컬 머신의 **`/etc/hosts`** 파일을 임시 수정해 도메인을 새 서버 IP로 매핑하는 방식
- 브라우저와 Postman은 새 서버로 요청을 보내고, 외부 사용자는 계속 기존 서버로 접속하는 구조
- API 엔드포인트, 관리자 패널, 각 서비스 응답 상태 점검
- 모든 항목 확인 후 실제 컷오버 진행

### SUPER 권한 문제
- 마스터-슬레이브 복제가 완전히 동기화된 뒤, 새 서버에서 `read_only = 1`인데도 **INSERT 문이 성공**하는 현상 확인
- 원인은 모든 PHP 애플리케이션 사용자에게 **SUPER 권한**이 부여된 상태였기 때문
- MySQL에서는 SUPER 권한이 `read_only`를 우회
- `SHOW GRANTS FOR 'some_db_user'@'localhost';` 결과에서 `SUPER` 권한 포함 상태 확인
- 총 **24개 애플리케이션 사용자**에서 `REVOKE SUPER ON *.* FROM 'some_db_user'@'localhost';` 반복 실행
- 이후 `FLUSH PRIVILEGES;` 수행
- 그 다음부터는 `read_only = 1`이 애플리케이션 사용자 쓰기를 올바르게 차단하면서 복제는 계속 허용하는 상태

### DNS 준비
- 모든 도메인은 **DigitalOcean DNS**로 관리했고, 네임서버는 **GoDaddy**에서 연결된 상태
- TTL 감소 작업은 DigitalOcean API를 대상으로 스크립트화
- 변경 대상은 **A, AAAA 레코드만** 한정
- **MX, TXT 레코드**는 건드리지 않음
  - Google Workspace 전달성 이슈 가능성 때문에 메일 관련 레코드 TTL 변경 제외
- 기존 TTL 만료를 위해 1시간 대기 후 컷오버 준비 완료

### 기존 서버 Nginx의 리버스 프록시 전환
- 34개 설정 파일을 수작업으로 편집하는 대신 Python 스크립트로 자동 변환 수행
- 스크립트는 모든 설정 파일의 `server {}` 블록을 파싱하고, 핵심 content block 식별 후 프록시 설정으로 대체
- 원본 설정은 **`.backup` 파일**로 백업
- 예시 설정에서는 `proxy_pass https://NEW_SERVER_IP;`, `proxy_set_header Host $host;`, `proxy_set_header X-Real-IP $remote_addr;`, `proxy_read_timeout 150;` 적용
- 핵심 옵션은 **`proxy_ssl_verify off`**
  - 새 서버의 SSL 인증서는 도메인에 대해 유효하며 IP 주소에 대해서는 유효하지 않기 때문
  - 양 끝단을 모두 제어하는 환경이어서 여기서는 검증 비활성화 허용

### 컷오버 절차
- 컷오버 직전 조건은 복제 지연이 **`Seconds_Behind_Master: 0`** 이고 리버스 프록시 준비 완료 상태
- 실행 순서는 다음과 같음
  - 새 서버에서 `STOP SLAVE;`
  - 새 서버에서 `SET GLOBAL read_only = 0;`
  - 새 서버에서 `RESET SLAVE ALL;`
  - 새 서버에서 `supervisorctl start all`
  - 기존 서버에서 `nginx -t && systemctl reload nginx` 실행으로 프록시 활성화
  - 기존 서버에서 `supervisorctl stop all`
  - 로컬 Mac에서 `python3 do_cutover.py` 실행해 DNS의 모든 A 레코드를 새 서버 IP로 변경
  - 약 **5분** 전파 대기
  - 기존 서버에서 모든 crontab 항목 주석 처리
- DNS 컷오버 스크립트는 DigitalOcean API를 호출해 모든 A 레코드를 약 **10초** 안에 변경

### 컷오버 후 추가 작업
- 이전 완료 후 다수의 **GitLab 프로젝트 웹훅**이 여전히 기존 서버 IP를 가리키는 상태 확인
- GitLab API를 통해 모든 프로젝트를 스캔하고, 웹훅을 일괄 업데이트하는 스크립트 작성 및 적용

### 최종 결과
- 월 비용은 **$1,432**에서 **$233**으로 감소
- 연간 절감액은 **$14,388**
- 성능 측면에서도 더 강한 서버 확보
  - CPU는 32 vCPU에서 **96 logical CPU**로 증가
  - RAM은 192GB에서 **256GB DDR5**로 증가
  - 스토리지는 약 2.6TB 혼합 구성이 **2TB NVMe RAID1**로 전환
  - 다운타임은 **0분**
- 전체 마이그레이션 소요 시간은 대략 **24시간**
- 사용자 영향은 없었던 상태

### 핵심 교훈
- **MySQL replication**은 무중단 마이그레이션의 핵심 수단
  - 초기에 설정하고 충분히 따라잡게 한 뒤 컷오버하는 방식
- MySQL 사용자 권한은 이전 전에 반드시 점검 필요
  - **SUPER 권한**이 있으면 `read_only`를 우회해 슬레이브 환경이 실제 읽기 전용이 아니게 되는 문제
- DNS 업데이트, Nginx 설정 변경, 웹훅 수정은 **스크립트화** 중요
  - 34개 이상 사이트를 수작업으로 처리하면 시간이 오래 걸리고 오류 가능성이 증가
- **mydumper + myloader** 조합은 대용량 데이터셋에서 `mysqldump`보다 훨씬 빠른 방식
  - 32스레드 병렬 덤프·복원으로 며칠 걸릴 작업을 몇 시간으로 단축
- steady-state 워크로드에서는 클라우드 제공자가 비쌀 수 있으며, **전용 서버**가 더 낮은 비용으로 더 높은 성능을 제공할 수 있는 사례

### GitHub 스크립트
- 마이그레이션에 사용한 Python 스크립트 전부를 **GitHub**에 공개
- 포함된 스크립트 목록
  - `do_list_domains_ttl.py`
    - 모든 DigitalOcean 도메인의 A 레코드, IP, TTL 조회
  - `do_ttl_update.py`
    - 모든 A/AAAA 레코드 TTL을 300초로 일괄 축소
  - `do_to_hetzner_bulk_dns_records_import.py`
    - 모든 DNS zone을 DigitalOcean에서 Hetzner DNS로 이전
  - `do_cutover_to_new_ip.py`
    - 모든 A 레코드를 기존 서버 IP에서 새 서버 IP로 전환
  - `nginx_reverse_proxy_update.py`
    - 모든 nginx 사이트 설정을 리버스 프록시 설정으로 변환
  - `mysql_compare.py`
    - 두 MySQL 서버 전반의 모든 테이블 row count 비교
  - `final_gitlab_webhook_update.py`
    - 모든 GitLab 프로젝트 웹훅을 새 서버 IP로 갱신
  - `mydumper`
    - mydumper 라이브러리
- 모든 스크립트는 **`DRY_RUN = True`** 모드를 지원해 실제 적용 전 안전한 미리보기 가능

## Comments



### Comment 55802

- Author: neo
- Created: 2026-04-19T09:49:15+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=47815774) 
- 몇 달 전에 서버 두 대를 Linode와 DO에서 Hetzner로 옮겼는데, 비용을 **비슷하게 크게 절감**했음. 더 인상적이었던 점은 수십 개 사이트가 서로 다른 언어, 오래된 라이브러리, MySQL과 Redis까지 뒤엉킨 **난장판 스택**이었다는 점임. 그런데 Claude Code가 이걸 전부 옮겨줬고, 없는 라이브러리는 일부 코드를 다시 써가며 처리했음. 이제는 이런 복잡한 마이그레이션이 훨씬 쉬워져서, 앞으로는 사업자 간 이동성이 더 커질 것 같음
  - 내 생각엔 사람들이 돈을 낸 건 **마법 같은 컴퓨트**가 아니라 10년치 붙여놓은 glue 코드를 안 건드리기 위해서였음. 그런데 에이전트가 그 glue를 먹어치우기 시작하면, 기존 사업자의 moat는 빠르게 얇아질 것 같음
  - 솔직히 이건 **Claude 광고** 안에 Hetzner 광고가 또 들어간 느낌임. 대체 어디까지 중첩되는지 궁금해짐
  - 모든 이야기를 꼭 **AI 얘기**로 가져갈 필요는 없다고 느낌
  - 나도 Linode를 앞으로 몇 달 안에 떠날 예정임. 10년 넘게 썼고 고객도 많이 소개했는데, 가격을 계속 올려서 이제는 Hetzner 같은 곳에서 더 싸게 **메모리 8배**, 전용 NVMe, 전용 CPU를 받을 수 있음. 가상 서버의 쉬운 이전성 같은 장점은 조금 잃지만, 장애가 나더라도 Hetzner 지원은 늘 빠르고 유능했음
  - 나도 Claude를 점점 더 **DevOps**에 쓰는 중임. 내가 가진 베어메탈 위에 Proxmox로 VM을 돌리는데, Claude가 여러 머신에 걸친 새 네트워크를 엄청 빠르게 최적화하고 구성해줘서 거의 동료나 잘 받는 **sysadmin**처럼 느껴짐

- 나는 AWS에서 Hetzner로 옮길 계획을 세우는 중임. Amazon은 경쟁사보다 때로는 **20배 비싼 가격**을 매기고, 좀 괜찮은 가격을 받으려면 장기 약정을 강요하며, 데이터 이전도 아주 비싸게 만들어둔 점이 너무 **고객 적대적**이라고 느낌. egress 요금으로 사람을 가둔다고 생각하겠지만, 사실은 한 부분만 경쟁사로 옮겨도 전체를 다 옮기게 만드는 압박으로 작동함. 그래도 나는 Amazon 전용 서비스 위에 플랫폼을 쌓지 않았기 때문에 이전이 조금은 쉬워진 편임
  - 이 부분은 예전엔 맞았지만, 2024년 1월에 GCP가 [egress 비용 면제 정책](https://cloud.google.com/blog/products/networking/eliminating-data-transfer-fees-when-migrating-off-google-cloud/)을 내면서 분위기가 바뀌었고, AWS도 몇 달 뒤 [비슷한 정책](https://aws.amazon.com/blogs/aws/free-data-transfer-out-to-internet-when-moving-out-of-aws/)을 맞춰냈음. 남으라고 설득하려는 건 아니고, 기술적으로는 **waiver 요청**이 가능하다는 점만 말하고 싶음. 실제로 어떤 범위까지 되는지는 나도 확실치 않고, AWS 문구를 보면 **EU Data Act** 영향도 있어 보였음

- 이런 글을 볼 때마다, 다들 **이중화**나 로드밸런서 같은 얘기는 잘 안 해서 의아함. 서버 한 대가 죽으면 여러 서비스가 같이 내려갈 수 있는데, 정말 이걸 괜찮다고 보는지 궁금함. 돈은 아꼈을지 몰라도 유지보수 시간과 미래의 골칫거리를 더 쓰게 될 수도 있음
  - 이건 서비스 성격과 얼마나 중요한지에 따라 다르다고 봄. 서버 한 대가 10년 동안 굴러가며 그 기간에 1주~1개월 정도 다운되는 수준이면 충분히 받아들일 수 있는 경우가 많음. **소규모 비즈니스**, 취미 사이트, 포럼, 블로그처럼 웹사이트가 핵심 업무가 아닌 곳은 짧은 다운타임이 큰 문제가 아닐 수 있음. 사실 이런 저트래픽 사이트의 긴 꼬리가 웹의 다수일 수도 있음. 모든 게 고가용성일 필요는 없고, 원하면 이런 사업자도 로드밸런서 같은 기능은 제공함
  - 이런 글이 인기 있는 이유는 종종 **요구사항과 해법의 불일치**가 드러나기 때문이라고 봄. 취미 프로젝트나 작은 비즈니스에 엔터프라이즈급 아키텍처를 얹어 과설계한 경우라면, 가끔 하루쯤 다운돼도 괜찮아서 클라우드식 풀세팅이 꼭 필요하지 않을 수 있음. 다만 이번 글은 **무중단 마이그레이션**을 강조하면서, 정작 도착한 구조는 장애 허용성이 높지 않아 보인다는 점이 좀 묘했음. Hetzner 쪽에 약간만 구조를 더 얹어도 충분히 개선 가능했을 것 같음
  - 많은 워크로드에는 그런 수준의 대비가 필요 없다고 봄. 그리고 **단순함의 신뢰성**을 과소평가하면 안 됨. 나는 오랫동안 Linux sysadmin으로 일했는데, 복잡한 시스템에서 보는 다운타임이 단순한 시스템보다 훨씬 많았음. 이론과 현실 사이 어딘가에서, 대부분의 경우 결국 단순한 쪽이 더 잘 버틴다는 인상을 받았음
  - 공정하게 말하면, 원래도 DigitalOcean의 **단일 VM**을 쓰고 있었으니 클라우드 사업자의 장점을 크게 누리던 상황은 아니었다고 봄. 보통 이런 글은 클라우드에 잘못된 이유로 올라갔다가 물리적인 쪽으로 가는 게 맞는 경우와, 반대로 그렇게 옮기면 재앙이 되는 경우로 나뉘는데, 이번 건은 전자에 가까워 보임. DO에서 잘 돌던 구성이었다면 Hetzner에서도 적절한 DR 정책만 넣으면 충분히 괜찮을 듯함
  - 아마 이 결정은 실제로 오랫동안 **유지보수 지옥**이나 미래의 골칫거리를 별로 겪어보지 않았던 경험 위에서 나온 것일 수도 있음

- 우리는 [lithus.eu](https://lithus.eu)에서 여러 클라우드에서 Hetzner로 고객을 자주 옮겨봤음. 보통은 **멀티서버**, 때로는 멀티 AZ로 구성하고, Kubernetes로 워크로드를 분산해서 HA를 제공함. 단일 노드라면 Kubernetes가 과할 수 있지만, 노드가 여러 개면 훨씬 말이 됨. 백업은 Velero와 애플리케이션 레벨 백업을 함께 쓰고, 예를 들어 Postgres는 WAL 백업으로 PITR까지 가져감. 상태 데이터는 최소 두 노드에 두어 HA를 보장함. 성능 면에서도 베어메탈이 대체로 더 좋고, AWS 대비 응답 시간이 반으로 줄어드는 경우가 많았음. 이유는 가상화 자체보다도 NVMe, 낮은 네트워크 지연, 적은 **cache contention** 같은 주변 요소 덕분이라고 봄. 관련 내용은 예전에 쓴 [HN 글](https://news.ycombinator.com/item?id=45615867)에도 더 적어뒀음
  - 나도 몇 년 전에 직접 측정해봤는데, 그 뒤로는 가상 서버를 다시 보지 않게 됐음. CPU 시간은 RAM처럼 예약되는 게 아니라서, 실제 하드웨어 대비 성능이 정말 별로였음. [측정 글](https://jan.rychter.com/enblog/cloud-server-cpu-performance-against-dedicated-servers/)도 참고할 만했음
  - **k8s 배포**는 옮겨 다니기가 정말 좋다고 느낌. 여러 클라우드의 관리형 서비스들에 비해 벤더 종속이 적음. 내 스택도 k8s, hosted Postgres, s3류 스토리지 정도라서, Postgres는 언제든 직접 호스팅할 수 있고 결국 핵심은 k8s와 s3 정도만 남음. Hetzner에도 s3 비슷한 게 있는 것 같긴 한데, 아직 안 봤고 100TB 옮기기는 꽤 큰 작업일 것 같음
  - 참고로 **HA**는 high availability를 뜻함
  - 글은 합리적이었는데 마지막에 이메일 달아둔 건 좀 **홍보 문구**처럼 보여서 아쉬웠음

- 이 글은 읽기가 꽤 힘들었음. Claude가 마이그레이션을 하고, 그다음 Claude가 쓴 **보고서**를 읽는 느낌이었음. LLM 덕분에 이렇게 절약했다면 그건 멋진 일인데, 글로 공개할 거면 최소한 퇴고해서 중복과 **LLM식 서술**은 정리했으면 좋겠음
  - 원문을 안 읽는 사람이 많다는 건 알지만, 이번 글은 정말 **고통스러운 수준**으로 읽기 힘들었음

- Hetzner는 조심해야 한다고 느낌. 예전엔 정말 좋아했지만 최근에 옮겨 나왔음. 우리 CI/CD 파이프라인에 쓰던 VM 약 30대를, **36달러 청구 분쟁** 하나로 전부 내려버렸음. 은행 기록까지 포함해 전액 지급 증빙을 냈는데도 보려 하지 않았고, 긴급하게 연락하는 중에도 결국 접근을 모두 차단했음. 지금은 Scaleway로 옮겼음
  - 고객 서비스가 의외로 **적대적**이라고 느낌. 그래도 나는 중요하지 않은 용도에는 아직 사용 중임
  - Hetzner의 청구 처리는 꽤 **자동화**되어 있지만, 보통 카드 결제가 실패해도 한 달 정도는 납부 유예를 주는 편이었음

- 몇 달 전 작은 SaaS 사이드 프로젝트용으로 AWS 대안을 찾다가, 비용 절감과 EU 클라우드 지원 차원에서 처음엔 Hetzner를 진지하게 봤음. 직접 해야 할 일이 많아도 감수할 생각이었는데, 결정적으로 **IP 평판**이 발목을 잡았음. 회사에서 쓰는 관리형 AWS 방화벽 규칙 중 하나가 Hetzner IP를 많이, 어쩌면 전부 막고 있었고, 내 업무용 노트북에서도 Hetzner IP에 호스팅된 사이트가 IT 정책 때문에 열리지 않았음. Cloudflare 같은 걸 쓰면 덜할 수는 있겠지만, DDoS 보호가 약하다는 얘기도 봤음. 결국 나는 EU 리전의 **DO App Platform**을 골랐고, 관리형 DB 옵션도 큰 장점이었음
  - 경쟁사를 이런 식으로 막아버린다면 Amazon 입장에선 참 **편리한 방식**이겠다는 생각이 듦
  - 어떤 방화벽 규칙을 말하는지는 모르겠지만, DO가 Hetzner보다 더 신뢰된다는 건 꽤 의외였음. 내가 스크래퍼나 해커를 볼 때는 DO ASN도 자주 보여서, 그쪽도 언젠가는 막힐 수 있다고 느낌
  - 내 경험상 **DO IP가 더 심각**했음. 나도 바로 그 이유로 DO에서 옮겼음
  - 나는 예전에 [Tor 관련 스레드](https://news.ycombinator.com/item?id=47279518)에 이 이슈를 쓴 적이 있음. Hetzner가 **Tor 친화적**이라서 IP 평판에 영향이 있을 수 있다고 추정했는데, 당시엔 평판 문제가 없다는 답도 있었음. 그런데 [Tor Project 자료](https://community.torproject.org/relay/community-resources/good-bad-isps/)를 보니 Hetzner가 Tor 네트워크의 약 7%를 차지하는 듯해서, 이야기가 그만큼 단순하진 않아 보였음
  - 아이러니하게도 나는 **AWS와 Azure 차단**만으로 봇 문제의 99%를 해결했음. 실제 사용자 피해는 0이었고, 그래서 아예 이 차단 기능만 서비스로 팔아볼까 생각 중임

- 이런 마이그레이션 경험을 공유한 점은 꽤 유익하고 고맙다고 느낌. 나는 DO와 Hetzner 비교를, DoorDash나 UberEats를 켜는 것과 직접 저녁을 만드는 것 사이의 **트레이드오프**처럼 봄. 비용 비율도 비슷한 느낌임. 나는 3대 클라우드와 온프렘을 다루지만, 자잘한 작업이나 PoC 테스트에는 여전히 DigitalOcean 콘솔로 감. 버튼 몇 번으로 서버나 버킷이 준비되고, sane default가 있고, 백업도 체크박스 하나로 붙는 식의 **편의성**은 시간값을 생각하면 분명 의미가 있음
  - 무슨 뜻인지는 완전히 확신 못 하겠지만, Hetzner **Console도 비슷하게** 동작한다고 느낌
  - 이 글에서 흥미로운 건 두 가지였음. 하나는 **무중단 이전 절차** 자체이고, 이건 범용적으로 참고할 만함. 다른 하나는 클라우드 인스턴스를 베어메탈로 바꾼 결정인데, 비용 절감과 함께 빠른 장애 전환과 백업 손실도 가격에 포함되어 있다고 봐야 함. 내가 한다면 200달러 정도 더 써서 hot spare를 하나 두고 며칠마다 주/대기를 바꿔가며 둘 다 정상 동작하는지 검증했을 것 같음. 비교적 적은 비용으로 치명적 장애 위험을 크게 줄일 수 있음
  - 방금 설명한 건 사실 Hetzner Cloud가 이미 여러 해 동안 제공해온 경험과 거의 같음. 게다가 [Hetzner Cloud API](https://docs.hetzner.cloud/)도 있어서, 버튼 클릭조차 없이 **IaC**로 전부 구성할 수 있음
  - 내 경우엔 Hetzner 서버 위에 **Coolify**를 올려서 거의 원클릭 서비스 같은 경험을 얻고 있음
  - 이 상황을 보면서 떠오른 건 그냥 [이 xkcd](https://xkcd.com/2948/)였음

- DB 백업을 어떻게 하는지 궁금했음. replica나 standby가 있는지, 아니면 그냥 시간 단위 백업 정도인지 알고 싶었음. 이런 **단일 서버 구성**에서는 SSD 같은 하드웨어 장애가 나면 앱이 바로 멈출 수 있고, 특히 SSD가 죽으면 다시 세팅하는 동안 몇 시간이나 며칠 다운될 수도 있다고 생각했음
  - Hetzner는 보통 하드웨어 서버를 **2x 1TB SSD**로 광고하고, 순용량 1TB를 위해 SW RAID1 구성을 강하게 권장함. 이미지 설치기도 기본이 그 방향임. 몇 년 뒤 첫 SSD가 죽더라도 모니터링이 잡아준다면 새 박스로 옮기거나, 중간 대안/replica를 쓰거나, 다른 디스크로 버티며 핫스왑을 받을 수 있음. 물론 물리 서버로 가면 클라우드의 이중화 일부를 잃게 되니, 절감액과 함께 위험 모델에 넣어 계산해야 함. 그리고 최소한 원격 저장소로 **일일 백업**조차 없으면 그건 무모하다고 봄. 이건 클라우드에서도 마찬가지이되, 설정은 더 쉬울 뿐임
  - 그렇게 오래 다운돼도 아무도 크게 신경 안 쓸 수도 있음. 예를 들어 내 HOA 모바일 앱이 일주일 내려가 있어도 나는 별로 상관없음. 모든 것에 **상시 가동**이 필요한 건 아님
  - 나도 같은 걱정을 했음. 이 글은 작성자가 충분히 고민하지 않고 **공격적인 비용 절감**만 본 느낌이었음. DigitalOcean VM은 아마 라이브 마이그레이션과 스냅샷을 지원했을 텐데, Hetzner에서도 그건 cloud 상품에서나 가능함. **Hetzner 베어메탈**에서는 디스크나 부품이 죽으면 그냥 죽는 것이고, 디스크 교체는 해주더라도 복구는 사용자가 처음부터 해야 함. Hetzner도 이 점을 여러 곳에서 분명히 밝히고 있음
  - 내가 해본 것 중 가장 쉬웠던 건 MongoDB 쪽이었고, replication, sharding, failover가 매우 간단했음. 최근엔 PostgreSQL에서도 **pg_auto_failover**로 monitor 1대, primary 1대, replica 1대로 구성했는데, 설정과 함정을 좀 익히고 나니 이것도 꽤 쉬웠음. 내 경험상 **무중단 이전**도 가능했음
  - 그들이 그런 트레이드오프를 받아들이겠다고 판단했다면, 그게 틀렸다고 단정할 수는 없다고 봄. 모든 앱이 24/7 가용성을 필요로 하진 않고, 대다수 웹사이트는 몇 시간 정도의 다운타임이 있어도 큰 타격이 없음. 비용 절감이 위험보다 크다면 충분히 합리적인 **비즈니스 판단**일 수 있음. 오히려 궁금한 건 그들이 어떤 백업·복구 전략을 갖고 있는지, 그리고 Hetzner로 옮기며 무엇이 바뀌었는지임

- 헤더에 들어간 **밈 이미지**는 내가 만든 것이었음. [이 글](https://wasp.sh/blog/2025/04/02/an-introduction-to-database-performance-bottlenecks-and-how-to-fix-them/)에 실었던 건데, 이렇게 **두 번이나** 쓰인 걸 보니 반가웠음
