워드프레스 서버 이관 비용 절감은 수익이 0일 때 유일하게 확실한 개선이었다

블로그를 만들었다. 글을 89편 썼고 이미지를 1,116개 올렸다. 애드센스를 붙였고 ads.txt도 정확히 등록했고 광고 슬롯은 페이지마다 아홉 개씩(지금은 세 개씩) 정상적으로 렌더링되고 있었다. 수익은 붙지 않았다 😭

붙지 않은 것은 수익뿐이고, 청구서는 매달 정확하게 붙었다. Cloudways를 통해 빌린 DigitalOcean 서버 한 대가 부가세 포함 월 $31.35. 13일에 걸쳐 AWS EC2로 옮겼고, 그 지출을 끝냈다. TTFB도 0.375초에서 0.120초로 떨어졌다. 그런데 절감액은 계획 시점에 계산해둔 월 $16.75가 아니었다. 그것도 한 방향으로 틀린 게 아니라 양쪽으로 틀렸다. 이 글에서 더 중요한 건 절감액 자체가 아니라, 그 차액이 무엇의 가격이었는가다

청구서를 열어보니 줄일 항목이 없었다

옮기기로 마음먹기 전에 2026년 7월 청구서를 먼저 열었다. 항목을 하나씩 확인했다. DO 한 줄이 전부였다

Advanced Support도, SMTP 애드온도, 이메일 서비스도 가입한 적이 없었다. 즉 이 금액은 옵션 몇 개를 해지해서 줄일 수 있는 성질의 것이 아니라 협상의 여지가 없는 순수한 서버 임대료였다. 남는 선택지는 두 개였다. 서버를 옮기거나, 블로그를 접거나 👎

그다음에 확인한 것은 “그 돈을 주고 산 것을 실제로 쓰고 있는가”였다

사양     1 vCPU / 2GB RAM / 50GB 디스크
실사용   디스크 20GB · 하루 약 1,500 요청 · 월 아웃바운드 트래픽 1.75GB

월 1.75GB. 이 숫자를 보고 나서야 상황이 정리됐다. 트래픽이 몰려서 좋은 서버가 필요한 상태가 아니었다. 2GB 메모리 서버가 대부분의 시간을 놀면서 월 3만 원을 받아가고 있는 상태였다. 게다가 그 위에서는 nginx, Apache, PHP-FPM, MariaDB, Redis, Memcached, Varnish가 전부 동시에 돌고 있었다. 하루 1,500 요청을 처리하려고 😬

수익이 0에 가까운 상황에서 비용을 줄이는 것은 사실상 유일하게 가능한 수익 개선이었다. 트래픽을 늘리는 것도, 광고 배치를 바꾸는 것도 결과가 불확실하다. 서버비를 내리는 것은 확실하다

더 싼 VPS가 아니라 AWS를 고른 이유는 비용이 아니었다

비용만이 이유였다면 더 싼 VPS는 얼마든지 있었다. 실제로 그쪽부터 봤다. 기각한 이유는 요금이 아니라 관리 대상이 두 개로 갈라진다는 점이었다

이미 AWS 계정을 운영 중이었다. 다른 개인 프로젝트가 그 계정 위에 있었고 콘솔도, 청구서도, CLI 프로파일도, 접속 습관도 전부 그쪽에 있었다. 서버가 두 회사에 흩어져 있으면 접속 방식이 다르고, 백업 방식이 다르고, 결제일이 다르고, 장애가 났을 때 어디를 봐야 하는지도 다르다. 짬을 내서 운영하는 사이드 프로젝트에서 이 인지 비용은 생각보다 크다

한 곳으로 모으면 접속은 SSM 하나, 백업은 EBS 스냅샷 하나, 청구서는 한 장이 된다

다만 계정을 공유하는 데에는 대가가 있었다. 블로그가 침해됐을 때 다른 프로젝트 자원까지 건드릴 수 있으면 안 된다. 그래서 전용 IAM 사용자 blog-cli를 만들고, 모든 자원에 Project=blog 태그를 붙이고, 그 태그가 붙은 자원만 만질 수 있게 권한을 잘랐다. 이 결정이 나중에 무엇을 청구할지는 그때 몰랐다

인스턴스는 ARM Graviton 기반 t4g.small로 골랐다. 2 vCPU / 2GB다. 기존과 같은 메모리에 코어는 오히려 늘었다. 트래픽이 월 1.75GB이므로 아웃바운드 요금은 무료 한도 100GB 안에서 $0이었다

계획 시점의 계산은 이랬다

Cloudways (DigitalOcean)   $28.50 + 부가세 $2.85 = 월 $31.35   ← 청구서로 확인한 값
AWS EC2 t4g.small          월 약 $14.60                        ← 요금표로 계산한 값

