AWS

Amazon CloudFront — Content Delivery Network

Детальне пояснення що таке CDN і навіщо він потрібен. CloudFront Distributions, Origins, Edge Locations, Cache Behaviors, OAC, CloudFront Functions, Invalidations. Повна лабораторна робота з React SPA на S3 + CloudFront + HTTPS з підключенням безкоштовного домену pp.ua.

Amazon CloudFront — Content Delivery Network

Що таке CDN і навіщо він потрібен

Перш ніж говорити про CloudFront — треба зрозуміти фундаментальну проблему, яку вирішує CDN.

Проблема: швидкість передачі даних через інтернет

Уявіть: ваш S3 bucket знаходиться у регіоні eu-central-1 — у дата-центрі у Франкфурті, Німеччина. Ваш React SPA завантажується звідти за 20–50 мс для користувача з Берліна. Чудово!

Але що якщо користувач знаходиться в Токіо, Японія? Відстань від Франкфурту до Токіо — ~9 200 км. Дані передаються зі швидкістю ~200 000 км/сек у оптоволоконному кабелі. Теоретичний мінімум: ~46 мс. Реальна затримка з урахуванням маршрутизації, перемикання вузлів, черг: 150–300 мс.

Це лише час одного запиту (round trip). Ваш React-додаток при першому завантаженні робить 20–50 запитів: HTML, CSS, JS chunks, зображення, шрифти. При 200 мс на кожен — загальний час завантаження 4–10 секунд. За статистикою Google, 53% мобільних користувачів покидають сайт якщо він завантажується довше 3 секунд.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam sequence {
    ArrowColor #374151
    ActorBorderColor #374151
    ActorBackgroundColor #f9fafb
    ParticipantBorderColor #374151
    ParticipantBackgroundColor #f9fafb
    NoteBackgroundColor #fef9c3
    NoteBorderColor #ca8a04
    LifeLineBorderColor #9ca3af
}

title Без CDN: кожен запит долає 9 200 км туди і назад

actor "🇯🇵 Користувач\n(Токіо)" as User
participant "Браузер\n(Токіо)" as Browser
participant "S3 Bucket\n(Франкфурт, eu-central-1)" as S3

note over Browser, S3
  Відстань: ~9 200 км
  Реальна затримка: ~200–300 мс на запит
end note

Browser -> S3 : GET /index.html
note right: ~200 мс ⏱
S3 --> Browser : index.html (1 KB)

Browser -> S3 : GET /main.abc123.js
note right: ~200 мс ⏱
S3 --> Browser : main.js (180 KB)

Browser -> S3 : GET /main.def456.css
note right: ~200 мс ⏱
S3 --> Browser : main.css (12 KB)

Browser -> S3 : GET /chunk.vendor.js
note right: ~200 мс ⏱
S3 --> Browser : vendor.js (95 KB)

Browser -> S3 : GET /logo.svg
note right: ~200 мс ⏱
S3 --> Browser : logo.svg (3 KB)

note over Browser
  ... ще 15–45 запитів ...
  (шрифти, зображення, JSON-дані)
end note

Browser -> S3 : GET /font.woff2
note right: ~200 мс ⏱
S3 --> Browser : font.woff2 (80 KB)

note over User
  ⏳ Загальний час: 20 запитів × 200 мс = 4 секунди
  53% мобільних користувачів покидають сайт
  якщо завантаження > 3 секунд (дані Google)
end note
@enduml

Рішення: CDN — Content Delivery Network

CDN (Content Delivery Network, Мережа доставки контенту) — це глобальна мережа серверів (їх називають Edge Locations або точки присутності), розподілених по всьому світу. Замість того, щоб всі запити йшли до одного сервера у Франкфурті — CDN зберігає копії ваших файлів на найближчих до користувача серверах.

Аналогія: уявіть мережу супермаркетів. Замість одного центрального складу (Франкфурт) — філії у кожному місті. Купуєте хліб не в центральному складі, а у найближчому магазині за 5 хвилин ходьби. CDN — це той самий принцип, але для веб-даних.

Як це працює крок за кроком:

  1. Ви завантажили React build у S3 (my-app.s3.amazonaws.com) — це Origin (першоджерело)
  2. Підключили CloudFront — він знає де ваш Origin
  3. Японський користувач відкриває ваш сайт → його браузер робить запит до CloudFront
  4. CloudFront перевіряє найближчий Edge Location до Токіо (~3 км від центру міста!)
  5. Якщо файл вже є в кеші токійського edge — повертає одразу (~5 мс!)
  6. Якщо немає — edge забирає файл з Frankfurt Origin (~300 мс), кешує у Токіо, повертає користувачу
  7. Наступні 100 000 японських користувачів отримають той самий файл з токійського edge за ~5 мс
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

database "S3 Bucket\nFrankfurt\n(Origin)" as S3 #fef3c7

cloud "Amazon CloudFront\n(400+ Edge Locations)" as CF #dbeafe

rectangle "Edge\nFrankfurt\n~5ms" as EFR #d1fae5
rectangle "Edge\nTokyo\n~5ms" as ETO #d1fae5
rectangle "Edge\nNew York\n~5ms" as ENY #d1fae5
rectangle "Edge\nSydney\n~5ms" as ESY #d1fae5

actor "🇩🇪 Берлін" as DE
actor "🇯🇵 Токіо" as JP
actor "🇺🇸 Нью-Йорк" as US
actor "🇦🇺 Сідней" as AU

S3 -right-> CF : Контент один раз
CF -down-> EFR
CF -down-> ETO
CF -down-> ENY
CF -down-> ESY

DE --> EFR
JP --> ETO
US --> ENY
AU --> ESY
@enduml

CloudFront — це CDN від Amazon Web Services. Він має 400+ Edge Locations у 90+ містах по всьому світу (станом на 2025 рік). Включно з Варшавою, Бухарестом, Стамбулом — тобто є і поблизу України.

CDN вирішує ще більше проблем

Швидкість доставки — лише одна з переваг CDN. Розглянемо повний спектр проблем, які вирішує CloudFront.

1. Оптимізація вартості трафіку

Передача даних з S3 напряму до кінцевих користувачів (так звана Data Transfer Out) є одним з найдорожчих компонентів рахунку AWS. Трафік з CloudFront Edge Locations коштує в 3–6 разів дешевше, ніж прямий вихідний трафік з S3, — це обумовлено тим, що AWS надає CloudFront пільгові оптові тарифи на передачу даних між власними дата-центрами.

Крім того, кешування на edge радикально зменшує кількість запитів до Origin. Якщо файл закешований на edge з HIT rate 95% — Origin отримує лише 5% запитів, і ви платите за трафік з S3 тільки за ці 5%.

Практичний ефект: великий медіасервіс з трафіком $50 000/міс через CloudFront заплатив би $150 000–300 000/міс при прямих запитах до S3. Тобто CDN буквально окупає себе багаторазово.

2. Захист від DDoS-атак

DDoS (Distributed Denial of Service) — атака, при якій зловмисник генерує мільйони запитів з тисяч заражених пристроїв по всьому світу, щоб перевантажити ваш сервер і зробити його недоступним для реальних користувачів.

CloudFront вбудовано інтегрований з AWS Shield Standard (безкоштовно для всіх CloudFront Distribution), який забезпечує:

  • Розподіл навантаження: атака поглинається одночасно у 400+ edge locations по всьому світу. Замість того щоб 1 000 000 RPS вдарило у ваш один Origin-сервер — вони розподіляються між сотнями точок, кожна з яких обробляє по 2 500 RPS
  • Фільтрацію на рівні мережі: CloudFront автоматично детектує та блокує аномальний трафік на рівнях L3/L4 (IP-флуд, SYN-флуд, UDP-флуд)
  • Ізоляцію Origin: ваш реальний сервер (S3, EC2, ALB) залишається прихованим за CloudFront — його IP-адреса недоступна зловмиснику

