ЁЯПл The SchoolтА║ЁЯУИ ScalingтА║ЁЯМН рдзрдбрд╛ 03 тАФ UI рд╕рд╛рдареА CloudFront + S3: рдкреНрд░рддреНрдпреЗрдХ рджрд╛рд░рд╛рд╡рд░ рдЭреЗрд░реЙрдХреНрд╕ рдкреНрд░рддреА
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯМН рдзрдбрд╛ 03 тАФ UI рд╕рд╛рдареА CloudFront + S3: рдкреНрд░рддреНрдпреЗрдХ рджрд╛рд░рд╛рд╡рд░ рдЭреЗрд░реЙрдХреНрд╕ рдкреНрд░рддреА

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 13 рдкреИрдХреА рдзрдбрд╛ 03 ┬╖ рдорд╛рдЧреЗ: lesson-02-measuring-load ┬╖ рдкреБрдвреЗ: lesson-04-dynamic-at-the-edge


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ02, рдЖрдгрд┐ рддреНрдпрд╛рд╢рд┐рд╡рд╛рдп рдкрд╣рд┐рд▓рд╛ tier: UI. Pages, scripts, styles рдЖрдгрд┐ рдЪрд┐рддреНрд░реЗ Amazon S3 рдордзреНрдпреЗ рд░рд╛рд╣рддрд╛рдд рдЖрдгрд┐ Amazon CloudFront рддреА рджреЗрддреЛ тАФ рдкреНрд░рддреНрдпреЗрдХ рднреЗрдЯ рджреЗрдгрд╛рд▒реНрдпрд╛рдЬрд╡рд│рдЪреНрдпрд╛ edge locations рд╡рд░ рдареЗрд╡рд▓реЗрд▓реНрдпрд╛ рдкреНрд░рддреА. рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ TTLs рд╕рд╛рдареА рддреБрдореНрд╣реА hit ratio рдореЛрдЬрддрд╛, рдЖрдгрд┐ invalidation рд╡ versioned file names рдкрд╛рд╣рддрд╛. scale/demo.py рдордзрд▓реЗ cdn() рдкреНрд░рддреНрдпреЗрдХ рдЧреЛрд╖реНрдЯ рджрд╛рдЦрд╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдЬрддреНрд░реЗрдЪреЗ рдПрдХрдЪ office рдЖрд╣реЗ, рд╢рд╛рд│реЗрдЪреНрдпрд╛ рдордзреЛрдордз. рдЬрддреНрд░реЗрдЪрд╛ рдирдХрд╛рд╢рд╛ рд╣рд╡рд╛ рдЕрд╕рд▓реЗрд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ рдкрд╛рд▓рдХ office рдкрд░реНрдпрдВрдд рдЪрд╛рд▓рдд рдЬрд╛рддреЛ, рдерд╛рдВрдмрддреЛ, рдЖрдгрд┐ рдПрдХ рдкреНрд░рдд рдШреЗрддреЛ. рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рджрд┐рд╡рд╢реА office рднрд░реВрди рдЬрд╛рддреЗ.