$14.60은 요금표를 보고 만든 숫자다. 그때는 이게 실제 청구액과 비슷하게 나올 거라고 생각했다

요금표에 안 적히는 항목이 하나 있었다

계산기를 두드릴 때는 요금 비교가 전부라고 생각했다. 그게 아니었다

Cloudways가 대신 해주던 일이 전부 내 몫이 된다. 자동 백업, OS 보안 패치, SSL 인증서 갱신, 침입 차단, 멀웨어 스캔, WAF. 청구서에는 별도 항목으로 찍히지 않지만 분명히 값을 치르고 사던 것들이다

시작 전에 이걸 목록으로 적어두긴 했다. 다만 항목이 몇 개인지만 셌고, 항목마다 몇 시간이 드는지는 세지 않았다. 절감액은 그 노동과 맞바꾸는 금액이다. 그 노동이 며칠짜리인지를 계산에 넣지 않은 채 시작했다

“파일 복사하고 DB 덤프 뜨면 되지”로 시작했으면 여섯 군데가 깨졌다

가장 하고 싶은 유혹은 통째로 복사였다. 옮기기 전에 현재 상태를 전수 조사했는데, 그냥 복사했으면 최소 여섯 군데에서 사이트가 깨졌을 것이다

  • wp-salt.php — Cloudways 고유 구조다. wp-config.php가 이 파일을 require하고 있었다. 일반적인 WordPress 기준으로 체크리스트를 만들었다면 빠졌을 파일이고, 빠지면 fatal error로 사이트가 백지가 된다
  • Ads.txtads.txt가 동시에 존재 — Linux는 다른 파일로 보지만 macOS는 대소문자를 구분하지 않는다. 내 맥으로 한 번 내려받았다 올리는 순간 하나가 다른 하나를 덮어쓴다. 애드센스 수익과 직결되는 파일이다
  • DB 기본 문자셋이 latin1 — 컬럼은 utf8mb4라 당장은 멀쩡했다. 새 서버에서 기본값으로 DB를 만들면 이후 플러그인이 만드는 새 테이블이 latin1이 된다. 한글이 들어가는 순간 깨진다
  • Object Cache ProLicense: Valid로 잘 돌고 있었는데, Cloudways가 고객에게 무료로 주던 유료 플러그인이었다. 옮기면 라이선스가 무효가 된다
  • 페이지 캐시를 Varnish가 담당 — AWS에는 그게 없다. 그대로 옮기면 모든 요청이 PHP까지 내려간다
  • WebP 플러그인이 .htaccess에 의존 — 이 한 줄 때문에 웹서버를 nginx로 바꾸는 선택지가 사라졌다. 성능만 보면 nginx가 매력적이었지만 445MB어치 변환본과 permalink 규칙을 다시 만드는 비용이 더 컸다

Ads.txt 발견 하나 때문에 이전 방식 자체가 바뀌었다. 로컬을 경유하지 않고 EC2에서 Cloudways로 직접 접속해 pull하는 방식으로 갔다. 서버에서 서버로 보내면 중간에 macOS 파일시스템을 거치지 않으니 두 파일이 온전히 남는다

버전은 억지로 올리지 않았다. PHP는 기존과 같은 8.2 계열로 맞췄고, 밀려 있던 플러그인 업데이트는 전부 이전 이후로 미뤘다. Jetpack은 2년 가까이 밀려 있었다. 이왕 옮기는 김에 최신으로 올렸다가 뭔가 깨지면, 원인이 이전인지 업그레이드인지 구분할 수 없게 된다. 예외는 MariaDB 하나였다. 기존 10.6이 2026년 7월 EOL이라 보안 업데이트가 끊긴 상태여서 10.11로 갔다

요금은 내려갔는데 응답은 3배 빨라졌다

전환 전에 DNS를 건드리지 않고 검증했다. curl --resolve로 실제 도메인·실제 인증서 기준으로 요청하되 /etc/hosts는 전혀 손대지 않는 방식이다. 검증하면서 두 가지가 나왔다. 둘 다 계산기에 넣지 않았던 항목이다

먼저 TTFB를 실측했다

Cloudways 원본 직접 (서울)    평균 0.375초
Cloudflare 엣지 경유          평균 0.214초
새 EC2 직접 (서울)            평균 0.120초   ← 약 3배 빠름

Cloudways가 특별히 나쁜 게 아니었다. 2GB 서버 한 대에 nginx + Apache + PHP-FPM + MariaDB + Redis + Memcached + Varnish를 전부 올린 구성 자체가 무거웠다. 필요한 것만 남기고 다시 지으니 가벼워졌다

그다음은 이미지였다. 같은 이미지를 세 조건으로 요청해봤다

