**Спойлер: если у тебя нет lifecycle policy — у тебя есть бомба замедленного действия**

---

## Вступление: история одного диска

Каждый второй проект, который я видел за 15 лет, рано или поздно падал из-за "внезапно" закончившегося места на диске. Внезапно — в кавычках, потому что логи, превью, экспорты и "временные" файлы копились там месяцами. Просто никто не смотрел.

Временный файл в продакшене — это как гость, который "на пару дней". Через полгода он уже переставил мебель и качает права.

---

## Собака vs Ваш бэкенд

**Собака** закапывает кость, возвращается через неделю, съедает. Цикл замкнут.

**Ваш бэкенд** создаёт файл `export_temp_final_v2_FINAL.xlsx`, кладёт в `/tmp/uploads/`, отдаёт юзеру, и... всё. Файл остаётся. Навсегда. Через год их 847 000.

Собака умнее. Факт.

---

**Собака** метит территорию. Запах выветривается за пару дней. Природный TTL.

**Архитектор Мудаков М.М.** проектирует систему отчётов. TTL? "Потом добавим". Прошло 3 года. Отчёты за 2021 год всё ещё занимают 2 ТБ. "Ну а вдруг понадобятся".

---

## Типичный диалог в чате

```
DevOps Задрищенко: Диск на 94%. Опять.
Backend: Это не мы
DevOps: uploads/ занимает 380 гигов
Backend: Ну это временные файлы
DevOps: Временные за 3 года?
Backend: Ну мы не знали что их надо удалять
DevOps: ...
Backend: А можешь крон написать?
DevOps: Могу. Но ты написал 40 сервисов которые пишут хуй знает куда
Backend: Ну... в разные папки
DevOps: В КАКИЕ
Backend: Надо посмотреть в коде
```

---

## Что такое "временный файл" в продакшене

Давай определимся с терминологией, потому что "временный" — это растяжимое понятие:

| Тип | Примеры | Реальный TTL |
|-----|---------|--------------|
| **Процессинговые** | Загруженный файл до обработки | Минуты |
| **Кэш** | Ресайзнутые картинки, PDF превью | Часы-дни |
| **Экспорты** | Сгенерированные отчёты для скачивания | Часы |
| **Сессионные** | Файлы редактора, драфты | Дни |
| **"Временные"** | Всё что создал Иванов до увольнения | ∞ |

---

## MinIO: Lifecycle Policies — твой спаситель

MinIO (как и S3, от которого он API скопировал) умеет удалять файлы автоматически. Это называется **Lifecycle Configuration**. И это то, что ты должен был настроить ещё вчера.

### Базовая концепция

Lifecycle policy — это правило вида "файлы в этом бакете/префиксе старше N дней — удалить/переместить в другой storage class".

### Способ 1: Через mc (MinIO Client)

Установка (если ещё нет):

```bash
# Linux
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
mv mc /usr/local/bin/

# Или через brew на маке
brew install minio/stable/mc
```

Настройка алиаса:

```bash
mc alias set myminio https://minio.example.com ACCESS_KEY SECRET_KEY
```

Создание lifecycle правила через JSON:

```json
{
"Rules": [
{
"ID": "delete-temp-after-7-days",
"Status": "Enabled",
"Filter": {
"Prefix": "temp/"
},
"Expiration": {
"Days": 7
}
},
{
"ID": "delete-exports-after-24-hours",
"Status": "Enabled",
"Filter": {
"Prefix": "exports/"
},
"Expiration": {
"Days": 1
}
},
{
"ID": "delete-cache-after-30-days",
"Status": "Enabled",
"Filter": {
"Prefix": "cache/"
},
"Expiration": {
"Days": 30
}
}
]
}
```

Применение:

```bash
mc ilm import myminio/mybucket < lifecycle.json

# Проверить что применилось
mc ilm ls myminio/mybucket
```

### Способ 2: Через API (для автоматизации)

```python
from minio import Minio
from minio.lifecycleconfig import LifecycleConfig, Rule, Expiration, Filter

client = Minio(
"minio.example.com",
access_key="ACCESS_KEY",
secret_key="SECRET_KEY",
secure=True
)

# Правило: всё в temp/ удаляем через 7 дней
config = LifecycleConfig(
[
Rule(
"Enabled",
rule_id="delete-temp-files",
rule_filter=Filter(prefix="temp/"),
expiration=Expiration(days=7),
),
Rule(
"Enabled",
rule_id="delete-old-exports",
rule_filter=Filter(prefix="exports/"),
expiration=Expiration(days=1),
),
],
)

client.set_bucket_lifecycle("mybucket", config)

# Проверка
config = client.get_bucket_lifecycle("mybucket")
print(config)
```

### Способ 3: Через MinIO Console (для тех кто любит кликать)

