Объектное хранилище S3‑класса давно стало стандартом де‑факто для хранения и раздачи статических файлов: медиа, CSS/JS, архивов, обновлений приложений. Его легко масштабировать, им просто управлять через API, оно устойчиво к сбоям и зачастую дешевле, чем держать те же объёмы на собственных серверах. Если поверх добавить корректные HTTP‑заголовки, а при необходимости — подписи ссылок и версионирование, объектное хранилище превращается в полнофункциональный CDN‑слой для статики.
Когда объектное хранилище уместно как CDN
Если ваш трафик преимущественно статический, нет сложной логики авторизации на каждом запросе, а география пользователей сосредоточена в 1–2 регионах, S3‑совместимое хранилище с правильно настроенным кэшированием даст отличный баланс цены и производительности. Для truly global доставки классические CDN‑сети с десятками точек по миру могут быть быстрее на дальних расстояниях, но и S3+Nginx‑кэш часто хватает для проектов, где важна предсказуемость и простота.
Плюсы подхода:
- Отдача напрямую из хранилища по HTTPS, без собственного файлового сервера.
- Долгие TTL через
Cache-Control
иimmutable
для «вечной» статики. - Подписи (pre‑signed URL) для приватного доступа, анти‑хотлинк и контроль времени жизни.
- Версионирование объектов для откатов и борьбы с «битым кэшем» у клиентов.
- Простой горизонтальный масштаб через прокси‑кэш Nginx.
Базовая архитектура
Публичный бакет
Самый простой вариант — сделать бакет публичным для чтения и отдавать файлы напрямую. Вы настраиваете Cache-Control
, Content-Type
, Content-Encoding
в метаданных объекта и получаете минимальную задержку и расходы. Минус — нет тонкого контроля доступа.
Приватный бакет + подписанные ссылки
Бакет приватный, доступ выдаётся через короткоживущие pre‑signed URL с подписью SigV4. Ссылки можно генерировать в приложении, ограничивая срок действия и путь к объекту. Так вы защищаете платный контент и ограничиваете хотлинк.
Проксирование через Nginx
Часто применяется гибрид: фронт‑домен (например, static.example.com
) обслуживается Nginx, который забирает файлы из S3‑совместимого бэкенда и кэширует их локально. Домен удобно оформить через регистрацию доменов, а шифрование трафика включить с помощью SSL-сертификаты. Развернуть такой слой удобнее всего на VDS.
- Единый домен и TLS‑политика (HSTS, современные наборы шифров) через SSL-сертификаты.
- Локальный кэш с
stale-while-revalidate
иstale-if-error
. - Гибкая нормализация заголовков и согласование форматов.
Важно: если используете Nginx‑кэш как внешний слой, делайте статике «вечные» имена через хэши в имени файла. Тогда истекание кэша и принудительные инвалидации почти не нужны.
S3‑совместимость и нюансы API
Провайдеры S3 совместимы на уровне API (PUT/GET/HEAD, список объектов, pre‑signed URLs), но есть отличия: стиль адресации (virtual hosted vs path style), поведение ETag
для multipart‑загрузок, поддержка Object Lock
, ограничения на длину ключей. Для миграции кода держите параметры эндпоинта и региона в конфиге, включайте SigV4, а для URL‑подписей учитывайте возможные расхождения в часовых поясах (skew) между клиентом и сервером.
HTTP‑заголовки для производительности
Cache-Control
Для неизменяемой статики (файлы с хэшами в имени: app.3f1c9a.js
) используйте:
Cache-Control: public, max-age=31536000, immutable
immutable
сообщает браузеру, что ресурс не будет меняться без смены URL, и избавляет от лишних условных запросов. Для часто меняющихся изображений можно указать более короткий max-age
, или применить s-maxage
для прокси‑кэшей, отличая их от клиентских браузеров.
ETag и Last-Modified
ETag
позволяет экономить на трафике за счёт If-None-Match
и ответов 304
. Учтите, что в S3 классический ETag
равен MD5 только для одиночных загрузок; при multipart он не совпадает с MD5 содержимого. Если хотите строгую идентичность, генерируйте хэш в самом билде и кладите его в имя файла или как отдельный метадатный заголовок. Last-Modified
уместен, но менее точен, чем контентный хэш.
Content-Type, Content-Encoding и Vary
Критично выставлять правильный Content-Type
при загрузке; иначе браузер может некорректно интерпретировать файл. Если храните заранее сжатые .br
или .gz
, на объекте должна быть выставлена Content-Encoding
, а для согласованности с кэшами — Vary: Accept-Encoding
. Более предсказуемый путь — хранить отдельные объекты на каждый формат и явно ссылаться на нужный.
CORS для веб‑шрифтов и SPA
Если статика обслуживает несколько доменов или SPA делает XHR к бакету, проверьте CORS‑политику бакета. Для веб‑шрифтов важно Access-Control-Allow-Origin
, иначе они могут не грузиться с другого домена.