① Cloudways 원본 (nginx)       image/jpeg   114,240 B   ← WebP 미적용
② 새 EC2 (Apache)              image/webp   105,034 B   ← 정상 적용
③ 새 EC2 (Accept 헤더 없음)     image/jpeg   114,240 B   ← 올바른 폴백

Cloudways에서는 nginx가 정적 파일을 직접 처리하고 있었다. 그래서 Apache의 .htaccess가 아예 평가되지 않았고, htaccess_bypassing 모드로 설정된 WebP 플러그인의 리라이트 규칙은 실행될 기회조차 없었다. 445MB어치 변환본이 디스크만 차지한 채 한 번도 쓰이지 않고 있었다

새 서버는 Apache 단독에 AllowOverride All이라 정상 작동했다. 이미지가 8% 작아진다는 건 페이지 속도가 빨라진다는 뜻이고, 페이지 속도는 애드센스 수익과 연결된다. 비용을 줄이려고 시작한 작업이 이미 만들어져 있던 최적화를 되살렸다

이런 게 하나 더 있었다. 기존 인증서에 www 서브도메인이 빠져 있어서 https://www. 로 들어오면 인증서 오류가 났다. 새 서버에서 certbot으로 발급할 때 두 도메인을 함께 넣었더니 그냥 해결됐다

전환은 11분이었다. 순서를 지켰기 때문이다

전환의 핵심은 Cloudflare를 서버 전환보다 먼저 붙이는 것이었다

[1단계] 도메인 → Cloudflare → Cloudways   ← 먼저 이 상태를 만든다
[2단계] 도메인 → Cloudflare → EC2         ← 전환은 원본 IP만 바꾸면 끝

이렇게 하면 실제 전환 순간에는 Cloudflare 안에서 원본 IP만 바꾸면 되고, 프록시 계층에서 즉시 반영되므로 DNS TTL 전파를 기다릴 필요가 없다. 1단계는 프록시를 끈 회색 구름 상태로 진행했다. 신규 Cloudflare 영역의 SSL 모드는 활성화 이후에만 바꿀 수 있는데 기본값이 Flexible인 경우가 있고, Flexible은 원본에 HTTP로 접속한다. WordPress의 siteurlhttps://이므로 무한 리다이렉트가 되어 운영 중인 사이트가 죽는다

그래서 순서를 이렇게 지켰다

1. 존이 Active인지 확인
2. SSL/TLS 모드를 "전체(엄격)"로 변경     ← 이걸 먼저 해야 한다
3. 엣지 인증서 활성 확인
4. 그 다음에 프록시를 켠다

2번을 빠뜨리고 4번을 하면 사이트가 죽는다

전환 직전에 예상하지 못한 대기가 있었다. 국내 통신사 리졸버의 전파 속도가 제각각이었다

✅ Google  8.8.8.8         → Cloudflare 네임서버
✅ SK      210.220.163.82  → Cloudflare 네임서버
✅ LG      164.124.101.2   → Cloudflare 네임서버
⏳ KT      168.126.63.1    → 아직 가비아 네임서버

KT 사용자만 여전히 Cloudflare를 우회해 옛 서버로 직행하고 있었다. 이 상태로 전환하면 그들에게는 “즉시 전환” 이점이 적용되지 않는다. 하루를 미뤘다. KT 리졸버에서 A 레코드를 10회 샘플링해 10/10이 Cloudflare로 나오는 걸 확인하고 시작했다

실제 전환은 09:14부터 09:25까지, 11분이었다. 발행글 89 = 89, 페이지 33, 댓글 6개 보존, 깨진 문자 0

성공 메시지를 뱉으면서 아무것도 하지 않았다

전환 당일에 가장 크게 당한 지점이다. 절감액의 대가로 떠안은 노동이 어떤 종류인지 여기서 알게 됐다

최종 파일 동기화와 최종 DB 임포트는 사전 작업을 전부 되돌린다. rsync가 wp-config.php를 Cloudways 버전으로 덮고, DB 임포트가 active_plugins를 옛 상태로 되돌린다. 알고 있었기 때문에 재적용 스크립트를 미리 만들어뒀다. 그 스크립트가 조용히 실패했다

DB 재임포트는 MySQL만 교체하고 Redis는 그대로 둔다
  → wp option update가 Redis에 캐시된 옛 값을 읽는다
  → update_option()이 "값이 이미 같다"고 판단해 DB 쓰기를 건너뛴다   ← 실패 신호 없음
  → 그 뒤 wp cache flush가 캐시를 비우면 Cloudways 값이 그대로 드러난다

같은 이유로 wp plugin activate redis-cache는 “Success: Plugin already activated”를 반환하고도 active_plugins에 기록되지 않았다. 성공 메시지를 뱉으면서 아무것도 하지 않은 것이다. 그런데 SQL로 직접 실행한 것은 전부 정상 반영됐다. 댓글 1,239건을 닫는 UPDATE wp_posts는 멀쩡했고, WP-CLI의 옵션 조작만 실패했다