1. Заходишь в MinIO Console → Buckets → твой бакет
2. Вкладка "Lifecycle"
3. "Add Lifecycle Rule"
4. Заполняешь: Prefix, Expiry Days
5. Save

Скриншоты не прикладываю — ты взрослый человек, разберёшься.

---

## Код с комментариями боли: типичный бэкенд

```python
# upload_service.py
# TODO: добавить удаление временных файлов
# UPD 2022-03: Петров сказал не приоритет
# UPD 2022-09: Петров уволился
# UPD 2023-01: диск закончился, добавили второй
# UPD 2023-06: второй тоже закончился
# UPD 2024-02: "а давайте в облако переедем"

def upload_temp_file(file):
filename = f"temp_{uuid4()}_{file.filename}"
path = f"uploads/temp/{filename}"

minio_client.put_object(
bucket_name="app-files",
object_name=path,
data=file,
length=file.size
)

return path
# Удаление? Какое удаление? Это же ВРЕМЕННЫЙ файл!
# Он сам удалится... наверное... когда-нибудь...
```

А вот как надо было сразу:

```python
# upload_service.py (версия для людей)

TEMP_PREFIX = "temp/" # Lifecycle policy удалит через 24 часа
EXPORTS_PREFIX = "exports/" # Lifecycle policy удалит через 7 дней
PERMANENT_PREFIX = "files/" # Это навсегда, тут осторожнее

def upload_temp_file(file) -> str:
"""
Загружает временный файл.

ВАЖНО: файлы с префиксом temp/ автоматически удаляются
через 24 часа по lifecycle policy. Если нужно дольше —
используй другой префикс или перемещай после обработки.
"""
filename = f"{uuid4()}_{secure_filename(file.filename)}"
path = f"{TEMP_PREFIX}{filename}"

minio_client.put_object(
bucket_name="app-files",
object_name=path,
data=file,
length=file.size,
metadata={
"uploaded-by": current_user.id,
"uploaded-at": datetime.utcnow().isoformat(),
"purpose": "temp-processing"
}
)

return path


def promote_to_permanent(temp_path: str, final_filename: str) -> str:
"""
Перемещает файл из временного хранилища в постоянное.
Вызывай после успешной обработки.
"""
if not temp_path.startswith(TEMP_PREFIX):
raise ValueError("Это не временный файл, чё ты делаешь")

permanent_path = f"{PERMANENT_PREFIX}{final_filename}"

# copy + delete, потому что MinIO не умеет rename
minio_client.copy_object(
bucket_name="app-files",
object_name=permanent_path,
source=CopySource("app-files", temp_path)
)
minio_client.remove_object("app-files", temp_path)

return permanent_path
```

---

## Структура бакетов: как не обосраться

### Антипаттерн (как делают все)

```
app-bucket/
├── file1.jpg
├── export_2023_01_15.xlsx
├── temp_abc123.pdf
├── users/
│ └── avatars/
├── отчет_финал_v3_FINAL.docx
├── image(1).png
├── image(2).png
└── хуй_знает_что_это.tmp
```

Один бакет, никакой структуры, lifecycle невозможен без префиксов — удалишь что-то нужное.

### Как надо

**Вариант А: Разные бакеты**

```
app-permanent/ # Lifecycle: никогда не удалять
├── users/
│ └── avatars/
└── documents/

app-temp/ # Lifecycle: удалять через 24 часа
└── processing/

app-exports/ # Lifecycle: удалять через 7 дней
└── reports/

app-cache/ # Lifecycle: удалять через 30 дней
├── thumbnails/
└── previews/
```

**Вариант Б: Один бакет, чёткие префиксы**

```
app-files/
├── permanent/ # Lifecycle: не трогать
│ ├── users/
│ └── documents/
├── temp/ # Lifecycle: 24 часа
├── exports/ # Lifecycle: 7 дней
└── cache/ # Lifecycle: 30 дней
```

Я предпочитаю Вариант А — меньше шансов проебаться с префиксами и удалить что-то важное.

---

## Lifecycle для разных сценариев

### Сценарий 1: Экспорт отчётов

Юзер жмёт "Выгрузить в Excel", система генерирует файл, даёт ссылку на скачивание. Ссылка должна работать 24 часа, потом файл не нужен.

```json
{
"Rules": [{
"ID": "expire-exports",
"Status": "Enabled",
"Filter": {"Prefix": "exports/"},
"Expiration": {"Days": 1}
}]
}
```

В коде:

```python
def generate_export(report_id: int) -> str:
data = get_report_data(report_id)
excel_file = create_excel(data)

filename = f"exports/report_{report_id}_{datetime.now():%Y%m%d_%H%M%S}.xlsx"

minio_client.put_object("app-exports", filename, excel_file, ...)

# Presigned URL на 24 часа (совпадает с lifecycle)
url = minio_client.presigned_get_object(
"app-exports",
filename,
expires=timedelta(hours=24)
)

return url
```

