CloudFront Cache Invalidation
CloudFront는 전 세계 엣지 로케이션에 콘텐츠를 캐싱해서 사용자에게 빠르게 전달하는 CDN이다. 그런데 S3에 새 파일을 올리거나 기존 파일을 수정했을 때, 엣지에 캐싱된 이전 버전이 계속 서빙되는 문제가 발생한다. CloudFront의 기본 TTL은 24시간이기 때문에, 배포 후 최대 하루까지 이전 콘텐츠가 보일 수 있다.
Cache Invalidation은 이 문제를 해결하기 위해 엣지 로케이션의 캐시를 강제로 무효화하는 메커니즘이다.
캐시가 필요한 이유와 무효화의 딜레마
CDN 캐시의 핵심 가치는 오리진(S3) 요청을 줄여서 응답 속도를 높이고 비용을 절감하는 것이다. 하지만 캐시가 있으면 콘텐츠 업데이트가 즉시 반영되지 않는다. 이건 본질적인 트레이드오프다.
사용자 → CloudFront Edge (캐시 HIT) → 이전 버전 반환
↑ 오리진에는 새 버전이 있지만 확인하지 않음
사용자 → CloudFront Edge (캐시 MISS or 만료) → S3 오리진 → 새 버전 반환
캐시 무효화 없이 이 문제를 해결하는 방법도 있다:
- 파일명에 해시/버전 포함:
style.abc123.css처럼 콘텐츠가 바뀌면 파일명도 바뀌니 자연스럽게 새 파일을 가져옴 - 짧은 TTL 설정: Cache-Control 헤더로 캐시 시간을 줄임
- Cache-Control: no-cache: 매번 오리진에 재검증 요청 (304 활용)
하지만 이미 배포된 URL을 변경할 수 없거나, 긴급하게 콘텐츠를 교체해야 하는 상황에서는 Invalidation이 필요하다.
Invalidation의 동작 원리
Invalidation 요청을 보내면 CloudFront가 전 세계 엣지 로케이션에 "이 경로의 캐시를 버려라"는 명령을 전파한다. 이후 해당 경로로 요청이 들어오면 엣지는 오리진에서 새 콘텐츠를 가져온다.
중요한 점은 Invalidation이 삭제가 아니라 무효화라는 것이다. 엣지에서 파일을 즉시 지우는 게 아니라 "다음 요청 시 오리진에서 다시 가져와라"고 표시하는 것이다. 전파에는 보통 수 초에서 수 분이 걸리며, 전 세계 모든 엣지에 완료되기까지 최대 10-15분 정도 소요된다.
AWS 콘솔에서 Invalidation 생성
가장 직관적인 방법은 AWS 콘솔이다.
- CloudFront → Distributions → 해당 배포 선택
- Invalidations 탭 → Create invalidation
- 무효화할 경로 입력
/images/logo.png # 특정 파일
/images/* # images 하위 전체
/* # 전체 캐시 무효화
와일드카드 *는 경로 끝에만 사용할 수 있다. /images/*.png 같은 패턴 매칭은 지원하지 않는다. /images/*로 하위 전체를 무효화하거나, 개별 파일 경로를 하나씩 지정해야 한다.
AWS CLI로 Invalidation
자동화할 때는 CLI를 사용한다.
aws cloudfront create-invalidation \
--distribution-id E1234567890ABC \
--paths "/images/logo.png" "/css/*"
여러 경로를 한 번에 지정할 수 있다. 응답으로 Invalidation ID가 반환되고, 이걸로 진행 상태를 확인할 수 있다.
aws cloudfront get-invalidation \
--distribution-id E1234567890ABC \
--id I1234567890ABC
상태가 Completed로 바뀌면 모든 엣지에 전파가 완료된 것이다.
AWS SDK로 프로그래밍 방식 Invalidation
배포 파이프라인이나 서버 코드에서 자동으로 Invalidation을 트리거할 때 SDK를 사용한다.
import {
CloudFrontClient,
CreateInvalidationCommand,
} from "@aws-sdk/client-cloudfront";
const client = new CloudFrontClient({ region: "us-east-1" });
async function invalidateCache(distributionId: string, paths: string[]) {
const command = new CreateInvalidationCommand({
DistributionId: distributionId,
InvalidationBatch: {
CallerReference: `invalidation-${Date.now()}`, // 중복 방지용 유니크 값
Paths: {
Quantity: paths.length,
Items: paths,
},
},
});
const response = await client.send(command);
console.log("Invalidation ID:", response.Invalidation?.Id);
console.log("Status:", response.Invalidation?.Status);
return response;
}
// 사용
await invalidateCache("E1234567890ABC", ["/images/*", "/index.html"]);
CallerReference는 같은 요청이 중복 전송되는 것을 방지하는 멱등성 키다. 같은 CallerReference로 다시 요청하면 CloudFront가 이전 요청의 결과를 반환한다. 보통 타임스탬프나 UUID를 사용한다.
Paths.Quantity는 실제 Items 배열 길이와 반드시 일치해야 한다. 안 맞으면 에러가 발생한다. SDK가 자동으로 맞춰주지 않으므로 주의.
비용과 제한
Invalidation은 무료가 아니다.
- 월 1,000개 경로까지 무료 (각 경로가 1개로 카운트)
- 초과 시 경로당 약 $0.005
/*와일드카드도 1개 경로로 카운트
즉, 파일 100개를 개별적으로 무효화하면 100개 경로를 사용하지만, /*로 전체 무효화하면 1개 경로만 사용한다. 비용 측면에서 와일드카드가 유리하지만, 불필요한 캐시까지 날리는 단점이 있다.
동시 실행 제한도 있다:
- 배포당 동시에 진행 중인 Invalidation 요청: 최대 3,000개 경로 (와일드카드 사용 시 15개 요청)
- 이 제한을 초과하면 이전 Invalidation이 완료될 때까지 새 요청이 거부됨
Invalidation vs 버저닝 전략 비교
실무에서는 Invalidation보다 버저닝을 기본 전략으로 사용하는 것이 권장된다.
파일명 해시 (Cache Busting)
# 빌드 결과물
/static/js/main.a1b2c3d4.js
/static/css/style.e5f6g7h8.css
Webpack, Vite 등 번들러가 빌드 시 파일 내용의 해시를 파일명에 포함시킨다. 콘텐츠가 바뀌면 파일명도 바뀌므로 캐시 문제가 원천적으로 발생하지 않는다. 이 파일들은 TTL을 1년으로 설정해도 안전하다.
Cache-Control: public, max-age=31536000, immutable
immutable은 브라우저에게 "이 파일은 절대 안 바뀌니 재검증도 하지 마라"고 알려준다.
언제 Invalidation이 필요한가
| 상황 | 권장 전략 |
|---|---|
| JS/CSS 번들 배포 | 파일명 해시 (Invalidation 불필요) |
index.html 업데이트 | Invalidation 또는 짧은 TTL |
| 사용자 업로드 이미지 교체 | Invalidation 또는 새 키 발급 |
| 긴급 콘텐츠 수정 | Invalidation |
| API 응답 캐시 갱신 | 짧은 TTL + s-maxage |
index.html은 파일명을 바꿀 수 없는 대표적인 케이스다. 사용자가 항상 /index.html로 접근하기 때문에 해시를 붙일 수 없고, 배포 후 Invalidation으로 갱신하거나 짧은 TTL(예: 5분)을 설정하는 게 일반적이다.
CI/CD 파이프라인에서의 활용
배포 후 자동으로 Invalidation을 실행하는 패턴이 일반적이다.
GitHub Actions 예시
- name: Deploy to S3
run: aws s3 sync ./dist s3://my-bucket --delete
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \
--paths "/*"
s3 sync --delete로 S3에 배포한 뒤 /* 전체 무효화를 실행한다. 간단하고 확실하지만, CDN의 캐시 효율을 한 번에 날려버리는 단점이 있다.
Serverless Framework에서의 활용
Serverless Framework로 배포하는 경우, CloudFront Distribution을 serverless.yml의 resources에 정의하고 배포 후 스크립트에서 Invalidation을 실행한다.
resources:
Resources:
AssetsCDN:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
DefaultCacheBehavior:
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
Compress: true
Origins:
- Id: S3MediaOrigin
DomainName: !Sub "${MediaBucket}.s3.${AWS::Region}.amazonaws.com"
S3OriginConfig:
OriginAccessIdentity: ''
OriginAccessControlId: !GetAtt MediaBucketOAC.Id
Outputs:
CDNDistributionId:
Value: !Ref AssetsCDN
배포 후 Output에서 Distribution ID를 가져와 Invalidation을 실행하는 방식이다.
Cache Policy와 TTL 전략
Invalidation에 의존하기보다 적절한 Cache Policy를 설정하는 것이 근본적인 해결책이다.
CloudFront 관리형 캐시 정책
CloudFront에서 기본 제공하는 캐시 정책들:
| 정책 | ID | TTL | 용도 |
|---|---|---|---|
| CachingOptimized | 658327ea-... | 기본 86400초 (24시간) | 정적 에셋 |
| CachingDisabled | 4135ea2d-... | 0 | API 프록시 |
| CachingOptimizedForUncompressedObjects | b2884449-... | 86400초 | 비압축 콘텐츠 |
커스텀 캐시 정책
CustomCachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
Name: ShortLivedCache
DefaultTTL: 300 # 5분
MaxTTL: 3600 # 1시간
MinTTL: 0
ParametersInCacheKeyAndForwardedToOrigin:
CookiesConfig:
CookieBehavior: none
HeadersConfig:
HeaderBehavior: none
QueryStringsConfig:
QueryStringBehavior: none
EnableAcceptEncodingGzip: true
EnableAcceptEncodingBrotli: true
- MinTTL: 오리진의
Cache-Control: max-age가 이 값보다 작아도 이 값만큼은 캐싱 - DefaultTTL: 오리진이
Cache-Control헤더를 보내지 않을 때 사용하는 기본값 - MaxTTL: 오리진의
max-age가 이 값을 초과하면 이 값으로 제한
오리진의 Cache-Control 헤더와 CloudFront의 TTL 설정이 상호작용하는 방식을 이해하는 것이 중요하다:
오리진 max-age | MinTTL | MaxTTL | 실제 캐시 시간
60 | 300 | 3600 | 300 (MinTTL 적용)
600 | 300 | 3600 | 600 (오리진 값 사용)
7200 | 300 | 3600 | 3600 (MaxTTL 적용)
(없음) | 300 | 3600 | DefaultTTL 적용
OAC (Origin Access Control)
CloudFront와 S3를 연결할 때 보안 설정도 중요하다. S3 버킷을 퍼블릭으로 열지 않고 CloudFront를 통해서만 접근하도록 하는 것이 OAC(Origin Access Control)다.
MediaBucketOAC:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: media-oac
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
OAC는 이전 방식인 OAI(Origin Access Identity)를 대체한다. OAI는 CloudFront 고유 사용자를 만들어 S3에 접근 권한을 부여하는 방식이었는데, SSE-KMS 암호화된 객체에 접근할 수 없고 서명 방식이 오래되었다. OAC는 SigV4 서명을 사용해 이런 제한이 없다.
S3 버킷 정책에서 CloudFront만 접근 허용:
MediaBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref MediaBucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: s3:GetObject
Resource: !Sub '${MediaBucket.Arn}/*'
Condition:
StringEquals:
AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${AssetsCDN}'
Condition으로 특정 Distribution만 접근을 허용한다. 같은 계정의 다른 CloudFront Distribution은 접근할 수 없다.
실전 팁
1. 배포 전략에 따른 Invalidation 패턴
# SPA 배포: HTML만 무효화 (JS/CSS는 해시 파일명)
aws cloudfront create-invalidation \
--distribution-id $DIST_ID \
--paths "/index.html" "/favicon.ico"
# 전체 무효화 (간단하지만 비효율적)
aws cloudfront create-invalidation \
--distribution-id $DIST_ID \
--paths "/*"
2. 사용자 업로드 콘텐츠
사용자가 프로필 이미지를 교체하는 경우, 같은 S3 키에 덮어쓰면 CDN 캐시 문제가 발생한다. 두 가지 해결 방법:
방법 1: 매번 새 키 발급 (권장)
/uploads/avatar/user123-v1.jpg → /uploads/avatar/user123-v2.jpg
방법 2: 쿼리 스트링 버저닝
/uploads/avatar/user123.jpg?v=1702000000
새 키를 발급하면 Invalidation이 필요 없고, 이전 버전도 보존할 수 있어서 롤백이 가능하다.
3. Invalidation 완료 대기
배포 스크립트에서 Invalidation이 완료될 때까지 기다려야 할 때:
aws cloudfront wait invalidation-completed \
--distribution-id E1234567890ABC \
--id I1234567890ABC
wait 명령은 상태가 Completed가 될 때까지 주기적으로 폴링한다. CI/CD 파이프라인에서 E2E 테스트를 실행하기 전에 캐시 갱신이 완료되었는지 확인할 때 유용하다.
4. 비용 절감 팁
❌ 파일 100개를 개별 Invalidation → 100개 경로 소비
✅ /directory/* 와일드카드 → 1개 경로 소비
✅ 여러 경로를 한 번의 요청에 묶기 → API 호출 횟수 절감
월 1,000개 경로 무료 한도를 초과하지 않도록 관리하고, 가능하면 와일드카드를 활용한다.
정리
- 해시 파일명이 기본 전략이고, Invalidation은 index.html처럼 경로를 바꿀 수 없는 파일이나 긴급 교체 상황에서 사용한다
- 와일드카드(
/*)는 1개 경로로 카운트되므로 비용 효율적이지만, 캐시 히트율을 통째로 날리는 트레이드오프가 있다 - OAC + 버킷 정책으로 S3 직접 접근을 차단하고, Cache Policy의 MinTTL/DefaultTTL/MaxTTL 상호작용을 이해해야 의도한 대로 캐시가 동작한다