태그 보관물: AWS

CloudFront + Legacy Origin 연동

개요

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 redirectset-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 자동 버전 관리)

참고

관련 포스트:

참고 문서: CloudFront Custom Origin Headers (AWS 공식) · HAProxy ACL 설정 가이드

AWS GWLB Review

AWS GWLB(Gateway Load Balancer)는 L3 기반 트래픽을 Geneve 프로토콜로 캡슐화하여 보안 어플라이언스(IPS/IDS/방화벽)에 인라인 검사(Inline Inspection)를 위임하는 AWS 관리형 로드밸런서입니다. IDC와 AWS Cloud가 DX로 연결된 하이브리드 클라우드 환경이었고 기존 Legacy IPS가 도태됨에 따라 보안 요건에 의해 AWS Cloud에 IPS 구성이 필요한 상황 이었습니다.

그에 따라 GWLB*를 검토하였고 신규 도입되는 Cisco IPS의 Geneve Protocol** 지원 여부를 검토 하였으며 다행히 지원이 가능한 스펙인 것을 확인했습니다.

설계 단계에서 기존 Network Architecture를 크게 변경시킬 경우 여러 account의 VPC에 영향이 있을 가능성이 있어 TGW등의 형상 변경은 최소화 하는 방향으로 진행하였고 IDC에서 DX를 거쳐 AWS로 진입하는 구간의 VPC에 GWLB Endpoint를 위치 시키고 별도의 Inspection VPC를 구성하여 해당 VPC에 GWLB와 GWLB Endpoint Service를 위치 시켰습니다.

그리고 진입구간 VPC의 라우팅 테이블에 Next hop을 GWLB Endpoint로 지정하고 GWLB Endpoint가 위치한 서브넷의 라우팅 테이블은 외부로 트래픽 처리가 되도록 기존 구성을 유지 시켰습니다.

이로서 DX를 거치는 모든 inbound/outbound 패킷은 GWLB Endpoint로 전달 되어 GWLB를 통해 IPS로 Inspection 되도록 하였고 Inspection이 끝난 패킷은 다시 GWLB Endpoint가 위치한 서브넷으로 되돌아와 정상적으로 라우팅 처리 되도록 하였습니다.

ALB, NLB 등과 GWLB의 구성상 차이점은 ALB 등은 Endpoint 구성시 AZ를 복수 선택하여 가용성 유지가 가능했지만 GWLB는 Endpoint 구성시 AZ를 1개만 선택이 가능했습니다.
라우팅테이블의 Nexthop에 GWLB Endpoint를 지정할 경우 1개의 AZ로만 트래픽이 흐르게 되어 구조상 가용성 유지에 문제가 발생하였습니다.

이를 해결하기 위해 AZ 단위로 라우팅 테이블 분리하고 각 AZ별로 GWLB Endpoint를 생성 후 지정하였지만 예를 들어 AWS Cloud C존에서 출발한 트래픽이 DX를 거쳐 IDC 통해 SYN 처리 후 SYN/ACK 처리시 A존으로 인입 된 후 IPS에 도달 하게 되면 세션 테이블과 불일치 하며 Asymmetric 트래픽을 Drop 처리 하는 현상이 발생 하였습니다.

결국 AZ단위 라우팅 테이블 분리는 적절하지 않은 구성이었고 가용성 확보를 위해 AZ 단위 트래픽 이상을 감시하는 Event Bridge 를 구성하고 Trigger용 Lambda와 라우팅테이블 Nexthop을 변경 하는 Lambda를 개발하여 보완 하였습니다.

패킷 감시용 어플라이언스를 사용하기 위해서는 필수적인 LB이지만 트래픽 흐름의 누수나 루프가 발생하지 않는지에 대한 신중한 검토가 필요한 구성이었습니다.

AWS GWLB 아키텍처 — DX 구간 Inspection VPC 구성

참고

참고 문서: AWS GWLB 공식 문서 · AWS GWLB 소개 블로그