### Сценарий 2: Обработка загруженных файлов

Юзер загружает картинку → система ресайзит → результат сохраняется постоянно → оригинал больше не нужен.

```python
def process_upload(file) -> str:
# 1. Сохраняем оригинал во временное хранилище
temp_path = f"temp/{uuid4()}/{file.filename}"
minio_client.put_object("app-files", temp_path, file, ...)

try:
# 2. Обрабатываем
processed = resize_image(
minio_client.get_object("app-files", temp_path)
)

# 3. Сохраняем результат в постоянное хранилище
final_path = f"permanent/images/{uuid4()}.webp"
minio_client.put_object("app-files", final_path, processed, ...)

return final_path

finally:
# 4. Удаляем оригинал сразу (не ждём lifecycle)
minio_client.remove_object("app-files", temp_path)
```

Но! Если процессинг упадёт между шагами 1 и 4 — файл останется. Lifecycle подчистит через 24 часа. Подстраховка.

### Сценарий 3: Кэш превьюшек

Превью генерируются лениво, хранятся 30 дней. Если запросили — продлить жизнь.

Тут сложнее. MinIO lifecycle считает от даты создания, а не от последнего доступа. Варианты:

**Вариант А: Забить и пересоздавать**

```json
{
"Rules": [{
"ID": "expire-cache",
"Status": "Enabled",
"Filter": {"Prefix": "cache/"},
"Expiration": {"Days": 30}
}]
}
```

Если через 30 дней превью запросили — сгенерится заново. Для большинства случаев норм.

**Вариант Б: Touch через copy-to-self**

```python
def get_preview(file_id: str) -> bytes:
cache_key = f"cache/previews/{file_id}.webp"

try:
# Пробуем достать из кэша
obj = minio_client.get_object("app-files", cache_key)
data = obj.read()

# "Продлеваем жизнь" — копируем объект сам в себя
# Это обновляет LastModified, от которого считается lifecycle
minio_client.copy_object(
"app-files",
cache_key,
CopySource("app-files", cache_key)
)

return data

except S3Error as e:
if e.code == "NoSuchKey":
# Кэш-мисс, генерируем
data = generate_preview(file_id)
minio_client.put_object("app-files", cache_key, data, ...)
return data
raise
```

Осторожно: это дополнительная операция на каждый запрос. Для hot cache может быть дорого.

---

## Мониторинг: как не пропустить момент "ой бля"

### Метрики которые нужно собирать

```yaml
# prometheus/alerts.yml
groups:
- name: minio-storage
rules:
- alert: BucketSizeGrowing
expr: |
rate(minio_bucket_usage_total_bytes[24h]) > 1073741824
for: 1h
labels:
severity: warning
annotations:
summary: "Бакет {{ $labels.bucket }} растёт быстрее 1GB/день"

- alert: BucketTooBig
expr: |
minio_bucket_usage_total_bytes > 107374182400
for: 5m
labels:
severity: critical
annotations:
summary: "Бакет {{ $labels.bucket }} больше 100GB, разберись"

- alert: TempFilesAccumulating
expr: |
minio_bucket_usage_object_total{bucket="app-temp"} > 10000
for: 1h
labels:
severity: warning
annotations:
summary: "Больше 10к объектов в temp, lifecycle работает?"
```

### Скрипт для аудита

```bash
#!/bin/bash
# storage_audit.sh — запускай раз в неделю

echo "=== Storage Audit $(date) ==="

# Размер каждого бакета
for bucket in $(mc ls myminio | awk '{print $NF}'); do
size=$(mc du myminio/$bucket --json | jq -r '.size')
count=$(mc ls -r myminio/$bucket | wc -l)
echo "$bucket: $size bytes, $count objects"
done

# Самые старые файлы в temp (если есть — lifecycle не работает)
echo ""
echo "=== Oldest files in temp (should be empty if lifecycle works) ==="
mc ls -r myminio/app-temp --json | \
jq -r 'select(.lastModified) | "\(.lastModified) \(.key)"' | \
sort | head -20

# Самые большие файлы
echo ""
echo "=== Largest files ==="
mc ls -r myminio/ --json | \
jq -r 'select(.size) | "\(.size) \(.key)"' | \
sort -rn | head -20
```

---

## Расчёт убытков: сколько стоит похуизм

Возьмём типичный кейс. Компания "ИнновацииБлять LLC", 50 разработчиков, продукт работает 3 года.

**Входные данные:**
- 10 микросервисов
- Каждый генерирует ~100 МБ временных файлов в день
- Никто не настроил lifecycle