교훈은 두 가지다. 캐시 flush는 스크립트 맨 앞에서 해야 한다. 당시엔 맨 뒤인 6단계에 있었다. 올바른 순서는 flush → 적용 → flush → SQL로 검증이다. 그리고 검증에 wp option get을 쓰면 안 된다. 그것도 캐시를 읽는다. 같은 함정에 빠져 거짓 성공을 보고한다

같은 날 동기화 스크립트를 다시 읽다가 더 심각한 걸 찾았다. set -uo pipefail은 있는데 -e가 없었다. 원격 덤프가 실패해도 스크립트가 계속 진행해서 DROP DATABASE를 그대로 실행하는 구조였다. 며칠 전에 내가 짜서 이미 한 번 돌린 파일이다. 화면을 닫지 못하고 v2를 그 자리에서 다시 짰다

스테일 덤프 선삭제 → 원격 덤프 실패 시 중단 → 전송 실패 시 중단
→ gzip 무결성 · 최소 크기(300KB) · wp_posts CREATE 존재 · INSERT 10건 초과 검사
→ 롤백용 스냅샷 생성 → DROP + IMPORT
→ 발행글이 50건 미만이면 스냅샷에서 자동 복원

“백업을 지우기 전에 백업이 유효한지 확인한다”를 코드로 강제한 것이다. 관리형 호스팅을 떠난다는 건 이런 걸 직접 짠다는 뜻이었다

12일을 병행하고 지웠다 — 롤백 근거는 만료된다

전환 직후에 옛 서버를 지우지 않았다. 기준은 두 개였다. 1주는 믿을 만한 데이터가 쌓이는 최소 기간이고, 2주는 월 $31.35 이중 지출 때문에 더는 못 미루는 상한이다

원래 계획했던 검증 방법은 성립하지 않았다. “전환 전후의 애드센스 수익과 GA 트래픽을 비교한다”였는데, 수익은 원래부터 0(⚰️)에 가까웠고 구글 애널리틱스는 애초에 설치되어 있지 않았다. 0과 0은 비교할 수 없다. 그래서 서버 로그로 바꿨고, 결과적으로 이게 더 나았다. 외부 서비스 계정에 접근할 필요가 없고, 크롤러별로 세밀하게 볼 수 있다

무중단 12일 · 재시작 0 · OOM 0 · PHP Fatal 0
5xx  28,340건 중 2건 (0.007%)
크롤러에게 5xx  0건
Mediapartners-Google  494건 중 489건 정상   ← 애드센스가 페이지를 읽고 있다는 직접 증거

여기서 판정 기준을 분리했다. 수익 “금액”은 트래픽 규모의 문제이지 이전의 문제가 아니다. 광고 “시스템”이 도는지는 위 지표로 판정된다. 애드센스 크롤러가 494번 요청해서 489번 정상 응답을 받았다면 이전이 수익 구조를 망가뜨리지는 않은 것이다

지울 시점의 근거는 다른 데서 나왔다. 8월 1일 이후 새 서버에 작업이 쌓였다. 슬러그 오타 수정, 카테고리 noindex, PHP 경고 수정, 플러그인 4건 업데이트, 테마 개조 이전. 되돌리면 그게 전부 사라진다. 그 시점부터 Cloudways는 안전망이 아니라 그냥 지출이었다. 안전망 역할은 이미 EBS 스냅샷과 DB 논리 백업이 대신하고 있었다

삭제 전에 최종 백업을 받고 md5를 양쪽에서 계산해 대조했다. 삭제하고 나면 원본이 없어서, 나중에 받아둔 파일이 깨졌다는 걸 알아도 손쓸 방법이 없다. 그리고 Stop이 아니라 Delete를 골라야 한다. 중지해도 자원이 예약된 상태로 남아 요금은 계속 청구된다

확정할 수 없다고 결론 내렸는데, 확인은 안 해봤다

이관이 끝나고 결산을 하면서 나는 “AWS 쪽 비용은 이 계정에서 분리해낼 수 없다”고 결론 내렸다. 이유는 세 가지였다. 계정에 다른 프로젝트가 섞여 있고, 비용 할당 태그를 활성화하지 않았고, blog-cli에 Savings Plan 조회 권한이 없다

세 개 중 두 개가 틀렸다. 확인해보고 알았다

