DNS 심화 & CDN
DNS 심화 & CDN
섹션 제목: “DNS 심화 & CDN”전제 지식: L2 dns-basics.md에서 DNS 쿼리 과정(Recursive/Iterative), A/CNAME/MX/TXT 레코드, TTL, Route 53 Hosted Zone 개념을 다뤘다. 이 문서는 그 위에서 시작한다.
1. DNSSEC — DNS에 서명을 추가하다
섹션 제목: “1. DNSSEC — DNS에 서명을 추가하다”1-1. 왜 필요한가
섹션 제목: “1-1. 왜 필요한가”DNS는 설계 당시 보안을 고려하지 않았다. Recursive Resolver가 캐시에서 응답을 돌려줄 때 그 응답이 진짜인지 위조된 것인지 확인할 방법이 없다. 이 약점을 악용하는 공격이 DNS Cache Poisoning이다.
공격자가 Resolver 캐시에 위조 응답을 주입 → 클라이언트는 bank.com → 공격자 IP 로 안내됨 → 피싱 사이트로 유도, 사용자는 인지 불가DNSSEC(DNS Security Extensions)은 DNS 응답에 디지털 서명을 추가하여 응답의 진위와 무결성을 보장한다. 암호화는 하지 않는다 — 내용은 평문이지만, 변조 여부를 검증할 수 있다.
1-2. 핵심 레코드 3종
섹션 제목: “1-2. 핵심 레코드 3종”| 레코드 | 역할 |
|---|---|
| DNSKEY | 존(Zone)의 공개키. ZSK(Zone Signing Key)와 KSK(Key Signing Key) 두 종류 |
| RRSIG | 각 RRset(레코드 집합)에 대한 디지털 서명 |
| DS (Delegation Signer) | 부모 존에 저장되는 자식 존 KSK의 해시. 신뢰 체인의 연결 고리 |
ZSK vs KSK
- ZSK: 실제 DNS 레코드(A, CNAME 등)를 서명. 주기적으로 교체(보통 30일).
- KSK: ZSK 공개키(DNSKEY 레코드)를 서명. 교체 주기가 더 길다(보통 1년). KSK의 해시가 DS 레코드로 부모 존에 등록된다.
1-3. 체인 오브 트러스트 (Chain of Trust)
섹션 제목: “1-3. 체인 오브 트러스트 (Chain of Trust)”DNSSEC의 신뢰는 루트(.)에서 시작하여 TLD → 도메인으로 전파된다.
루트(.) Zone ├── DNSKEY (루트 KSK, ZSK) ├── RRSIG (루트 ZSK로 서명된 레코드들) └── DS for .com ←── .com KSK의 해시
.com Zone ├── DNSKEY (.com KSK, ZSK) ├── RRSIG (.com ZSK로 서명된 레코드들) └── DS for example.com ←── example.com KSK의 해시
example.com Zone ├── DNSKEY (example.com KSK, ZSK) ├── A 레코드 → 93.184.216.34 └── RRSIG (example.com ZSK로 서명된 A 레코드)Resolver가 example.com의 A 레코드를 검증하는 흐름:
example.com에서 A 레코드 + RRSIG 수신example.com의 DNSKEY(ZSK)로 RRSIG 검증example.com의 DNSKEY(KSK)가 유효한지 확인 →.com에서 DS 레코드 조회.com의 DS 레코드와example.comKSK 해시가 일치하는지 확인.com의 DNSKEY 역시 루트의 DS로 검증- 루트는 IANA가 관리하는 Trust Anchor(공개키)로 검증 — 브라우저/OS에 하드코딩
# DNSSEC 검증 확인 (dig +dnssec)dig +dnssec example.com A
# DS 레코드 조회dig DS example.com @a.gtld-servers.net
# DNSKEY 조회dig DNSKEY example.com
# RRSIG 확인dig +dnssec +multi A example.com예상 출력 (DNSSEC 활성화된 도메인):
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2;; "ad" 플래그(Authenticated Data)가 있으면 Resolver가 DNSSEC 검증 성공
;; ANSWER SECTION:example.com. 3600 IN A 93.184.216.34example.com. 3600 IN RRSIG A 8 2 3600 ( 20250215000000 20250115000000 12345 example.com. abc123def456ghi789== )
# RRSIG 필드 설명:# A → 서명 대상 레코드 타입# 8 → 서명 알고리즘 (8 = RSA/SHA-256)# 2 → 레이블 수# 3600 → 원본 TTL# 20250215000000 → 서명 만료 시각# 12345 → Key Tag (서명에 사용된 DNSKEY 식별자)DNSSEC 검증이 왜 DNS Cache Poisoning을 막는가:
캐시 포이즈닝 시도:공격자가 "example.com → 10.1.2.3" (위조 응답) 주입 시도
DNSSEC 없을 때: Resolver는 응답의 진위를 확인할 방법이 없음 → 공격 성공
DNSSEC 있을 때: 1. 위조 응답에는 RRSIG가 없거나 잘못된 서명이 있음 2. Resolver: example.com의 ZSK(DNSKEY)로 RRSIG 검증 3. 서명이 맞지 않음 → SERVFAIL 반환 → 공격 실패
핵심: 공격자는 example.com의 ZSK 개인키 없이는 유효한 RRSIG를 만들 수 없음📖 더 보기: DNSSEC Basics — Internet Society — RRSIG/DNSKEY/DS의 역할과 Chain of Trust 시각 자료. 이 섹션의 1-2~1-3 심화 학습용
1-4. DNSSEC 배포 시 주의사항
섹션 제목: “1-4. DNSSEC 배포 시 주의사항”- 키 교체(Key Rollover): ZSK 교체 시 기존 키와 새 키를 동시에 게시하는 기간(Pre-publish) 필요. 캐시 TTL보다 길게 유지.
- 부정적 응답 보호: 존재하지 않는 도메인 쿼리에 대해 NSEC/NSEC3 레코드로 서명된 “없음” 응답을 제공.
- NSEC3: NSEC의 존 열거(Zone Walking) 취약점을 해결. 레코드 이름을 해시하여 응답.
2. DoH / DoT — DNS 트래픽 암호화
섹션 제목: “2. DoH / DoT — DNS 트래픽 암호화”2-1. 기존 DNS의 문제
섹션 제목: “2-1. 기존 DNS의 문제”전통적인 DNS는 UDP 53번 포트로 평문 전송된다. ISP, 네트워크 관리자, 중간자가 DNS 쿼리를 볼 수 있다.
클라이언트 → "api.example.com 어디야?" → Resolver (평문, 누구나 볼 수 있음)2-2. DoT (DNS over TLS)
섹션 제목: “2-2. DoT (DNS over TLS)”- 포트: 853 TCP
- 방식: DNS 메시지를 TLS 1.3으로 래핑
- 특징: 기존 DNS 프로토콜 구조 그대로, TLS 레이어만 추가
- 단점: 853 포트가 차단된 환경에서 우회 불가. ISP나 방화벽이 식별하기 쉬움.
# kdig로 DoT 테스트 (knot-dnsutils 패키지)kdig -d @1.1.1.1 +tls-ca example.com A
# curl로 DoH 테스트curl -s "https://cloudflare-dns.com/dns-query?name=example.com&type=A" \ -H "accept: application/dns-json"2-3. DoH (DNS over HTTPS)
섹션 제목: “2-3. DoH (DNS over HTTPS)”- 포트: 443 TCP (일반 HTTPS와 동일)
- 방식: HTTP/2 또는 HTTP/3 위에서 DNS 쿼리를 JSON 또는 binary wire format으로 전송
- 엔드포인트 예시:
- Cloudflare:
https://1.1.1.1/dns-query - Google:
https://8.8.8.8/dns-query
- Cloudflare:
- 장점: 443 포트를 사용하므로 차단이 어렵고 일반 HTTPS 트래픽과 구분이 어려움
- 단점: 기존 네트워크 모니터링 도구와 충돌 가능성. 기업 환경에서 보안 정책 우회 우려.
2-4. 브라우저와 OS의 DoH 지원
섹션 제목: “2-4. 브라우저와 OS의 DoH 지원”Firefox: 기본값으로 Cloudflare DoH 사용 (Trusted Recursive Resolver)Chrome: 시스템 DNS가 DoH 지원하면 자동 업그레이드Android 9+: Private DNS 설정으로 DoT 지원iOS/macOS: DNS-over-HTTPS profile 설치 가능2-5. DoH vs DoT 비교
섹션 제목: “2-5. DoH vs DoT 비교”| 항목 | DoT | DoH |
|---|---|---|
| 포트 | 853 | 443 |
| 프로토콜 | DNS + TLS | DNS + HTTPS |
| 식별 용이성 | 쉬움 (전용 포트) | 어려움 (HTTPS와 혼재) |
| 기업 환경 적합성 | 높음 | 낮음 (정책 우회 우려) |
| 개인 프라이버시 | 좋음 | 더 좋음 |
| 구현 복잡도 | 낮음 | 높음 |
3. DNS 기반 로드밸런싱 — Route 53 라우팅 정책
섹션 제목: “3. DNS 기반 로드밸런싱 — Route 53 라우팅 정책”Route 53은 단순한 DNS 서버가 아니다. 헬스 체크와 결합하여 지능형 트래픽 라우팅 플랫폼으로 동작한다.
3-1. Simple 라우팅
섹션 제목: “3-1. Simple 라우팅”가장 기본적인 방식. 하나의 레코드에 여러 IP를 등록하면 랜덤하게 반환한다.
api.example.com → [1.2.3.4, 5.6.7.8] (무작위 순서로 반환)헬스 체크 불가. 장애 시 자동 제외 없음.
3-2. Weighted 라우팅 (가중치)
섹션 제목: “3-2. Weighted 라우팅 (가중치)”트래픽 비율을 직접 제어한다. A/B 테스트, 카나리 배포에 적합.
api.example.com ├── 1.2.3.4 (Weight: 90) → 90% 트래픽 └── 5.6.7.8 (Weight: 10) → 10% 트래픽 (새 버전 카나리)가중치는 절대값이 아닌 비율. Weight 70 + 30이면 70%, 30% 분배.
# Route 53 CLI로 Weighted 레코드 생성aws route53 change-resource-record-sets \ --hosted-zone-id Z1234567890 \ --change-batch '{ "Changes": [{ "Action": "CREATE", "ResourceRecordSet": { "Name": "api.example.com", "Type": "A", "SetIdentifier": "primary", "Weight": 90, "TTL": 60, "ResourceRecords": [{"Value": "1.2.3.4"}] } }] }'3-3. Latency-based 라우팅 (지연시간)
섹션 제목: “3-3. Latency-based 라우팅 (지연시간)”클라이언트에서 각 AWS 리전까지의 측정된 지연시간을 기준으로 가장 빠른 리전으로 라우팅한다.
Seoul 사용자 → ap-northeast-2 (도쿄) 리전 서버London 사용자 → eu-west-1 (아일랜드) 리전 서버실측 데이터 기반이므로 지리적 위치와 항상 일치하지는 않는다. 해저 케이블 라우팅, 피어링 상황에 따라 달라진다.
3-4. Geolocation 라우팅 (지리적)
섹션 제목: “3-4. Geolocation 라우팅 (지리적)”클라이언트의 IP 기반 위치로 라우팅한다. 지연시간과 달리 콘텐츠 규정 준수(법적 제약), 언어별 서비스에 사용.
대한민국 → kr.example.com (한국어 서비스)유럽 → eu.example.com (GDPR 준수 서버)기본값 → default.example.com (위치 불명 트래픽)주의: 기본값(Default) 레코드를 반드시 설정해야 한다. 미설정 시 매핑되지 않는 지역에서 NXDOMAIN 반환.
3-5. Failover 라우팅
섹션 제목: “3-5. Failover 라우팅”Active-Passive 고가용성 구성. Primary 헬스 체크 실패 시 Secondary로 자동 전환.
api.example.com ├── Primary: 1.2.3.4 (헬스 체크 연결) │ └── 헬스 체크 실패 시 ↓ └── Secondary: 5.6.7.8 (fallback)Route 53 헬스 체크 설정:
{ "Type": "HTTPS", "ResourcePath": "/health", "FullyQualifiedDomainName": "api.example.com", "RequestInterval": 30, "FailureThreshold": 3}RequestInterval: 30초마다 체크FailureThreshold: 3회 연속 실패 시 Unhealthy 판정- 복구는 반대로 3회 연속 성공 시 Healthy
3-6. Geoproximity 라우팅 (Traffic Policy)
섹션 제목: “3-6. Geoproximity 라우팅 (Traffic Policy)”지리적 위치 + **Bias(편향값)**로 라우팅 경계를 조정한다. 특정 리전으로 더 많은 트래픽을 유도하거나 줄일 수 있다.
Bias +50: 해당 리전의 서비스 반경을 확장 (더 많은 트래픽 흡수)Bias -50: 해당 리전의 서비스 반경을 축소Traffic Flow 시각화 도구에서 지도 기반으로 확인 가능.
3-7. 헬스 체크 + 알림 연동
섹션 제목: “3-7. 헬스 체크 + 알림 연동”# CloudWatch 알람과 연동하여 SNS 알림aws route53 create-health-check \ --caller-reference "my-health-check-$(date +%s)" \ --health-check-config '{ "IPAddress": "1.2.3.4", "Port": 443, "Type": "HTTPS", "ResourcePath": "/health", "FullyQualifiedDomainName": "api.example.com", "RequestInterval": 30, "FailureThreshold": 3 }'4. CDN 아키텍처 — 엣지에서 콘텐츠를 제공하다
섹션 제목: “4. CDN 아키텍처 — 엣지에서 콘텐츠를 제공하다”4-1. CDN의 핵심 아이디어
섹션 제목: “4-1. CDN의 핵심 아이디어”사용자와 가까운 **엣지 서버(POP, Point of Presence)**에 콘텐츠를 캐시하여 오리진 서버 부하를 줄이고 응답 속도를 높인다.
[사용자] → [엣지 서버(서울)] → [오리진 서버(us-east-1)] ↑ 캐시 히트 시 여기서 응답 (RTT 수십ms) 캐시 미스 시 오리진에서 가져와 캐시 후 응답CDN 없이:
- 서울 사용자 → 미국 오리진: ~150ms RTT × 왕복 수회 = 수백ms 지연
CDN 있을 때:
- 서울 사용자 → 서울 엣지: ~5ms RTT
- 엣지 → 오리진: 백그라운드에서 캐시 갱신
4-2. 캐시 계층
섹션 제목: “4-2. 캐시 계층”L1 캐시 (메모리): 엣지 서버 RAM — 가장 빠름, 용량 제한L2 캐시 (디스크): 엣지 서버 SSD — L1 미스 시 조회Regional Cache: 중간 계층 — 여러 엣지가 공유 (CloudFront의 Regional Edge Cache)Origin: 원본 서버 — 모든 캐시 미스의 최종 목적지CloudFront의 3계층:
사용자 └→ Edge Location (전 세계 450+ 개) └→ Regional Edge Cache (12개, 더 큰 캐시) └→ Origin (S3, ALB, EC2 등)4-3. Cache Hit Ratio 최적화
섹션 제목: “4-3. Cache Hit Ratio 최적화”Cache Hit Ratio = 캐시에서 응답한 요청 수 / 전체 요청 수
목표: 90%+ (정적 자산 기준)히트율을 높이는 방법:
- TTL 늘리기 (versioned URL과 병행)
- 불필요한 쿼리 파라미터 제거 (Cache Key 정규화)
- 동일 콘텐츠가 여러 URL로 캐시되는 것 방지
5. CloudFront 동작 원리와 설정
섹션 제목: “5. CloudFront 동작 원리와 설정”5-1. 핵심 구성 요소
섹션 제목: “5-1. 핵심 구성 요소”Distribution ├── Origin (어디서 가져올 것인가) │ ├── S3 Bucket │ ├── ALB / EC2 │ ├── API Gateway │ └── Custom HTTP 서버 │ └── Behavior (어떤 요청에 어떻게 반응할 것인가) ├── Path Pattern 매칭 ├── Cache Policy ├── Origin Request Policy └── Response Headers Policy5-2. Origin 설정
섹션 제목: “5-2. Origin 설정”S3 Origin:
{ "DomainName": "my-bucket.s3.amazonaws.com", "S3OriginConfig": { "OriginAccessIdentity": "origin-access-identity/cloudfront/ABCDEF" }}OAI(Origin Access Identity) 또는 OAC(Origin Access Control)를 사용하면 S3 버킷을 CloudFront를 통해서만 접근 가능하게 잠글 수 있다.
ALB Origin (SSR 서버):
{ "DomainName": "my-alb.ap-northeast-2.elb.amazonaws.com", "CustomOriginConfig": { "HTTPSPort": 443, "OriginProtocolPolicy": "https-only", "OriginSSLProtocols": ["TLSv1.2"] }}5-3. Behavior와 경로 매칭
섹션 제목: “5-3. Behavior와 경로 매칭”하나의 Distribution에서 경로별로 다른 오리진과 캐시 정책을 적용할 수 있다.
/* → Default Behavior (SPA index.html, S3 오리진)/api/* → API Behavior (ALB 오리진, 캐시 비활성화)/static/* → Static Behavior (S3 오리진, 캐시 1년)/images/* → Image Behavior (S3 오리진, 이미지 최적화)경로 매칭 우선순위: 가장 구체적인 패턴이 우선. /*는 최후 fallback.
5-4. Cache Policy
섹션 제목: “5-4. Cache Policy”Cache Key를 무엇으로 구성할지 정의한다. Cache Key가 같으면 동일한 캐시 엔트리로 처리.
{ "CachePolicyConfig": { "Name": "api-cache-policy", "DefaultTTL": 0, "MaxTTL": 31536000, "MinTTL": 0, "ParametersInCacheKeyAndForwardedToOrigin": { "EnableAcceptEncodingGzip": true, "EnableAcceptEncodingBrotli": true, "HeadersConfig": { "HeaderBehavior": "none" }, "CookiesConfig": { "CookieBehavior": "none" }, "QueryStringsConfig": { "QueryStringBehavior": "whitelist", "QueryStrings": { "Quantity": 1, "Items": ["version"] } } } }}관리형 Cache Policy:
CachingOptimized: 정적 파일 최적화 (기본 TTL 86400초)CachingDisabled: 동적 콘텐츠 (캐시하지 않음)CachingOptimizedForUncompressedObjects: 압축 없는 파일용
5-5. Origin Request Policy
섹션 제목: “5-5. Origin Request Policy”오리진에 전달할 헤더, 쿠키, 쿼리스트링을 제어한다. Cache Key와 독립적으로 설정 가능.
Cache Key에는 포함하지 않지만 오리진에는 전달해야 하는 것들: - Authorization 헤더 (캐시 키에 넣으면 유저별로 캐시 분리됨) - CloudFront-Viewer-Country 헤더 (지역 정보) - User-Agent (오리진에서 참고용)6. 캐시 무효화 전략
섹션 제목: “6. 캐시 무효화 전략”6-1. TTL 기반 자연 만료
섹션 제목: “6-1. TTL 기반 자연 만료”응답 헤더: Cache-Control: max-age=3600 → 1시간 후 자동 만료, 오리진에서 재조회장점: 구현 간단, 비용 없음 단점: 배포 즉시 반영 불가, TTL 동안 구버전 서빙
6-2. Invalidation (수동 무효화)
섹션 제목: “6-2. Invalidation (수동 무효화)”# 특정 경로 무효화aws cloudfront create-invalidation \ --distribution-id E1234567890 \ --paths "/index.html" "/app.js"
# 전체 무효화 (주의: 비용 발생 + 일시적 오리진 부하 증가)aws cloudfront create-invalidation \ --distribution-id E1234567890 \ --paths "/*"비용: 월 1,000개 무효화 경로까지 무료, 이후 경로당 $0.005. /*는 1개 경로로 계산.
전파 지연: 무효화 완료까지 보통 15~60초. 그 전까지 엣지에 따라 구버전 서빙 가능.
6-3. Versioned URL (권장)
섹션 제목: “6-3. Versioned URL (권장)”파일 이름이나 쿼리스트링에 콘텐츠 해시를 포함시켜 변경 시 새 URL로 접근하게 한다.
변경 전: /static/app.js → Cache-Control: max-age=31536000변경 후: /static/app.abc123.js → 새 파일, 새 캐시 엔트리
index.html: Cache-Control: max-age=0, must-revalidate → 항상 최신 index.html을 가져오고, index.html이 새 해시 파일을 참조Webpack/Vite 설정:
export default { build: { rollupOptions: { output: { entryFileNames: "assets/[name].[hash].js", chunkFileNames: "assets/[name].[hash].js", assetFileNames: "assets/[name].[hash][extname]", }, }, },};6-4. Stale-While-Revalidate
섹션 제목: “6-4. Stale-While-Revalidate”Cache-Control: max-age=60, stale-while-revalidate=300
→ 60초: 캐시에서 즉시 응답 (fresh)→ 60~360초: 캐시에서 응답 + 백그라운드 갱신 (stale이지만 서빙)→ 360초 이후: 반드시 오리진에서 재조회사용자는 항상 빠른 응답을 받고, 백그라운드에서 조용히 갱신된다.
6-5. 캐시 전략 결정 트리
섹션 제목: “6-5. 캐시 전략 결정 트리”콘텐츠 유형에 따른 전략:
정적 자산 (JS, CSS, 이미지) ├── 해시가 포함된 파일명? → max-age=31536000 (1년) └── 해시 없음 → max-age=3600 + Invalidation on deploy
HTML 파일 (index.html) └── no-cache 또는 max-age=0, must-revalidate
API 응답 ├── 공통 데이터 (설정, 목록) → max-age=60~300 ├── 사용자별 데이터 → private, no-store └── 실시간 데이터 → no-cache, no-store7. 엣지 컴퓨팅 — CloudFront Functions와 Lambda@Edge
섹션 제목: “7. 엣지 컴퓨팅 — CloudFront Functions와 Lambda@Edge”7-1. 엣지에서 코드를 실행하는 이유
섹션 제목: “7-1. 엣지에서 코드를 실행하는 이유”오리진 서버에 요청이 도달하기 전에 엣지에서 로직을 처리하면:
- 오리진 부하 감소
- 지연시간 최소화
- 지역별 맞춤 처리
7-2. 실행 위치와 이벤트
섹션 제목: “7-2. 실행 위치와 이벤트”클라이언트 │ ↓ [Viewer Request] ← CloudFront Functions / Lambda@EdgeEdge Location │ ↓ [Origin Request] ← Lambda@Edge만Regional Edge Cache / Origin │ ↓ [Origin Response] ← Lambda@Edge만Edge Location │ ↓ [Viewer Response] ← CloudFront Functions / Lambda@Edge클라이언트7-3. CloudFront Functions
섹션 제목: “7-3. CloudFront Functions”- 런타임: JavaScript (ES5.1 제한)
- 실행 위치: 모든 엣지 로케이션 (450+개)
- 최대 실행시간: 1ms
- 메모리: 2MB
- 용도: URL 리다이렉트, 헤더 추가/수정, A/B 테스트 쿠키 설정, 간단한 인증
// CloudFront Function 예시: SPA 라우팅 처리function handler(event) { var request = event.request; var uri = request.uri;
// 파일 확장자가 없는 경로는 index.html로 리다이렉트 if (!uri.includes(".")) { request.uri = "/index.html"; }
return request;}// A/B 테스트 쿠키 설정function handler(event) { var request = event.request; var cookies = request.cookies;
if (!cookies["ab-variant"]) { // 새 방문자에게 랜덤으로 A 또는 B 배정 var variant = Math.random() < 0.5 ? "A" : "B"; request.cookies["ab-variant"] = { value: variant }; }
return request;}7-4. Lambda@Edge
섹션 제목: “7-4. Lambda@Edge”- 런타임: Node.js 또는 Python
- 실행 위치: Regional Edge Cache (12개 리전)
- 최대 실행시간: Viewer 이벤트 5초, Origin 이벤트 30초
- 메모리: 최대 10GB
- 용도: 복잡한 인증/인가, 이미지 리사이징, A/B 테스트, 동적 콘텐츠 생성
// Lambda@Edge: Origin Request — 인증 헤더 추가exports.handler = async (event) => { const request = event.Records[0].cf.request;
// JWT 검증 (Viewer Request에서 했다면 Origin Request에서는 헤더 추가만) request.headers["x-internal-token"] = [ { key: "X-Internal-Token", value: process.env.INTERNAL_TOKEN, }, ];
return request;};// Lambda@Edge: 이미지 리사이징 (Origin Response)const sharp = require("sharp");
exports.handler = async (event) => { const response = event.Records[0].cf.response; const request = event.Records[0].cf.request;
const params = new URLSearchParams(request.querystring); const width = parseInt(params.get("w") || "800");
if (response.status === "200" && response.body) { const imageBuffer = Buffer.from(response.body, "base64"); const resized = await sharp(imageBuffer).resize(width).toBuffer();
response.body = resized.toString("base64"); response.bodyEncoding = "base64"; }
return response;};7-5. CloudFront Functions vs Lambda@Edge 선택 기준
섹션 제목: “7-5. CloudFront Functions vs Lambda@Edge 선택 기준”| 항목 | CloudFront Functions | Lambda@Edge |
|---|---|---|
| 실행 시간 | < 1ms | 5~30초 |
| 실행 위치 | 모든 엣지 (450+) | Regional (12) |
| 언어 | JS (ES5.1) | Node.js, Python |
| 비용 | $0.1 / 1M 실행 | $0.6 / 1M 실행 |
| 사용 사례 | URL 재작성, 헤더 조작 | 인증, 이미지 처리 |
| 네트워크 접근 | 불가 | 가능 |
| 환경변수 | 불가 | 가능 |
8. DNS 장애 사례와 대응
섹션 제목: “8. DNS 장애 사례와 대응”8-1. TTL 설정 실수 — “마이그레이션 실패”
섹션 제목: “8-1. TTL 설정 실수 — “마이그레이션 실패””상황: 서버를 새 IP로 마이그레이션하면서 DNS TTL을 변경하지 않음.
기존 TTL: 86400초 (24시간)새 IP로 레코드 변경 → 24시간 동안 일부 사용자는 구 IP로 접근구 서버는 이미 종료됨 → 연결 실패교훈: 마이그레이션 전 최소 TTL의 2배 전부터 TTL을 낮춰야 한다.
마이그레이션 계획:T-48h: TTL을 86400 → 300으로 변경 (캐시 만료 대기)T-0: IP 변경. 최대 5분 내 전파T+1h: 안정 확인 후 TTL 다시 상향8-2. Propagation Delay — “변경했는데 왜 안 바뀌지?”
섹션 제목: “8-2. Propagation Delay — “변경했는데 왜 안 바뀌지?””상황: DNS 레코드를 변경했는데 일부 사용자에게는 아직 구버전이 보임.
원인:
- 이전 TTL 동안 캐시된 레코드가 만료되지 않음
- ISP Resolver가 TTL을 무시하고 더 오래 캐시하는 경우
- OS 레벨 캐시 (
nscd,dnsmasq)
# 전파 상태 확인dig api.example.com @8.8.8.8 # Google DNSdig api.example.com @1.1.1.1 # Cloudflare DNSdig api.example.com @168.126.63.1 # KT DNSdig api.example.com +trace # 전체 경로 추적
# OS DNS 캐시 초기화 (macOS)sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
# Linux (systemd-resolved)sudo systemctl restart systemd-resolved8-3. NXDOMAIN Flood — “DNS 서버 과부하”
섹션 제목: “8-3. NXDOMAIN Flood — “DNS 서버 과부하””상황: 봇이 존재하지 않는 서브도메인을 대량으로 쿼리하여 Authoritative 서버에 과부하.
공격: random123.example.com, random456.example.com, ...→ 캐시 미스 → Authoritative 서버 직접 조회 → 과부하대응:
- Wildcard 레코드:
*.example.com → 안내 페이지 IP로 NXDOMAIN 대신 응답 캐시 - Rate Limiting: Route 53 Resolver에서 쿼리 제한
- DNS Firewall: Route 53 Resolver DNS Firewall로 패턴 차단
8-4. Route 53 헬스 체크 오탐
섹션 제목: “8-4. Route 53 헬스 체크 오탐”상황: 서버는 정상인데 헬스 체크가 실패하여 Failover 발생.
원인:
- 헬스 체크 IP 대역이 서버 보안 그룹에서 차단됨
/health엔드포인트가 DB 연결까지 확인하다가 타임아웃
Route 53 헬스 체크 IP 대역: 54.183.x.x, 54.228.x.x 등→ 보안 그룹에서 이 대역을 허용해야 함// 얕은 헬스 체크 엔드포인트 (DB 의존 없음)app.get("/health/shallow", (req, res) => { res.status(200).json({ status: "ok", timestamp: Date.now() });});
// 깊은 헬스 체크 (내부 모니터링용)app.get("/health/deep", async (req, res) => { const dbOk = await checkDatabase(); const cacheOk = await checkRedis(); const status = dbOk && cacheOk ? 200 : 503; res.status(status).json({ db: dbOk, cache: cacheOk });});8-5. Split-horizon DNS 혼용 실수
섹션 제목: “8-5. Split-horizon DNS 혼용 실수”상황: VPC 내부에서는 Private Hosted Zone, 외부에서는 Public Hosted Zone을 사용하는데 레코드가 불일치.
Public: api.example.com → 52.1.2.3 (ALB 공개 IP)Private: api.example.com → 10.0.1.50 (내부 ALB DNS)
Lambda 함수가 내부에서 api.example.com 호출→ Private Zone 적용 → 10.0.1.50 → 정상→ Private Zone 없는 환경에서 → 52.1.2.3 → NAT 통해 외부로 나갔다 돌아옴 → 비용 + 지연8.5 트러블슈팅 (CloudFront & Route53)
섹션 제목: “8.5 트러블슈팅 (CloudFront & Route53)”🔧 배포 후에도 구버전 콘텐츠가 보임 — CloudFront 캐시 미제거
섹션 제목: “🔧 배포 후에도 구버전 콘텐츠가 보임 — CloudFront 캐시 미제거”증상:
# 배포 후 브라우저에서 구버전 JS 파일이 계속 반환됨curl -I https://cdn.example.com/app.js# Cache-Control: max-age=31536000# Age: 3600 ← 1시간 전 캐시된 파일이 반환됨# x-cache: Hit from cloudfront원인: app.js 파일명에 콘텐츠 해시가 없어 배포해도 파일명이 동일하고, CloudFront 캐시가 만료되지 않았다. 또는 캐시 무효화(Invalidation)를 /app.js로 했지만 실제 CloudFront에 캐시된 경로가 /app.js?v=1 처럼 쿼리스트링을 포함한 경우 무효화가 적용되지 않는다.
해결:
# 1. 즉시 무효화 (와일드카드 사용)aws cloudfront create-invalidation \ --distribution-id E1234567890 \ --paths "/*"# 응답 예시:# {# "Invalidation": {# "Id": "I3EINBK10X87YF",# "Status": "InProgress",# "CreateTime": "2025-01-15T12:00:00Z",# "InvalidationBatch": {"Paths": {"Quantity": 1, "Items": ["/*"]}}# }# }
# 2. 무효화 완료 대기 확인aws cloudfront wait invalidation-completed \ --distribution-id E1234567890 \ --id I3EINBK10X87YF
echo "Invalidation complete"
# 3. 근본 해결: 콘텐츠 해시 기반 파일명 사용# vite.config.ts# output: { entryFileNames: 'assets/[name].[hash].js' }# → 파일 변경 시 자동으로 다른 URL → 무효화 불필요🔧 Route 53 Failover가 작동하지 않음 — 헬스 체크 IP 차단
섹션 제목: “🔧 Route 53 Failover가 작동하지 않음 — 헬스 체크 IP 차단”증상:
서버가 실제로 503인데 Route 53 헬스 체크 상태가 "Healthy"로 표시됨→ Failover 라우팅이 발동하지 않아 사용자에게 에러 노출원인: Route 53 헬스 체크는 특정 IP 대역(54.183.0.0/16, 54.228.0.0/16 등)에서 실행된다. 이 IP 대역이 EC2 보안 그룹에서 차단되어 있으면 헬스 체크 요청이 서버에 도달하지 못하고, Route 53은 타임아웃을 “Healthy”로 오해하거나 단순히 응답을 받지 못해 Unhealthy 판정이 지연된다.
해결:
# 1. Route 53 헬스 체크 IP 대역 확인curl -s https://ip-ranges.amazonaws.com/ip-ranges.json \ | jq '.prefixes[] | select(.service == "ROUTE53_HEALTHCHECKS") | .ip_prefix'# 출력:# "54.183.255.128/26"# "54.228.16.0/26"# "54.232.40.64/26"# ... (15개 내외)
# 2. 보안 그룹에 Route 53 IP 허용 추가aws ec2 authorize-security-group-ingress \ --group-id sg-xxxxxx \ --protocol tcp \ --port 443 \ --cidr 54.183.255.128/26
# 3. 헬스 체크 엔드포인트를 경량으로 분리 (DB 의존 없음)# GET /health/shallow → 200 OK {"status": "ok"} ← Route 53용# GET /health/deep → DB/Redis 확인 ← 내부 모니터링용
# 4. 헬스 체크 상태 확인aws route53 get-health-check-status \ --health-check-id abcdef12-3456-7890-abcd-ef1234567890 \ --query 'HealthCheckObservations[*].{Region:Region,Status:StatusReport.Status}'🔧 CloudFront 배포 후 SPA에서 새로고침 시 403/404
섹션 제목: “🔧 CloudFront 배포 후 SPA에서 새로고침 시 403/404”증상:
https://app.example.com/users/123 에서 새로고침 시: AccessDenied - This XML file does not appear to have any style information associated with it.# 또는: The specified key does not exist.원인: SPA는 클라이언트 라우팅을 사용하므로 /users/123 경로에 해당하는 파일이 S3에 없다. CloudFront가 S3에서 해당 파일을 찾지 못하면 S3가 403(OAC 설정 시) 또는 404를 반환한다.
해결:
# CloudFront 에러 응답 설정 (AWS CLI)aws cloudfront update-distribution \ --id E1234567890 \ --distribution-config '{ "CustomErrorResponses": { "Quantity": 2, "Items": [ { "ErrorCode": 403, "ResponsePagePath": "/index.html", "ResponseCode": "200", "ErrorCachingMinTTL": 0 }, { "ErrorCode": 404, "ResponsePagePath": "/index.html", "ResponseCode": "200", "ErrorCachingMinTTL": 0 } ] } }'
# 또는 CloudFront Function으로 처리 (더 효율적)# Viewer Request 이벤트:# 파일 확장자 없는 경로 → /index.html로 uri 변경# → S3에 도달하기 전에 처리되어 에러 자체가 발생하지 않음9. 프론트엔드 → 플랫폼 브릿지: SPA/SSR 배포와 CDN
섹션 제목: “9. 프론트엔드 → 플랫폼 브릿지: SPA/SSR 배포와 CDN”9-1. SPA 배포 패턴
섹션 제목: “9-1. SPA 배포 패턴”빌드 산출물: dist/ index.html ← 항상 최신 (no-cache) assets/ app.abc123.js ← 콘텐츠 해시 포함 (1년 캐시) style.def456.css logo.ghi789.pngS3 + CloudFront 배포 파이프라인:
# 1. 빌드npm run build
# 2. S3 동기화 (해시 파일은 캐시 1년)aws s3 sync dist/ s3://my-bucket/ \ --exclude "index.html" \ --cache-control "max-age=31536000,public,immutable"
# 3. index.html은 별도로 no-cacheaws s3 cp dist/index.html s3://my-bucket/index.html \ --cache-control "no-cache,no-store,must-revalidate" \ --content-type "text/html"
# 4. CloudFront 캐시에서 index.html 무효화aws cloudfront create-invalidation \ --distribution-id E1234567890 \ --paths "/index.html"9-2. SPA에서 클라이언트 라우팅 처리
섹션 제목: “9-2. SPA에서 클라이언트 라우팅 처리”React Router 등 클라이언트 라우팅을 사용하면 /users/123 같은 경로로 직접 접근 시 S3가 404를 반환한다.
CloudFront 에러 페이지 설정:
{ "CustomErrorResponses": [ { "ErrorCode": 403, "ResponsePagePath": "/index.html", "ResponseCode": "200", "ErrorCachingMinTTL": 0 }, { "ErrorCode": 404, "ResponsePagePath": "/index.html", "ResponseCode": "200", "ErrorCachingMinTTL": 0 } ]}또는 CloudFront Function으로 처리:
function handler(event) { var request = event.request; var uri = request.uri;
// 정적 파일 확장자가 있으면 그대로 if (uri.match(/\.(js|css|png|jpg|svg|ico|woff2|json|xml|txt)$/)) { return request; }
// 그 외는 index.html로 request.uri = "/index.html"; return request;}9-3. SSR 배포 패턴 (Next.js)
섹션 제목: “9-3. SSR 배포 패턴 (Next.js)”Next.js SSR 아키텍처: CloudFront ├── /static/* → S3 (정적 자산, 캐시 1년) ├── /_next/static/* → S3 (Next.js 빌드 산출물) └── /* → ALB → ECS/EC2 (Next.js 서버, SSR)Cache-Control 전략:
// Next.js pages에서 캐시 제어export async function getServerSideProps(context) { context.res.setHeader( "Cache-Control", "public, s-maxage=60, stale-while-revalidate=300", ); // s-maxage: CDN 캐시 TTL (60초) // stale-while-revalidate: 최대 300초까지 stale 허용 return { props: { data: await fetchData() } };}9-4. dns-prefetch와 preconnect
섹션 제목: “9-4. dns-prefetch와 preconnect”브라우저가 실제 요청 전에 미리 DNS 조회와 TCP/TLS 연결을 수행하도록 힌트를 준다.
<!-- DNS 조회만 미리 수행 (외부 도메인, 실제 연결 여부 불확실) --><link rel="dns-prefetch" href="https://cdn.example.com" /><link rel="dns-prefetch" href="https://analytics.example.com" />
<!-- DNS + TCP + TLS 핸드셰이크까지 미리 수행 (곧 사용할 것이 확실한 경우) --><link rel="preconnect" href="https://cdn.example.com" crossorigin /><link rel="preconnect" href="https://fonts.googleapis.com" />언제 사용하는가:
dns-prefetch: - 외부 CDN 도메인 - 분석 스크립트 도메인 - A/B 테스트 서비스 - 연결 여부가 불확실한 외부 서비스
preconnect: - Google Fonts (반드시 사용) - 메인 API 서버 - 이미지 CDN (첫 화면에 이미지가 많을 때) - 비디오 스트리밍 서버주의: preconnect는 연결 비용이 있다. 실제로 사용하지 않을 도메인에 남용하면 오히려 리소스 낭비.
<!-- 권장: 핵심 리소스에만 preconnect, 나머지는 dns-prefetch --><link rel="preconnect" href="https://api.example.com" crossorigin /><link rel="dns-prefetch" href="https://cdn.analytics.com" />9-5. Resource Hints 성능 측정
섹션 제목: “9-5. Resource Hints 성능 측정”// Navigation Timing API로 DNS 조회 시간 측정const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (entry.entryType === "resource") { const dnsTime = entry.domainLookupEnd - entry.domainLookupStart; const connectTime = entry.connectEnd - entry.connectStart; console.log(`${entry.name}: DNS=${dnsTime}ms, Connect=${connectTime}ms`); } });});observer.observe({ entryTypes: ["resource", "navigation"] });10. 실무 체크리스트
섹션 제목: “10. 실무 체크리스트”배포 전
섹션 제목: “배포 전”□ 마이그레이션 시 TTL을 사전에 낮췄는가? (최소 현재 TTL만큼 전)□ CloudFront 배포 시 index.html에 no-cache 설정했는가?□ 정적 자산에 콘텐츠 해시가 포함되어 있는가?□ S3 버킷이 CloudFront 외 직접 접근을 차단하고 있는가? (OAC 설정)□ HTTPS 강제 설정이 있는가? (HTTP → HTTPS 리다이렉트)운영 중
섹션 제목: “운영 중”□ Cache Hit Ratio가 90% 이상인가? (CloudFront 메트릭 확인)□ Route 53 헬스 체크 IP 대역이 보안 그룹에서 허용되는가?□ TTL 만료 후 오리진 트래픽 급증(Dog Pile Effect)에 대비했는가?□ CloudFront 에러율(5xx) 알람이 설정되어 있는가?□ DNS Failover 테스트를 주기적으로 수행하는가?□ DNSSEC가 활성화되어 있는가?□ DNS over HTTPS/TLS를 지원하는 Resolver를 사용하는가?□ Route 53 Resolver DNS Firewall로 알려진 악성 도메인을 차단하는가?□ CloudFront에 WAF(Web Application Firewall)가 연결되어 있는가?□ 보안 헤더(HSTS, CSP 등)를 Response Headers Policy로 추가했는가?| 주제 | 핵심 포인트 |
|---|---|
| DNSSEC | DNS 응답에 디지털 서명. DS→DNSKEY→RRSIG의 체인 오브 트러스트 |
| DoH/DoT | DNS 평문 노출 해소. DoH는 443 포트로 차단 어려움 |
| Route 53 | 단순/가중치/지연시간/지리/Failover 라우팅 + 헬스 체크 |
| CDN 계층 | Edge → Regional Edge → Origin 3계층 캐시 |
| CloudFront | Distribution > Origin + Behavior + Cache Policy 구조 |
| 캐시 무효화 | Versioned URL + index.html no-cache 조합이 베스트 프랙티스 |
| 엣지 컴퓨팅 | 단순 변환은 CF Functions, 복잡한 로직은 Lambda@Edge |
| DNS 장애 | TTL 사전 조정, 전파 확인 도구, 헬스 체크 IP 허용 |
| SPA/SSR | index.html no-cache, 정적 자산 해시 1년, CloudFront Function으로 SPA 라우팅 |
| dns-prefetch | 외부 도메인 사전 조회, preconnect는 확실히 사용할 도메인에만 |
📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 How Does DNSSEC Work? — Cloudflare — DNSSEC의 Chain of Trust, DNSKEY/RRSIG/DS 레코드 역할을 시각 자료와 함께 설명 (입문)
- 📖 DNSSEC Basics — Internet Society — ZSK/KSK 구분, 키 롤오버 절차, NSEC3 배경을 IETF 관점에서 설명 (중급)
- 📖 Amazon CloudFront CDN Comprehensive Guide — aws-certs.com — Distribution, Behavior, Cache Policy, Lambda@Edge를 실무 중심으로 정리 (중급)
- 📖 Troubleshooting Common CloudFront Distribution Issues — Reintech — 캐시 무효화, 오리진 오류, HTTPS 설정 등 실무 에러 사례별 해결 가이드 (입문)
- 📖 Improve web application availability with CloudFront and Route53 hybrid origin failover — AWS Blog — CloudFront origin failover + Route 53 Failover 라우팅 결합 아키텍처. 섹션 3-5 Failover 라우팅 심화용 (고급)
직접 확인해보기
섹션 제목: “직접 확인해보기”# 1. DNSSEC 검증 여부 확인dig +dnssec cloudflare.com A예상 출력:
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1;; ANSWER SECTION:cloudflare.com. 265 IN A 104.16.132.229cloudflare.com. 265 IN RRSIG A 13 2 300 ( 20250201000000 20250115000000 34505 cloudflare.com. abc123def456...== )# "ad" 플래그 = Authenticated Data → DNSSEC 검증 성공# 2. DNS 전파 상태 확인 (여러 Resolver 비교)for dns in 8.8.8.8 1.1.1.1 168.126.63.1; do echo -n "DNS $dns: " dig +short api.example.com @$dnsdone예상 출력:
DNS 8.8.8.8: 52.1.2.3DNS 1.1.1.1: 52.1.2.3DNS 168.126.63.1: 52.1.2.3 ← 모두 동일하면 전파 완료# 3. CloudFront 캐시 히트 여부 확인curl -sI https://cdn.example.com/app.abc123.js | grep -E "x-cache|Age|Cache"예상 출력 (캐시 히트):
x-cache: Hit from cloudfrontAge: 7200Cache-Control: max-age=31536000, public, immutable예상 출력 (캐시 미스):
x-cache: Miss from cloudfrontAge: 0Cache-Control: max-age=31536000, public, immutable# 4. Route 53 레코드 조회 및 TTL 확인aws route53 list-resource-record-sets \ --hosted-zone-id Z1234567890 \ --query 'ResourceRecordSets[?Name==`api.example.com.`]' \ --output table예상 출력:
-------------------------------------------------| ListResourceRecordSets |+----+------+----------------------+-----+------+|Name|Type |Value |TTL |Weight|+----+------+----------------------+-----+------+|api.|A |52.1.2.3 |60 |90 ||api.|A |52.4.5.6 |60 |10 |+----+------+----------------------+-----+------+# 5. CloudFront 무효화 상태 확인aws cloudfront list-invalidations \ --distribution-id E1234567890 \ --query 'InvalidationList.Items[0:3].{Id:Id,Status:Status,Created:CreateTime}'예상 출력:
[ {"Id": "I3ABC123", "Status": "Completed", "Created": "2025-01-15T10:00:00Z"}, {"Id": "I3DEF456", "Status": "InProgress", "Created": "2025-01-15T12:00:00Z"}]