**Расчёт:**

```
10 сервисов × 100 МБ/день × 365 дней × 3 года = 1 095 000 МБ ≈ 1 ТБ

Хранение 1 ТБ в облаке:
- AWS S3: $23/мес × 36 мес = $828
- Собственное железо: ну купили диск за $100, потом ещё один

Но главное не это. Главное — инциденты:

Диск закончился в пятницу вечером:
- 3 часа даунтайма = потеря заказов
- При конверсии $10k/час = $30 000
- Ночная работа DevOps = $500 + выгорание

За 3 года таких инцидентов было 4:
4 × $30 000 = $120 000

Плюс:
- Время на расследование каждого инцидента: 8 человеко-часов
- 4 инцидента × 8 часов × $50/час = $1 600

ИТОГО: ~$122 000 за три года
```

**Стоимость решения:**

```
Настройка lifecycle: 2 часа DevOps × $50 = $100
Ревью через полгода: 1 час × $50 = $50

ИТОГО: $150
```

Экономия: **$121 850** и нервы команды.

---

## Частые ошибки

### Ошибка 1: "Сделаем крон который чистит"

```bash
# cleanup.sh
# "Гениальное" решение от Джуна Джуниорова

find /data/uploads -mtime +7 -delete
```

Проблемы:
- Работает только на одной ноде
- Не работает с object storage
- Упадёт если путь изменится
- Нет логов что удалилось
- Нет алертов если не отработал

Lifecycle в MinIO — это встроенный механизм, который работает на уровне хранилища. Не изобретай велосипед.

### Ошибка 2: TTL в названии файла

```python
# "Умное" решение
filename = f"temp_TTL-7d_{uuid4()}.pdf"
```

И потом крон парсит имена файлов? Серьёзно? Lifecycle policies существуют именно для этого.

### Ошибка 3: Удаление в коде приложения

```python
def process_and_cleanup(file_id):
process(file_id)
delete_temp_file(file_id) # А если process() упал?
```

Если между процессингом и удалением что-то падает — файл остаётся навсегда. Lifecycle — это страховка.

### Ошибка 4: Один бакет без префиксов

```python
# Всё в одну кучу
minio.put_object("files", f"{uuid4()}.pdf", data)
```

Теперь попробуй настроить lifecycle так, чтобы удалять только временные файлы. Правильно, никак без структурирования.

---

## Чек-лист для внедрения

### Для DevOps / Инфраструктуры

- [ ] Определить категории файлов и их TTL
- [ ] Создать отдельные бакеты или договориться о префиксах с разработкой
- [ ] Настроить lifecycle policies
- [ ] Добавить мониторинг размера бакетов
- [ ] Настроить алерты на аномальный рост
- [ ] Задокументировать соглашения в README/Confluence

### Для разработки

- [ ] Использовать правильные префиксы при сохранении файлов
- [ ] Не хардкодить пути — использовать константы/конфиги
- [ ] Добавить метаданные к файлам (кто загрузил, зачем, когда)
- [ ] Документировать какие типы файлов куда идут
- [ ] Удалять временные файлы сразу после использования (lifecycle — страховка, не основной механизм)

### Для менеджмента

- [ ] Выделить 1 день на настройку (окупится за неделю)
- [ ] Не говорить "потом сделаем" — сделайте сейчас
- [ ] Включить в Definition of Done для новых фич

---

## Альтернативы MinIO

Если ты не на MinIO:

**AWS S3** — lifecycle policies работают так же, синтаксис почти идентичный.

**Google Cloud Storage** — Object Lifecycle Management, похожая концепция.

**Azure Blob Storage** — Lifecycle management policies.

**Локальный диск** — используй tmpwatch/tmpreaper для Linux:

```bash
# Установка
apt install tmpreaper

# /etc/tmpreaper.conf
TMPREAPER_TIME=7d
TMPREAPER_DIRS="/var/app/uploads/temp"
```

Но серьёзно, если у тебя продакшен и локальный диск для файлов — у тебя проблемы посерьёзнее lifecycle.

---

## Финал

Правило простое: **каждый файл должен знать когда ему умирать**.

Если ты создаёшь файл и не определил его TTL — ты создал бессмертную сущность, которая будет жить вечно, занимать место и когда-нибудь уронит тебе прод в самый неподходящий момент.

Настройка lifecycle занимает 2 часа. Расследование "почему кончился диск в 3 ночи" — гораздо больше.

Собака закапывает кость и возвращается за ней. Или не возвращается — и кость разлагается. Естественный lifecycle.

Твой бэкенд должен работать так же.

---

*P.S. Если после прочтения этой статьи ты пошёл проверять свои бакеты и охуел от количества мусора — напиши в комменты сколько гигов нашёл. Люблю такие истории.*