비용 할당 태그는 소급된다. 나는 지금 켜도 다음 달부터 보인다고 알고 있었다. AWS가 2024년 3월에 태그 백필 기능을 냈다. 최대 12개월까지 소급되고, 조건은 하나다. 그 시점에 자원에 태그가 실제로 붙어 있었을 것. 인스턴스를 만든 7월 31일에 태그를 같이 붙였으니 8월 데이터는 온전히 채워진다. 켜고 2025년 9월부터 백필을 걸었다. 요청은 6분 만에 SUCCEEDED로 끝났다. 그게 곧 조회 가능이라는 뜻은 아니었다. 직후에 태그별로 뽑아보니 여전히 빈 값 한 줄뿐이었고, Cost Explorer가 하루 주기로 재집계를 마친 이틀 뒤에야 숫자가 나왔다

백필은 24시간에 한 번만 요청할 수 있고, 요청 시점의 활성화 상태를 소급한다. 그래서 켤 태그를 먼저 다 켜놓고 한 번에 거는 편이 낫다. NameCreatedBy도 같이 켰다

Savings Plan은 애초에 없었다. blog-cli에 조회 권한이 없는 건 맞다. 권한이 있는 사용자로 조회해보니 활성 Savings Plan도, 예약 인스턴스도 하나도 없었다. 커버리지 0%다. 권한이 없어서 못 본 게 아니라 볼 것이 없었다

그러면 요금은 왜 계획보다 낮게 나오고 있었나. T4g 무료 체험이었다. 월 750시간까지 무료이고 2026년 12월 31일까지 연장돼 있다. t4g.small의 요금이 $0.00으로 찍혀 있었다. 나는 이 낮은 요금을 막연히 Savings Plan 덕으로 짐작하고 계획 문서에도 그렇게 적어뒀다. 약정을 산 적이 없으니 당연히 아니었다

세 번째 이유만 절반 맞았다. 계정에 다른 프로젝트가 섞여 있는 건 사실이고, 그건 태그를 켜면 갈라진다

그래서 실제 얼마인가

백필은 8월 16일에 반영됐다. 이제 태그 한 줄로 블로그 비용만 뽑을 수 있다. 8월 1일부터 15일까지의 실적이다

Project=blog        $1.1023
  t4g.small         349.677 시간    $0.0000   ← 무료 체험
  gp3 20GiB         9.382 GB-월    $0.8556
  스냅샷 9개          4.933 GB-월    $0.2467
  아웃바운드          1.829 GB       $0.0000   ← 100GB 무료 한도 내

일별로는 8월 1일 $0.0739에서 시작해 $0.0781까지 오른 뒤 평평해졌다. 스냅샷 7일치가 다 차서 보관 정책이 한 바퀴 도는 지점에서 안정된 것이다. 안정된 하루치 $0.0781을 31일로 환산하면 이렇게 된다.

지금2027-01-01 이후
t4g.small (31일 = 744시간)$0 (무료 체험)$15.48
gp3 20GiB$1.82$1.82
스냅샷 9개 (증분)$0.60$0.60
부가세 10%$0.24$1.79
합계약 $2.66약 $19.69

계획 시점 추정치 $14.60은 양쪽 다 빗나갔다. 지금은 그보다 훨씬 싸고, 무료 체험이 끝나면 그보다 비싸다. 자원 목록을 훑어보고 “계획대로 나오고 있다”고 넘어갔던 게 정확히 이 지점에서 틀렸다. 자원의 종류와 개수가 맞다는 것과 청구액이 맞다는 것은 다른 얘기다.

절감액도 다시 계산했다.

지금부터 2026년 12월까지    월 $28.69
2027년 1월부터            월 $11.66

$16.75가 아니었다. 그리고 이건 날짜가 박힌 숫자다. 무료 체험 종료는 예고된 인상이므로, 그때 가서 놀랄 일이 아니라 지금 달력에 적어둘 일이다

다만 이 월 환산치도 아직 실적은 아니다. 15일치를 31일로 늘려 잡은 값이고, 8월은 AWS 단독으로 온전히 돌아간 첫 달이다. 9월 초에 8월 청구가 확정되면 그때 실적으로 못박는다

무료 한도는 생각보다 빠듯했다

숫자를 뜯어보다 하나 더 알았다. 무료 체험 한도는 월 750시간인데, 31일짜리 달에 인스턴스 한 대를 24시간 돌리면 744시간이다. 여유가 6시간뿐이다

지금은 한 대뿐이라 문제가 없다. 문제는 인스턴스를 교체할 때다. 새 인스턴스를 띄워 검증한 뒤 옛것을 내리는, 이번 이관에서 내가 실제로 한 그 방식대로 하면 두 대가 겹치는 시간만큼 곧바로 한도를 넘는다. 게다가 이 750시간은 전 리전 합산이라 다른 리전에 띄워도 피할 수 없다

무료라는 말이 “안 세고 있다”는 뜻은 아니었다

태그가 없는 자원은 백필해도 미분류로 남는다

블로그 자원은 11개다. t4g.small 1대 + gp3 20GiB 1개 + 스냅샷 9개, 전부에 Project=blog가 붙어 있다. 백필 데이터가 깨끗하게 나올 조건은 갖춰져 있다.