рдореНрд╣рдгреВрди рджреАрдкрд┐рдХрд╛ рдкреНрд░рддреНрдпреЗрдХ рдлрд╛рдЯрдХрд╛рд╡рд░ рдПрдХ рдЭреЗрд░реЙрдХреНрд╕ рдЯреЗрдмрд▓ ЁЯУДЁЯЪк рдареЗрд╡рддреЗ. рдкреНрд░рддреНрдпреЗрдХ рдЯреЗрдмрд▓рд╛рд╡рд░ рдирдХрд╛рд╢рд╛рдЪреНрдпрд╛ рдкреНрд░рддреА рдЕрд╕рддрд╛рдд. рдкрд╛рд▓рдХ рдЬрд╡рд│рдЪреНрдпрд╛ рдлрд╛рдЯрдХрд╛рд╡рд░ рд╡рд┐рдЪрд╛рд░рддреЛ рдЖрдгрд┐ рд▓рдЧреЗрдЪ рдкреНрд░рдд рдорд┐рд│рд╡рддреЛ тАФ рд╣рд╛ hit. рдлрдХреНрдд рдЬреЗрд╡реНрд╣рд╛ рдПрдЦрд╛рджреНрдпрд╛ рдЯреЗрдмрд▓рд╛рд╡рд░ рдкреНрд░рддрдЪ рдирд╕рддреЗ, рдХрд┐рдВрд╡рд╛ рддреА рдкреНрд░рдд рдЦреВрдк рдЬреБрдиреА рдЕрд╕рддреЗ, рддреЗрд╡реНрд╣рд╛рдЪ рдПрдХ рдорджрддрдиреАрд╕ рдирд╡реА рдкреНрд░рдд рдЖрдгрд╛рдпрд▓рд╛ office рдХрдбреЗ рдзрд╛рд╡рддреЛ тАФ рд╣рд╛ miss.

рдкреНрд░рддреНрдпреЗрдХ рдкреНрд░рддреАрд╡рд░ "рдЗрддрдХреНрдпрд╛ рд╡реЗрд│рдкрд░реНрдпрдВрдд рдЪрд╛рд▓реЗрд▓" рдЕрд╢реА рд╡реЗрд│ рд▓рд┐рд╣рд┐рд▓реЗрд▓реА рдЕрд╕рддреЗ тАФ рд╣рд╛ TTL. 1 рд╕реЗрдХрдВрдж рдЪрд╛рд▓рдгрд╛рд░реА рдкреНрд░рдд рдореНрд╣рдгрдЬреЗ рджрд░ рд╕реЗрдХрдВрджрд╛рд▓рд╛ office рдХрдбреЗ рдПрдХ рдлреЗрд░реА. 10 рд╕реЗрдХрдВрдж рдЪрд╛рд▓рдгрд╛рд░реА рдкреНрд░рдд рдореНрд╣рдгрдЬреЗ рджрд╣рд╛рдкрдЯ рдХрдореА рдлреЗрд▒реНрдпрд╛.