Для розширеного захисту (L7 атаки — HTTP-флуд, SQL-ін'єкції через HTTP) підключається AWS WAF (Web Application Firewall) — окремий платний сервіс, що інтегрується з CloudFront.

3. SSL/TLS-термінація на edge

SSL/TLS-термінація — це процес, при якому шифрування HTTPS-з'єднання обробляється не на вашому Origin-сервері, а безпосередньо на найближчому до користувача edge.

Схема роботи:

Браузер ──HTTPS──► Edge Location ──HTTP──► Origin (S3/EC2 всередині AWS)
         (зашифровано)           (відкрито, але всередині захищеної
                                  приватної мережі AWS — безпечно)

Переваги такої архітектури:

  • Зменшення latency: TLS handshake (встановлення шифрованого з'єднання) займає 1–3 RTT. Якщо handshake відбувається на edge за 5 мс від користувача — це набагато швидше, ніж 300 мс до Origin у Франкфурті
  • Зниження навантаження на Origin: крипто-операції (шифрування/дешифрування) потребують CPU. CloudFront знімає цей тягар з вашого сервера
  • Централізоване управління сертифікатами: AWS Certificate Manager (ACM) автоматично поновлює SSL-сертифікати — ніяких ручних renewals

4. Автоматичне стиснення контенту

CloudFront автоматично стискає текстові ресурси перед відправкою клієнту, якщо увімкнено параметр Compress objects automatically.

Підтримувані алгоритми:

  • Gzip — класичний алгоритм, підтримується всіма браузерами з 1990-х. Стискає текст у 5–10 разів
  • Brotli — сучасніший алгоритм від Google (2015), дає на 15–25% кращий рівень стиснення порівняно з gzip для HTML/JS/CSS. CloudFront обирає brotli якщо браузер надсилає заголовок Accept-Encoding: br

Що стискається: HTML, CSS, JavaScript, JSON, XML, SVG, текстові файли (text/*, application/json, application/javascript).

Що не стискається: зображення (JPEG, PNG, WebP), відео, PDF — вони вже стиснені власними алгоритмами, повторне стиснення лише збільшить CPU-навантаження без виграшу у розмірі.

5. HTTP/2 та HTTP/3 за замовчуванням

CloudFront підтримує сучасні протоколи HTTP/2 та HTTP/3 (QUIC) без жодної додаткової конфігурації:

  • HTTP/2 (2015): мультиплексування — всі ресурси сторінки (HTML, JS, CSS, шрифти) передаються в одному TCP-з'єднанні паралельно, без black-of-head-of-line blocking властивого HTTP/1.1
  • HTTP/3 (2022): базується на протоколі QUIC поверх UDP замість TCP. Критично швидший при нестабільному з'єднанні (мобільний інтернет, втрата пакетів) — повторна передача відбувається лише для конкретного стриму, а не всього з'єднання

6. Географічне обмеження доступу (Geo Restriction)

CloudFront дозволяє блокувати або дозволяти доступ до контенту на основі країни користувача (визначається за IP-адресою через GeoIP базу даних).

Приклади застосування:

  • Стримінгові сервіси: блокування контенту в країнах, де немає ліцензії (геолокаційні обмеження для відео)
  • Відповідність законодавству: GDPR вимагає певних обмежень для ЄС, а різні юрисдикції мають різні вимоги до даних
  • Безпека: блокування країн, звідки надходить найбільше атак

Як CloudFront кешує дані

CloudFront — це HTTP-проксі з кешем. Він повністю дотримується стандарту HTTP/1.1 (RFC 7234) щодо кешування: рішення про те, кешувати відповідь чи ні, приймається на основі HTTP-заголовків, які повертає ваш Origin-сервер.

HTTP-заголовки кешування

Коли edge отримує відповідь від Origin, він аналізує заголовок Cache-Control і вирішує:

ДирективаЗначенняПоведінка CloudFront
max-age=Nкешувати N секундзберігає копію на N секунд
s-maxage=Nспеціально для проксі-кешівCloudFront використовує s-maxage замість max-age якщо обидва присутні
no-cacheне використовувати кеш без ревалідаціїCloudFront робить умовний запит до Origin (If-None-Match, If-Modified-Since)
no-storeне зберігати взагаліCloudFront не кешує, завжди звертається до Origin
privateлише для браузера, не для проксіCloudFront не кешує відповіді з Cache-Control: private

TTL (Time To Live) — час, протягом якого кешована копія вважається актуальною. Після закінчення TTL edge робить новий запит до Origin.

Cache Miss та Cache Hit: послідовність подій

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam sequence {
    ArrowColor #374151
    ActorBorderColor #374151
    ActorBackgroundColor #f9fafb
    ParticipantBorderColor #374151
    ParticipantBackgroundColor #f9fafb
    NoteBackgroundColor #dbeafe
    NoteBorderColor #3b82f6
    LifeLineBorderColor #9ca3af
}

title CloudFront: Cache Miss -> Cache Hit

actor "Браузер" as B
participant "CloudFront\nEdge (Токіо)" as CF
participant "Origin\n(S3 / ALB)" as O

== Перший запит: Cache Miss ==

B -> CF : GET /main.abc123.js
note right of CF: Перевіряє кеш\n<b>MISS</b> — файл відсутній

CF -> O : GET /main.abc123.js
O --> CF : 200 OK\nCache-Control: max-age=31536000\n[тіло відповіді: 180 KB]
note right of CF: Зберігає у кеш\nна 365 днів
CF --> B : 200 OK\nx-cache: Miss from cloudfront\n[тіло відповіді: 180 KB]

== Наступні запити: Cache Hit ==

B -> CF : GET /main.abc123.js
note right of CF: Перевіряє кеш\n<b>HIT</b> — є актуальна копія
CF --> B : 200 OK\nx-cache: Hit from cloudfront\n[тіло відповіді: 180 KB]\n<b>Origin не турбується!</b>

B -> CF : GET /main.abc123.js
CF --> B : 200 OK\nx-cache: Hit from cloudfront

note over CF : TTL закінчився через 365 днів

== TTL протух: Revalidation ==

B -> CF : GET /main.abc123.js
note right of CF: Кеш застарів,\nпотрібна реваліція

CF -> O : GET /main.abc123.js\nIf-None-Match: "abc123etag"
O --> CF : 304 Not Modified\n(тіло порожнє — файл не змінився)
note right of CF: Оновлює TTL,\nзберігає стару копію
CF --> B : 200 OK\nx-cache: Hit from cloudfront
@enduml

Заголовки відповіді CloudFront

Кожна відповідь від CloudFront містить діагностичні заголовки:

HTTP/2 200
x-cache: Hit from cloudfront          ← HIT або Miss from cloudfront
x-amz-cf-pop: NRT12-C2               ← Edge location (NRT = Narita, Tokyo)
x-amz-cf-id: abc123...               ← Унікальний ID запиту для дебагу
age: 3847                             ← Скільки секунд файл у кеші
cache-control: max-age=31536000       ← Заголовок від Origin (прокидається клієнту)

Заголовок age дозволяє дізнатись «вік» кешованої копії: якщо max-age=86400 і age=3600 — до закінчення TTL ще 82 800 секунд (23 години).

Стратегії Cache-Control для різних типів контенту

Правильна стратегія кешування залежить від типу ресурсу та частоти його зміни.

React SPA (Vite / Create React App)

Vite та CRA автоматично додають content hash до імен файлів при production build: main.a1b2c3d4.js. Якщо вміст файлу змінився — змінюється і hash, тобто URL стає іншим. Це дозволяє кешувати JS/CSS назавжди.

# Стратегія деплою React на S3:

# index.html — ніколи не кешувати (він містить посилання на актуальні hash-файли)
aws s3 cp ./dist/index.html s3://my-bucket/index.html \
    --cache-control "no-cache, no-store, must-revalidate" \
    --content-type "text/html; charset=utf-8"

# JS/CSS з hash у назві — кешувати назавжди (immutable = "цей файл ніколи не змінюється")
aws s3 sync ./dist/assets s3://my-bucket/assets \
    --cache-control "public, max-age=31536000, immutable"

# Статичні asset без hash (favicon, robots.txt) — кешувати добу
aws s3 cp ./dist/favicon.ico s3://my-bucket/favicon.ico \
    --cache-control "public, max-age=86400"

ASP.NET Core — статичні файли (wwwroot)

У ASP.NET Core middleware UseStaticFiles() автоматично виставляє заголовки кешування. Можна налаштувати:

// Program.cs
app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        var path = ctx.File.Name;
        var headers = ctx.Context.Response.Headers;

        if (path.EndsWith(".js") || path.EndsWith(".css"))
        {
            // Файли з hash у назві (Vite/webpack output) — кеш на рік
            if (path.Contains('.') && path.Split('.').Length > 2)
            {
                headers.CacheControl = "public, max-age=31536000, immutable";
            }
            else
            {
                headers.CacheControl = "public, max-age=3600"; // 1 година
            }
        }
        else if (path.EndsWith(".html"))
        {
            headers.CacheControl = "no-cache, no-store, must-revalidate";
        }
        else
        {
            headers.CacheControl = "public, max-age=86400"; // 24 години
        }
    }
});

ASP.NET Core — API відповіді

Для API-ендпоінтів CloudFront може кешувати лише GET/HEAD запити з явним Cache-Control. За замовчуванням ASP.NET Core не виставляє Cache-Control — тому CloudFront не кешує API-відповіді автоматично.

// Кешований GET-ендпоінт (список категорій — змінюється рідко)
[HttpGet("categories")]
[ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any, VaryByHeader = "Accept-Language")]
public IActionResult GetCategories()
{
    // Cache-Control: public, max-age=300, vary: Accept-Language
    return Ok(_categoryService.GetAll());
}

// НЕ кешований ендпоінт (персональні дані користувача)
[HttpGet("profile")]
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
public IActionResult GetProfile()
{
    // Cache-Control: no-store
    return Ok(_userService.GetCurrentUser(User));
}
Важливо: ніколи не кешуйте на CloudFront відповіді, що містять персональні дані (JWT-токени, профілі, кошики покупок). Якщо Cache-Control: public, max-age=60 і CloudFront закешував відповідь з даними Аліси — наступний користувач отримає дані Аліси. Завжди використовуйте Cache-Control: private або no-store для персоналізованого контенту.

Порівняльна таблиця стратегій

Тип ресурсуПрикладиРекомендований Cache-Control
HTML-оболонка SPAindex.htmlno-cache, no-store
JS/CSS з content hashmain.a1b2c3.jspublic, max-age=31536000, immutable
Зображення (статичні)logo.png, hero.webppublic, max-age=2592000 (30 днів)
ШрифтиInter.woff2public, max-age=31536000, immutable
API — довідникиGET /api/countriespublic, max-age=3600, s-maxage=86400
API — публічний контентGET /api/articles/123public, max-age=60
API — персональнийGET /api/profileprivate, no-store
API — мутаціїPOST /api/ordersno-store

CloudFront Distributions

Distribution — це центральний об'єкт конфігурації CloudFront, який описує як саме CloudFront обслуговує ваш застосунок: звідки брати контент, як його кешувати, який домен та SSL-сертифікат використовувати.

Аналогія: якщо CloudFront — це глобальна служба доставки, то Distribution — це конкретний договір на доставку для одного вашого застосунку. У вас може бути кілька Distribution: один для production сайту, інший для staging, третій для API.

Після створення Distribution отримує:

  • Унікальний домен вигляду d1234abcd.cloudfront.net — автоматично, безкоштовно
  • Distribution ID (наприклад EDFDVBD6EXAMPLE) — ідентифікатор для CLI/API операцій
  • ARN — для IAM-політик та моніторингу

Distribution складається з чотирьох ключових концепцій: Origins, Cache Behaviors, Edge Locations та SSL/Domain.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam defaultTextAlignment center

package "CloudFront Distribution\nd1234abcd.cloudfront.net" as DIST #dbeafe {

    package "Origins (звідки брати контент)" as ORIGINS #fef3c7 {
        component "S3 Bucket\n(статика, SPA)" as S3O
        component "ALB\n(.NET API)" as ALBO
        component "Custom HTTP\n(будь-який сервер)" as CUSTO
    }

    package "Cache Behaviors (правила маршрутизації)" as BEHAVIORS #d1fae5 {
        component "/api/* → ALB\nno-cache" as B1
        component "/static/* → S3\nmax-age=31536000" as B2
        component "/* (default) → S3\nmax-age=86400" as B3
    }

    package "Edge Locations\n(400+ по всьому світу)" as EDGES #f3e8ff {
        component "Edge\nТокіо" as ET
        component "Edge\nФранкфурт" as EF
        component "Edge\nНью-Йорк" as EN
    }

    ORIGINS --> BEHAVIORS : Origin вказується\nв кожному Behavior
    BEHAVIORS --> EDGES : Кешований контент\nрозповсюджується на edge
}

actor "Користувач" as U
U --> DIST : HTTPS запит
@enduml

Origins — звідки брати контент

Origin — це сервер або сховище, який є першоджерелом контенту. CloudFront звертається до Origin лише при cache miss — тобто коли потрібного файлу ще немає в кеші найближчого edge або його TTL закінчився. Для всіх інших запитів CloudFront відповідає безпосередньо з edge-кешу, Origin при цьому не навантажується.

Один Distribution може мати кілька Origins — наприклад, S3 для статики і ALB для API.

S3 Origin з OAC (рекомендовано)

Найпоширеніший сценарій: React SPA або статичний сайт зберігається у S3, а CloudFront роздає його по всьому світу.

Проблема безпеки: якщо S3 bucket публічний — будь-хто може звернутись до нього напряму (обійшовши CloudFront і весь захист від DDoS, WAF тощо). Вирішення — OAC (Origin Access Control).

OAC дозволяє зробити S3 bucket повністю приватним (Block Public Access увімкнений), але дозволити CloudFront читати файли. Технічно: CloudFront підписує кожен запит до S3 власним ключем за протоколом AWS Signature Version 4 (SigV4). S3 перевіряє підпис і дозволяє запит тільки якщо він прийшов від вашого конкретного CloudFront Distribution.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam sequence {
    ArrowColor #374151
    ParticipantBorderColor #374151
    ParticipantBackgroundColor #f9fafb
    NoteBackgroundColor #dcfce7
    NoteBorderColor #16a34a
    LifeLineBorderColor #9ca3af
}

title S3 + OAC: приватний bucket, доступ тільки через CloudFront

actor "Зловмисник" as Hacker #fca5a5
actor "Користувач" as User
participant "CloudFront\nEdge" as CF
participant "S3 Bucket\n(Block Public Access: ON)" as S3

== Пряме звернення до S3 (заблоковано) ==

Hacker -> S3 : GET s3.amazonaws.com/my-bucket/index.html
S3 --> Hacker : 403 Forbidden\n(bucket приватний!)

== Запит через CloudFront (дозволено) ==

User -> CF : GET d1234abcd.cloudfront.net/index.html
CF -> S3 : GET /index.html\nAuthorization: AWS4-HMAC-SHA256 ...\n(SigV4 підпис від CloudFront OAC)
note right of S3: Перевіряє підпис:\n- чи від CloudFront?\n- чи від ТОГО Distribution?\n✅ Дозволяє
S3 --> CF : 200 OK + файл
CF --> User : 200 OK + файл\n(кешує на edge)
@enduml

Bucket Policy при OAC виглядає так:

{
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": { "Service": "cloudfront.amazonaws.com" },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-app-bucket/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
                }
            }
        }
    ]
}

Умова AWS:SourceArn — це найважливіша частина: навіть інші CloudFront Distribution (наприклад, конкурентів) не зможуть читати ваш bucket. Дозволений лише конкретний Distribution.

ALB Origin

CloudFront ставиться перед Application Load Balancer — типово для архітектури, де є бекенд (.NET API, Node.js). ALB розподіляє трафік між EC2-інстансами або контейнерами.

Навіщо CloudFront перед ALB?

  • Кешування GET-відповідей API (публічні дані: каталоги, статті)
  • SSL-термінація на edge замість на ALB (швидше для далеких користувачів)
  • DDoS-захист: мільйони запитів поглинаються на edge, до ALB доходить нормальний трафік
  • Єдиний домен для SPA і API: app.example.com/ → S3, app.example.com/api/ → ALB

Custom HTTP Origin

Будь-який HTTP/HTTPS сервер — сервер у вашому офісі (on-premises), VPS, або навіть інший хмарний провайдер. CloudFront виступає як глобальний CDN-акселератор перед будь-яким HTTP-сервером незалежно від місця розташування.


Origin Groups та Origin Failover — висока доступність

Origin Group — це логічне об'єднання двох Origins: primary (основний) та secondary (резервний). Якщо primary Origin повертає помилку, CloudFront автоматично повторює запит до secondary — без участі клієнта, прозоро.

Коли спрацьовує failover? За замовчуванням CloudFront перемикається при таких HTTP статусах від primary Origin: 500, 502, 503, 504. Ви можете налаштувати конкретний список кодів — наприклад, додати 403 якщо хочете перемикатись і при проблемах з авторизацією.

Типовий сценарій: S3 bucket у eu-central-1 (primary) + S3 bucket у us-east-1 (secondary) з увімкненою Cross-Region Replication. AWS автоматично реплікує об'єкти між bucket'ами. При регіональному збої CloudFront починає роздавати контент з резервного регіону.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam sequence {
    ArrowColor #374151
    ParticipantBorderColor #374151
    ParticipantBackgroundColor #f9fafb
    NoteBackgroundColor #dcfce7
    NoteBorderColor #16a34a
    LifeLineBorderColor #9ca3af
}

title Origin Failover: автоматичне перемикання при збої

actor "Користувач" as U
participant "CloudFront\nEdge" as CF
participant "S3 Primary\n(eu-central-1)" as S3P
participant "S3 Secondary\n(us-east-1)" as S3S

== Нормальна робота ==

U -> CF : GET /index.html
CF -> S3P : GET /index.html
S3P --> CF : 200 OK
CF --> U : 200 OK ✅

== Збій primary Origin ==

U -> CF : GET /index.html
CF -> S3P : GET /index.html
S3P --> CF : 503 Service Unavailable

note over CF #fee2e2
  Primary повернув 503 (у списку failover кодів)
  → автоматично звертаємось до secondary
end note

CF -> S3S : GET /index.html (повторний запит)
S3S --> CF : 200 OK
CF --> U : 200 OK ✅\n(користувач навіть не помітив)
@enduml

Origin Group vs Route 53 Failover: Route 53 failover перемикає DNS-записи (займає час на TTL). Origin Group перемикається на рівні CloudFront — миттєво, у межах одного HTTP-запиту клієнта. Для статики Origin Group є кращим варіантом.

Обмеження: failover спрацьовує тільки для GET та HEAD запитів. POST, PUT, DELETE CloudFront не повторює автоматично — небезпечно (мутації не можна безпечно повторювати).


Cache Behaviors — правила для різних URL-шляхів

Cache Behavior — це правило, яке визначає: для запитів на певний URL-шлях — до якого Origin звертатись і як кешувати відповідь.

Проблема, яку вирішує: у реальному застосунку різний контент має абсолютно різні вимоги до кешування:

  • Статичні JS/CSS — кешувати рік
  • API /api/articles — кешувати 60 секунд
  • API /api/profile — не кешувати взагалі (персональні дані!)
  • API POST /api/orders — не кешувати (мутація)

Один Distribution може мати багато Behaviors. CloudFront перебирає їх зверху вниз і застосовує перший що підходить. Default behavior (*) — завжди останній, спрацьовує якщо жодне інше правило не підійшло.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title Cache Behaviors: маршрутизація запитів в одному Distribution

rectangle "CloudFront Distribution\napp.example.com" as CF #dbeafe {
    rectangle "Behavior 1\nPath: /api/*\nOrigin: ALB\nCache: no-store\nMethods: GET,POST,PUT,DELETE" as B1 #fee2e2
    rectangle "Behavior 2\nPath: /static/*\nOrigin: S3\nCache: max-age=31536000\nMethods: GET,HEAD" as B2 #dcfce7
    rectangle "Behavior 3 (default)\nPath: /*\nOrigin: S3\nCache: no-cache\nMethods: GET,HEAD" as B3 #fef9c3
}

component "ALB\n(.NET API\nEC2)" as ALB #fee2e2
component "S3 Bucket\n(React SPA\nстатика)" as S3 #dcfce7

B1 --> ALB
B2 --> S3
B3 --> S3

actor "GET /api/users" as R1
actor "GET /static/main.abc.js" as R2
actor "GET /" as R3
actor "GET /about" as R4

R1 --> B1
R2 --> B2
R3 --> B3
R4 --> B3
@enduml

Практичний приклад для React SPA + .NET API:

Path PatternOriginCache-ControlAllowed MethodsCompress
/api/*ALBno-store (з Origin)GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONSNo
/static/*S3max-age=31536000, immutableGET, HEADYes
/* (default)S3no-cache (index.html)GET, HEADYes

Завдяки цьому app.example.com і app.example.com/api/usersодин домен, один SSL-сертифікат, але принципово різна логіка обробки.


Cache Keys та Cache Policies — що саме кешується

Cache Key — це унікальний ідентифікатор, за яким CloudFront шукає і зберігає запис у кеші. За замовчуванням cache key = лише URL path: запити GET /products і GET /products потраплять в один кеш-запис незалежно від будь-яких заголовків, cookies чи query string.

Це може бути як правильно, так і катастрофічно — залежить від ситуації.

Сценарій 1 — все добре: ви роздаєте публічний список продуктів. URL /api/products дає однакову відповідь для всіх. Cache key = тільки URL — ідеально, high cache hit rate.

Сценарій 2 — катастрофа: ваш /api/products повертає різний контент залежно від cookie region=UA або region=DE. Якщо CloudFront не включає cookie у cache key — перший запит від українського користувача закешовується, і всі наступні (включно з німецькими) отримують кеш для України. Це критичний баг.

Cache Policy

Cache Policy — це окремий об'єкт конфігурації, що прикріплюється до Cache Behavior і визначає три речі:

  1. Що входить у cache key — query string, cookies, заголовки
  2. TTL — мінімальний, максимальний та дефолтний TTL
  3. Стиснення — gzip/brotli

CloudFront має набір Managed Cache Policies, що покривають більшість сценаріїв без ручного налаштування:

Managed PolicyCache KeyTTLВикористання
CachingOptimizedтільки URL1 рікСтатика з hash-іменами
CachingDisabled0API без кешу
CachingOptimizedForUncompressedObjectsтільки URL1 рікФайли що вже стиснені (zip, mp4)
UseOriginCacheControlHeadersтільки URLз OriginДовіряємо Cache-Control з Origin

Origin Request Policy

Важливо розрізняти: що в cache keyщо CloudFront надсилає до Origin.

Cache Policy визначає cache key. Origin Request Policy — що CloudFront додає до запиту при зверненні до Origin (наприклад, передати Accept-Language заголовок до Origin, але не включати його у cache key). Це незалежні налаштування.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam sequence {
    ArrowColor #374151
    ParticipantBorderColor #374151
    ParticipantBackgroundColor #f9fafb
    NoteBackgroundColor #fef3c7
    NoteBorderColor #d97706
    LifeLineBorderColor #9ca3af
}

title Cache Key: що входить і що не входить

actor "Користувач A\nregion=UA" as UA
actor "Користувач B\nregion=DE" as DE
participant "CloudFront\nEdge" as CF
participant "Origin\n(API)" as O

== Cache Policy: лише URL (помилка!) ==

UA -> CF : GET /products\nCookie: region=UA
note right of CF #fee2e2: Cache key = /products\nShukає у кеші...
CF -> O : GET /products (Cache MISS)
O --> CF : Список для UA 🇺🇦
CF --> UA : Список для UA ✅ (кешує як "/products")

DE -> CF : GET /products\nCookie: region=DE
note right of CF #fee2e2: Cache key = /products\nКЕШ ЗНАЙДЕНО!
CF --> DE : Список для UA 🇺🇦 ❌ (НЕПРАВИЛЬНО!)

== Cache Policy: URL + Cookie "region" (правильно) ==

UA -> CF : GET /products\nCookie: region=UA
note right of CF #dcfce7: Cache key = /products?cookie:region=UA\nShukає у кеші...
CF -> O : GET /products (Cache MISS)
O --> CF : Список для UA 🇺🇦
CF --> UA : Список для UA ✅

DE -> CF : GET /products\nCookie: region=DE
note right of CF #dcfce7: Cache key = /products?cookie:region=DE\nВідмінний ключ!
CF -> O : GET /products (Cache MISS)
O --> CF : Список для DE 🇩🇪
CF --> DE : Список для DE ✅
@enduml
Правило безпеки: якщо ваш Origin повертає різний контент залежно від cookie авторизації (session_id, jwt) — обов'язково або включіть ці cookies у cache key, або вимкніть кешування (CachingDisabled) для цього Behavior. Інакше один користувач може побачити дані іншого.

CloudFront Functions та Lambda@Edge — код на edge

CloudFront дозволяє виконувати JavaScript прямо на edge-серверах — до того як запит дійшов до Origin, або до того як відповідь пішла до браузера. Це дозволяє трансформувати запити/відповіді без звернення до вашого сервера.

Є чотири точки, де можна "вставити" свою логіку:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam sequence {
    ArrowColor #374151
    ParticipantBorderColor #374151
    ParticipantBackgroundColor #f9fafb
    NoteBackgroundColor #f3e8ff
    NoteBorderColor #9333ea
    LifeLineBorderColor #9ca3af
}

title Точки виконання CloudFront Functions та Lambda@Edge

actor "Браузер" as B
participant "CloudFront\nEdge" as CF
participant "Origin\n(S3 / ALB)" as O

B -> CF : HTTP запит

note over CF #f3e8ff
  1. <b>Viewer Request</b>
  CloudFront Functions / Lambda@Edge
  Виконується на кожному запиті
  (до перевірки кешу!)
  Приклад: URL rewrite, A/B тест,
  перевірка JWT токену
end note

CF -> CF : Перевірка кешу

alt Cache HIT
    CF --> B : Відповідь з кешу
else Cache MISS
    note over CF #f3e8ff
      2. <b>Origin Request</b>
      Lambda@Edge тільки
      Виконується перед запитом до Origin
      Приклад: підстановка заголовків,
      вибір Origin динамічно
    end note

    CF -> O : Запит до Origin
    O --> CF : Відповідь

    note over CF #f3e8ff
      3. <b>Origin Response</b>
      Lambda@Edge тільки
      Виконується після відповіді Origin
      Приклад: модифікація заголовків,
      A/B тест на рівні контенту
    end note

    CF -> CF : Зберігає у кеш
end

note over CF #f3e8ff
  4. <b>Viewer Response</b>
  CloudFront Functions / Lambda@Edge
  Виконується перед відправкою клієнту
  Приклад: додавання security headers
end note

CF --> B : HTTP відповідь
@enduml

CloudFront Functions vs Lambda@Edge: коли що

ХарактеристикаCloudFront FunctionsLambda@Edge
Де виконуєтьсяНа всіх 400+ edge locationsУ ~13 регіональних кешах
Час виконання< 1 мс (жорсткий ліміт)до 5 с (origin req) / 30 с (origin resp)
Мережеві запити❌ Неможливо✅ Можна (HTTP виклики до API)
Доступ до body❌ Тільки заголовки та URI✅ Повний доступ
МоваJavaScript (ES5.1)Node.js або Python
Ціна$0.10 / 1M запитів$0.60 / 1M запитів
Коли використовуватиURL rewrite, redirect, security headersАутентифікація, A/B з зовнішнім API, трансформація body

Приклад 1 — CloudFront Function: index.html fallback для React Router

React Router обробляє маршрути клієнтсайд (/about, /users/123). Але якщо CloudFront отримує запит GET /about — він шукає у S3 файл з назвою about, не знаходить і повертає 403. Потрібно переписати всі SPA-маршрути на /index.html:

function handler(event) {
    var request = event.request
    var uri = request.uri

    // Якщо URI має розширення файлу (.js, .css, .png) — віддаємо як є
    // Якщо URI без розширення — це SPA-маршрут, перенаправляємо на index.html
    if (!uri.includes('.')) {
        request.uri = '/index.html'
    }

    return request
}

Приклад 2 — CloudFront Function: security headers у відповіді

Додавання HTTP security headers до кожної відповіді без змін у backend:

function handler(event) {
    var response = event.response
    var headers = response.headers

    // Захист від clickjacking
    headers['x-frame-options'] = { value: 'DENY' }
    // Захист від MIME-sniffing
    headers['x-content-type-options'] = { value: 'nosniff' }
    // HSTS: змушує браузер завжди використовувати HTTPS
    headers['strict-transport-security'] = {
        value: 'max-age=63072000; includeSubDomains; preload',
    }
    // Content Security Policy
    headers['content-security-policy'] = {
        value: "default-src 'self'; script-src 'self'",
    }

    return response
}

Cache Invalidation — примусове оновлення кешу

Уявіть ситуацію: ви задеплоїли нову версію сайту. Файл index.html у S3 оновлено. Але CloudFront закешував старий index.html з TTL 24 години — і ще 23 години роздаватиме стару версію всім користувачам у світі.

Cache Invalidation — це команда CloudFront негайно викинути вказані файли з кешу всіх edge locations. При наступному запиті edge буде змушений завантажити свіжу версію з Origin.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff
skinparam sequence {
    ArrowColor #374151
    ParticipantBorderColor #374151
    ParticipantBackgroundColor #f9fafb
    NoteBackgroundColor #fee2e2
    NoteBorderColor #dc2626
    LifeLineBorderColor #9ca3af
}

title Cache Invalidation: проблема та вирішення

participant "CI/CD\n(GitHub Actions)" as CI
participant "S3 Bucket" as S3
participant "CloudFront\n(control plane)" as CF_CP
participant "Edge Токіо" as ET
participant "Edge Франкфурт" as EF
actor "Користувач" as U

CI -> S3 : aws s3 sync ./dist (нова версія)
S3 --> CI : OK

note over ET, EF #fee2e2
  Проблема: обидва edge ще мають
  старий index.html у кеші (TTL: 23 год)
end note

CI -> CF_CP : aws cloudfront create-invalidation\n--paths "/index.html"
CF_CP -> ET : Invalidate /index.html
CF_CP -> EF : Invalidate /index.html
ET --> CF_CP : Done
EF --> CF_CP : Done
CF_CP --> CI : Invalidation created\n(propagates ~30 sec)

U -> ET : GET /index.html
note right of ET: Кеш порожній (invalidated)\nCache MISS
ET -> S3 : GET /index.html
S3 --> ET : 200 OK (нова версія)
ET --> U : 200 OK (нова версія) ✅
@enduml
# Інвалідувати тільки index.html (оптимальний підхід для React SPA)
aws cloudfront create-invalidation \
    --distribution-id EDFDVBD6EXAMPLE \
    --paths "/index.html" \
    --region us-east-1

# Інвалідувати кілька файлів
aws cloudfront create-invalidation \
    --distribution-id EDFDVBD6EXAMPLE \
    --paths "/index.html" "/asset-manifest.json" "/robots.txt" \
    --region us-east-1

# Інвалідувати весь кеш (/* = один path у рахунку)
aws cloudfront create-invalidation \
    --distribution-id EDFDVBD6EXAMPLE \
    --paths "/*" \
    --region us-east-1

Чому React SPA потребує інвалідації лише index.html?

Vite та CRA генерують JS/CSS файли з content hash у назві: main.a1b2c3d4.js. Hash — це відбиток вмісту файлу. Якщо вміст змінився — змінюється hash, отже й URL (main.a1b2c3d4.jsmain.e5f6g7h8.js). Новий URL CloudFront ще ніколи не бачив — отже одразу піде за ним до S3. Старий файл у кеші так і залишиться, але до нього вже ніхто не звернеться (бо index.html вказує на новий URL).

Тому достатньо інвалідувати тільки index.html — він вказує на актуальні версії всіх файлів.

Вартість: перші 1 000 invalidation paths на місяць безкоштовно. Далі $0.005 за кожен path. /* вважається одним path незалежно від кількості файлів у кеші — тому /* завжди вигідніший за перерахування 500 файлів окремо.

Versioning vs Invalidation — стратегії оновлення контенту

Є дві принципово різні стратегії, що забезпечують доставку актуального контенту після деплою:

Стратегія 1 — Invalidation (явне очищення кешу): ви залишаєте ті самі URL, але після кожного деплою виконуєте create-invalidation, щоб CloudFront забув старі копії.

Стратегія 2 — File Versioning (іменовані версії): ви змінюєте URL файлу при кожній зміні вмісту. Старий URL нікуди не зникає — він просто більше не використовується. Новий URL CloudFront ще не бачив → автоматично завантажить з Origin.

Versioning буває двох форм:

  • Content hash: bundle.a1b2c3.js — hash залежить від вмісту. Якщо файл не змінився — hash той самий, URL той самий, кеш не стає недійсним. Якщо змінився — новий hash, новий URL.
  • Explicit version: bundle.v2.js або /api/v2/products — ручне версіонування.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title Versioning vs Invalidation: порівняння підходів

rectangle "Після деплою нового bundle.js" as TITLE #f3f4f6

package "Стратегія 1: Invalidation\n(один і той самий URL)" as INV #fee2e2 {
    rectangle "S3: /static/bundle.js\n(новий вміст)" as S1
    rectangle "CloudFront: кеш /static/bundle.js\n(ще старий!)" as C1
    rectangle "create-invalidation → кеш очищено\nНаступний запит → Origin" as I1
    S1 --> C1
    C1 --> I1
}

package "Стратегія 2: Content Hash\n(новий URL = нова версія)" as HASH #dcfce7 {
    rectangle "S3: /static/bundle.a1b2.js (старий)\n     /static/bundle.e5f6.js (новий)" as S2
    rectangle "CloudFront: кеш /static/bundle.e5f6.js\n= порожній, піде до Origin" as C2
    rectangle "Старий кеш /static/bundle.a1b2.js\nзалишається, але ніхто не звертається" as O2
    S2 --> C2
    S2 --> O2
}
@enduml

Порівняння стратегій

КритерійInvalidationContent Hash Versioning
Гарантія оновленняТак, але ~30 сек propagationМиттєво (новий URL)
ВартістьПерші 1000 безкоштовно, далі $0.005/pathБезкоштовно
Складність деплоюcreate-invalidation командаВбудовано у build інструменти (Vite, webpack)
Якщо щось пішло не такЛегкий rollback: видати старий файлСкладніший: потрібно оновити посилання
Підходить дляindex.html, robots.txt, будь-які файли без hashJS, CSS, зображення — усе що проходить build
TTL у Cache-ControlЗазвичай no-cache або короткийmax-age=31536000, immutable (рік)

Рекомендована гібридна стратегія (React / Vite)

Обидва підходи не виключають один одного — навпаки, правильна архітектура поєднує їх:

ФайлСтратегіяCache-ControlПояснення
index.htmlInvalidationno-cacheТочка входу, завжди повинна бути свіжою
asset-manifest.jsonInvalidationno-cacheСписок актуальних хешованих файлів
static/js/*.abc123.jsContent Hashmax-age=31536000, immutableHash у назві = нова назва при змінах
static/css/*.def456.cssContent Hashmax-age=31536000, immutableАналогічно
favicon.ico, зображення без hashInvalidation або короткий TTLmax-age=86400Оновлюються рідко

Логіка: index.html завжди свіжий (invalidation або no-cache) → він посилається на актуальні хешовані JS/CSS → ті закешовані на рік → CloudFront витрачає bandwidth лише на дійсно змінені файли.


Практичний приклад: React SPA на S3 + CloudFront + HTTPS від А до Я

Передумови

Цей практичний приклад є продовженням Практичного прикладу з Модуля 6 — Amazon S3. Якщо ви ще не проходили той модуль — поверніться до розділу «Практичний приклад: React SPA на S3 від А до Я» та виконайте всі кроки. Ми продовжуємо роботу з тим самим React застосунком та bucket my-react-app-2024.

Ключова архітектурна відмінність: у Модулі 6 bucket був публічним — увімкнений S3 Static Website Hosting, відкрита Bucket Policy. У цьому прикладі переходимо на приватну архітектуру з CloudFront + OAC — безпечніший production-підхід. Крок 1 цілком присвячений цьому переходу.

Переконайтесь:

  • AWS CLI встановлений та налаштований (aws configure)
Важливо про регіони та CloudFront! ACM сертифікат для CloudFront ОБОВ'ЯЗКОВО повинен бути створений у регіоні us-east-1 (US East, N. Virginia), навіть якщо ваш S3 та решта інфраструктури у eu-central-1. Це глобальна вимога AWS — якщо сертифікат в іншому регіоні, він просто не з'явиться у списку при налаштуванні Distribution.

Крок 1: Підготовка S3 bucket — перехід з публічного на приватний

У Модулі 6 bucket був публічним: увімкнено S3 Static Website Hosting, Bucket Policy дозволяла s3:GetObject для "Principal": "*". Для CloudFront + OAC це потрібно повністю прибрати — bucket має бути закритим, доступ тільки через CloudFront SigV4-підписаними запитами.

Три обов'язкові дії:

BUCKET="my-react-app-2024"

# 1. Вимкнути S3 Static Website Hosting (більше не потрібен — CloudFront сам роздає файли)
aws s3api delete-bucket-website \
    --bucket $BUCKET \
    --region eu-central-1

# 2. Увімкнути Block Public Access
aws s3api put-public-access-block \
    --bucket $BUCKET \
    --public-access-block-configuration \
        "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" \
    --region eu-central-1

# 3. Видалити публічну Bucket Policy (яка була для Static Website Hosting)
aws s3api delete-bucket-policy --bucket $BUCKET --region eu-central-1

echo "Bucket $BUCKET тепер приватний. Перевіряємо файли..."
aws s3 ls s3://$BUCKET/ --region eu-central-1
Перевірка файлів у bucket
$ aws s3 ls s3://my-react-app-2024/
PRE static/
2024-01-15 10:30:00 1024 index.html
2024-01-15 10:30:00 3442 favicon.ico
2024-01-15 10:30:00 492 asset-manifest.json
2024-01-15 10:30:00 671 robots.txt

Після цих дій стара адреса http://my-react-app-2024.s3-website.eu-central-1.amazonaws.com/ повертатиме 403 Forbidden — це очікувана поведінка. Прямий доступ до S3 більше не працює, і це правильно.

Якщо aws s3 ls нічого не виводить — bucket порожній. Поверніться до Модуля 6 і виконайте npm run build && aws s3 sync ./build s3://my-react-app-2024/ --delete --region eu-central-1, потім повертайтесь сюди.

Крок 2: Створення CloudFront Distribution

  1. Відкрийте CloudFront у AWS Console
  2. Натисніть Create distribution

Origin settings:

  • Origin domain: у dropdown оберіть ваш S3 bucket my-react-app-2024.s3.eu-central-1.amazonaws.com
  • Origin access: оберіть Origin access control settings (recommended) (це OAC, не OAI!)
  • Натисніть Create new OAC → Name: my-react-app-oacCreate
  • AWS покаже жовте попередження «You must update the S3 bucket policy» — це нормально, зробимо це пізніше

Default cache behavior:

  • Viewer protocol policy: Redirect HTTP to HTTPS (автоматично перенаправляти з http на https)
  • Allowed HTTP methods: GET, HEAD
  • Compress objects automatically: Yes

Settings:

  • Price class: Use only North America, Europe, Asia, Middle East, and Africa (дешевше за All Edge Locations — ваша аудиторія переважно в цих регіонах)
  • Alternate domain names (CNAMEs): залиште порожнім поки що (додамо пізніше після отримання сертифіката)
  • Custom SSL certificate: залиште Default CloudFront certificate поки що
  • Default root object: index.html (що повертати при запиті /)
  1. Натисніть Create distribution
  2. Зачекайте 5–15 хвилин поки Distribution розгорнеться на всіх edge locations. Статус зміниться з In Progress на Deployed
  3. Запишіть Distribution domain name: d1234abcd.cloudfront.net
  4. Запишіть Distribution ID: EDFDVBD6EXAMPLE (знадобиться для CLI команд)

Крок 3: Оновлення S3 Bucket Policy для OAC

Після створення Distribution і OAC — потрібно надати CloudFront доступ до закритого bucket:

  1. AWS Console покаже банер: «The S3 bucket policy needs to be updated» → натисніть Copy policy
  2. Перейдіть у S3my-react-app-2024PermissionsBucket policyEdit
  3. Вставте скопійовану Policy → Save changes

Тепер перевіримо, що CloudFront роздає ваш React додаток:

Тест CloudFront domain
$ curl -I https://d1234abcd.cloudfront.net/
HTTP/2 200
content-type: text/html
x-cache: Miss from cloudfront
via: 1.1 abc123.cloudfront.net (CloudFront)
$ curl -I https://d1234abcd.cloudfront.net/
HTTP/2 200
x-cache: Hit from cloudfront

x-cache: Miss from cloudfront — перший запит, брав з S3. x-cache: Hit from cloudfront — повторний запит, відповів з кешу.


Крок 4: Custom Error Pages для React Router

React Router використовує client-side navigation — маршрути обробляє JavaScript у браузері, а не сервер. Проблема: якщо користувач відкриє https://yoursite.com/about напряму або перезавантажить сторінку — CloudFront звернеться до S3 за файлом /about. Оскільки bucket приватний з OAC, S3 повертає 403 Forbidden для будь-якого відсутнього об'єкта (не 404!). Це навмисна поведінка S3: не розкривати, які об'єкти існують, а які ні. CloudFront передає цей 403 браузеру — і замість React-додатку користувач бачить помилку.

Custom Error Pages вирішують це: CloudFront перехоплює 403/404 від Origin і відповідає клієнту вмістом /index.html зі статусом 200. Браузер завантажує React, React Router читає URL і рендерить правильну сторінку.

  1. CloudFront → ваш Distribution → вкладка Error pages
  2. Create custom error response:
    • HTTP error code: 403
    • Customize error response: Yes
    • Response page path: /index.html
    • HTTP response code: 200
  3. Повторіть для 404

Крок 5: Налаштування Cache-Control заголовків для React

React build генерує файли з hash у назвах (main.abc123.js). При кожному npm run build hash змінюється — тому старі файли ніколи не конфліктують з новими. Це дозволяє кешувати JS/CSS на рік.

BUCKET="my-react-app-2024"

# index.html — без кешу (завжди свіжий)
aws s3 cp s3://$BUCKET/index.html s3://$BUCKET/index.html \
    --metadata-directive REPLACE \
    --content-type "text/html; charset=utf-8" \
    --cache-control "no-cache, no-store, must-revalidate" \
    --region eu-central-1

# JS файли з hash — кеш на 1 рік (незмінні!)
aws s3 cp s3://$BUCKET/static/js/ s3://$BUCKET/static/js/ \
    --recursive --metadata-directive REPLACE \
    --content-type "application/javascript" \
    --cache-control "public, max-age=31536000, immutable" \
    --region eu-central-1

# CSS файли — кеш на 1 рік
aws s3 cp s3://$BUCKET/static/css/ s3://$BUCKET/static/css/ \
    --recursive --metadata-directive REPLACE \
    --content-type "text/css" \
    --cache-control "public, max-age=31536000, immutable" \
    --region eu-central-1

Або ще краще — автоматизуйте у deploy.sh:

#!/bin/bash
# deploy.sh — повний скрипт деплою React SPA
set -e  # зупинитись при будь-якій помилці

BUCKET="my-react-app-2024"
DIST_ID="EDFDVBD6EXAMPLE"  # ЗАМІНІТЬ на ваш Distribution ID

echo "1. Building React app..."
npm run build

echo "2. Uploading JS/CSS (long cache)..."
aws s3 sync ./build/static s3://$BUCKET/static \
    --cache-control "public, max-age=31536000, immutable" \
    --delete --region eu-central-1

echo "3. Uploading other assets..."
aws s3 sync ./build s3://$BUCKET \
    --exclude "index.html" --exclude "static/*" \
    --cache-control "public, max-age=86400" \
    --delete --region eu-central-1

echo "4. Uploading index.html (no cache)..."
aws s3 cp ./build/index.html s3://$BUCKET/index.html \
    --cache-control "no-cache, no-store, must-revalidate" \
    --content-type "text/html; charset=utf-8" \
    --region eu-central-1

echo "5. Invalidating CloudFront cache (index.html only)..."
aws cloudfront create-invalidation \
    --distribution-id $DIST_ID \
    --paths "/index.html" "/asset-manifest.json" \
    --region us-east-1  # CloudFront завжди us-east-1!

echo "Done! Site deployed."

Крок 6: Підключення власного домену через pp.ua (безкоштовно)

pp.ua — безкоштовний сервіс для реєстрації субдоменів третього рівня в зоні .pp.ua. Ви можете безкоштовно отримати домен виду yourname.pp.ua і прив'язати його до CloudFront. Це ідеальний варіант для студентів та навчальних проєктів.

Загальна схема:

yourname.pp.ua
    ↓ CNAME запис (DNS)
d1234abcd.cloudfront.net
    ↓ CloudFront Distribution
S3 bucket (React SPA)

Крок 6a: Реєстрація на pp.ua

  1. Перейдіть на https://pp.ua
  2. Введіть бажане ім'я субдомену, наприклад my-react-app
  3. Натисніть перевірку — якщо my-react-app.pp.ua вільний, зареєструйте
  4. Введіть email, пароль → підтвердіть email
  5. Увійдіть у панель управління доменом

Крок 6b: Отримання ACM SSL сертифіката (ОБОВ'ЯЗКОВО у us-east-1!)

ACM сертифікат для CloudFront ПОВИНЕН бути у регіоні us-east-1. Навіть якщо ваш S3 у Франкфурті. Якщо сертифікат в іншому регіоні — CloudFront просто не покаже його у списку. Переключіться у Console на us-east-1 перед наступними кроками!
  1. Переключіть регіон у Console на US East (N. Virginia) us-east-1 (важливо!)
  2. Відкрийте ACM (Certificate Manager)
  3. Request a certificateRequest a public certificateNext
  4. Fully qualified domain name: my-react-app.pp.ua
  5. Validation method: DNS validation
  6. Request
  7. Відкрийте щойно створений сертифікат → у розділі Domains → скопіюйте CNAME запис для валідації:
    • CNAME name: _abc123.my-react-app.pp.ua (скопіюйте повністю)
    • CNAME value: _def456.acm-validations.aws. (скопіюйте повністю)

Крок 6c: Додавання DNS записів у pp.ua

У панелі pp.ua вам потрібно додати два CNAME записи:

Запис 1 — для валідації сертифіката ACM:

Зайдіть у панель pp.ua → DNS Management → Add Record:

  • Type: CNAME
  • Name/Host: _abc123 (лише частина до .my-react-app.pp.ua — приставку домену не вводьте)
  • Value/Target: _def456.acm-validations.aws.
  • TTL: 300
У деяких DNS панелях потрібно вводити повне ім'я (_abc123.my-react-app.pp.ua), а в інших — лише частину до домену (_abc123). Спробуйте обидва варіанти і перевірте через nslookup.

Зачекайте 5–30 хвилин поки ACM перевірить DNS запис. Статус сертифіката зміниться з Pending validation на Issued.

Запис 2 — для підключення домену до CloudFront:

Зайдіть у pp.ua → DNS Management → Add Record:

  • Type: CNAME
  • Name/Host: my-react-app (або @ якщо хочете щоб pp.ua вказував на корінь, але субдомен краще)
  • Value/Target: d1234abcd.cloudfront.net (ваш CloudFront domain name)
  • TTL: 300

Перевірка через термінал:

DNS перевірка CNAME
$ nslookup -type=CNAME my-react-app.pp.ua
Server: 1.1.1.1
Address: 1.1.1.1#53
Non-authoritative answer:
my-react-app.pp.ua canonical name = d1234abcd.cloudfront.net.

Що таке nslookup? Це консольна утиліта для перевірки DNS записів. nslookup -type=CNAME domain показує CNAME запис для домену. Якщо бачите d1234abcd.cloudfront.net — DNS налаштований правильно.

Крок 6d: Додавання домену у CloudFront Distribution

Тепер потрібно сказати CloudFront що він обслуговує ваш домен:

  1. CloudFront → ваш Distribution → вкладка GeneralEdit
  2. Alternate domain names (CNAMEs): додайте my-react-app.pp.ua
  3. Custom SSL certificate: оберіть ваш ACM сертифікат my-react-app.pp.ua (має бути в списку, якщо він Issued у us-east-1)
  4. Save changes
  5. Зачекайте 3–5 хвилин поки зміни розгорнуться

Тепер відкрийте у браузері: https://my-react-app.pp.ua 🎉

Фінальна перевірка
$ curl -I https://my-react-app.pp.ua/
HTTP/2 200
content-type: text/html; charset=utf-8
x-cache: Hit from cloudfront
via: 1.1 abc123.cloudfront.net (CloudFront)
server: AmazonS3
cache-control: no-cache, no-store, must-revalidate

Крок 7 (опціонально): Підключення платного домену через Route 53

Якщо у вас є власний домен (куплений у будь-якого реєстратора: GoDaddy, Namecheap, Porkbun тощо) — процес аналогічний pp.ua, але через Route 53.

  1. Route 53Hosted zonesCreate hosted zone → введіть ваш домен
  2. Route 53 надасть 4 NS-сервери (наприклад ns-1234.awsdns-12.org) — вкажіть їх у вашого реєстратора
  3. ACM сертифікатus-east-1) → DNS validation → Route 53 додасть CNAME автоматично (кнопка Create records in Route 53)
  4. Route 53 → Hosted zone → Create record:
    • Record name: app (для app.example.com)
    • Record type: A
    • Alias → CloudFront distribution (оберіть вашу Distribution)
    • Route 53 + CloudFront краще використовувати A record з Alias замість CNAME — це ефективніше і коштує менше

Крок 8: ОБОВ'ЯЗКОВО — Очищення

CloudFront Distribution коштує ~$0.0085 за 10,000 HTTPS запитів + трафік. Для навчального проєкту з нульовим трафіком — практично безкоштовно. Але якщо не потрібен — вимкніть.
# Через Console (найпростіше):
# CloudFront → Distribution → Disable → зачекати статус Deployed → Delete

# Через CLI:
DIST_JSON=$(aws cloudfront get-distribution-config \
    --id $DIST_ID --region us-east-1)
ETAG=$(echo $DIST_JSON | python3 -c "import sys,json; print(json.load(sys.stdin)['ETag'])")

# Встановити Enabled: false
echo $DIST_JSON | python3 -c "
import sys, json
d = json.load(sys.stdin)
config = d['DistributionConfig']
config['Enabled'] = False
print(json.dumps(config))
" > /tmp/disable-dist.json

aws cloudfront update-distribution \
    --id $DIST_ID \
    --distribution-config file:///tmp/disable-dist.json \
    --if-match $ETAG \
    --region us-east-1

echo "Distribution вимкнено. Зачекайте статус Deployed (~5-10 хв), потім видаліть."

# Після статусу Deployed — отримати новий ETag і видалити
NEW_ETAG=$(aws cloudfront get-distribution-config \
    --id $DIST_ID --region us-east-1 --query "ETag" --output text)
aws cloudfront delete-distribution \
    --id $DIST_ID --if-match $NEW_ETAG --region us-east-1

Резюме

  • CDN — глобальна мережа серверів, що кешують контент поблизу користувача. Вирішує проблему latency та зменшує навантаження на Origin.
  • CloudFront — CDN від AWS. 400+ Edge Locations. Кешує контент, термінує SSL, захищає від DDoS.
  • Distribution — одиниця конфігурації CloudFront. Містить Origins, Behaviors, домени, SSL.
  • OAC (Origin Access Control) — правильний спосіб підключити закритий S3 до CloudFront. Bucket залишається приватним.
  • OAI (застарілий) → замінено OAC (рекомендовано).
  • Cache Behaviors: різні TTL та правила для різних URL. /api/* без кешу, /static/* на рік.
  • TTL та Cache-Control: index.htmlno-cache. JS/CSS з hash → max-age=31536000, immutable.
  • Invalidation: очищення кешу після деплою. Лише index.html зазвичай достатньо.
  • CloudFront Functions: JS код на edge для URL rewriting, A/B тестування.
  • ACM сертифікат для CloudFront: ОБОВ'ЯЗКОВО у регіоні us-east-1.
  • pp.ua: безкоштовний субдомен. CNAME → CloudFront domain. Ідеально для навчання.

Практичні завдання

Рівень 1 (Базовий)

Завдання 1. Поясніть власними словами що таке CDN і навіщо він потрібен. Що станеться якщо користувач у Токіо відкриє сайт без CDN, розміщений у Франкфурті?

Завдання 2. Чому ACM сертифікат для CloudFront потрібно створювати у us-east-1, а не в тому регіоні де ваш S3?

Рівень 2 (Практичний)

Завдання 3. Задеплойте React SPA на S3 + CloudFront за інструкцією. Налаштуйте власний домен через pp.ua. Перевірте x-cache: Hit from cloudfront у заголовках відповіді.

Завдання 4. Напишіть deploy.sh скрипт, що: білдить React, синхронізує S3 з правильними Cache-Control заголовками, інвалідує лише index.html у CloudFront. Протестуйте: задеплойте нову версію і переконайтесь, що зміни видно одразу.

Рівень 3 (Архітектура)

Завдання 5. Спроектуйте CloudFront Distribution для SPA з .NET API: фронтенд (S3 → CloudFront, max-age=31536000), API запити (/api/* → ALB без кешу), статика (/public/* → S3 max-age=86400). Додайте CloudFront Function для index.html fallback без помилки 403. Намалюйте схему у PlantUML.

Copyright © 2026