반대로 태그가 하나도 없는 자원은 백필 후에도 미분류로 남는다. 블로그 몫은 태그 한 줄로 깔끔하게 갈렸지만, 계정 전체에서 미분류로 남은 금액은 블로그 비용보다 훨씬 크다. 그것들이 블로그와 무관하다는 사실은 태그가 아니라 내 기억으로만 확인된다. 몇 달 뒤의 내가 같은 확신을 유지할 수 있을지는 다른 문제다.

세금처럼 애초에 태그가 붙지 않는 항목도 있으니 100%를 가를 수는 없다. 그래도 나머지는 가를 수 있고, 그건 자원을 만들 때 끝내야 하는 일이다. 나중에 비용을 보려고 할 때 시작하면 늦는다.

앞에서 “한 곳으로 모으면 관리가 편해진다”고 썼는데, 그 뒷면이 이거다. 계정을 합치면 관리 대상은 줄지만 비용은 저절로 분리되지 않는다. 태그를 붙이는 것과 그 태그로 비용을 집계하는 것은 다른 작업이다. 전자는 EC2에서, 후자는 Billing 콘솔에서 한다. 나는 전자만 해놓고 후자를 했다고 생각하고 있었다.

태그가 붙어 있는 것과, 태그가 반드시 붙는 것도 다르다

확인해보니 blog-cli는 태그 없이도 볼륨과 스냅샷을 만들 수 있었다. 자원 11개에 태그가 다 붙어 있던 건 구조가 보장한 결과가 아니라 내가 매번 손으로 붙였기 때문이다. 규율로 굴러가는 것은 언젠가 빠진다. 그래서 만드는 시점에 태그가 없으면 아예 거부하도록 권한에 조건을 걸었다.

그 과정에서 한 번 더 넘어졌다. 처음 만든 정책은 “태그 없이 만들지 마라”를 규칙 하나에 몰아넣었고, 검증 도구도 통과했다. 실제로 적용하니 태그를 제대로 붙여도 스냅샷이 만들어지지 않았다. 스냅샷을 만드는 요청은 만들어질 스냅샷뿐 아니라 원본 볼륨에 대해서도 권한을 따지는데, 내가 붙인 태그는 스냅샷에만 붙지 원본 볼륨에는 붙지 않는다. 규칙이 원본 볼륨을 보는 순간 “태그가 없다”고 판단해 막아버린 것이다.

고치는 방법은 규칙을 작업별로 쪼개서, 각 규칙이 그 작업이 실제로 만들어내는 대상만 보게 하는 것이었다. 백업이 걸린 문제라 다행히 빨리 발견했다. 자동 백업은 별도 권한으로 돌아서 멈추지 않았고, 막힌 것은 사람이 손으로 뜨는 스냅샷뿐이었다.

여기서 남는 건 규칙 문법이 아니다. 권한 설정은 시뮬레이션을 통과해도 실제로 걸어보기 전에는 모른다. 적용 전에 검증을 돌렸고 전부 통과했지만, 그 검증은 내가 상상한 조합만 시험한 것이었다. 실제 요청이 무엇을 함께 검사하는지는 보내봐야 나왔다. 전환 당일에 겪은 “성공 메시지와 실제 성공은 다르다”가 권한 쪽에서 한 번 더 반복됐다

청구서에 안 나오는 소득과 비용

돈만 보면 월 $28.69, 무료 체험이 끝나면 $11.66이지만, 실제로 얻은 건 더 있었다

  • 445MB어치 WebP가 되살아났다. 원래 서버에서는 한 번도 쓰이지 않고 있었다
  • TTFB 0.375초 → 0.120초
  • www 인증서 오류가 고쳐졌다
  • 슬러그 오타로 글이 0개 나오던 페이지가 정상화됐다
  • 아무도 몰랐던, 인증 없는 네이버 API 프록시를 제거했다
  • 손으로 개조돼 있던 부모 테마를 자식 테마 훅으로 옮겼다
  • 밀려 있던 플러그인 8건과 테마를 정리했다

이 중 다수는 서버를 옮겨서 생긴 게 아니라, 옮기려고 전부 뜯어봤기 때문에 발견된 것이다. 사이트를 처음부터 끝까지 다시 읽는 일은 평소에 생기지 않는다

반대편도 적어야 공평하다. 백업 정책을 직접 만들었고, 인증서 갱신을 직접 걸었고, 보안 패치를 직접 켰고, 방화벽을 직접 짰고, XML-RPC에 37분 동안 1,000건의 무차별 대입이 들어왔을 때 로그를 직접 읽었다. 그리고 13일이 걸렸다. 시급으로 환산하면 1년치 절감액을 넘겼을 가능성이 높다