рдирдХрд╛рд╢рд╛ рдмрджрд▓рд▓рд╛ рдХреА рджреАрдкрд┐рдХрд╛ рдкреНрд░рддреНрдпреЗрдХ рдлрд╛рдЯрдХрд╛рд▓рд╛ рдУрд░рдбреВрди рд╕рд╛рдВрдЧреВ рд╢рдХрддреЗ "рдЬреБрдиреЗ рдирдХрд╛рд╢реЗ рдлреЗрдХреВрди рджреНрдпрд╛!" (invalidation). рдХрд┐рдВрд╡рд╛ тАФ рддреНрдпрд╛рд╣реВрди рдЪрд╛рдВрдЧрд▓реЗ тАФ рддреА рдирд╡рд╛ рдирдХрд╛рд╢рд╛ рдирд╡реНрдпрд╛ рдирд╛рд╡рд╛рдиреЗ рдЫрд╛рдкрддреЗ: "рдирдХрд╛рд╢рд╛, version 2". рдХреЛрдгрддреНрдпрд╛рдЪ рдЯреЗрдмрд▓рд╛рд╡рд░ рдЕрдЬреВрди version 2 рдЪреА рдкреНрд░рдд рдирд╛рд╣реА, рдореНрд╣рдгреВрди рдкреНрд░рддреНрдпреЗрдХ рдЯреЗрдмрд▓ рддреА рдПрдХрджрд╛рдЪ рдЖрдгрддреЗ, рдЖрдгрд┐ рдХреБрдгреАрд╣реА рдЪреБрдХреВрди рдЬреБрдирд╛ рдирдХрд╛рд╢рд╛ рджреЗрдд рдирд╛рд╣реА.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    u1["ЁЯСк parent in Pune"] --> e1["ЁЯУД edge location<br/>Mumbai"]
    u2["ЁЯСк parent in Delhi"] --> e2["ЁЯУД edge location<br/>Delhi"]
    e1 -->|"miss"| rec["ЁЯЧДя╕П regional edge cache"]
    e2 -->|"miss"| rec
    rec -->|"miss"| s3["ЁЯкг S3 bucket (origin)<br/>private ┬╖ OAC"]
    e1 -.->|"hit: answer at once"| u1
    ttl["тП▓я╕П TTL 0 s тЖТ 3,000 to S3<br/>TTL 1 s тЖТ 359<br/>TTL 10 s тЖТ 36<br/>TTL 60 s тЖТ 6"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/scaling/lesson-diagrams.html#l03

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдирд┐рдХрд╛рд▓рд╛рдЪреНрдпрд╛ рджрд┐рд╡рд╢реА рдмрд╣реБрддреЗрдХ requests рддреНрдпрд╛рдЪ рдореЛрдЬрдХреНрдпрд╛ files рд╕рд╛рдареА рдЕрд╕рддрд╛рдд: home page, script, style sheet, рд╢рд╛рд│реЗрдЪрд╛ logo. рддреНрдпрд╛ рдПрдХрд╛рдЪ origin рд╡рд░реВрди рджреЗрдгреЗ рдореНрд╣рдгрдЬреЗ рддреЛ рд╡рд╛рдпрд╛ рдШрд╛рд▓рд╡рдгреЗ; рдкреНрд░рддреНрдпреЗрдХ рдкрд╛рд▓рдХрд╛рдЬрд╡рд│рдЪреНрдпрд╛ edges рд╡рд░реВрди рджреЗрдгреЗ рддреНрдпрд╛рдВрдирд╛ рдЬрд▓рдж рдЖрдгрд┐ рддреБрдордЪреНрдпрд╛ servers рд╡рд░рдЪрд╛ рднрд╛рд░ рдХрдореА рдХрд░рддреЗ. UI tier scale рдХрд░рд╛рдпрд▓рд╛ рд╕рд░реНрд╡рд╛рдд рд╕реЛрдкрд╛ рдЖрд╣реЗ: рддреНрдпрд╛рдЪрд╛ рдмрд╣реБрддреЗрдХ рднрд╛рдЧ рд╕рдЧрд│реАрдХрдбреЗ copy рдХрд░рддрд╛ рдпреЗрддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

scale/sim.py рдордзрд▓реЗ EdgeCache(ttl) рдореНрд╣рдгрдЬреЗ рдПрдХ edge location. get(path, t) рдХрдбреЗ ttl рд╕реЗрдХрдВрджрд╛рдВрдкреЗрдХреНрд╖рд╛ рдирд╡реА рдкреНрд░рдд рдЕрд╕реЗрд▓ рддрд░ рддреЗ HIT рдкрд░рдд рдХрд░рддреЗ; рдирд╛рд╣реАрддрд░ рддреЗ рдирд╡реА рдкреНрд░рдд рдШреЗрддреЗ (MISS, origin рд▓рд╛ рдПрдХ request). fair_traffic(60, 50, PAGES) рдПрдХрд╛ рдорд┐рдирд┐рдЯрд╛рдЪреЗ traffic рдмрдирд╡рддреЗ тАФ 6 pages рд╡рд░ 50 req/s, рдЬрд┐рдереЗ рд▓реЛрдХрдкреНрд░рд┐рдп pages рдЦреВрдк рдЬрд╛рд╕реНрдд рд╡реЗрд│рд╛ рдорд╛рдЧрд┐рддрд▓реА рдЬрд╛рддрд╛рдд (Zipf рд╕рд╛рд░рдЦреЗ рд╡рд╛рдЯрдк). cdn() рддреЗ TTL 0, 1, 10 рдЖрдгрд┐ 60 рд▓рд╛ рдкреБрдиреНрд╣рд╛ рдЪрд╛рд▓рд╡рддреЗ, рдордЧ /app.js рдЪреА рдПрдХ рджрд┐рд╡рд╕рд╛рдЪреА рдкреНрд░рдд, рдПрдХ invalidation, рдЖрдгрд┐ versioned-name рдЪреА рдХрд▓реНрдкрдирд╛ рджрд╛рдЦрд╡рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 scale/demo.py cdn
python3 - <<'EOF'
import sys; sys.path.insert(0, "scale"); from sim import EdgeCache, fair_traffic; from demo import PAGES
from collections import Counter
traffic = fair_traffic(60, 50, PAGES)
for p, n in Counter(p for _, p in traffic).most_common(): print(f"{p:<24} {n:>4} requests")
for ttl in (2, 5, 30, 300):
    e = EdgeCache(ttl)
    for t, p in traffic: e.get(p, t)
    print(f"TTL {ttl:>3} s тЖТ hit ratio {e.hit_ratio():6.1%} ┬╖ origin sees {e.misses:>4}")
e = EdgeCache(31536000)
print("app.3f9c.js:", e.get("/app.3f9c.js", 0), e.get("/app.3f9c.js", 60), "┬╖ new release app.7b21.js:", e.get("/app.7b21.js", 61))
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

cdn рд╣реЗ print рдХрд░рддреЗ:

тФАтФА 3,000 requests in one minute for 6 pages, popular pages asked far more
   TTL   0 s тЖТ hit ratio   0.0% ┬╖ origin (S3) sees 3,000 requests
   TTL   1 s тЖТ hit ratio  88.0% ┬╖ origin (S3) sees   359 requests
   TTL  10 s тЖТ hit ratio  98.8% ┬╖ origin (S3) sees    36 requests
   TTL  60 s тЖТ hit ratio  99.8% ┬╖ origin (S3) sees     6 requests
тФАтФА /app.js cached for a day: MISS then HIT; new release тЖТ invalidate тЖТ MISS

рддреБрдордЪрд╛ snippet рдЖрдзреА рд╡рд╛рдЯрдк print рдХрд░рддреЛ тАФ / 1190 requests, /results 645, /results/3A 416, /app.js 287, /style.css 252, /photos/sports-day.jpg 210 тАФ рдордЧ:

TTL   2 s тЖТ hit ratio  94.0% ┬╖ origin sees  180
TTL   5 s тЖТ hit ratio  97.6% ┬╖ origin sees   72
TTL  30 s тЖТ hit ratio  99.6% ┬╖ origin sees   12
TTL 300 s тЖТ hit ratio  99.8% ┬╖ origin sees    6
app.3f9c.js: MISS HIT ┬╖ new release app.7b21.js: MISS

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Cache рдирд╕рддрд╛рдирд╛ (TTL 0) origin рд▓рд╛ рд╕рдЧрд│реНрдпрд╛ 3,000 requests рджрд┐рд╕рддрд╛рдд. 1 рд╕реЗрдХрдВрджрд╛рдЪреА рдкреНрд░рддрдЪ рддреНрдпрд╛рдВрдкреИрдХреА 88% рдХрд╛рдвреВрди рдЯрд╛рдХрддреЗ; 10 рд╕реЗрдХрдВрдж 98.8% рдХрд╛рдврддрд╛рдд. рд╕реБрдорд╛рд░реЗ 60 s рдирдВрддрд░ рд╡рдХреНрд░ рд╕рдкрд╛рдЯ рд╣реЛрддреЛ тАФ 6 misses рдореНрд╣рдгрдЬреЗ рдкреНрд░рддреНрдпреЗрдХ page рд╕рд╛рдареА рдПрдХ, рддреЗ рдкрд╣рд┐рд▓реНрдпрд╛рдВрджрд╛ рдорд╛рдЧрд┐рддрд▓реЗ рдЬрд╛рддреЗ рддреЗрд╡реНрд╣рд╛. рдЖрдгрд┐ рдирд╡реЗ file name рдЖрдкреЛрдЖрдкрдЪ miss рдЕрд╕рддреЗ: invalidation рдЪреА рдЧрд░рдЬ рдирд╛рд╣реА.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

On a real account тАФ build рдпреЛрдЧреНрдп headers рд╕рд╣ upload рдХрд░рд╛: versioned files рдПрдХ рд╡рд░реНрд╖рд╛рд╕рд╛рдареА, index.html рддрдкрд╛рд╕рд▓реНрдпрд╛рд╢рд┐рд╡рд╛рдп рдХрдзреАрдЪ cache рдирд╛рд╣реА:

aws s3 sync dist/ s3://school-ui-prod/ --exclude index.html \
    --cache-control "public, max-age=31536000, immutable"
aws s3 cp dist/index.html s3://school-ui-prod/index.html --cache-control "no-cache"
aws cloudfront create-invalidation --distribution-id E1A2B3C4D5E6F7 --paths "/index.html"

Terraform тАФ рдПрдХ private bucket, OAC, рдЖрдгрд┐ AWS managed CachingOptimized cache policy рд╡рд╛рдкрд░рдгрд╛рд░реЗ distribution:

resource "aws_cloudfront_origin_access_control" "ui" {
  name                              = "school-ui-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "ui" {
  enabled             = true
  default_root_object = "index.html"
  origin {
    domain_name              = aws_s3_bucket.ui.bucket_regional_domain_name
    origin_id                = "s3-ui"
    origin_access_control_id = aws_cloudfront_origin_access_control.ui.id
  }
  default_cache_behavior {
    target_origin_id       = "s3-ui"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    compress               = true
    cache_policy_id        = "658327ea-f89d-4fab-a63d-7e88639e58f6"   # Managed-CachingOptimized
  }
  restrictions { geo_restriction { restriction_type = "none" } }
  viewer_certificate { cloudfront_default_certificate = true }
}

Bucket policy рдлрдХреНрдд рдпрд╛рдЪ distribution рд▓рд╛ рд╡рд╛рдЪреВ рджреЗрддреЗ (AWS:SourceArn рдореНрд╣рдгрдЬреЗ distribution рдЪрд╛ ARN):

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "cloudfront.amazonaws.com" },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::school-ui-prod/*",
    "Condition": { "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1A2B3C4D5E6F7" } }
  }]
}

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: CloudFront console рдордзреНрдпреЗ distribution рдЪрд╛ cache hit rate рдкрд╛рд╣рд╛. Static UI 90% рдЪреНрдпрд╛ рдмрд▒реНрдпрд╛рдЪ рд╡рд░ рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡рд╛. рдирд╕реЗрд▓, рддрд░ cache key (query strings, cookies, headers) рдЖрдгрд┐ рддреБрдордЪрд╛ build рдкрд╛рдард╡рддреЛ рддреЛ Cache-Control рддрдкрд╛рд╕рд╛.

тПня╕П рдкреБрдвреЗ

рдХрдзреАрдЪ рди рдмрджрд▓рдгрд╛рд▒реНрдпрд╛ files рд╕реЛрдкреНрдпрд╛ рдЖрд╣реЗрдд. рдкрдг рд╢рд┐рдХреНрд╖рд┐рдХреЗрдиреЗ grade save рдХреЗрд▓реА рдХреА /results/3A рдмрджрд▓рддреЗ. Live page рдЪреА рд╕реБрджреНрдзрд╛ рдлрд╛рдЯрдХрд╛рд╡рд░ рдкреНрд░рдд рдареЗрд╡рддрд╛ рдпреЗрдИрд▓ рдХрд╛? рдХрд╛рд╣реА рд╕реЗрдХрдВрджрд╛рдВрд╕рд╛рдареА тАФ рд╣реЛ.

git checkout lesson-04-dynamic-at-the-edge

ЁЯМН Lesson 03 тАФ CloudFront + S3 for the UI: photocopies at every gate

ЁЯУН You are here: Lesson 03 of 13 ┬╖ Previous: lesson-02-measuring-load ┬╖ Next: lesson-04-dynamic-at-the-edge


ЁЯУж What's in this branch

Lessons 01тАУ02, plus the first tier: the UI. The pages, scripts, styles and pictures live in Amazon S3 and are served by Amazon CloudFront тАФ copies kept at edge locations close to each visitor. You measure the hit ratio for different TTLs, and see invalidation and versioned file names. cdn() in scale/demo.py shows each one.

ЁЯзТ Explain like I'm 5

The fair has one office, in the middle of the school. Every parent who wants the fair map walks to the office, waits, and gets a copy. On results day the office is full.

So Dipika puts a photocopy desk at every gate ЁЯУДЁЯЪк. Each desk keeps copies of the map. A parent asks at the nearest gate and gets a copy at once тАФ that is a hit. Only when a desk has no copy, or its copy is too old, does a helper run to the office for a fresh one тАФ that is a miss.

Each copy has a "good until" time written on it тАФ the TTL. A copy that is good for 1 second means a run to the office every second. A copy good for 10 seconds means ten times fewer runs.

When the map changes, Dipika can shout "throw away your old maps!" to every gate (invalidation). Or тАФ better тАФ she prints the new map with a new name: "map, version 2". No desk has a copy of version 2 yet, so every desk fetches it once, and nobody hands out the old one by mistake.

ЁЯЧ║я╕П Diagram

flowchart LR
    u1["ЁЯСк parent in Pune"] --> e1["ЁЯУД edge location<br/>Mumbai"]
    u2["ЁЯСк parent in Delhi"] --> e2["ЁЯУД edge location<br/>Delhi"]
    e1 -->|"miss"| rec["ЁЯЧДя╕П regional edge cache"]
    e2 -->|"miss"| rec
    rec -->|"miss"| s3["ЁЯкг S3 bucket (origin)<br/>private ┬╖ OAC"]
    e1 -.->|"hit: answer at once"| u1
    ttl["тП▓я╕П TTL 0 s тЖТ 3,000 to S3<br/>TTL 1 s тЖТ 359<br/>TTL 10 s тЖТ 36<br/>TTL 60 s тЖТ 6"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/scaling/lesson-diagrams.html#l03

тЭУ What

ЁЯдФ Why

Because on results day most requests are for the same few files: the home page, the script, the style sheet, the school logo. Serving them from one origin wastes it; serving them from edges near each parent makes them fast and takes them off your servers. The UI tier is the easiest one to scale: most of it can be copied everywhere.

ЁЯФз How (in this repo)

EdgeCache(ttl) in scale/sim.py is one edge location. get(path, t) returns HIT when it has a copy younger than ttl seconds; otherwise it takes a fresh copy (MISS, one origin request). fair_traffic(60, 50, PAGES) makes one minute of traffic тАФ 50 req/s over 6 pages, where popular pages are asked for far more often (a Zipf-like spread). cdn() replays it at TTL 0, 1, 10 and 60, then shows a one-day copy of /app.js, an invalidation, and the versioned-name idea.

ЁЯзк Try it

python3 scale/demo.py cdn
python3 - <<'EOF'
import sys; sys.path.insert(0, "scale"); from sim import EdgeCache, fair_traffic; from demo import PAGES
from collections import Counter
traffic = fair_traffic(60, 50, PAGES)
for p, n in Counter(p for _, p in traffic).most_common(): print(f"{p:<24} {n:>4} requests")
for ttl in (2, 5, 30, 300):
    e = EdgeCache(ttl)
    for t, p in traffic: e.get(p, t)
    print(f"TTL {ttl:>3} s тЖТ hit ratio {e.hit_ratio():6.1%} ┬╖ origin sees {e.misses:>4}")
e = EdgeCache(31536000)
print("app.3f9c.js:", e.get("/app.3f9c.js", 0), e.get("/app.3f9c.js", 60), "┬╖ new release app.7b21.js:", e.get("/app.7b21.js", 61))
EOF

тЬЕ Verify тАФ what you should see

cdn prints:

тФАтФА 3,000 requests in one minute for 6 pages, popular pages asked far more
   TTL   0 s тЖТ hit ratio   0.0% ┬╖ origin (S3) sees 3,000 requests
   TTL   1 s тЖТ hit ratio  88.0% ┬╖ origin (S3) sees   359 requests
   TTL  10 s тЖТ hit ratio  98.8% ┬╖ origin (S3) sees    36 requests
   TTL  60 s тЖТ hit ratio  99.8% ┬╖ origin (S3) sees     6 requests
тФАтФА /app.js cached for a day: MISS then HIT; new release тЖТ invalidate тЖТ MISS

Your snippet prints the spread first тАФ / 1190 requests, /results 645, /results/3A 416, /app.js 287, /style.css 252, /photos/sports-day.jpg 210 тАФ then:

TTL   2 s тЖТ hit ratio  94.0% ┬╖ origin sees  180
TTL   5 s тЖТ hit ratio  97.6% ┬╖ origin sees   72
TTL  30 s тЖТ hit ratio  99.6% ┬╖ origin sees   12
TTL 300 s тЖТ hit ratio  99.8% ┬╖ origin sees    6
app.3f9c.js: MISS HIT ┬╖ new release app.7b21.js: MISS

ЁЯПБ What you just proved

With no cache (TTL 0) the origin sees all 3,000 requests. A 1-second copy already removes 88% of them; 10 seconds removes 98.8%. After about 60 s the curve is flat тАФ 6 misses is one per page, the first time it is asked. And a new file name is a miss by itself: no invalidation needed.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ upload the build with the right headers: versioned files for a year, index.html never cached without checking:

aws s3 sync dist/ s3://school-ui-prod/ --exclude index.html \
    --cache-control "public, max-age=31536000, immutable"
aws s3 cp dist/index.html s3://school-ui-prod/index.html --cache-control "no-cache"
aws cloudfront create-invalidation --distribution-id E1A2B3C4D5E6F7 --paths "/index.html"

Terraform тАФ a private bucket, OAC, and a distribution that uses the AWS managed CachingOptimized cache policy:

resource "aws_cloudfront_origin_access_control" "ui" {
  name                              = "school-ui-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "ui" {
  enabled             = true
  default_root_object = "index.html"
  origin {
    domain_name              = aws_s3_bucket.ui.bucket_regional_domain_name
    origin_id                = "s3-ui"
    origin_access_control_id = aws_cloudfront_origin_access_control.ui.id
  }
  default_cache_behavior {
    target_origin_id       = "s3-ui"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    compress               = true
    cache_policy_id        = "658327ea-f89d-4fab-a63d-7e88639e58f6"   # Managed-CachingOptimized
  }
  restrictions { geo_restriction { restriction_type = "none" } }
  viewer_certificate { cloudfront_default_certificate = true }
}

The bucket policy lets only this distribution read (AWS:SourceArn is the distribution ARN):

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "cloudfront.amazonaws.com" },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::school-ui-prod/*",
    "Condition": { "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1A2B3C4D5E6F7" } }
  }]
}

ЁЯПн Why this matters in production: watch the distribution's cache hit rate in the CloudFront console. A static UI should be well above 90%. If it is not, look at the cache key (query strings, cookies, headers) and at the Cache-Control your build sends.

тПня╕П Next

Files that never change are easy. But /results/3A changes when a teacher saves a grade. Can a live page be copied at the gate too? For a few seconds тАФ yes.

git checkout lesson-04-dynamic-at-the-edge
тЖР Previousmeasuring loadNext тЖТdynamic at the edge

This page is the lesson's README from the lesson-03-cloudfront-s3 branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.