Версионирование: имена файлов, query‑параметры и bucket versioning
Есть три взаимодополняющих подхода.
- Хэш в имени файла — самый надёжный вариант. Например,
app.3f1c9a.js
иstyles.9a2c.css
. Даём «вечный»Cache-Control
, публикуем новый файл — и все клиенты получают свежий код по новому URL. - Query‑версия —
/app.js?v=123
. Применимо, но некоторые прокси/кэши могут вести себя менее предсказуемо, а «вечные» TTL использовать осторожно. - Bucket versioning на стороне хранилища — помогает с быстрыми откатами и защитой от случайного удаления. Не заменяет хэш‑фингерпринт в URL, но дополняет его.
Если вы не можете внедрить хэши в имена, используйте короткие TTL и механизмы принудительной очистки кэша на своём прокси‑уровне. Но будьте готовы к всплескам запросов «сквозь кэш» при релизах.
Подписанные ссылки (pre‑signed URL)
Подписанные ссылки позволяют временно открыть доступ к приватному объекту без выдачи ключей. Приложение формирует URL с параметрами подписи и временем истечения. Браузер или клиент скачивают объект напрямую из хранилища.
- Срок жизни — делайте минимально достаточным (минуты или часы).
- Привязывайте ссылку к конкретному ключу объекта и методу
GET
. - Следите за рассинхронизацией часов: допустим небольшой clock skew (1–5 минут).
- Для хотлинк‑защиты можно включать в подпись часть реферера или IP, если это поддерживается вашей реализацией.
Генерация ссылок: пример на Python (boto3)
import boto3
from botocore.config import Config
s3 = boto3.client(
's3',
endpoint_url='https://s3.example',
region_name='us-east-1',
aws_access_key_id='KEY',
aws_secret_access_key='SECRET',
config=Config(signature_version='s3v4')
)
url = s3.generate_presigned_url(
ClientMethod='get_object',
Params={'Bucket': 'cdn-bucket', 'Key': 'media/video.mp4'},
ExpiresIn=600
)
print(url)
Логику генерации выносите на бэкенд; не генерируйте подписи в браузере.
Nginx как обратный прокси и кэширующий слой
Размещение перед S3 небольшого кэширующего слоя решает сразу несколько задач: снижает стоимость запросов к хранилищу при пиках, выравнивает задержки, даёт гибкость заголовков без перезаливки объектов и позволяет вводить защитные механизмы. Удобно размещать этот слой на VDS с достаточным SSD под кэш.
Базовый шаблон конфигурации
proxy_cache_path /var/cache/nginx/s3 levels=1:2 keys_zone=s3cache:100m max_size=10g inactive=7d use_temp_path=off;
map $sent_http_content_type $cache_ttl {
default 1h;
~*text/css 30d;
~*javascript 30d;
~*image/ 30d;
}
server {
listen 443 ssl http2;
server_name static.example.com;
# TLS и прочие настройки опущены для краткости
location / {
resolver 1.1.1.1 valid=300s ipv6=off;
set $s3_endpoint https://s3.example;
set $bucket cdn-bucket;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $bucket.s3.example;
proxy_hide_header Set-Cookie;
proxy_ignore_headers Set-Cookie;
proxy_intercept_errors on;
proxy_buffering on;
proxy_cache s3cache;
proxy_cache_key "$scheme://$host$uri?$args";
proxy_cache_valid 200 206 304 $cache_ttl;
proxy_cache_valid 301 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Vary "Accept-Encoding" always;
proxy_pass $s3_endpoint/$bucket$uri$is_args$args;
}
}
Здесь Nginx кэширует ответы S3, отдаёт их с длинными TTL и подменяет заголовки. В продакшене часто делают отдельные location для «вечных» путей (например, /assets/
) и для «живой» статики с коротким TTL.
Условные запросы и экономия трафика
Чтобы не тянуть лишние байты из бэкенда, включайте условные запросы: Nginx автоматически передаст If-None-Match
и If-Modified-Since
, если сохранит ETag
и Last-Modified
в кэше. Это уменьшает как исходящий трафик, так и число полных чтений из хранилища.
Диапазоны (Range) и крупные файлы
Для видео, архивов и установщиков важна поддержка Range
и ответов 206 Partial Content
. Проверьте, что и хранилище, и Nginx корректно пробрасывают такие запросы. Кэшировать диапазоны сложнее; иногда разумнее оставить прямую отдачу из S3 для отдельных префиксов.
Загрузка и метаданные
При загрузке объектов сразу проставляйте тип и политику кеширования. Пример для CLI:
aws s3 cp dist/ s3://cdn-bucket/assets/ --recursive --content-type text/css --cache-control "public, max-age=31536000, immutable"
aws s3 cp dist/app.3f1c9a.js s3://cdn-bucket/assets/app.3f1c9a.js --content-type application/javascript --cache-control "public, max-age=31536000, immutable"
Для изображений экономию даст предварительная оптимизация (WebP/AVIF) и генерация превью с нужными размерами. Подробно о профилях сжатия и согласовании форматов — в разборе WebP/AVIF и Nginx map.

