CloudFront + Lambda@Edge to Cloudflare Worker + R2 PoC

chanheeW
  • #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๋กœ ์˜ฎ๊ธด๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด ๋น„์šฉ ์™ธ์—๋„ ๋‹ค์Œ ์„ธ ๊ฐ€์ง€๋ฅผ ํ•จ๊ป˜ ๊ฒ€ํ† ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.

  1. CloudFront ๋ฝ์ธ์„ ์–ด๋–ป๊ฒŒ ํ’€ ๊ฒƒ์ธ๊ฐ€. Signed Cookies๋Š” CloudFront ์ „์šฉ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด๋ผ, CDN์„ ๊ต์ฒดํ•˜๋ ค๋ฉด ์ธ์ฆ ๋กœ์ง ์ž์ฒด๋ฅผ ์ƒˆ๋กœ ์„ค๊ณ„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. Lambda@Edge๊ฐ€ ํ•˜๋˜ ์ผ์„ Worker๊ฐ€ ๋Œ€์ฒดํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€. ํ˜„์žฌ Lambda@Edge๋Š” ํŠน์ • ๊ฒฝ๋กœ ์š”์ฒญ์— ๋Œ€ํ•ด ์ธ์ฆ์šฉ ์ฟ ํ‚ค๋ฅผ Set-Cookie๋กœ ๋ฐœ๊ธ‰ํ•˜๊ณ , ๊ทธ ์™ธ ์‘๋‹ต์—๋Š” ์‚ฌ๋‚ด ์ •์ฑ…์— ๋งž๋Š” Cache-Control ํ—ค๋”๋ฅผ ๋ถ€์—ฌํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. Worker ๋ชจ๋ธ์—์„œ๋„ ์ด ๋‘ ๋™์ž‘(๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ์ฟ ํ‚ค ๋ฐœ๊ธ‰ + ์‘๋‹ต ํ—ค๋” ๊ฐ€๊ณต)์„ ๋™์ผํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธ์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.
  3. 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 CookiesCloudflare + ํ† ํฐ ๊ธฐ๋ฐ˜(์˜ˆ: JWT ๊ฐ€์ •)
๊ฒ€์ฆ ์ฃผ์ฒดCloudFront ๋‚ด๋ถ€ (์ž๋™)Worker ์ฝ”๋“œ (๋งค ์š”์ฒญ)
ํ† ํฐ ๊ตฌ์„ฑ3๊ฐœ ์ฟ ํ‚ค (Policy/Signature/Key-Pair-Id)๋‹จ์ผ ํ† ํฐ ๊ฐ€์ •
๊ถŒํ•œ ์ œ์–ด์ œํ•œ์ Worker ๋กœ์ง์— ๋”ฐ๋ผ ์„ค๊ณ„ ์ž์œ ๋„ ๋†’์Œ
์บ์‹œ ์œ„์น˜CloudFront Cache โ†’ Origin โ†’ Lambda@EdgeCloudflare Cache โ†’ Worker โ†’ R2

2-2. Lambda@Edge viewer_response๋Š” ์ •ํ™•ํžˆ ์–ด๋–ค ์ผ์„ ํ•˜๊ณ  ์žˆ์—ˆ๋Š”๊ฐ€

๊ธฐ์กด Lambda@Edge ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•ด๋ณด๋‹ˆ, ์‹ค์ œ๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” ์ž‘์—…์€ ์‘๋‹ต ํ—ค๋”๋ฅผ ์†๋ณด๋Š” ๋‘ ๊ฐ€์ง€ ์ž‘์—…์ด ์ „๋ถ€์˜€์Šต๋‹ˆ๋‹ค.

  1. ํŠน์ • ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ์ฟ ํ‚ค ๋ฐœ๊ธ‰ โ€” ์ธ์ฆ์šฉ ์ฟ ํ‚ค๋ฅผ Set-Cookie ํ—ค๋”๋กœ ๋ฐœ๊ธ‰ํ•˜๋Š” ์ž‘์—….
  2. ๊ทธ ์™ธ ๋ชจ๋“  ์‘๋‹ต์— ๋Œ€ํ•œ ์บ์‹œ ์ •์ฑ… ๋ถ€์—ฌ โ€” ์‚ฌ์šฉ์ž๋ณ„ ๊ถŒํ•œ์ด ๋‹ค๋ฅธ ์ฝ˜ํ…์ธ ๋ผ 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@EdgeWorker๊ฒฐ๊ณผ
์ฟ ํ‚ค ๋ฐœ๊ธ‰ ๊ฒฝ๋กœโœ…โœ…๋™์ผ
ํŒŒ์ผ ์„œ๋น™โœ…โœ…๋™์ผ
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

๊ตฌํ˜„ ์‹œ ์ฃผ์˜ํ•  ์  ๋„ค ๊ฐ€์ง€

  1. credentials: 'include' ์–‘์ชฝ ๋ชจ๋‘ ์„ค์ • ํ•„์š” โ€” ํ”„๋ก ํŠธ์˜ fetch ํ˜ธ์ถœ๊ณผ Worker์˜ CORS ์‘๋‹ต ํ—ค๋” ์–‘์ชฝ์— ๋ชจ๋‘ ๋ช…์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•œ์ชฝ์ด๋ผ๋„ ๋น ๋œจ๋ฆฌ๋ฉด ์ฟ ํ‚ค๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.
  2. Access-Control-Allow-Origin: * ์‚ฌ์šฉ ๋ถˆ๊ฐ€ โ€” credentials ๋ชจ๋“œ์—์„œ๋Š” ์™€์ผ๋“œ์นด๋“œ๊ฐ€ ํ—ˆ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์š”์ฒญ Origin์„ ๊ทธ๋Œ€๋กœ ์‘๋‹ต์— ๋‹ด์•„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  3. SameSite=None์ด๋ฉด Secure ์†์„ฑ ํ•„์ˆ˜ โ€” ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์ฟ ํ‚ค ์ „์†ก์€ HTTPS ํ™˜๊ฒฝ์—์„œ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
  4. ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ ์œ ์˜์‚ฌํ•ญ โ€” ์ฟ ํ‚ค๊ฐ€ ์œ ํšจํ•œ ์ƒํƒœ์—์„œ ํ•œ ๋ฒˆ ๋ฐ›์€ ์ด๋ฏธ์ง€๋Š” ์‚ฌ๋‚ด ์บ์‹œ ์ •์ฑ…์— ๋”ฐ๋ผ ๋ธŒ๋ผ์šฐ์ €์— ์žฅ๊ธฐ๊ฐ„ ์บ์‹ฑ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ฟ ํ‚ค๋ฅผ ์‚ญ์ œํ•œ ๋’ค ๋‹ค์‹œ ์š”์ฒญํ•˜๋”๋ผ๋„ 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 ๊ฐ™์€ ํŠน์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ์€ ๊ธฐ์กด ์Šคํƒ์„ ์œ ์ง€ํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ตฌ์กฐ๊ฐ€ ํ˜„์‹ค์ ์ธ ๋‹ต์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