Объектное хранилище выглядит как «положил и забыл» — но без операционной дисциплины бакеты превращаются в десятки терабайт бесхозных данных с непредсказуемым счётом. Разберём, как правильно настроить резервное копирование, защититься от потери данных и не переплачивать.
Версионирование меняет смысл операций: PUT кладёт новую версию поверх старой, DELETE вообще ничего не стирает — он добавляет метку, из-за которой GET отвечает 404. Поэтому «удалённые» данные продолжают занимать место и попадать в счёт, пока их не заберёт lifecycle, а восстановление сводится к снятию метки. Репликация подхватывает только то, что записано после её включения.
S3 как место для резервных копий
S3 — удобное место для резервных копий любых систем: дампы PostgreSQL, снапшоты MongoDB и Elasticsearch, конфигурационные файлы, зашифрованные секреты.
Почему S3 хорошо подходит для этой роли:
- Надёжность 99.999999999% (11 девяток) — данные хранятся в нескольких зонах доступности внутри одного региона.
- Холодные классы хранения — Glacier Deep Archive стоит около $1 за терабайт в месяц; ежедневные бэкапы годичной давности хранить почти бесплатно.
- Версионирование защищает от случайного удаления и вирусов-шифровальщиков.
Типичная структура бакета с резервными копиями выглядит так:
s3://backup-bucket/
├── pg/
├── mongo/
├── es/
└── app/
Под префиксами лежат дампы PostgreSQL, дампы MongoDB, снапшоты Elasticsearch и экспорты приложения.
На бакете включают: версионирование, lifecycle-политику (переход в Glacier через 30 дней, удаление старых версий через 90), репликацию в другой регион.
Кросс-региональная репликация
AWS даёт 11 девяток надёжности, но главный враг данных — человеческая ошибка: случайно удалили бакет, скомпрометировали учётную запись.
Cross-Region Replication (CRR) решает обе проблемы. Каждый объект, помещённый в исходный бакет, асинхронно копируется в бакет-приёмник в другом регионе (или аккаунте). Если что-то случится с продакшн-аккаунтом — резервная копия недосягаема для атакующего.
Настройка через AWS CLI:
{
"Role": "arn:aws:iam::ACCOUNT:role/replication-role",
"Rules": [{
"Priority": 1,
"Filter": {},
"Status": "Enabled",
"Destination": {
"Bucket": "arn:aws:s3:::backup-dr-bucket",
"StorageClass": "STANDARD_IA"
},
"DeleteMarkerReplication": { "Status": "Disabled" }
}]
}
DeleteMarkerReplication выключен намеренно, и это главная строка всей конфигурации. Если метки удаления реплицировать, то скомпрометированная учётка «удалит» объекты сразу в обоих бакетах: метка уедет следом, и GET в бакете-приёмнике тоже начнёт отвечать 404. А весь смысл копии в другом регионе — в том, чтобы она пережила такое удаление.
Ещё одно условие: репликация требует включённого версионирования на обоих бакетах, иначе AWS просто не примет конфигурацию.
Важный момент: репликация работает только для новых объектов — для уже лежащих в бакете нужен aws s3 sync или S3 Batch Replication.
Версионирование и MFA Delete
Версионирование включают на уровне бакета. После включения каждый PUT создаёт новую версию объекта, а DELETE добавляет метку удаления — физически объект остаётся.
Отсюда неожиданный эффект: GET после DELETE отвечает 404, хотя обе версии лежат на месте и оплачиваются, а восстановление сводится к снятию метки. Механика видна и без S3:
живой пример
import java.util.ArrayDeque;
import java.util.Deque;
public class VersionedBucket {
record Version(String id, String body, boolean deleteMarker) {}
private static final Deque<Version> versions = new ArrayDeque<>();
private static int counter = 0;
static void put(String body) {
versions.push(new Version("v" + (++counter), body, false));
}
static void delete() {
versions.push(new Version("v" + (++counter), null, true));
}
static String get() {
Version top = versions.peek();
return top == null || top.deleteMarker() ? "404 NoSuchKey" : top.body();
}
public static void main(String[] args) {
put("dump-15.09");
System.out.println("PUT -> GET: " + get());
put("dump-16.09");
System.out.println("PUT -> GET: " + get());
delete();
System.out.println("DELETE -> GET: " + get() + ", версий в бакете: " + versions.size());
versions.pop();
System.out.println("сняли метку -> GET: " + get() + ", версий в бакете: " + versions.size());
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
живой пример
package main
import "fmt"
type version struct {
id, body string
deleteMarker bool
}
var versions []version
var counter int
func put(body string) {
counter++
versions = append(versions, version{fmt.Sprintf("v%d", counter), body, false})
}
func remove() {
counter++
versions = append(versions, version{fmt.Sprintf("v%d", counter), "", true})
}
func get() string {
if len(versions) == 0 || versions[len(versions)-1].deleteMarker {
return "404 NoSuchKey"
}
return versions[len(versions)-1].body
}
func main() {
put("dump-15.09")
fmt.Println("PUT -> GET: " + get())
put("dump-16.09")
fmt.Println("PUT -> GET: " + get())
remove()
fmt.Printf("DELETE -> GET: %s, версий в бакете: %d\n", get(), len(versions))
versions = versions[:len(versions)-1]
fmt.Printf("сняли метку -> GET: %s, версий в бакете: %d\n", get(), len(versions))
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
живой пример
const versions = [];
let counter = 0;
const put = (body) => versions.push({ id: `v${++counter}`, body, deleteMarker: false });
const del = () => versions.push({ id: `v${++counter}`, body: null, deleteMarker: true });
function get() {
const top = versions.at(-1);
return !top || top.deleteMarker ? '404 NoSuchKey' : top.body;
}
put('dump-15.09');
console.log('PUT -> GET: ' + get());
put('dump-16.09');
console.log('PUT -> GET: ' + get());
del();
console.log(`DELETE -> GET: ${get()}, версий в бакете: ${versions.length}`);
versions.pop();
console.log(`сняли метку -> GET: ${get()}, версий в бакете: ${versions.length}`);
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
живой пример
from dataclasses import dataclass
@dataclass(frozen=True)
class Version:
id: str
body: str | None
delete_marker: bool
versions: list[Version] = []
counter = 0
def put(body: str) -> None:
global counter
counter += 1
versions.append(Version(f"v{counter}", body, False))
def delete() -> None:
global counter
counter += 1
versions.append(Version(f"v{counter}", None, True))
def get() -> str:
if not versions or versions[-1].delete_marker:
return "404 NoSuchKey"
return versions[-1].body
put("dump-15.09")
print("PUT -> GET: " + get())
put("dump-16.09")
print("PUT -> GET: " + get())
delete()
print(f"DELETE -> GET: {get()}, версий в бакете: {len(versions)}")
versions.pop()
print(f"сняли метку -> GET: {get()}, версий в бакете: {len(versions)}")
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
В настоящем бакете стопку версий показывает aws s3api list-object-versions, а метку снимает delete-object --version-id с идентификатором самой метки.
Для критичных бакетов с резервными копиями добавляют MFA Delete: удалить версию навсегда или выключить версионирование можно только с одноразовым кодом MFA-устройства root-аккаунта. Скомпрометированная учётка поставит метку удаления, но стереть данные не сможет.
aws s3api put-bucket-versioning \
--bucket backup-bucket \
--versioning-configuration Status=Enabled,MFADelete=Enabled \
--mfa "arn:aws:iam::ACCOUNT:mfa/user 123456"
Object Lock — ещё жёстче: режим WORM (write-once-read-many) делает удаление физически невозможным на заданный срок, даже для администратора. Используют там, где этого требует регулятор: финансовые и медицинские данные.
Lifecycle-политики для разных случаев
Lifecycle управляет переходами между классами хранения и удалением объектов, и сроки в политике берутся из того, кто и когда данные читает: логи смотрят первые дни, а через месяц открывают только при разборе инцидента; дампы нужны, пока живёт риск отката, обычно неделю, дальше их держат ради регламента. Несколько типичных наборов:
Пользовательский контент (аватарки, загрузки):
- prefix: "tmp/"
Expiration: 7 days
- prefix: "users/"
NoncurrentVersionExpiration: 30 days
AbortIncompleteMultipartUpload: 7 days
Журналы и аудит:
- prefix: "audit/"
Transitions:
- 30 days → STANDARD_IA
- 90 days → GLACIER
- 365 days → DEEP_ARCHIVE
Expiration: 2555 days # 7 лет по требованию регулятора
Резервные копии PostgreSQL:
- prefix: "pg/"
Transitions:
- 30 days → STANDARD_IA # раньше 30 дней S3 в этот класс не переведёт
- 90 days → GLACIER_IR # доступ мгновенный, как в Standard
- 180 days → DEEP_ARCHIVE # долгосрочное хранение
Expiration: 365 days
NoncurrentVersionExpiration: 30 days
AbortIncompleteMultipartUpload: 1 day
Даты здесь не случайные, и «переводить в холодный класс пораньше, чтобы сэкономить» не получится. У холодных классов есть минимальный срок оплачиваемого хранения: STANDARD_IA — 30 дней, GLACIER_IR — 90, Deep Archive — 180. Уберёте объект раньше срока (удалением или переходом в ещё более холодный класс) — заплатите за весь минимум, как будто он всё это время лежал. Плюс сам S3 не примет переход в STANDARD_IA или ONEZONE_IA раньше 30-го дня жизни объекта: такое правило отклоняется. Поэтому цепочка и идёт ровно по этим порогам — 30, 90, 180, — а удаление на 365-м дне оставляет объекту 185 дней в Deep Archive, то есть больше его минимума.
Правило AbortIncompleteMultipartUpload стоит добавлять везде: незавершённые составные загрузки незаметно накапливаются и тоже биллятся.
Из чего складывается стоимость S3
AWS S3 выставляет счёт по трём статьям.
Три статьи счёта на одном примере в 100 ТБ: хранение и запросы вместе дают меньше четверти суммы, а исходящий трафик в интернет стоит почти вчетверо дороже них.
Хранение
Оплата за гигабайт в месяц. Класс хранения определяет цену:
| Класс | $/ГБ/месяц (us-east-1) | Применение |
|---|---|---|
| Standard | $0.023 | Активные данные |
| Standard-IA | $0.0125 | Резервные копии, доступ раз в месяц |
| Glacier Instant | $0.004 | Архив с возможностью быстрого доступа |
| Glacier Flexible | $0.0036 | Долгий архив, доступ за часы |
| Deep Archive | $0.00099 | Холодный архив, доступ за сутки |
100 ТБ на Standard — около $2300 в месяц. На Deep Archive — около $100 в месяц.
Запросы
PUT/COPY/POST/LIST: ~$0.005 за 1000 запросов. GET: ~$0.0004 за 1000 запросов.
Для большинства приложений это незаметные суммы. Миллион операций LIST в день — уже $5/день, или $150 в месяц.
Типичная ошибка — писать миллион файлов по 1 КБ вместо одного файла в 1 ГБ. На Standard хранение обойдётся одинаково, а запросов будет в миллион раз больше. В холодных классах хуже: STANDARD_IA и Glacier Instant тарифицируют каждый объект минимум как 128 КБ, Glacier Flexible и Deep Archive — как 40 КБ. Миллион килобайтных файлов в STANDARD_IA оплачивается как 128 ГБ.
Исходящий трафик
Самая коварная статья. Трафик из S3 в интернет: ~$0.09 за ГБ.
100 ТБ в месяц = $9000. Для публичных бакетов с медиаконтентом эта статья часто превышает стоимость самого хранения.
Способы снизить:
- CloudFront перед S3 — за трафик из S3 в CloudFront AWS не берёт денег вовсе, платите только за отдачу пользователю: $0.085/ГБ на первые 10 ТБ в месяц и дешевле дальше.
- VPC Endpoint для трафика из EC2 в том же регионе — бесплатно. Для backend-сервисов, работающих в AWS, это нужно включать всегда.
Альтернативы AWS S3
| Провайдер | Исходящий трафик | Особенность |
|---|---|---|
| AWS S3 | $0.09/ГБ | Стандарт |
| Backblaze B2 | $0.01/ГБ | S3-совместимый API, дешёвый трафик |
| Cloudflare R2 | бесплатно | S3-совместимый API, нет платы за трафик |
| Wasabi | бесплатно (с ограничениями) | Дешёвое хранение |
| Yandex Object Storage | 1,68 ₽/ГБ, первые 100 ГБ в месяц бесплатно | Российская юрисдикция |
| MinIO на своём сервере | только ваш канал | S3-совместимый API, но железо и дежурство тоже ваши |
Cloudflare R2 — особенно интересен для публичного контента с большим трафиком: S3-совместимый API позволяет переключиться через endpointOverride в SDK без переписывания кода.
Последняя строка таблицы стоит отдельно. MinIO — это не провайдер, а сервер, который вы поднимаете сами: тот же S3-совместимый API, но на своём железе. Счёта за гигабайты и запросы не будет вовсе, вместо него — диски, их замена и дежурный, который на это смотрит. Почти всё, о чём шла речь выше, у MinIO есть: версионирование, lifecycle-политики с переходом на медленные диски, репликация между установками, блокировка объектов в режиме WORM. Чего нет — CloudWatch, CloudTrail и S3 Inventory: их роль берут на себя метрики Prometheus и журнал аудита, которые MinIO отдаёт сам. Чек-лист ниже читается для него так же, только «включить в консоли AWS» превращается в «настроить у себя».
Кто съел деньги: Storage Lens и теги затрат
Счёт показывает сумму, а вопрос всегда другой: какой префикс вырос. Отвечают на него два инструмента.
Storage Lens — встроенная аналитика по хранилищу: сколько объектов и байт в каждом бакете, как они разложены по классам, сколько незавершённых составных загрузок, сколько неактуальных версий, какова средняя величина объекта. Бесплатный уровень даёт сводку по аккаунту и бакетам, платный — разрезы по префиксам. Именно он отвечает на «откуда миллион мелких файлов» и «сколько лежит брошенных частей».
Теги распределения затрат. Бакету (или объектам) проставляют теги вида team=search, env=prod, включают их как теги распределения затрат — и счёт начинает раскладываться по командам и окружениям. Без этого «хранилище» в счёте остаётся одной строкой, и разговор о снижении затрат упирается в то, что никто не знает, чьи это данные.
Опись (inventory) — ежедневная или еженедельная выгрузка списка всех объектов с размером, классом и датой в виде файла. Это дешёвый способ считать что угодно по бакету с миллионами объектов, не делая миллион запросов на листинг.
Intelligent-Tiering: когда режим доступа неизвестен
Правило жизненного цикла требует знать, через сколько дней файл остывает. Когда этого не знает никто (пользовательские файлы: одни смотрят каждый день, другие ни разу), выбирают класс, который перекладывает сам: он следит за обращениями к каждому объекту и двигает его между уровнями — горячим, редкого доступа и архивными, — возвращая обратно при первом обращении.
Плата за это — небольшой сбор за мониторинг каждого объекта. Отсюда единственное ограничение: для множества мелких файлов сбор съедает экономию, поэтому такой класс берут для объектов от нескольких сотен килобайт и больше, а мелочь оставляют в горячем классе.
Практический выбор: знаете режим доступа — правило жизненного цикла (дешевле); не знаете и объекты крупные — автоматический класс; объекты мелкие — горячий класс и пересмотр раскладки.
Переложить миллион объектов: Batch Operations
aws s3 sync годится для тысяч объектов и не годится для миллионов: это последовательный обход с запросом на каждый объект. Для массовых операций есть отдельный механизм пакетных операций: на вход берётся опись (или свой список ключей), на выход — задание, которое само распараллеливает работу, повторяет неудачные объекты и пишет отчёт.
Что им делают: перекладывают объекты в другой класс хранения, копируют бакет в другой регион, проставляют теги и метаданные, запускают функцию на каждом объекте (например, пересчитать превью), включают блокировку объектов. Отдельная его разновидность догоняет репликацию по объектам, которые уже лежали в бакете до её включения, — обычная репликация работает только для новых объектов.
Правило простое: до нескольких тысяч объектов — скрипт, дальше — пакетная операция с отчётом об ошибках.
Надёжность и доступность — разные обещания
Их путают постоянно, а решения из них следуют разные.
Надёжность — это про потерю данных: одиннадцать девяток означают, что вероятность потерять объект за год исчезающе мала (данные хранятся в нескольких зонах и постоянно проверяются). Резервная копия в другом месте нужна не от этого — она нужна от ваших ошибок: удалили по скрипту, перезаписали не тем, шифровальщик получил ключи.
Доступность — это про «ответит ли хранилище прямо сейчас»: обещание уровня 99,99 % для горячего класса означает до примерно часа недоступности в год, а у холодных классов оно ниже. То есть бакет может быть недоступен, и приложение обязано это переживать: таймауты, повторы, деградация (показать страницу без картинок, а не пятисотую ошибку), очередь для записи.
Отсюда и два разных решения: от потери данных — версионирование, блокировка объектов и копия в другом аккаунте; от недоступности — повторы, кеш и запасной путь в приложении.
Что уже включено по умолчанию
Чек-лист стоит читать, зная состояние по умолчанию у новых бакетов: блокировка публичного доступа включена, списки управления доступом отключены (владелец бакета владеет всеми объектами), шифрование на стороне сервера применяется само. То есть пункты чек-листа про публичный доступ и шифрование — это проверка, что никто не отключил, а не действие. Действия остаются там, где умолчания нет: версионирование, правила жизненного цикла, журналирование обращений, репликация, политика с запретом незашифрованного соединения.
Мониторинг
Метрики CloudWatch
Размер бакета и число объектов приходят в CloudWatch сами — раз в сутки и бесплатно. Метрики запросов включают на бакете отдельно, и они платные:
| Метрика | Что значит | Повод для сигнала |
|---|---|---|
BucketSizeBytes | Размер бакета (раз в день) | Внезапный скачок |
NumberOfObjects | Число объектов (раз в день) | Рост на тысячи в день |
AllRequests / 4xxErrors / 5xxErrors | Запросы и ошибки | Ошибок > 0.1% |
BytesDownloaded | Исходящий трафик | Основа для оценки затрат |
Журналы доступа
Server access logging пишет каждый запрос в текстовый файл в другой бакет: IP, время, действие, ключ объекта, статус, размер, задержка, user-agent.
aws s3api put-bucket-logging --bucket production-bucket \
--bucket-logging-status '{
"LoggingEnabled": {
"TargetBucket": "audit-bucket",
"TargetPrefix": "logs/production-bucket/"
}
}'
Альтернатива — CloudTrail data events: формат JSON, чуть большая задержка, зато интегрируется с остальными событиями CloudTrail.
S3 Inventory
S3 Inventory формирует ежедневный (или еженедельный) отчёт по всем объектам бакета в формате CSV или Parquet. Колонки: ключ, размер, класс хранения, дата изменения, статус шифрования.
Полезно для проверки, что lifecycle-политика работает как ожидается, и для поиска «сирот» — объектов, у которых нет записей в базе данных.
Типичные проблемы и защита
Компрометация учётки и удаление данных
Сценарий: атакующий получил учётную запись с правами s3:DeleteObject и удалил всё содержимое. Спасает связка из разделов выше: версионирование оставляет данные под метками, MFA Delete и Object Lock не дают стереть их физически, а репликация в другой аккаунт держит копию вне досягаемости этой учётки.
Объекты ушли в Deep Archive по ошибке
Lifecycle-политика с неточным фильтром применилась к горячим данным. Чтение из Deep Archive занимает до 12 часов и стоит денег.
Защита: всегда проверяйте lifecycle-политику на небольшом тестовом префиксе перед применением ко всему бакету.
Восстановление: запрос restore-object, ожидание (до 12 часов), потом копирование в Standard.
Бакет случайно стал публичным
Ошибочные правки в bucket policy сделали весь бакет доступным извне. Возможна утечка данных и огромный счёт за исходящий трафик.
Защита: Block Public Access на уровне аккаунта — глобальный переключатель, который не позволяет ни одному бакету стать публичным, даже если кто-то изменил политику. Включайте на корневом аккаунте всегда.
Миллионы мелких файлов — неожиданный счёт
Каждое событие логируется отдельным PUT. Через месяц — миллионы файлов и заметные расходы на запросы.
Решение: накапливать события локально и сбрасывать в S3 пакетами раз в несколько минут или по достижении порогового размера. Amazon Data Firehose (до февраля 2024 года — Amazon Kinesis Data Firehose) делает это автоматически.
Чек-лист production-бакета
Минимальный набор настроек, который должен быть на каждом бакете в продакшне:
- Block Public Access включён (на уровне аккаунта тоже)
- Versioning включён
- SSE-S3 или SSE-KMS (с января 2023 года SSE-S3 применяется ко всем новым объектам в любом бакете; SSE-KMS включают отдельно)
- Lifecycle-политика: прерывание незавершённых составных загрузок через 7 дней, удаление старых версий, переход в холодные классы
- Сигнал CloudWatch на резкий рост размера и числа объектов
- Журналы доступа или CloudTrail data events для аудита
- Кросс-региональная репликация для критичных бакетов
- VPC Endpoint для трафика из EC2/EKS
- MFA Delete на бакетах с резервными копиями
- IAM с минимальными правами —
s3:*без ограничений недопустим
Глубже: раздача пользователям: CDN, подписанные ссылки и чужие сайтырасширенное
Отдать файл пользователю можно двумя путями, и они различаются не удобством, а тем, кто несёт трафик. Проксирование через сервис (скачали из бакета, отдали в ответ) занимает поток и сетевой канал приложения на всё время скачивания; на сотне одновременных загрузок видео сервис перестаёт отвечать на остальное. Прямая ссылка в бакет снимает нагрузку с сервиса целиком, и для закрытых файлов это presigned GET: подписанный адрес на несколько минут, который сервис отдаёт в JSON, а браузер идёт по нему сам. Имя файла при скачивании и тип задают параметрами в момент подписи (response-content-disposition=attachment; filename="...", response-content-type), потому что заголовки объекта в бакете могут быть не те, что нужны этому пользователю.
Для публичных и часто читаемых файлов между пользователем и бакетом ставят CDN: копии файлов расходятся по узлам поближе к пользователям, бакет получает один запрос вместо миллиона, а трафик из CDN дешевле трафика из бакета. Бакет при этом закрывают от прямого доступа и открывают только для CDN (у AWS это Origin Access Control, у других провайдеров «приватный бакет с доступом для CDN»), иначе ссылку на бакет достанут из любой страницы и будут ходить мимо кэша.
Закрытые файлы через CDN раздают подписанными ссылками уже самой CDN (у CloudFront это signed URL или signed cookie с политикой доступа), а не presigned ссылками бакета. Причина в кэше: ключ кэша это адрес, а presigned адрес у каждого пользователя свой, с подписью и сроком, и CDN не находит в кэше ничего, каждый запрос уходит в бакет. Подписанная ссылка CDN проверяется на узле, а к бакету идёт один и тот же адрес. Если подписанных адресов бакета не избежать, срок подписи округляют до часа, чтобы адрес совпадал у всех, кто пришёл в этот час.
Вставку ваших картинок на чужие сайты (они показывают контент, вы платите за трафик) полностью не запретить, но ограничить можно. Проверка заголовка Referer на CDN или бакете отсекает ленивых, подделывается легко. Подписанные ссылки со сроком жизни отсекают всех, но ломают кэш браузера и закладки. Разумная середина для витрины: публичные превью без подписи под CDN с лимитом на трафик и оповещением, оригиналы только по подписанной ссылке.
Коротко
- S3 — надёжное место для резервных копий баз данных и файлов; Glacier Deep Archive снижает стоимость долгосрочного хранения до $1/ТБ в месяц.
- Версионирование защищает от случайного удаления и вирусов-шифровальщиков:
DELETEкладёт метку, версии остаются на месте и оплачиваются, пока их не уберёт lifecycle. - MFA Delete и Object Lock закрывают безвозвратное удаление даже при компрометации учётки; репликация в другой аккаунт уводит копию из-под её прав.
- Cross-Region Replication копирует только новые объекты; уже лежащие догоняют пакетной операцией с отчётом об ошибках — ею же перекладывают миллион объектов в другой класс, а
aws s3 syncгодится лишь на тысячах. - Счёт складывается из хранения, запросов и исходящего трафика ($0.09/ГБ в интернет) — трафик и снижают в первую очередь: VPC Endpoint обнуляет EC2 → S3 внутри региона, CloudFront — участок S3 → кэш, а Cloudflare R2 не берёт за исходящий трафик вовсе.
- CloudWatch, журналы доступа и опись бакета покрывают мониторинг, а кто съел деньги, показывают Storage Lens (разрезы по бакетам и префиксам, брошенные загрузки, старые версии) и теги распределения затрат.
- Файлы пользователю отдают не через сервис, а прямой ссылкой: presigned GET для закрытых, CDN с закрытым бакетом для публичных; закрытое через CDN раздают подписанными ссылками самой CDN, иначе кэш не работает.
- Когда режим доступа неизвестен, берут класс с автоматическим перекладыванием — но только для крупных объектов: сбор за мониторинг съедает экономию на мелочи.
- Надёжность (одиннадцать девяток) и доступность (99,99 %) — разные обещания: от потери данных спасают версии и копия в другом аккаунте, от недоступности — повторы и деградация в приложении.
Что почитать дальше
- Fundamentals — устройство S3: бакет, объект, ключ, классы хранения.
- Spring + AWS SDK v2 для S3 — работа с S3 из приложения на примере Java; в Go это
aws-sdk-go-v2, в Node@aws-sdk/client-s3, в Pythonboto3, и все они говорят с MinIO тем же протоколом. - Резервные копии PostgreSQL — что именно кладут в такой бакет:
pg_dump,pg_basebackup, архив WAL. - Эксплуатация Elasticsearch — S3 как цель для снапшотов Elasticsearch.