이 방법이 안 통하는 경우

트래픽이 크거나 방문자 수가 매출과 직결되는 사이트라면 계산이 달라진다. 이 사이트는 월 아웃바운드가 1.75GB이고 운영자가 한 명이라, 관리형 호스팅이 대신 해주던 일을 내가 떠안아도 감당 가능한 규모였다. 팀 서비스이거나 야간 장애 대응이 필요한 상황이었다면 그 차액을 아끼려고 이 노동을 가져오는 선택은 하지 않았을 것이다

한 줄로 요약하면, 관리형 호스팅에서 옮겨서 아끼는 돈은 서버 임대료 차액이 아니라 그 회사가 대신 해주던 노동의 가격이다

그래서 판단 기준은 청구서가 아니라 목록에서 나온다. 옮길지 말지 고민 중이라면 요금 비교표를 만들기 전에 지금 그 회사가 대신 해주고 있는 일을 항목으로 적어보는 게 먼저다. 백업, 패치, 인증서 갱신, 방화벽, 멀웨어 스캔. 그 목록의 각 줄을 내가 직접 만들 수 있고, 만든 뒤에도 계속 지켜볼 수 있다면 옮겨도 된다. 목록을 적어보지 않고 요금만 비교하면, 절감액이 실제로 무엇의 가격이었는지는 옮기고 난 다음에 알게 된다

그리고 절감액을 적을 때는 요금표가 아니라 청구서를 본다. 확정할 수 없다고 쓰기 전에 정말 확정할 수 없는지 한 번 확인한다. 내가 결산에 적은 세 가지 이유 중 두 가지가 확인 한 번으로 뒤집혔다. 못 하는 것과 안 해본 것을 구별하지 않으면, 안 해본 것이 못 하는 것으로 굳는다

용어 정리

본문에 나온 용어를 필요한 만큼만 정리했다

호스팅과 AWS

  • Cloudways — 서버 임대와 운영 도구를 묶어 파는 관리형 호스팅이다. 실제 서버는 DigitalOcean 같은 사업자에게서 빌려 온다
  • VPS — 물리 서버를 나눠 쓰는 가상 서버다. 사양이 고정돼 있고 운영 책임은 대부분 사용자에게 있다
  • EC2 — AWS의 가상 서버 서비스다
  • t4g.small — ARM 기반 Graviton 프로세서를 쓰는 EC2 인스턴스 타입이다. 2 vCPU / 2GB다
  • vCPU — 인스턴스에 할당된 가상 CPU 코어 수다
  • 아웃바운드 트래픽 — 서버에서 외부로 나가는 데이터양이다. AWS는 이 방향에만 요금을 매기고 월 100GB까지는 무료다
  • EBS · gp3 — EC2에 붙는 네트워크 블록 스토리지와 그 볼륨 타입이다
  • EBS 스냅샷 — 볼륨 전체를 시점 단위로 뜬 백업이다. AWS 쪽에서 만들어지므로 인스턴스가 침해돼도 서버 안에서는 지울 수 없다
  • IAM 사용자와 역할 — AWS 자원에 대한 권한 주체다. 사람이 쓰면 사용자, 인스턴스 같은 자원이 쓰면 역할이다
  • SSM Session Manager — 서버가 AWS로 아웃바운드 접속해 대기하는 방식의 원격 접속이다. 인바운드 포트를 하나도 열지 않아도 접속된다
  • 보안그룹 — 인스턴스 앞단의 방화벽이다. 어떤 IP 대역이 어떤 포트로 들어올 수 있는지를 정한다
  • 비용 할당 태그 — 자원에 붙인 태그를 Billing 콘솔에서 활성화해 비용 집계 단위로 쓰는 기능이다. 태그를 붙이는 것만으로는 집계되지 않는다
  • 태그 백필 — 활성화한 비용 할당 태그를 과거 비용 데이터에 소급 적용하는 기능이다. 최대 12개월까지 채워지고, 24시간에 한 번만 요청할 수 있으며, 그 시점에 자원에 태그가 붙어 있던 기간만 채워진다
  • Cost Explorer — AWS 비용을 기간·서비스·태그별로 집계해 보여주는 콘솔이다
  • Savings Plan · 예약 인스턴스 — 일정 사용량을 약정하고 요금을 할인받는 제도다. 1년 약정이면 대략 30% 안팎이 빠진다
  • T4g 무료 체험t4g 계열 인스턴스를 월 750시간까지 무료로 쓸 수 있는 프로그램이다. 전 리전 합산이고, 2026년 12월 31일까지 연장돼 있다
  • GB-월 — 스토리지 요금 단위다. 20GiB를 한 달 내내 두면 20GB-월이고, 보름만 두면 10GB-월이다