Инвалидация кэша: когда она нужна
Если используете хэш‑фингерпринт в именах файлов — инвалидация почти не нужна. Новый билд публикует новые имена, а старые объекты продолжают жить до естественного истечения TTL. Без фингерпринтов инвалидация нужна при каждом релизе: сброс локального кэша Nginx, возможная перезагрузка объектов в бакете (с обновлением ETag
), и короткие TTL на клиенте.
Безопасность и защита от хотлинка
Для публичной статики полезен реферальный контроль и ограничение источников. Если доступ идёт напрямую к бакету, используйте pre‑signed URL и короткий срок жизни. Если через Nginx, добавьте простую валидацию подписи в query‑строке и отбрасывайте запросы без неё. Для приватного контента храните бакет закрытым и не публикуйте постоянные публичные URL объектов.
Логи и метрики
Собирайте метрики попадания в кэш (hit ratio), среднее время ответа и долю 304. Если видите много промахов при релизах — вероятно, длинные TTL установлены для файлов без фингерпринта. Если высока доля 404 — оптимизируйте билдер или индексацию в бакете. Сравнивайте стоимость запросов к S3 с экономией от локального кэша: иногда увеличение памяти под кэш на порядок уменьшает счёт за операции.
Стоимость и практические лайфхаки
- Долго живущие TTL + фингерпринт = минимум запросов и дешёвая раздача.
- Минифицируйте и объединяйте ресурсы, используйте современную графику — меньше байт, ниже счёт.
- 304 тоже считаются запросами к S3 — уменьшайте их за счёт
immutable
и фингерпринтов. - Крупные релизы прогревайте: заранее прокатите горячие пути через прокси, чтобы заполнить кэш.
Чек‑лист внедрения
- Создайте бакет и определите модель доступа: публичная статика vs приватный контент с подписями.
- Включите versioning на бакете для безопасности и откатов.
- Настройте пайплайн билда с хэшами в именах файлов и генерацией корректных
Content-Type
/Cache-Control
. - Загрузите объекты, проверьте метаданные, CORS для нужных доменов.
- Поставьте перед бакетом Nginx с кэшем, настройте
stale-while-revalidate
и ключи кэша. - Для приватного — добавьте генерацию pre‑signed URL на бэкенде, ограничьте срок жизни ссылок.
- Снимите метрики: hit ratio, latency, 304 share; подстройте TTL и размеры кэша.
Итоги
Объектное хранилище S3‑класса легко превращается в CDN для статики, если соблюсти три принципа: «вечные» имена файлов с хэшами, строгие HTTP‑заголовки кэширования и грамотный прокси‑кэш на переднем плане. Подписанные ссылки закрывают задачи приватного доступа, а версионирование в бакете страхует от ошибок. Такой стек прост, прозрачен в обслуживании, предсказуем по стоимости и хорошо масштабируется от небольших сайтов до крупных проектов.