Сквозной пример: контейнер в Cloud Run
Пройдём весь путь целиком: от файла на ноутбуке до публичного HTTPS-адреса, который сам поднимает копии под нагрузкой и сам гасит их до нуля в простое.
Cloud Run — сервис, который берёт готовый контейнер и превращает его в HTTPS-эндпоинт: масштабирование, TLS-сертификат и балансировка достаются вам бесплатно, а платите вы только за то время, пока контейнер реально обрабатывает запросы.
В предыдущих разделах мы разбирали сервисы по отдельности: проекты и биллинг, Compute Engine, Artifact Registry внутри Cloud Build, IAM. Теперь соберём из этих кубиков одно работающее приложение. Это тот самый сценарий, который чаще всего встречается на реальной работе: у команды есть Docker-образ, и его нужно выкатить так, чтобы он держал нагрузку, не требовал администрирования и не съедал бюджет по ночам, когда пользователей нет.
Зачем именно Cloud Run
Сравните два способа выкатить один и тот же контейнер. Виртуальная машина в Compute Engine: вы платите за неё круглосуточно, сами ставите Docker, сами настраиваете systemd, сами добываете TLS-сертификат, сами следите за автоскейлингом через managed instance group. Cloud Run: одна команда, через минуту у вас есть адрес вида https://hello-abc123-ew.a.run.app, а ночью, когда трафика нет, сервис масштабируется в ноль и счёт не растёт.
Плата в Cloud Run считается по vCPU-секундам и GiB-секундам памяти, потраченным на обработку запросов, плюс мелочь за сам факт запроса. Бесплатный лимит щедрый: порядка 2 миллионов запросов, 180 000 vCPU-секунд и 360 000 GiB-секунд в месяц. Пет-проект и учебный сервис в эти рамки укладываются целиком — то есть стоят ровно ноль.
Шаг 0. Подготовка проекта
Выбираем проект, регион и включаем нужные API. Без включённых API любая следующая команда честно ответит ошибкой API [run.googleapis.com] not enabled.
export PROJECT_ID=$(gcloud config get-value project)
export REGION=europe-west1
gcloud config set run/region $REGION
gcloud services enable \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com
Результат:
Operation "operations/acat.p2-..." finished successfully.
Шаг 1. Приложение
Возьмём минимальный веб-сервис на Flask. Важно: он читает порт из переменной окружения PORT и слушает 0.0.0.0, а не 127.0.0.1. Это контракт Cloud Run, и нарушение именно этого пункта — самая частая причина, по которой первый деплой не взлетает.
import os
from flask import Flask, jsonify
app = Flask(__name__)
@app.get("/")
def index():
return jsonify(
message="Привет из Cloud Run!",
service=os.environ.get("K_SERVICE", "local"),
revision=os.environ.get("K_REVISION", "local"),
)
@app.get("/heavy")
def heavy():
# намеренно тяжёлый обработчик — на нём будем смотреть автоскейлинг
total = sum(i * i for i in range(3_000_000))
return {"result": total}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
Переменные K_SERVICE и K_REVISION Cloud Run подставляет сам — по ним удобно понимать, какая именно ревизия обслуживает запрос.
Файл зависимостей:
flask==3.0.3
gunicorn==22.0.0
Шаг 2. Dockerfile
Встроенный сервер Flask в проде не используют — он однопоточный и не рассчитан на нагрузку. Запускаем через gunicorn.
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Cloud Run сам подставит PORT; жёстко зашивать 8080 в код нельзя
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
Почему --workers 1 --threads 8? Cloud Run сам решает, сколько копий контейнера запустить. Внутри одного контейнера нам нужна лишь конкурентность на уровне потоков — плодить процессы бессмысленно, они просто съедят память. А --timeout 0 отключает встроенный таймаут gunicorn: таймаутом запроса управляет сам Cloud Run.
Шаг 3. Репозиторий в Artifact Registry
Artifact Registry — это хранилище образов (наследник Container Registry). Cloud Run не умеет тянуть образы откуда угодно: они должны лежать в Artifact Registry, к которому у сервис-аккаунта есть доступ.
gcloud artifacts repositories create app-repo \
--repository-format=docker \
--location=$REGION \
--description="Образы приложений"
gcloud artifacts repositories list
Полное имя образа складывается по схеме РЕГИОН-docker.pkg.dev/ПРОЕКТ/РЕПОЗИТОРИЙ/ИМЯ:ТЕГ. Запомните её — половина ошибок деплоя это опечатка именно здесь.
Шаг 4. Сборка и пуш образа
Есть два пути. Первый — отдать сборку в облако, локальный Docker при этом не нужен вовсе:
export IMAGE=$REGION-docker.pkg.dev/$PROJECT_ID/app-repo/hello:v1
gcloud builds submit --tag $IMAGE .
Cloud Build упакует папку, соберёт образ на своей машине и сам положит его в Artifact Registry. Первые 120 минут сборки в сутки бесплатны.
Второй путь — собрать локально. Здесь есть ловушка для владельцев Mac на Apple Silicon: по умолчанию Docker соберёт образ под arm64, а Cloud Run работает на amd64, и контейнер упадёт с exec format error. Поэтому платформу указываем явно.
gcloud auth configure-docker $REGION-docker.pkg.dev
docker build --platform linux/amd64 -t $IMAGE .
docker push $IMAGE
Шаг 5. Деплой
gcloud run deploy hello \
--image=$IMAGE \
--region=$REGION \
--allow-unauthenticated \
--memory=512Mi \
--cpu=1 \
--concurrency=80 \
--min-instances=0 \
--max-instances=10
Результат:
Deploying container to Cloud Run service [hello] in project [my-project] region [europe-west1]
Building and deploying... Done.
Creating Revision... Done.
Routing traffic... Done.
Service [hello] revision [hello-00001-abc] has been deployed and is serving 100 percent of traffic.
Service URL: https://hello-abc123-ew.a.run.app
Проверяем:
SERVICE_URL=$(gcloud run services describe hello --region=$REGION --format="value(status.url)")
curl $SERVICE_URL
Результат:
{"message":"Привет из Cloud Run!","revision":"hello-00001-abc","service":"hello"}
Всё: у вас есть публичный HTTPS-эндпоинт с валидным сертификатом. Никакого nginx, никакого certbot, никакого сервера.
Шаг 6. Смотрим автоскейлинг вживую
Пока запросов нет, работает ноль инстансов. Дадим нагрузку на тяжёлый обработчик и посмотрим, что произойдёт. Утилита hey шлёт запросы в несколько потоков:
hey -z 30s -c 50 $SERVICE_URL/heavy
# логи сервиса
gcloud run services logs read hello --region=$REGION --limit=20
В метриках сервиса (Cloud Console → Cloud Run → hello → Metrics) вы увидите характерную картинку: график «Container instance count» прыгает с нуля до нескольких единиц, держится под нагрузкой и через несколько минут после её окончания снова уползает в ноль. Никто ничего не настраивал — это поведение по умолчанию.
Как это работает
Под капотом Cloud Run — это контейнерный контракт плюс автоскейлер.
- Контракт контейнера. Платформа запускает образ, передаёт переменную
PORTи ждёт, что процесс начнёт слушать HTTP на этом порту по адресу0.0.0.0. Не дождался за отведённое время — деплой откатывается, ревизия помечается как нерабочая. - Конкурентность. Параметр
--concurrency(по умолчанию 80) говорит, сколько запросов один инстанс обслуживает одновременно. Это ключевое отличие от «одна функция — один запрос» в классическом serverless: Cloud Run дешевле именно потому, что один контейнер тянет десятки одновременных запросов. - Автоскейлер смотрит на очередь входящих запросов и загрузку CPU. Не хватает мощности — поднимает новые инстансы вплоть до
--max-instances. Нагрузка ушла — гасит их, вплоть до нуля при--min-instances=0. - Холодный старт. Первый запрос после простоя ждёт, пока контейнер поднимется: обычно сотни миллисекунд, но толстый образ с тяжёлым фреймворком легко превращает это в 3–5 секунд.
- CPU выдаётся только на время запроса. Вне обработки запроса процессор у контейнера почти отбирают (CPU throttling). Фоновый поток, который вы запустили «в фоне после ответа», будет исполняться мучительно медленно или не исполнится вовсе. Для фоновой работы есть Cloud Tasks и Pub/Sub, а если задача действительно нужна внутри — флаг
--no-cpu-throttling(и он же переводит биллинг в режим оплаты за всё время жизни инстанса). - Ревизии неизменяемы. Каждый
gcloud run deployсоздаёт новую ревизию, трафик переключается на неё. Откат — это просто перевод трафика на предыдущую ревизию, что удобно для канареечных выкаток:gcloud run services update-traffic hello --to-revisions=hello-00002-xyz=10.
Частые ошибки
- «Container failed to start and listen on the port defined by the PORT variable». Классика номер один. Причины: порт зашит числом вместо чтения из
PORT; приложение слушает127.0.0.1(внутрь контейнера снаружи не достучаться); процесс упал на старте — смотрите логи ревизии, там будет настоящий traceback. exec format error. Образ собран под arm64 на Apple Silicon. Лечится флагом--platform linux/amd64или сборкой через Cloud Build.- Регион Artifact Registry не совпадает с регионом сервиса. Работать будет, но каждый холодный старт тянет образ через полмира — медленнее и с платой за межрегиональный трафик. Держите репозиторий и сервис в одном регионе.
--min-instances=1«чтобы не было холодных стартов». Это стоит денег: инстанс живёт 730 часов в месяц и тарифицируется даже в простое (по сниженной idle-ставке). Для учебного проекта это разница между «бесплатно» и «счёт каждый месяц». Включайте только там, где задержка реально критична.- Не выставлен
--max-instances. Значение по умолчанию — сотня инстансов. Один бесконечный цикл ретраев у клиента, случайный трафик от ботов или ошибка в коде, замедлившая обработчик, — и вы масштабируетесь в сто контейнеров, каждый из которых тикает счётчиком. Ставьте осознанный потолок: это ваш предохранитель. --allow-unauthenticatedна внутреннем API. Флаг делает сервис публичным для всего интернета. Для внутреннего сервиса уберите его и выдайте рольroles/run.invokerконкретному сервис-аккаунту.- Забытые образы в Artifact Registry. Каждый деплой оставляет слой в хранилище. Полгода активной разработки — это десятки гигабайт, за которые вы платите примерно $0,10 за гигабайт в месяц сверх бесплатных 0,5 ГБ. Настройте cleanup policy на репозитории.
Итоги
- Путь всегда один: код → Dockerfile → образ в Artifact Registry →
gcloud run deploy→ публичный URL. - Приложение обязано слушать
0.0.0.0на порту из переменнойPORT— это контракт платформы. - Собирать образ проще через
gcloud builds submit: не нужен локальный Docker и не будет проблемы с arm64. - Автоскейлинг работает из коробки: от нуля до
--max-instances, ориентируясь на очередь запросов и CPU. - Два флага решают судьбу вашего счёта:
--min-instances(сколько платить в простое) и--max-instances(потолок в пике). - Бесплатного лимита Cloud Run — около 2 млн запросов в месяц — обычному пет-проекту хватает с запасом.