개요
AWS CloudFront와 HAProxy를 활용한 CloudFront Legacy Origin 연동 구성 및 트러블슈팅을 정리합니다. legacy 서버를 보호하기 위해 CloudFront와 WAF 레이어를 구성하며 발생한 다양한 이슈를 기록한 내용입니다.
도메인 변경과 HAProxy를 origin으로 하는 구조에서 모든 트래픽이 반드시 CloudFront를 경유하도록 강제하고, origin의 직접 접근을 차단하는 것이 주요 목표입니다.
본문은 CloudFront 구성이 주요 목적이 아니므로 전체적인 과정은 생락하고 Legacy Origin을 연동하는 과정만 다루도록 하겠습니다.
아키텍처
전체 트래픽 흐름은 다음과 같습니다.
Browser (HTTPS)
→ CloudFront (WAF, CDN, TLS 종료)
→ HAProxy :80 (X-CF-Secret 검증)
→ Apache :443
WAF bypass 방지 원리: CloudFront는 모든 origin 요청에 X-CF-Secret custom origin header를 추가합니다. HAProxy는 www.example.com로 들어오는 요청 중 이 헤더가 없는 경우 403을 반환합니다. 브라우저가 HAProxy IP로 직접 접근하더라도 CloudFront를 거치지 않으면 403으로 차단됩니다.
HAProxy 핵심 설정
frontend http_front
bind *:80
acl is_cf_www hdr(host) -i www.example.com
acl has_cf_secret hdr(X-CF-Secret) -i <SECRET_VALUE>
# CloudFront 아닌 직접 접근 차단
http-request deny deny_status 403 if is_cf_www !has_cf_secret
# CF 경유 표시 후 backend 라우팅
http-request set-var(req.cf_routed) str(yes) if is_cf_www has_cf_secret
http-request set-header X-Forwarded-Proto https if is_cf_www has_cf_secret
http-request redirect scheme https code 301 unless { var(req.cf_routed) -m found }
use_backend www.example.com if { var(req.cf_routed) -m found }
frontend https_front
bind *:443 ssl crt /etc/letsencrypt/live/complex_dev.pem
acl is_www.example.com hdr(host) -i www.example.com
acl is_www.example.com hdr(host) -i <MY_IP>
acl has_cf_secret hdr(X-CF-Secret) -i <SECRET_VALUE>
http-request deny deny_status 403 if is_www.example.com !has_cf_secret
use_backend www.example.com if is_www.example.com
주의: HAProxy의 redirect 지시어는 use_backend보다 항상 먼저 처리됩니다. CloudFront 트래픽을 판별한 뒤 리다이렉트를 선택적으로 건너뛰려면 http-request redirect와 set-var를 조합해야 합니다.
CloudFront Behaviors 구성
| 우선순위 | Path pattern | Cache policy | Origin request policy |
|---|---|---|---|
| 0 | /wp-admin/* | CachingDisabled | AllViewer |
| 1 | /wp-login.php | CachingDisabled | AllViewer |
| 2 (Default) | * | UseOriginCacheControlHeaders-QueryStrings | – |
/wp-admin/*와 /wp-login.php는 세션·쿠키 의존 동적 페이지이므로 캐시하지 않고 AllViewer로 모든 요청 정보를 origin에 전달합니다. Default behavior는 공개 페이지 캐시에 origin의 Cache-Control 헤더를 그대로 사용합니다.
트러블슈팅
1. 로그인 페이지 스타일 깨짐 — CloudFront 쿼리스트링 미전달
현상: /wp-login.php 등의 페이지가 스타일 미적용 상태로 표시됨.
원인: CloudFront의 Behavior 기본 Cache policy는 UseOriginCacheControlHeaders는 쿼리스트링을 cache key에 포함하지 않고 origin에도 전달하지 않습니다. WordPress의 load-styles.php?c=0&dir=ltr&load[]=...가 파라미터 없이 호출되어 0바이트 빈 CSS를 반환했고, CloudFront가 이를 max-age=31536000(1년) TTL로 캐시했습니다.
# 진단: 쿼리스트링 유무에 따른 응답 차이
curl -s -o /dev/null -w "%{size_download}" "http://<origin>/wp-admin/load-styles.php"
# → 0 bytes (파라미터 없으면 빈 응답)
curl -s -o /dev/null -w "%{size_download}" "http://<origin>/wp-admin/load-styles.php?c=0&dir=ltr&load[chunk_0]=login,..."
# → 110,881 bytes (정상 CSS)
해결: Cache policy를 UseOriginCacheControlHeaders-QueryStrings로 변경하고 CloudFront invalidation(/*) 실행. 이후 정상 표시 확인.
2. 캐시 플러그인 충돌 — W3 Total Cache drop-in 잔존
현상: admin-ajax.php 500 오류, CSS 응답 0바이트.
원인: W3 Total Cache 플러그인 삭제 후에도 WordPress drop-in 파일 /wp-content/object-cache.php가 남아 모든 object cache 요청을 가로채 오류를 반환.
# 오류 메시지 W3 Total Cache Error: some files appear to be missing or out of place. Please re-install plugin or remove /var/www/html/wp-content/object-cache.php. # 해결 rm /var/www/html/wp-content/object-cache.php
교훈: 캐시 플러그인 삭제 시 drop-in 파일(object-cache.php, advanced-cache.php, db.php)은 자동 삭제되지 않습니다. CloudFront 등 외부 CDN을 사용하는 경우 서버 사이드 캐시 플러그인은 제거하는 것이 좋습니다.
3. 배경 이미지 미표시 — PHP 직렬화 데이터 손상
현상: 메인 페이지 배경 이미지가 외부에서 표시되지 않음.
원인 1: DB의 theme_mods_twentyfourteen에 배경 이미지 URL이 구 도메인(www.sierracloud.dev)으로 저장.
원인 2: SQL REPLACE()로 URL을 교체했으나 PHP 직렬화 문자열의 길이 prefix(s:N:)가 업데이트되지 않아 unserialize 실패 → 테마 설정 전체 소실.
-- 잘못된 방법 (직렬화 길이 불일치 발생)
UPDATE sc_options
SET option_value = REPLACE(option_value,
'https://www.sierracloud.dev',
'https://www.example.com')
WHERE option_name = 'theme_mods_twentyfourteen';
-- s:79:"https://www.example.com/..." → 실제 길이는 76 → unserialize 실패
-- 올바른 방법: 전체 직렬화 값을 정확히 재구성하여 UPDATE
UPDATE sc_options
SET option_value = 'a:10:{...s:16:"background_image";s:76:"https://www.example.com/wp-content/uploads/.../image.jpg";}'
WHERE option_name = 'theme_mods_twentyfourteen';
교훈: WordPress DB의 PHP 직렬화 데이터에 SQL REPLACE()를 사용하면 문자열 길이 prefix가 맞지 않아 반드시 손상됩니다. URL 교체 작업에는 wp search-replace CLI를 사용하세요.
4. HTML 30일 캐시 — .htaccess mod_expires 설정
현상: CloudFront가 메인 페이지 HTML을 30일간 캐시. 콘텐츠 업데이트 미반영.
# 수정 전 ExpiresByType text/html "access 1 month" ExpiresDefault "access 1 month" # 수정 후 ExpiresByType text/html "access 0 seconds" ExpiresDefault "access 1 day"
구성 수정 요약
| 항목 | 설정 |
|---|---|
| CloudFront → Origin 프로토콜 | HTTP (port 80) |
| WAF bypass 방지 | X-CF-Secret custom origin header 검증 |
| 기본 Cache policy | UseOriginCacheControlHeaders-QueryStrings |
| wp-admin / wp-login 처리 | CachingDisabled + AllViewer |
| HTML Cache-Control | max-age=0 (캐시 제외) |
| 정적 자산 Cache-Control | max-age=31536000 (WordPress 자동 버전 관리) |
참고
관련 포스트:
- ingress-nginx Traefik 마이그레이션 — Traefik v3.7.5 설치 및 와일드카드 TLS 자동화
- Let’s Encrypt 인증서 생성 실패 트러블슈팅
- Kubernetes cluster upgrade
참고 문서: CloudFront Custom Origin Headers (AWS 공식) · HAProxy ACL 설정 가이드