CloudFront + Lambda@Edge to Cloudflare Worker + R2 PoC
- #Cloudflare
- #R2
- #Worker
- #CloudFront
- #Lambda@Edge
- #AWS
- #MediaConvert
๋ค์ด๊ฐ๋ฉฐ
์๋ ํ์ธ์, ๋ฐฑ์๋ ๊ฐ๋ฐ์ ์ด์ฐฌํฌ์ ๋๋ค. ์ด๋ฒ ๊ธ์์๋ ์ ํฌ๊ฐ ์ด์ ์ค์ธ AWS ๋ฏธ๋์ด ์คํ(CloudFront + S3 + Lambda@Edge + MediaConvert)์ Cloudflare ์คํ(Worker + R2 + Stream)์ผ๋ก ์ฎ๊ธธ ์ ์์์ง ๊ฒํ ํ 4-Stage PoC ๊ณผ์ ์ ์ ๋ฆฌํฉ๋๋ค. ๊ฒฐ๋ก ๋ถํฐ ๋ง์๋๋ฆฌ๋ฉด, ์ด๋ฏธ์งยท์ผ๋ฐ VOD ์์ญ์ ์ถฉ๋ถํ ์ฎ๊ธธ ๋งํ๊ณ 4KยทAES-128 ์ํธํ๊ฐ ํ์ํ ์์ญ์ MediaConvert๋ฅผ ์ ์งํ๋ ํ์ด๋ธ๋ฆฌ๋ ๊ตฌ์กฐ๊ฐ ๊ฐ์ฅ ํฉ๋ฆฌ์ ์ด๋ผ๊ณ ํ๋จํ์ต๋๋ค.
TL;DR
- ํธํ์ฑ: R2๋ S3 SDK๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์๊ณ , Worker๋ Lambda@Edge๊ฐ ํ๋ ์ผ(์ฟ ํค ๋ฐ๊ธ, Cache-Control ์ค์ )์ ๊ทธ๋๋ก ๋์ฒดํ ์ ์์์ต๋๋ค.
- ์ด๋: R2 ์์ Worker๋ฅผ ๋ถ์ด๋ ๊ตฌ์กฐ์์๋ ์บ์ Hit ์ฌ๋ถ์ ๋ฌด๊ดํ๊ฒ ๋ชจ๋ ์์ฒญ์ Worker๊ฐ ์คํ๋ฉ๋๋ค. ๋ค๋ง Worker๋ ์์ฒญ ์์ CPU ์ฐ์ฐ ์๊ฐ ๊ธฐ์ค์ผ๋ก ๊ณผ๊ธ๋์ด ๋จ๊ฐ๊ฐ ๋งค์ฐ ๋ฎ๊ณ , egress ๋น์ฉ์ด 0์์ด๋ผ๋ ์ ๊ณผ ์บ์ ์ ์ฑ ์ ์ ์ค๊ณํ๋ฉด R2 ์ ๊ทผ ์์ฒด๋ฅผ ์ค์ผ ์ ์๋ค๋ ์ ์์ ๋น์ฉ ํจ์จ์ ์ธ ๊ตฌ์กฐ์ ๋๋ค.
- ํ๊ณ: Cloudflare Stream์ MediaConvert๊ฐ ์ ๊ณตํ๋ ์ธ๋ฐํ ์ธ์ฝ๋ฉ ์ ์ด(๋นํธ๋ ์ดํธ, GOP, B-Frame, AES-128, 4K ๋ฑ)๋ฅผ ๋๋ถ๋ถ ์ง์ํ์ง ์์ต๋๋ค. ์ค์ ํญ๋ชฉ ๊ธฐ์ค์ผ๋ก ์ฝ 30%๋ง ํธํ๋ฉ๋๋ค.
- ๊ฒฐ๋ก : ์ผ๋ฐ VOD/์ด๋ฏธ์ง๋ Cloudflare๋ก ์ฎ๊ธธ ๋งํฉ๋๋ค. 4KยทAES-128์ด ํ์์ธ ํ๋ฆฌ๋ฏธ์ ์ฝํ ์ธ ๋ MediaConvert๋ฅผ ์ ์งํ๋ ํ์ด๋ธ๋ฆฌ๋ ๊ตฌ์กฐ๋ก ๊ฐ๊ธฐ๋ก ํ์ต๋๋ค.
1. ์ ์ฎ๊ธฐ๋ ค ํ๋
๊ฐ์ฅ ํฐ ์ด์ ๋ ๋น์ฉ์ด์์ต๋๋ค
2025๋ 9์ ๊ธฐ์ค ์ ํฌ์ AWS ๋ฏธ๋์ด ์คํ ์ฒญ๊ตฌ์๋ฅผ ๋ฏ์ด๋ณด๋, ์ด ๋น์ฉ์ 90% ์ด์์ด CloudFront Egress(๋ฐ์ดํฐ ์ ์ก) ๋น์ฉ์ด์์ต๋๋ค. ๊ฐ์ ์ฌ์ฉ๋์ Cloudflare๋ก ํ์ฐํ์ ๋ ์์๋๋ ์ ๊ฐ๋ฅ ์ ๋ค์๊ณผ ๊ฐ์์ต๋๋ค.
| ํญ๋ชฉ | ์ ๊ฐ๋ฅ |
|---|---|
| ๋ฐ์ดํฐ ์ ์ก (Egress) | 100% (๋ฌด์ ํ) |
| ์คํ ๋ฆฌ์ง (S3 โ R2) | 34% |
| Edge Function (Lambda@Edge โ Workers) | ์ฝ 35% |
| API ์์ฒญ (PUT/GET) | 10% |
| ์ ์ฒด ํฉ๊ณ | ์ฝ 92% |
ํต์ฌ์ Cloudflare์ ๋ฌด์ ํ Egress ์ ์ฑ ์ ๋๋ค. CloudFront๋ GB๋น ๊ณผ๊ธ์ด ๋ฐ์ํ์ง๋ง Cloudflare๋ ๋ฐ์ดํฐ ์ ์ก ๋น์ฉ ์์ฒด๊ฐ 0์ด๊ธฐ ๋๋ฌธ์, ํธ๋ํฝ์ด ๋์ด๋ ์๋ก ๊ฒฉ์ฐจ๋ ๋ ๋ฒ์ด์ง๋ ๊ตฌ์กฐ์์ต๋๋ค. ๋ถ์์ ์ผ๋ก R2 ์คํ ๋ฆฌ์ง๊ฐ S3 ๋๋น ์ฝ 34% ์ ๋ ดํ๊ณ , Workers๋ ์ 300๋ง ์์ฒญ๊น์ง ๋ฌด๋ฃ๋ก ์ ๊ณต๋์ด Lambda@Edge ๋๋น ์ถ๊ฐ ์ ๊ฐ ์ฌ์ง๋ ์์์ต๋๋ค.
์ฎ๊ธฐ๋ ค๋ฉด ํจ๊ป ํ์ด์ผ ํ ์์ ๋ค
๊ธฐ์กด ์คํ์ ์ ํ์ ์ธ AWS ๊ตฌ์ฑ์ด์์ต๋๋ค.
Client
โ CloudFront (CDN + Signed Cookies ๊ฒ์ฆ)
โ Behaviors (๊ฒฝ๋ก๋ณ ๋ผ์ฐํ
)
โ S3 (์คํ ๋ฆฌ์ง)
โ Lambda@Edge (์๋ต ํค๋ ์ถ๊ฐ)
โ Client
์์์ MediaConvert๋ก ์ธ์ฝ๋ฉํ์ฌ S3์ ์ ์ฌํ๊ณ , ์ ๊ทผ ์ ์ด๋ CloudFront Signed Cookies 3์ข
์ธํธ(Policy/Signature/Key-Pair-Id)๋ก ์ฒ๋ฆฌํฉ๋๋ค. ํ์ฌ ๊ตฌ์กฐ ์์ฒด๋ ์ ๋์ํ์ง๋ง, Cloudflare๋ก ์ฎ๊ธด๋ค๊ณ ๊ฐ์ ํ๋ฉด ๋น์ฉ ์ธ์๋ ๋ค์ ์ธ ๊ฐ์ง๋ฅผ ํจ๊ป ๊ฒํ ํด์ผ ํ์ต๋๋ค.
- CloudFront ๋ฝ์ธ์ ์ด๋ป๊ฒ ํ ๊ฒ์ธ๊ฐ. Signed Cookies๋ CloudFront ์ ์ฉ ๋ฉ์ปค๋์ฆ์ด๋ผ, CDN์ ๊ต์ฒดํ๋ ค๋ฉด ์ธ์ฆ ๋ก์ง ์์ฒด๋ฅผ ์๋ก ์ค๊ณํด์ผ ํฉ๋๋ค.
- Lambda@Edge๊ฐ ํ๋ ์ผ์ Worker๊ฐ ๋์ฒดํ ์ ์๋๊ฐ. ํ์ฌ Lambda@Edge๋ ํน์ ๊ฒฝ๋ก ์์ฒญ์ ๋ํด ์ธ์ฆ์ฉ ์ฟ ํค๋ฅผ
Set-Cookie๋ก ๋ฐ๊ธํ๊ณ , ๊ทธ ์ธ ์๋ต์๋ ์ฌ๋ด ์ ์ฑ ์ ๋ง๋Cache-Controlํค๋๋ฅผ ๋ถ์ฌํ๋ ์ญํ ์ ํฉ๋๋ค. Worker ๋ชจ๋ธ์์๋ ์ด ๋ ๋์(๊ฒฝ๋ก ๊ธฐ๋ฐ ์ฟ ํค ๋ฐ๊ธ + ์๋ต ํค๋ ๊ฐ๊ณต)์ ๋์ผํ๊ฒ ๊ตฌํํ ์ ์๋์ง ํ์ธ์ด ํ์ํ์ต๋๋ค. - MediaConvert ์ํฌํ๋ก์ฐ(12๋จ๊ณ)๋ฅผ Stream์ผ๋ก ์ฎ๊ธธ ์ ์๋๊ฐ. ํ์ฌ๋ presigned URL ๋ฐ๊ธ๋ถํฐ S3 ์ ๋ก๋, S3 Event โ Lambda โ MediaConvert Job โ EventBridge โ DB ์ ๋ฐ์ดํธ๊น์ง ๋จ๊ณ๊ฐ ๊ธธ๊ฒ ์ด์ด์ง๋๋ค. Stream์ผ๋ก ๋จ์ํํ ์ ์๋ค๋ฉด ์ด๋์ด ํฌ์ง๋ง, ์ธ์ฝ๋ฉ ์ต์ ํธํ์ฑ์ ๋จผ์ ํ์ธํด์ผ ํ์ต๋๋ค.
๊ทธ๋์ ์ ํฌ๊ฐ ๊ฒ์ฆํ๊ณ ์ถ์๋ ๊ฒ์ Cloudflare๋ก ์ฎ๊ธฐ๋ฉด ๋น์ฉ๋ ์ก๊ณ ์ด ์ธ ๊ฐ์ง ๋ฌธ์ ๋ ํจ๊ป ํ ์ ์๋๊ฐ์์ต๋๋ค.
2. ๋ฌด์์ ๊ฒ์ฆํด์ผ ํ๋ โ ์ฌ์ ์กฐ์ฌ
2-1. ์ ๊ทผ ์ ์ด: Signed Cookies๋ฅผ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ๋์ฒดํ ์ ์์๊น
CloudFront Signed Cookies๋ CloudFront ์ ์ฉ ๋ฉ์ปค๋์ฆ์ด๋ผ, CDN์ ์ฎ๊ธด๋ค๋ฉด ์ธ์ฆ ๋ฐฉ์๋ ์๋ก ์ ํํด์ผ ํฉ๋๋ค. PoC ๋จ๊ณ์์๋ ํ๋ณด๋ก JWT ๊ธฐ๋ฐ ๊ฒ์ฆ์ ๊ฐ์ ํด๋๊ณ ๋น๊ต๋ง ํด๋ดค์ต๋๋ค. ์ด๋ค ํ ํฐ/๊ฒ์ฆ ๋ฐฉ์์ ์ต์ข ์ ์ผ๋ก ์ฑํํ ์ง๋ ๋ณ๋ ์ค๊ณ ๊ฒํ ๊ฐ ํ์ํฉ๋๋ค.
| ํญ๋ชฉ | CloudFront + Signed Cookies | Cloudflare + ํ ํฐ ๊ธฐ๋ฐ(์: JWT ๊ฐ์ ) |
|---|---|---|
| ๊ฒ์ฆ ์ฃผ์ฒด | CloudFront ๋ด๋ถ (์๋) | Worker ์ฝ๋ (๋งค ์์ฒญ) |
| ํ ํฐ ๊ตฌ์ฑ | 3๊ฐ ์ฟ ํค (Policy/Signature/Key-Pair-Id) | ๋จ์ผ ํ ํฐ ๊ฐ์ |
| ๊ถํ ์ ์ด | ์ ํ์ | Worker ๋ก์ง์ ๋ฐ๋ผ ์ค๊ณ ์์ ๋ ๋์ |
| ์บ์ ์์น | CloudFront Cache โ Origin โ Lambda@Edge | Cloudflare Cache โ Worker โ R2 |
2-2. Lambda@Edge viewer_response๋ ์ ํํ ์ด๋ค ์ผ์ ํ๊ณ ์์๋๊ฐ
๊ธฐ์กด Lambda@Edge ์ฝ๋๋ฅผ ๋ถ์ํด๋ณด๋, ์ค์ ๋ก ์ํํ๋ ์์ ์ ์๋ต ํค๋๋ฅผ ์๋ณด๋ ๋ ๊ฐ์ง ์์ ์ด ์ ๋ถ์์ต๋๋ค.
- ํน์ ๊ฒฝ๋ก์ ๋ํ ์ฟ ํค ๋ฐ๊ธ โ ์ธ์ฆ์ฉ ์ฟ ํค๋ฅผ
Set-Cookieํค๋๋ก ๋ฐ๊ธํ๋ ์์ . - ๊ทธ ์ธ ๋ชจ๋ ์๋ต์ ๋ํ ์บ์ ์ ์ฑ ๋ถ์ฌ โ ์ฌ์ฉ์๋ณ ๊ถํ์ด ๋ค๋ฅธ ์ฝํ ์ธ ๋ผ CDN/ํ๋ก์ ๋จ์ ๊ณต์ ์บ์ฑ์ ์ฐจ๋จํ๊ณ , ๋ธ๋ผ์ฐ์ ๋จ์์๋ง ์บ์ฑ์ด ์ ์ง๋๋๋ก ํ๋ ์ ์ฑ .
๊ธฐ๋ฅ์ ์ผ๋ก๋ ์ด ๋ ๊ฐ์ง๊ฐ ์ ๋ถ์์ต๋๋ค. ๋น์ฉ ์์ฒด๋ ํฌ์ง ์๋๋ผ๋, ๋งค ์๋ต๋ง๋ค ํจ์๊ฐ ํ ๋ฒ์ฉ ์คํ๋๋ค๋ ๊ตฌ์กฐ ์์ฒด๊ฐ ๋นํจ์จ์ ์ด๋ผ๊ณ ํ๋จํ์ต๋๋ค.
2-3. MediaConvert๋ฅผ Cloudflare Stream์ผ๋ก ์ด๋๊น์ง ์ฎ๊ธธ ์ ์๋๊ฐ
์ด ๋ถ๋ถ์์ ๊ฐ์ฅ ๋ง์ ์ ์ฝ์ด ๋ฐ๊ฒฌ๋์์ต๋๋ค. ํ์ฌ MediaConvert์์ ์ฌ์ฉ ์ค์ธ ์ค์ ์ฝ 20๊ฐ ์ค Cloudflare Stream๊ณผ ํธํ๋๋ ํญ๋ชฉ์ 6๊ฐ ์ ๋, ๋น์จ๋ก๋ ์ฝ 30% ์์ค์ด์์ต๋๋ค.
ํธํ ๊ฐ๋ฅํ ํญ๋ชฉ
- H.264 / AAC ์ฝ๋ฑ
- 360p / 720p / 1080p ํด์๋
- HLS ์ถ๋ ฅ
- ์ธ๋ก ์์ ์๋ ์ฒ๋ฆฌ, ์ ์ค์ผ์ผ ๋ฐฉ์ง, ์ธํฐ๋ ์ด์ฑ ์ ๊ฑฐ
ํธํ ๋ถ๊ฐ๋ฅํ ํญ๋ชฉ
- 4K(2160p) โ Stream์ ์ต๋ 1080p๊น์ง๋ง ์ง์ํฉ๋๋ค.
- ๋นํธ๋ ์ดํธ ์๋ ์ค์ โ Stream์ ABR(Adaptive Bitrate) ์๋ ๋ฐฉ์๋ง ์ง์ํฉ๋๋ค.
- Quality Level, GOP, B-Frames, Codec Profile, Adaptive Quantization โ ๋ชจ๋ ์๋ ์ค์ ๋ง ๊ฐ๋ฅํฉ๋๋ค.
- HLS Segment ๊ธธ์ด โ ์๋ ์ค์ ๋ง ์ง์๋ฉ๋๋ค.
- AES-128 Segment ์ํธํ โ Signed URLs ๋ฐฉ์๋ง ์ง์ํ๊ณ , ์ธ๊ทธ๋จผํธ ๋จ์ ์ํธํ๋ ๋ถ๊ฐ๋ฅํฉ๋๋ค.
- Multi-Pass Encoding, Scene Change Detection, HRD Buffer
๋์ ์ํฌํ๋ก์ฐ๋ ๋งค์ฐ ๋จ์ํด์ง๋๋ค.
MediaConvert: 12๋จ๊ณ (Lambda 3๊ฐ + S3 2๊ฐ + MediaConvert + EventBridge)
Stream: 5๋จ๊ณ (Stream API ๋จ์ผ)
๊ฒฐ๊ตญ ์์ฌ๊ฒฐ์ ์ ํต์ฌ์ ์ธ๋ฐํ ์ธ์ฝ๋ฉ ์ ์ด๋ฅผ Stream์ ์๋ ์ต์ ํ์ ์์ํ ์ ์๋๊ฐ์์ต๋๋ค.
3. PoC 4-Stage
Stage 1. R2 ๊ธฐ๋ณธ ์ฐ๊ฒฐ
๊ฐ์ฅ ๋จผ์ ํ
์คํธ์ฉ R2 ๋ฒํท์ ์์ฑํ๊ณ , Public Development URL์ ํ์ฑํํ ๋ค ์ด๋ฏธ์ง ํ ์ฅ์ ์
๋ก๋ํด๋ณด์์ต๋๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก S3์ ๋์ผํ ๋ฐฉ์์ผ๋ก ๋์ํ์ต๋๋ค. ๋ค๋ง Cache-Control ํค๋๋ฅผ ์ปค์คํฐ๋ง์ด์งํ๋ ค๋ฉด Worker๊ฐ ํ์ํฉ๋๋ค.
Stage 2. S3 SDK๊ฐ R2๋ฅผ ๊ทธ๋๋ก ๋ค๋ฃฐ ์ ์๋๊ฐ
์ด ๋จ๊ณ์ ํต์ฌ์ API ํ ์ค๋ง ๋ณ๊ฒฝํด๋ ๊ธฐ์กด ์ฝ๋๊ฐ ๊นจ์ง์ง ์๋๊ฐ์์ต๋๋ค.
// ๊ธฐ์กด S3 ํด๋ผ์ด์ธํธ์์ endpoint๋ง ๋ณ๊ฒฝ
const r2 = new S3Client({
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
// ... credentials ๋ฑ์ ๋์ผ
});
await r2.send(new PutObjectCommand({ ... }));
@aws-sdk/client-s3๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.PutObjectCommand,Presigned URL๋ชจ๋ ์ ์ ๋์ํฉ๋๋ค.- ๊ฒ์ฆ๋ ์๋ํฌ์ธํธ: Base64 ์ ๋ก๋ / ํ๋ก ํธ ์ง์ ์ ๋ก๋์ฉ presigned URL ๋ฐ๊ธ / ํ์ผ ์กฐํ ๋ชจ๋ ์ ์.
์ถ๊ฐ๋ก Custom Domain์ ์ฐ๊ฒฐํด ์์ฒด ๋๋ฉ์ธ์ผ๋ก๋ ์๋น์ด ๋๋์ง ํ์ธํ๊ณ , CORS๋ ๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ๊ณผ dev API ๋๋ฉ์ธ๋ง ํ์ดํธ๋ฆฌ์คํธ์ ์ถ๊ฐํ์ต๋๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก ๊ธฐ์กด S3 ์ฝ๋๋ฅผ ๊ฑฐ์ ์์ ํ์ง ์๊ณ ๋ R2๋ก ์ ํ์ด ๊ฐ๋ฅํ์ต๋๋ค. SDK, ์ธ์ฆ ๋ฐฉ์, ๋ช ๋ น์ด ์ฒด๊ณ๊ฐ ๋ชจ๋ ๋์ผํฉ๋๋ค.
Stage 3. Worker๊ฐ Lambda@Edge๋ฅผ ์์ ํ ๋์ฒดํ ์ ์๋๊ฐ
Worker ํ ํ์ผ๋ก Lambda@Edge์ ๋ ๊ฐ์ง ์ญํ (์ฟ ํค ๋ฐ๊ธ, ํ์ผ ์๋น)์ ๋ชจ๋ ๊ตฌํํ์ต๋๋ค. ํต์ฌ ํ๋ฆ๋ง ์ถ๋ฆฐ ์์ฌ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
async function handleSetCookie(request) {
const url = new URL(request.url);
const cookies = [];
const domain = COOKIE_DOMAIN;
for (const [key, value] of url.searchParams) {
cookies.push(`${key}=${value}; Domain=${domain}; SameSite=None; Secure`);
}
return new Response("Cookies set successfully", {
status: 200,
headers: {
"Set-Cookie": cookies.join(", "),
"Cache-Control": "no-cache, no-store, must-revalidate",
"Content-Type": "text/plain",
},
});
}
async function handleFileServing(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
const object = await env.MY_BUCKET.get(key, {
range: request.headers.has("Range")
? request.headers.get("Range")
: undefined,
});
if (!object) return new Response("Not Found", { status: 404 });
const headers = new Headers();
headers.set(
"Content-Type",
object.httpMetadata.contentType || "application/octet-stream"
);
if (object.etag) headers.set("ETag", object.etag);
headers.set("Cache-Control", CACHE_POLICY); // ์ฌ๋ด ์ ์ฑ
์ ๋ง์ถฐ ์ค์
headers.set("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Range");
if (request.headers.has("Range")) {
headers.set("Content-Range", object.range);
return new Response(object.body, { status: 206, headers });
}
return new Response(object.body, { status: 200, headers });
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === COOKIE_PATH) return handleSetCookie(request, env);
return handleFileServing(request, env);
},
};
MY_BUCKET์ Worker์ R2 ๋ฐ์ธ๋ฉ์ผ๋ก ์ฐ๊ฒฐํ ๋ณ์์
๋๋ค. ๋ณ๋์ ํค ๊ด๋ฆฌ ์์ด R2๋ฅผ ๋ง์น ๋ฉ๋ชจ๋ฆฌ ๊ฐ์ฒด์ฒ๋ผ ๋ค๋ฃฐ ์ ์๋ค๋ ์ ์ด ์ธ์์ ์ด์์ต๋๋ค.
๊ฒ์ฆ: Cache-Control ํค๋
์ด๋ฏธ์ง ์์ฒญ์ ๋ํด Lambda@Edge๊ฐ ๋ถ์ฌํ๋ ๊ฒ๊ณผ ๋์ผํ ์บ์ ์ ์ฑ ํค๋๊ฐ ์๋ต์ ์ ์์ ์ผ๋ก ๋ถ์ฌ๋๋ ๊ฒ์ ํ์ธํ์ต๋๋ค.
$ curl -I https://<worker-domain>/<path>
HTTP/2 200
content-type: image/png
cache-control: <๊ธฐ์กด Lambda@Edge์ ๋์ผํ ์ ์ฑ
>
etag: <hash>
access-control-allow-origin: <ํ์ฉ Origin>
๊ฒ์ฆ: ์ฟ ํค ๋ฐ๊ธ ๊ฒฝ๋ก
๊ธฐ์กด Signed Cookies 3์ข ์ด ๋์ผํ ํ์์ผ๋ก ๋ฐ๊ธ๋๋ ๊ฒ์ ํ์ธํ์ต๋๋ค.
$ curl -v "https://<worker-domain>/<cookie-path>?Policy=xxx&Signature=yyy&Key-Pair-Id=zzz"
HTTP/2 200
set-cookie: Policy=xxx; Domain=<cdn-domain>; SameSite=None; Secure,
Signature=yyy; Domain=<cdn-domain>; SameSite=None; Secure,
Key-Pair-Id=zzz; Domain=<cdn-domain>; SameSite=None; Secure
cache-control: no-cache, no-store, must-revalidate
์ฟ ํค 3๊ฐ๊ฐ ๊ธฐ์กด๊ณผ ๋์ผํ ํ์์ผ๋ก ์ ์ ๋ฐ๊ธ๋๋ ๊ฒ์ ํ์ธํ์ต๋๋ค. Range ์์ฒญ(206 Partial Content)๋ ์ ์์ ์ผ๋ก ์ฒ๋ฆฌ๋์ด HLS ์คํธ๋ฆฌ๋ฐ์๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
Lambda@Edge์ Worker์ ๊ธฐ๋ฅ ๋งคํ
| ๊ธฐ๋ฅ | Lambda@Edge | Worker | ๊ฒฐ๊ณผ |
|---|---|---|---|
| ์ฟ ํค ๋ฐ๊ธ ๊ฒฝ๋ก | โ | โ | ๋์ผ |
| ํ์ผ ์๋น | โ | โ | ๋์ผ |
| Cache-Control | โ | โ | ๋์ผ |
| Range ์์ฒญ | โ | โ | ๋์ผ |
| ๋๋ฏธ ํ์ผ ํ์ | S3์ ๋๋ฏธ ์ค๋ธ์ ํธ ํ์ | ๋ถํ์ | Worker๊ฐ ๋ ๊ฐ๋จ |
| ์ ๊ทผ ์ ์ด | Signed Cookies | ์ฝ๋๋ก ์ปค์คํ | Worker๊ฐ ๋ ์ ์ฐ |
์ฟ ํค ๋ฐ๊ธ์ฉ ๋๋ฏธ ํ์ผ์ ๋ํด ์ ๊น ๋ถ์ฐ ์ค๋ช
์ ๋๋ฆฌ์๋ฉด, CloudFront์์ Lambda@Edge viewer_response๋ origin์ด ํ ๋ฒ ์๋ต์ ๋ฐํํด์ผ ์คํ๋๋ ๊ตฌ์กฐ์
๋๋ค. ๊ทธ๋์ S3์ ๋น ๋๋ฏธ ์ค๋ธ์ ํธ๋ฅผ ๋๊ณ ์ด๋ฅผ ํธ๋ฆฌ๊ฑฐ๋ก ํ์ฉํ๊ณ ์์์ต๋๋ค. Worker๋ origin์ด ์๊ธฐ ์์ ์ด๊ธฐ ๋๋ฌธ์ ๋๋ฏธ ํ์ผ์ด ํ์ํ์ง ์์ต๋๋ค. ์์ ์ฐจ์ด์ฒ๋ผ ๋ณด์ด์ง๋ง, ์ด์ ๊ด์ ์์๋ โS3์ ์๋ฏธ ์๋ ํ์ผ ํ๋๊ฐ ํญ์ ์กด์ฌํด์ผ ํ๋คโ๋ ๋ถ๋ด์ด ์ฌ๋ผ์ง๋ ๊ฒ์ด๋ผ ๊ฝค ์๋ฏธ ์๋ ๊ฐ์ ์ด์์ต๋๋ค.
Stage 3.5. ์ ๊ทผ ์ ์ด โ Worker์์ ์ง์ ๊ฒ์ฆ
์ด ๋จ๊ณ๋ ์ด๋๊น์ง๋ โWorker ๋ด์์ ์์์ ์ธ์ฆ ๋ก์ง์ ๋ผ์ ๋ฃ์ ์ ์๋๊ฐโ ๋ง ํ์ธํ๊ธฐ ์ํ PoC์์ต๋๋ค. ๋ฐ๋ผ์ JWT ๊ฐ์ ์ค์ ํ ํฐ ๊ฒ์ฆ์ ๊ตฌํํ์ง ์๊ณ , โํน์ ๊ฐ์ด Cookie์ ํฌํจ๋์ด ์์ผ๋ฉด ํต๊ณผโ ์ ๋๋ก๋ง ๋จ์ํํ์ต๋๋ค. ์ค์ ๋์ ์ ์ด๋ค ํ ํฐ/๊ฒ์ฆ ๋ฐฉ์์ ์ฌ์ฉํ ์ง๋ ๋ณ๋ ์ค๊ณ๊ฐ ํ์ํฉ๋๋ค.
# (1) public ์ด๋ฏธ์ง โ 200
$ curl https://<worker-domain>/<public-path> -I
HTTP/2 200
cache-control: <์ฌ๋ด ์บ์ ์ ์ฑ
>
# (2) private ์ด๋ฏธ์ง, ์ฟ ํค ์์ โ 401
$ curl https://<worker-domain>/<private-path>
Unauthorized: No token provided
# (3) private ์ด๋ฏธ์ง, ์๋ชป๋ ์ฟ ํค โ 401
$ curl https://<worker-domain>/<private-path> -H "Cookie: access_token=<invalid>"
Unauthorized: Invalid token
# (4) private ์ด๋ฏธ์ง, ์ฌ๋ฐ๋ฅธ ์ฟ ํค โ 200
$ curl https://<worker-domain>/<private-path> -H "Cookie: access_token=<valid>" -I
HTTP/2 200
๊ธฐ์กด ๊ตฌ์กฐ์ ๊ฐ์ฅ ํฐ ์ฐจ์ด๋ ๊ฒ์ฆ ์ฃผ์ฒด์ ๋๋ค. ๊ธฐ์กด์๋ ๋ฐฑ์๋๊ฐ ๋ฐ๊ธํ Signed Cookies๋ฅผ ํด๋ผ์ด์ธํธ๊ฐ ์ ์ฅํ ๋ค, ๋งค ์ด๋ฏธ์ง ์์ฒญ๋ง๋ค ํจ๊ป ์ ์กํ๋ ๋ฐฉ์์ด์์ต๋๋ค. Worker ๋ฐฉ์์์๋ ๊ฒ์ฆ ๋ก์ง ์์ฒด๋ฅผ ์ ํฌ๊ฐ ์ง์ ๊ตฌ์ฑํ ์ ์๊ธฐ ๋๋ฌธ์, ์ด๋ค ๊ถํ ๋ชจ๋ธ(์ฌ์ฉ์ ๋จ์, ๊ฒฝ๋ก ๋จ์, ๋ง๋ฃ ์๊ฐ ๋ฑ)๋ก ๊ฐ์ ธ๊ฐ์ง๋ฅผ ๋น๊ต์ ์์ ๋กญ๊ฒ ์ค๊ณํ ์ ์์ต๋๋ค. ๋ค๋ง ๊ตฌ์ฒด์ ์ธ ํ ํฐ ํฌ๋งท๊ณผ ๊ฒ์ฆ ์ ์ฑ ์ PoC ์ดํ ๋ณ๋ ์ค๊ณ ๋จ๊ณ์์ ์ ํ ์์ ์ ๋๋ค.
Stage 4. ํฌ๋ก์ค ๋๋ฉ์ธ ์ฟ ํค
PoC ๊ณผ์ ์์ ๊ฐ์ฅ ๋ง์ ์๊ฐ์ ์๋นํ ๋ถ๋ถ์ ๋๋ค. ํ๋ก ํธ ๋๋ฉ์ธ๊ณผ CDN ๋๋ฉ์ธ์ด ๋ถ๋ฆฌ๋์ด ์๋ ๊ตฌ์กฐ์ด๊ธฐ ๋๋ฌธ์, ์ฟ ํค๋ฅผ ์ ์์ ์ผ๋ก ๋ฐ๊ธํ๊ณ ์ ์กํ๋ ค๋ฉด ์ถฉ์กฑํด์ผ ํ ์กฐ๊ฑด์ด ๊น๋ค๋ก์ ์ต๋๋ค.
ํ๋ก ํธ ํธ์ถ ์ฝ๋
async function setCookie() {
const token = document.getElementById("jwtToken").value.trim();
await fetch(`${WORKER_URL}/<cookie-path>?token=${token}`, {
method: "GET",
credentials: "include", // โ
ํฌ๋ก์ค ๋๋ฉ์ธ ์ฟ ํค ์ ์ก/์ ์ฅ ํ์ฉ
mode: "cors",
});
}
async function testFileAccess() {
await fetch(`${WORKER_URL}/<private-path>`, {
credentials: "include", // โ
์ฟ ํค ์๋ ์ ์ก
});
}
Worker ์๋ต ํค๋ ์์
Set-Cookie: auth_token=<token>; Domain=<worker-domain>;
Path=/; SameSite=None; Secure; HttpOnly
Access-Control-Allow-Origin: <์์ฒญ Origin>
Access-Control-Allow-Credentials: true
๊ตฌํ ์ ์ฃผ์ํ ์ ๋ค ๊ฐ์ง
credentials: 'include'์์ชฝ ๋ชจ๋ ์ค์ ํ์ โ ํ๋ก ํธ์ fetch ํธ์ถ๊ณผ Worker์ CORS ์๋ต ํค๋ ์์ชฝ์ ๋ชจ๋ ๋ช ์ํด์ผ ํฉ๋๋ค. ํ์ชฝ์ด๋ผ๋ ๋น ๋จ๋ฆฌ๋ฉด ์ฟ ํค๊ฐ ์ฌ๋ผ์ง๋๋ค.Access-Control-Allow-Origin: *์ฌ์ฉ ๋ถ๊ฐ โ credentials ๋ชจ๋์์๋ ์์ผ๋์นด๋๊ฐ ํ์ฉ๋์ง ์์ต๋๋ค. ์์ฒญ Origin์ ๊ทธ๋๋ก ์๋ต์ ๋ด์ ๋ฐํํด์ผ ํฉ๋๋ค.SameSite=None์ด๋ฉดSecure์์ฑ ํ์ โ ํฌ๋ก์ค ์ฌ์ดํธ ์ฟ ํค ์ ์ก์ HTTPS ํ๊ฒฝ์์๋ง ๊ฐ๋ฅํฉ๋๋ค.- ๋ธ๋ผ์ฐ์ ์บ์ ์ ์์ฌํญ โ ์ฟ ํค๊ฐ ์ ํจํ ์ํ์์ ํ ๋ฒ ๋ฐ์ ์ด๋ฏธ์ง๋ ์ฌ๋ด ์บ์ ์ ์ฑ
์ ๋ฐ๋ผ ๋ธ๋ผ์ฐ์ ์ ์ฅ๊ธฐ๊ฐ ์บ์ฑ๋ฉ๋๋ค. ๋ฐ๋ผ์ ์ฟ ํค๋ฅผ ์ญ์ ํ ๋ค ๋ค์ ์์ฒญํ๋๋ผ๋ Worker๊น์ง ๋๋ฌํ์ง ๋ชปํ๊ณ ๋ธ๋ผ์ฐ์ ์บ์์์ ์๋ต์ด ๋ฐํ๋ฉ๋๋ค. DevTools์
Disable cache์ต์ ์ ์ผ์ง ์๊ณ ํ ์คํธํ๋ฉด 401์ด ๋ฐ์ํ์ง ์์ โ์ธ์ฆ์ด ์ ๋๋ก ๊ฑธ๋ฆฌ์ง ์๋ ๊ฒ ๊ฐ๋คโ๋ ์๋ชป๋ ๊ฒฐ๋ก ์ ๋๋ฌํ ์ ์์ต๋๋ค. ์ํฌ๋ฆฟ ๋ชจ๋๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๊ฐ์ฅ ๋น ๋ฅธ ํด๊ฒฐ ๋ฐฉ๋ฒ์ด์์ต๋๋ค.
์๋๋ ์คํจ ์ผ์ด์ค๋ ์ ์์ ์ผ๋ก ์กํ์ต๋๋ค.
[11:31:00] ์ธ์ฆ ์์ด ์ ๊ทผ ํ
์คํธ (์ฟ ํค ์์)
[11:31:01] ์์๋๋ก 401 Unauthorized
[11:31:01] Response: Unauthorized: No token provided
4. ํ๊ณ์ ํธ๋ ์ด๋์คํ
MediaConvert๋ฅผ ์์ ํ ๋์ฒดํ ์๋ ์์์ต๋๋ค.
์ค์ ํธํ๋ฅ ์ด 30% ์์ค์ด๊ธฐ ๋๋ฌธ์, ๋ค์ ๋ ์ผ์ด์ค๋ MediaConvert๋ฅผ ์ ์งํด์ผ ํฉ๋๋ค.
- 4K ์์์ด ํ์ํ ๊ฒฝ์ฐ (Stream์ 1080p๊ฐ ์ํ์ ๋๋ค)
- AES-128 ์ธ๊ทธ๋จผํธ ์ํธํ๊ฐ ํ์ํ ๊ฒฝ์ฐ (Stream์ Signed URL๋ง ์ง์ํฉ๋๋ค)
๋ฐ๋๋ก 1080p ์ผ๋ฐ VOD์ ํด๋นํ๋ค๋ฉด Stream์ผ๋ก ์ถฉ๋ถํ ์ฎ๊ธธ ์ ์๊ณ , ์ธํ๋ผ ๋จ๊ณ๊ฐ 12๋จ๊ณ์์ 5๋จ๊ณ๋ก ์ค์ด๋ญ๋๋ค. ๋น์ฉ์ 80~95% ์ ๊ฐ์ด ๊ฐ๋ฅํ ๊ฒ์ผ๋ก ์์ํ๊ณ ์์ง๋ง, ์ ํํ ์์น๋ ์ค์ ํธ๋ํฝ ๊ธฐ๋ฐ์ผ๋ก ๋ค์ ๊ฒ์ฆ์ด ํ์ํฉ๋๋ค.
5. ๊ฒฐ๋ก
| ์์ญ | ๊ฒฐ์ |
|---|---|
| ์คํ ๋ฆฌ์ง (S3 โ R2) | ์ด์ ๊ฐ๋ฅ. SDK ๊ทธ๋๋ก ํ์ฉ, endpoint๋ง ๋ณ๊ฒฝ |
| ์ ๊ทผ ์ ์ด (Signed Cookies โ ํ ํฐ ๊ธฐ๋ฐ) | ์ด์ ๊ฐ๋ฅ์ฑ ํ์ธ. Worker์์ ์์์ ๊ฒ์ฆ ๋ก์ง ๊ตฌ์ฑ ๊ฐ๋ฅ (์ค์ ํ ํฐ/์ ์ฑ ์ ๋ณ๋ ์ค๊ณ ํ์) |
| Edge ํจ์ (Lambda@Edge โ Worker) | ์ด์ ๊ถ์ฅ. Worker๋ ๋งค ์์ฒญ ์คํ๋์ง๋ง ๋จ๊ฐ๊ฐ ๋ฎ๊ณ , egress 0์ + ์บ์ ์ ์ฑ ์ผ๋ก ๋น์ฉ ์ฐ์ |
| ์ธ์ฝ๋ฉ (MediaConvert โ Stream) | ํ์ด๋ธ๋ฆฌ๋. ์ผ๋ฐ VOD๋ Stream, 4K/AES ์ฝํ ์ธ ๋ MediaConvert ์ ์ง |
| ๋ผ์ด๋ธ ๋ฐฉ์ก ์๋ณธ | ๊ฒํ ์ค. Brightcove์์ R2๋ก์ ์ ์ฅ ๊ฒฝ๋ก ๋ฏธํด๊ฒฐ |
๋ค์ ๋จ๊ณ๋ ํ๋ก ํธ ๊ฐ๋ฐ์๋ฒ ์ฝ๋๋ฅผ ์์ ํ์ฌ ์ค์ ์์ ์ฌ์ ํ๋ฆ์ ๋๊น์ง ํต๊ณผ์ํค๋ ๊ฒ์ ๋๋ค. ์ด ๊ฒ์ฆ์ด ๋ง๋ฌด๋ฆฌ๋๋ฉด ์ ๊ท ์ฝํ ์ธ ๋ถํฐ Cloudflare ๋ผ์ธ์ผ๋ก ํ๋ ค๋ณด๋ผ ๊ณํ์ด๋ฉฐ, ๊ธฐ์กด ์์ฐ ๋ง์ด๊ทธ๋ ์ด์ ์ ๊ทธ ์ดํ์ ๋ณ๋๋ก ๊ฒํ ํ ์์ ์ ๋๋ค.
๋ง์น๋ฉฐ
์ด๋ฒ PoC๋ฅผ ํตํด โAWS ๋ฏธ๋์ด ์คํ์ Cloudflare๋ก ์ฎ๊ธธ ์ ์๋๊ฐโ๋ผ๋ ์ง๋ฌธ์ ๋ํด ์ด๋ ์ ๋ ๋ช ํํ ๋ต์ ์ป์ ์ ์์์ต๋๋ค. ๋น์ฉ ์ธก๋ฉด์ ์ํฉํธ๊ฐ ํฌ๋ค๋ ์ ์ด ๊ฐ์ฅ ํฐ ์ํ์ด์๊ณ , ๋์์ Cloudflare Stream์ ์ธ์ฝ๋ฉ ์ ์ฝ์ฒ๋ผ ์ฎ๊ธธ ์ ์๋ ์์ญ๋ ๋ถ๋ช ํ ์กด์ฌํ๋ค๋ ์ ์ ํ์ธํ ์ ์์์ต๋๋ค. ๋ชจ๋ ๊ฒ์ ํ ๋ฒ์ ์ฎ๊ธฐ๊ธฐ๋ณด๋ค๋, ๋น์ฉ ํจ๊ณผ๊ฐ ๊ฐ์ฅ ํฐ ์์ญ๋ถํฐ ๋จ๊ณ์ ์ผ๋ก ์ ํํ๋ฉด์ 4KยทAES-128 ๊ฐ์ ํน์ ์๊ตฌ์ฌํญ์ ๊ธฐ์กด ์คํ์ ์ ์งํ๋ ํ์ด๋ธ๋ฆฌ๋ ๊ตฌ์กฐ๊ฐ ํ์ค์ ์ธ ๋ต์ด๋ผ๊ณ ํ๋จํ์ต๋๋ค.