Amazon S3 (Simple Storage Service) — первый коммерчески успешный сервис object storage, де-факто стандарт в индустрии. Сейчас «S3-API» — общий язык: его поддерживают MinIO, Cloudflare R2, Yandex Object Storage, Backblaze B2, Google Cloud Storage (через interoperability layer) и десятки других. Эта статья — про модель и ключевые свойства, без привязки к конкретному провайдеру.
Что такое object storage
Три уровня хранения, которые senior должен различать:
| Тип | Модель | Доступ | Когда |
|---|---|---|---|
| Block storage (EBS, локальные диски) | Сырые блоки | Драйвер ФС поверх | БД, любая файловая система |
| File storage (NFS, EFS, SMB) | Иерархия папок и файлов | POSIX-семантика | Shared-storage между серверами |
| Object storage (S3, GCS, Azure Blob) | Плоский namespace объектов с key | HTTP API | Большие файлы, бэкапы, статика, неизменяемые данные |
Object storage не файловая система. Нет seek, нет partial-write, нет rename, нет атомарных переименований папки. Объект — единица записи и чтения целиком (или диапазона). Это и ограничение, и источник масштабируемости: можно хранить триллионы объектов общим объёмом эксабайты.
Модель: bucket, object, key
s3://my-bucket/products/2026/05/cover-3.jpg
│ │ │
│ └─ key ────────────┘
└─ bucket
- Bucket — корневой namespace. Имя глобально-уникально (в AWS S3). Регион — у каждого bucket один.
- Object — единица хранения. Состоит из payload (байт), metadata (system + user-defined), и versionId (если versioning включён).
- Key — путь внутри bucket. Это строка, не файловая система:
products/2026/05/cover-3.jpg— это один ключ, никакой иерархии S3 внутри не строит. Префиксы (products/2026/) — только для удобства листинга.
Из этого вытекает: в S3 нет mkdir. Префикс существует ровно столько, сколько есть объект с этим префиксом. UI AWS Console показывает «папки», но это эмуляция.
Consistency-гарантии
До декабря 2020 S3 был eventually consistent: после PUT новой версии объекта read мог вернуть старую в течение секунд. Это рождало массу багов в production-сценариях.
С декабря 2020 — strong read-after-write consistency для всех операций. Read после Write возвращает самые свежие данные гарантированно.
Однако нюансы остались:
- Listing (list-bucket) тоже strong-consistent, но не атомарный: между read'ом и обновлением объект может появиться/исчезнуть.
- Versioning — Read возвращает «текущую» версию, если specifically не запрошен
versionId. - MinIO / Yandex / GCS — поведение consistency может отличаться от AWS. Yandex Object Storage следует AWS S3 (strong consistency), MinIO — тоже. Cloudflare R2 — strong consistency.
Storage classes
S3 хранит объекты в разных классах с разным trade-off «стоимость хранения vs стоимость retrieval»:
| Класс | Стоимость хранения | Retrieval | Когда |
|---|---|---|---|
| Standard | base × 1 | мгновенно, бесплатно | Активные данные, hot path |
| Standard-IA (Infrequent Access) | base × 0.5 | мгновенно, $/GB на read | Бэкапы, доступ раз в месяц-два |
| One Zone-IA | base × 0.4 | мгновенно, $/GB | То же, но один AZ — данные могут пропасть при сбое AZ |
| Glacier Instant Retrieval | base × 0.25 | мгновенно (с 12.2021), $$$/GB | Архивы с возможным быстрым доступом |
| Glacier Flexible Retrieval | base × 0.15 | минуты-часы | Аудит, compliance, редкий доступ |
| Glacier Deep Archive | base × 0.05 | часы-сутки | Долгосрочный архив, compliance ≥7 лет |
| Intelligent-Tiering | базовая + monitoring fee | автоматический выбор класса по pattern доступа | Когда не уверен в pattern доступа |
Главное правило: storage classes не равны производительности — все, кроме Glacier Flexible/Deep, отдают объект мгновенно. Различие только в цене. Перевести объект в более холодный класс — дёшево; обратно — требует копирования.
Класс ставится при PUT через header x-amz-storage-class или через lifecycle policy (см. ниже).
Versioning
Bucket-level setting: при включении каждое PUT/DELETE создаёт новую версию, старые сохраняются.
PUT s3://bucket/file.txt → versionId=v1
PUT s3://bucket/file.txt → versionId=v2 (v1 остаётся доступной)
DELETE s3://bucket/file.txt → создаёт "delete marker", v1 и v2 остаются
Запрос GET s3://bucket/file.txt после DELETE возвращает 404 (читается delete marker). Но GET s3://bucket/file.txt?versionId=v1 всё ещё работает.
Включать versioning обязательно для production-bucket'ов с пользовательскими файлами. Защита от:
- Кейс «случайно удалили». Восстановить версию.
- Кейс ransomware / компрометация AccessKey. Атакующий не сможет физически удалить, только пометить как удалённое.
- Кейс «выкатили баг, перезаписали неправильным контентом». Откатить на старую версию.
Минус: stale-версии копятся. Решается lifecycle policy (см. ниже).
Encryption
S3 шифрует данные на сервере (server-side encryption, SSE). Четыре варианта:
| Вариант | Ключ управляется | Когда |
|---|---|---|
| SSE-S3 (default с 2023) | AWS | Самый простой, бесплатно, для большинства случаев |
| SSE-KMS | AWS KMS (ваш ключ под AWS-управлением) | Compliance, audit trail на ключи, разделение прав |
| SSE-C | Клиент держит ключ, передаёт с запросом | Очень специфичные требования compliance |
| Client-side | Клиент шифрует до отправки | Когда S3 не должен видеть plaintext |
С 5 января 2023 AWS включает SSE-S3 по умолчанию для всех новых bucket. Ничего настраивать не нужно.
В UCP-стеке: SSE-S3 для большинства, SSE-KMS для финансовых/медицинских данных где нужен audit trail.
TLS in transit — отдельный аспект. Все запросы к S3 идут через HTTPS, ничего не настраивать.
Lifecycle policies
XML/JSON-правила на уровне bucket: «через N дней после создания — сделать действие». Действия:
- Transition в более холодный класс хранения (Standard → IA → Glacier).
- Expire (удалить).
- Abort incomplete multipart uploads (см. ниже).
- Expire non-current versions (удалить старые версии при versioning).
<LifecycleConfiguration>
<Rule>
<ID>logs-retention</ID>
<Filter><Prefix>logs/</Prefix></Filter>
<Status>Enabled</Status>
<Transition>
<Days>30</Days>
<StorageClass>STANDARD_IA</StorageClass>
</Transition>
<Transition>
<Days>90</Days>
<StorageClass>GLACIER</StorageClass>
</Transition>
<Expiration><Days>365</Days></Expiration>
</Rule>
<Rule>
<ID>cleanup-old-versions</ID>
<Status>Enabled</Status>
<NoncurrentVersionExpiration>
<NoncurrentDays>30</NoncurrentDays>
</NoncurrentVersionExpiration>
</Rule>
<Rule>
<ID>abort-incomplete-uploads</ID>
<Status>Enabled</Status>
<AbortIncompleteMultipartUpload>
<DaysAfterInitiation>7</DaysAfterInitiation>
</AbortIncompleteMultipartUpload>
</Rule>
</LifecycleConfiguration>
Это обязательный конфиг для любого production-bucket'а. Без него:
- Старые версии копятся бесконечно — стоимость хранения растёт.
- Incomplete multipart uploads (см. ниже) — невидимые в обычном листинге, но за них платится.
- Логи / временные файлы / экспорты — никогда не удаляются.
Presigned URLs — прямая загрузка/скачивание
Типичная задача: пользователь загружает аватарку. Наивная реализация — клиент → backend → S3 — гоняет файл через backend, нагружая канал и CPU.
Лучше — presigned URL: backend генерирует временную (TTL 5-15 минут) подписанную ссылку, клиент шлёт PUT напрямую в S3.
// backend
PresignedPutObjectRequest req = s3Presigner.presignPutObject(b -> b
.signatureDuration(Duration.ofMinutes(10))
.putObjectRequest(p -> p
.bucket("avatars")
.key("users/" + userId + "/avatar.jpg")
.contentType("image/jpeg")));
return req.url().toString();
// client
fetch(presignedUrl, {
method: 'PUT',
body: fileBlob,
headers: { 'Content-Type': 'image/jpeg' }
});
То же работает для скачивания: presignGetObject даёт ссылку, по которой клиент скачивает напрямую с S3. Подходит для шеринга приватных файлов с ограниченным временем доступа.
Безопасность
- TTL — минимально нужный. 10 минут на upload, 5 минут на download.
- В подпись включаются bucket, key, метод (PUT/GET), content-type. Клиент не может изменить что-либо из этого, иначе подпись невалидна.
- Дополнительно: размер файла можно ограничить через
conditionsв S3 POST policy.
Multipart upload — большие файлы
Для файлов > 100 MB рекомендуется multipart upload: файл бьётся на части (5 MB — 5 GB каждая), части грузятся параллельно и независимо, в конце — CompleteMultipartUpload собирает их в один объект.
1. InitiateMultipartUpload → uploadId
2. UploadPart (part 1, uploadId) → ETag-1
3. UploadPart (part 2, uploadId) → ETag-2
... параллельно
4. CompleteMultipartUpload (uploadId, [ETag-1, ETag-2, ...]) → объект собран
Преимущества:
- Параллельность — N частей одновременно, ограничено только bandwidth.
- Resume на ошибку — упала загрузка части 5 из 10? Можно ретраить только её.
- Single-request limit обход — без multipart S3 принимает максимум 5 GB в одном PUT. С multipart — до 5 TB.
Подводный камень: incomplete multipart uploads = деньги. Если процесс прервался между InitiateMultipart и Complete, части лежат в storage, видны через ListMultipartUploads, но не видны через ListObjects. Платится за них так же. Решение — lifecycle rule «abort incomplete after 7 days».
В AWS SDK v2 multipart прозрачен через S3TransferManager:
S3TransferManager tm = S3TransferManager.builder().s3Client(s3).build();
Upload upload = tm.uploadFile(b -> b
.source(Paths.get("big-video.mp4"))
.putObjectRequest(p -> p.bucket("videos").key("user-42/video.mp4")));
upload.completionFuture().join();
// SDK сам решает: multipart или regular put, в зависимости от размера
Что почитать дальше
- Spring + AWS SDK v2 — клиентский код.
- Operations — backup, replication, costs.
- AWS S3 docs — официальная документация.