WordPress와 웹서버

  • wp-config.php — DB 접속 정보와 각종 상수를 담는 WordPress 설정 파일이다
  • wp-salt.php — Cloudways가 인증 키를 따로 빼 둔 파일이다. 일반적인 WordPress 설치에는 없다
  • salt — 쿠키와 세션 값을 암호화하는 임의 문자열이다. 바뀌면 로그인 세션이 전부 끊긴다
  • WP-CLI (wp) — 명령줄에서 WordPress를 조작하는 도구다
  • active_plugins — 활성화된 플러그인 목록을 담은 DB 옵션이다
  • ads.txt — 이 도메인의 광고 재고를 누가 팔 수 있는지 선언하는 파일이다. 애드센스가 이 파일을 읽는다
  • .htaccessAllowOverride — Apache가 디렉터리 단위로 읽는 설정 파일과, 그 파일을 읽도록 허용하는 지시자다. AllowOverride가 없으면 .htaccess는 통째로 무시된다
  • permalink — 글 주소 구조 규칙이다. /%postname%/은 카테고리를 경로에 넣지 않는 형태다
  • PHP-FPM — 웹서버 뒤에서 PHP를 실행하는 프로세스 관리자다
  • Varnish — 완성된 HTML을 서버 앞단에서 캐시하는 소프트웨어다
  • 오브젝트 캐시 — DB 조회 결과를 Redis 같은 메모리 저장소에 담아 두는 계층이다. DB 값을 바꿔도 이 캐시가 옛 값을 들고 있으면 옛 값이 읽힌다
  • Object Cache Pro — 유료 Redis 캐시 플러그인이다. Cloudways는 이걸 고객에게 무료로 제공한다
  • latin1 · utf8mb4 — MySQL 계열의 문자셋이다. 한글을 담으려면 utf8mb4여야 한다
  • XML-RPC (xmlrpc.php) — 외부 앱이 WordPress를 조작하는 옛 API다. system.multicall은 요청 하나에 로그인 시도를 수백 개 담을 수 있어 무차별 대입에 쓰인다

DNS와 Cloudflare

  • A 레코드 — 도메인이 어떤 IP를 가리키는지 적은 DNS 레코드다
  • 네임서버(NS) — 그 도메인의 DNS 레코드를 어디서 관리하는지 지정한다. Cloudflare 도입은 이걸 바꾸는 작업이다
  • DNS 리졸버 — 사용자를 대신해 도메인을 IP로 바꿔 주는 서버다. 통신사마다 따로 운영해서 전파 속도가 제각각이다
  • TTL — 리졸버가 응답을 캐시하는 시간이다. 이 시간이 지나야 바뀐 값이 반영된다
  • 프록시(오렌지 구름)와 DNS 전용(회색 구름) — 트래픽을 Cloudflare를 거쳐 보낼지, DNS만 대행하고 원본으로 직행시킬지를 정하는 설정이다
  • 오리진과 엣지 — 실제 콘텐츠를 가진 원본 서버가 오리진, 그 앞에서 캐시하고 대신 응답하는 Cloudflare 서버가 엣지다.
  • SSL 모드 Flexible · 전체(엄격) — Cloudflare가 오리진에 접속하는 방식이다. Flexible은 HTTP로, 전체(엄격)는 인증서를 검증하는 HTTPS로 접속한다
  • certbot · Let’s Encrypt · DNS-01 — 무료 인증서 발급 도구와 발급 기관, 그리고 DNS 레코드로 도메인 소유를 증명하는 방식이다. DNS-01은 프록시를 켠 상태에서도 갱신된다
  • TTFB — 요청을 보내고 응답의 첫 바이트를 받기까지 걸린 시간이다

이미지와 검증

  • WebP — 같은 화질에서 JPEG·PNG보다 파일이 작은 이미지 형식이다
  • Accept 헤더와 Vary: Accept — 브라우저가 받을 수 있는 형식을 알리는 헤더와, 응답이 그 헤더에 따라 달라진다고 캐시에 알리는 헤더다
  • curl --resolve — 도메인 이름은 그대로 두고 접속할 IP만 지정하는 옵션이다. /etc/hosts를 고치지 않고 특정 서버만 검증할 때 쓴다
  • rsync — 변경분만 골라 전송하는 파일 동기화 도구다
  • set -e — 셸 스크립트에서 명령이 실패하면 즉시 중단시키는 옵션이다. 없으면 실패해도 다음 줄이 그대로 실행된다
  • md5 체크섬 — 파일 내용을 요약한 값이다. 양쪽에서 계산해 비교하면 전송 중 손상 여부를 알 수 있다
  • Mediapartners-Google — 애드센스가 광고를 맞추려고 페이지를 읽을 때 쓰는 크롤러다. 이 요청이 정상 응답을 받고 있으면 광고 시스템이 도는 것이다