Cloud Run: контейнеры без серверов
Вы отдаёте Google контейнер — он сам поднимает его на HTTP-запрос, сам масштабирует под нагрузку и сам гасит до нуля, когда трафика нет. Разбираемся, почему Cloud Run стал главной вычислительной платформой GCP.
Cloud Run — управляемая платформа, которая запускает ваш контейнер как HTTP-сервис: сама создаёт инстансы под нагрузку, сама убивает их при простое и берёт деньги только за то время, когда контейнер реально что-то делал.
Какую боль он снимает
Представьте типичный путь небольшого API. Сначала вы поднимаете VM. Потом настраиваете systemd, nginx, сертификаты Let's Encrypt, автообновления. Потом приходит нагрузка — и вы городите группу инстансов с автоскейлером и балансировщиком. Потом нагрузка уходит, а счёт остаётся, потому что три машины крутятся вхолостую всю ночь.
Cloud Run убирает весь этот слой. Ваша ответственность заканчивается на контейнере. Всё остальное — HTTPS-домен с валидным сертификатом, балансировка, масштабирование, перезапуск упавших инстансов, деплой без даунтайма — берёт на себя платформа.
Почему именно контейнер, а не «функция»? Потому что контейнер — это честный универсальный интерфейс. Любой язык, любая версия рантайма, любые системные библиотеки, ImageMagick или ffmpeg внутри — Cloud Run всё равно, что там. Вы не привязаны к платформе: тот же образ запустится в Kubernetes, в Docker на ноутбуке или у другого провайдера. Именно за это Cloud Run и любят.
Контракт контейнера: всего два правила
Чтобы контейнер поехал в Cloud Run, он должен соблюдать ровно два условия:
- Слушать HTTP на порту из переменной окружения
PORT(по умолчанию 8080). - Слушать на
0.0.0.0, а не на127.0.0.1— иначе снаружи в контейнер никто не достучится.
Вот минимальное приложение на Python с Flask. Обратите внимание на чтение PORT — это не украшение, а часть контракта:
import os
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return {"status": "ok", "revision": os.environ.get("K_REVISION", "local")}
if __name__ == "__main__":
port = int(os.environ.get("PORT", 8080))
app.run(host="0.0.0.0", port=port)
И Dockerfile к нему. В проде обычно запускают через gunicorn, а не через встроенный сервер Flask:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PORT=8080
CMD exec gunicorn --bind 0.0.0.0:$PORT --workers 1 --threads 8 main:app
Деплой: две команды на выбор
Если у вас уже есть собранный образ, вы указываете его напрямую. Образы Cloud Run берёт из Artifact Registry — реестра контейнеров внутри GCP:
gcloud run deploy api \
--image=us-central1-docker.pkg.dev/my-project/apps/api:v1 \
--region=us-central1 \
--allow-unauthenticated \
--memory=512Mi \
--cpu=1
А если образа нет, Cloud Run соберёт его сам прямо из вашей папки с кодом — через Cloud Build. Есть Dockerfile — соберёт по нему; нет Dockerfile — определит язык и соберёт образ через Buildpacks:
gcloud run deploy api --source . --region=us-central1 --allow-unauthenticated
Результат:
Building using Dockerfile and deploying container to Cloud Run service [api]
✓ Building and deploying new service... Done.
Service [api] revision [api-00001-abc] has been deployed and is serving 100 percent of traffic.
Service URL: https://api-7x4k2mnq3a-uc.a.run.app
Вы получили работающий HTTPS-эндпоинт с валидным сертификатом. Никаких nginx, никакого certbot. Флаг --allow-unauthenticated означает «сервис публичный»; без него Cloud Run будет требовать IAM-токен и отвечать 403 всем чужим.
Масштабирование до нуля и concurrency
Вот здесь Cloud Run принципиально отличается от классического serverless вроде AWS Lambda. Один инстанс Cloud Run умеет обрабатывать несколько запросов одновременно — по умолчанию до 80.
Это огромная разница в деньгах. Если ваш сервис в основном ждёт ответа от базы (то есть простаивает на I/O), сотня одновременных пользователей может уместиться в два инстанса, а не в сотню. Формула простая:
нужно инстансов ≈ (запросов в секунду × среднее время ответа) / concurrency
500 запросов в секунду при ответе за 200 мс и concurrency 80 дают всего ≈ 1,25 → два инстанса. А когда трафик прекращается, Cloud Run гасит всё до нуля — и вы не платите вообще ничего.
| Флаг | Что делает |
--concurrency=80 | Сколько запросов один инстанс обрабатывает параллельно. Ставьте 1, если код не потокобезопасен или жрёт много CPU на запрос. |
--min-instances=0 | Минимум «прогретых» инстансов. 0 — экономия, но первый запрос после простоя ловит холодный старт. |
--max-instances=100 | Потолок масштабирования. Ваш главный предохранитель от разорительного счёта. |
--timeout=300 | Максимальное время обработки запроса, секунды. По умолчанию 5 минут, максимум — 60. |
--cpu, --memory | Ресурсы одного инстанса. Их произведение на время и есть основа счёта. |
Сколько это стоит
Cloud Run считает деньги за vCPU-секунды и GiB-секунды, потраченные на обработку запросов, плюс копейки за миллион запросов. Бесплатный уровень щедрый и обновляется каждый месяц: порядка 2 миллионов запросов, 180 000 vCPU-секунд и 360 000 GiB-секунд. Небольшой пет-проект, личный API или бот у вас реально будут работать за 0 долларов.
Ключевой момент: при стандартной модели (CPU выделяется только на время запроса) простаивающий инстанс, который ещё не потушен, почти ничего не стоит. Платите вы за работу, а не за аренду.
Как это работает
Каждый инстанс запускается в песочнице gVisor — облегчённом слое, который перехватывает системные вызовы контейнера. Это даёт изоляцию уровня VM при скорости запуска контейнера, но и объясняет мелкие странности: некоторые низкоуровневые syscalls там не работают.
Дальше три факта, из которых растут почти все ошибки новичков:
- CPU отбирают между запросами. Как только вы вернули ответ, процессор инстанса дросселируется почти до нуля. Фоновый поток, который вы запустили «доделать потом», просто зависнет — до следующего входящего запроса. Лечится флагом
--no-cpu-throttling(тогда платите за всё время жизни инстанса) или, что правильнее, выносом фоновой работы в Cloud Tasks или Pub/Sub. - Файловая система — это память. Всё, что вы пишете в
/tmp, лежит в RAM инстанса и учитывается в лимите памяти. Скачали 400-мегабайтный файл в контейнер с лимитом 512 Mi — получите OOM. - Инстансы смертны. Cloud Run в любой момент может погасить инстанс, прислав
SIGTERM. Хранить сессии пользователей или кэш в локальной памяти нельзя — состояние живёт в Redis (Memorystore), базе или Cloud Storage.
Каждый деплой создаёт новую ревизию — неизменяемый снимок «образ + конфигурация». Трафик можно переключать между ревизиями плавно, что даёт бесплатные canary-выкатки:
# 10% трафика на новую ревизию, 90% на старую
gcloud run services update-traffic api \
--to-revisions=api-00002-xyz=10,api-00001-abc=90 \
--region=us-central1
Частые ошибки
- «Container failed to start and listen on the port». Ошибка №1 всех новичков. Причина почти всегда одна: приложение слушает
127.0.0.1вместо0.0.0.0, либо порт захардкожен и не читается изPORT. - Фоновая работа после ответа. Отправили ответ клиенту и в фоне пишете отчёт? Он не допишется — CPU уже забрали. Ставьте
--no-cpu-throttlingили выносите задачу в очередь. - Не выставлен
--max-instances. Это ошибка, которая стоит денег. Бот-атака, петля ретраев или кривой цикл в клиенте — и Cloud Run честно поднимет сотню инстансов, потому что вы его об этом попросили. Всегда ставьте вменяемый потолок. --min-instances«чтобы не было холодного старта». Один прогретый инстанс работает 24/7 и оплачивается 24/7 — вы фактически вернулись к аренде сервера. Для внутреннего сервиса, к которому обращаются трижды в день, это выброшенные деньги.- Состояние в памяти. Кэш, счётчик, сессия в переменной приложения ведут себя непредсказуемо: инстансов несколько, и запрос попадает в случайный.
- Тяжеленный образ. Базовый
python:3.12вместоpython:3.12-slim— это лишние сотни мегабайт, которые платформа тянет на каждом холодном старте. Чем меньше образ, тем быстрее старт. - Забыли
--allow-unauthenticated. Сервис задеплоился, URL есть, но браузер отдаёт 403. Это не баг — это включённая по умолчанию IAM-авторизация.
Итоги
- Cloud Run запускает ваш контейнер как HTTPS-сервис: масштабирование, сертификаты, балансировка и перезапуски — на стороне Google.
- Контракт контейнера прост: слушать
0.0.0.0на порту из$PORT. Всё остальное — ваше дело. gcloud run deploy --source .соберёт образ и выкатит сервис одной командой, даже без Dockerfile.- Один инстанс держит до 80 запросов одновременно — это резко дешевле «одна функция = один запрос» и главная причина, почему Cloud Run вытесняет функции.
- Масштабирование до нуля означает нулевой счёт при отсутствии трафика; бесплатный лимит покрывает 2 млн запросов в месяц.
- Между запросами CPU дросселируется,
/tmp— это RAM, а инстанс могут убить в любой момент. Проектируйте сервис stateless. - Всегда задавайте
--max-instances: это ваш предохранитель от счёта на четырёхзначную сумму.