Cloud Build: CI/CD
Настраиваем конвейер: разработчик делает git push — Google Cloud сам гоняет тесты, собирает образ и выкатывает новую версию в Cloud Run.
Cloud Build — управляемый сервис CI/CD в Google Cloud. Он берёт ваш исходный код, запускает над ним последовательность шагов (каждый шаг — контейнер) и складывает результат туда, куда вы скажете: в Artifact Registry, в Cloud Run, в бакет.
Зачем это на практике
Ручной деплой выглядит невинно: docker build, docker push, gcloud run deploy. Три команды, минута работы. Пока не выясняется, что:
- Маша собрала образ на M1, и он не запустился на amd64;
- Петя забыл прогнать тесты, потому что «правка на одну строчку»;
- в проде крутится образ, собранный из незакоммиченного кода, и никто не знает, из какого именно;
- деплоить умеет только тот, у кого настроен доступ, — а он в отпуске.
CI/CD убирает человека из повторяемой части. Сборка всегда в одном окружении, тесты пропустить нельзя, образ помечен хешем коммита — по нему всегда видно, какой код сейчас в проде.
Про деньги сразу: у Cloud Build есть бесплатный лимит (порядка 2500 build-минут в месяц на аккаунт), дальше — поминутная оплата, и она зависит от типа машины. Дефолтная машина дешёвая; если вы поставите machineType: E2_HIGHCPU_32 «чтобы быстрее», минута подорожает в разы. Актуальный прайс всегда проверяйте на странице Cloud Build Pricing.
cloudbuild.yaml: шаги — это контейнеры
Главная идея Cloud Build: каждый шаг сборки — это запуск Docker-образа. Нужен Node — берёте образ node. Нужен gcloud — берёте cloud-sdk. Нужен свой инструмент — берёте свой образ. Все шаги работают в общей папке /workspace, куда Cloud Build распаковал ваш код.
steps:
# 1. Тесты. Если упадут — сборка остановится здесь.
- name: 'python:3.12-slim'
id: test
entrypoint: bash
args:
- '-c'
- |
pip install --quiet -r requirements.txt
pytest -q
# 2. Сборка образа. Тег — короткий хеш коммита.
- name: 'gcr.io/cloud-builders/docker'
id: build
args:
- 'build'
- '-t'
- 'europe-west1-docker.pkg.dev/$PROJECT_ID/apps/shop-api:$SHORT_SHA'
- '.'
# 3. Публикация в Artifact Registry.
- name: 'gcr.io/cloud-builders/docker'
id: push
args:
- 'push'
- 'europe-west1-docker.pkg.dev/$PROJECT_ID/apps/shop-api:$SHORT_SHA'
# 4. Деплой новой ревизии в Cloud Run.
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
id: deploy
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- 'shop-api'
- '--image=europe-west1-docker.pkg.dev/$PROJECT_ID/apps/shop-api:$SHORT_SHA'
- '--region=europe-west1'
options:
logging: CLOUD_LOGGING_ONLY
timeout: 900s
Разберём неочевидное.
$PROJECT_ID и $SHORT_SHA — подстановки (substitutions). Cloud Build подставляет их сам: ID проекта и первые 7 символов хеша коммита. Есть ещё $COMMIT_SHA, $BRANCH_NAME, $TAG_NAME, $BUILD_ID. Свои переменные объявляют с подчёркиванием: _ENV, _REGION.
Тег :$SHORT_SHA вместо :latest — не эстетика, а необходимость. С latest вы не сможете ответить на вопрос «какая ревизия сейчас в проде и из какого она коммита», и не сможете откатиться на конкретную сборку.
entrypoint переопределяет команду образа. У cloud-sdk точка входа — не gcloud, поэтому её указывают явно.
timeout: 900s — общее время сборки. По умолчанию всего 10 минут, и сборка тяжёлого фронтенда в них не влезает; ошибка выглядит как загадочный обрыв на середине.
Запуск: вручную и по коммиту
Проверить конвейер локально можно одной командой — она заархивирует папку, зальёт в облако и запустит сборку:
gcloud builds submit --config cloudbuild.yaml .
# посмотреть историю сборок
gcloud builds list --limit=5
# почитать лог конкретной сборки
gcloud builds log BUILD_ID
Настоящий CI/CD начинается с триггера — правила «при таком-то событии в репозитории запусти такую-то сборку». Репозиторий подключают один раз через Cloud Build → Repositories (для GitHub это установка приложения Google Cloud Build), дальше:
gcloud builds triggers create github \
--name=shop-api-main \
--repo-name=shop-api \
--repo-owner=my-org \
--branch-pattern='^main$' \
--build-config=cloudbuild.yaml
Типичная схема для команды: пуш в main — тесты, сборка, деплой в prod; пул-реквест — только тесты и сборка (--pull-request-pattern), без деплоя. Так ветка не может выкатиться в прод, минуя ревью.
Права: где спотыкаются все
Cloud Build работает не от вашего имени, а от сервисного аккаунта сборки. Ему нужно выдать ровно те роли, которые нужны шагам конвейера:
| Роль | Зачем |
roles/artifactregistry.writer | чтобы docker push прошёл |
roles/run.admin | чтобы создать новую ревизию Cloud Run |
roles/iam.serviceAccountUser | чтобы «подставить» сервисный аккаунт, под которым бежит сам сервис |
roles/logging.logWriter | чтобы писать логи сборки |
PROJECT_ID=$(gcloud config get-value project)
BUILD_SA="cloudbuild-runner@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${BUILD_SA}" \
--role="roles/run.admin"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${BUILD_SA}" \
--role="roles/iam.serviceAccountUser"
Если в логе сборки вы видите Permission 'iam.serviceaccounts.actAs' denied — забыли третью строку таблицы. Это ошибка номер один у всех, кто настраивает деплой в Cloud Run впервые.
Как это работает
Когда триггер срабатывает, Cloud Build выделяет одноразовую виртуальную машину (worker). На неё выкачивается ваш коммит, содержимое кладётся в /workspace. Дальше сервис по очереди запускает шаги: для каждого — docker run нужного образа с /workspace, примонтированным внутрь.
Отсюда следуют неочевидные, но важные вещи.
- Состояние между шагами живёт только в
/workspace. Установили пакеты в/usr/localна шаге 1 — на шаге 2 их нет: это другой контейнер. Всё, что должно пережить шаг, кладите в рабочую папку. - Шаги по умолчанию последовательны. Первый же ненулевой код возврата останавливает сборку — поэтому шаг с тестами и ставят первым.
- Машина одноразовая. Кеша слоёв Docker между сборками нет по умолчанию, и «холодная» сборка каждый раз тянет зависимости заново. Отсюда — минуты и деньги. Лечится либо Kaniko-кешем, либо шагом
docker pull ... :latestс флагом--cache-from, либо аккуратным Dockerfile, где слой с зависимостями идёт до слоя с кодом. - Всё, что лежит в папке, уезжает в облако. Файл
.gcloudignoreработает как.gitignore: без негоnode_modulesи.venvбудут заливаться при каждомbuilds submit.
Частые ошибки
- Тег
:latestв проде. Невозможно понять, что развёрнуто, и невозможно откатиться. Всегда$SHORT_SHAили семантическая версия. - Забытый
iam.serviceAccountUser. Сборка проходит, деплой падает наactAs denied. - Дорогая машина «на всякий случай».
machineType: E2_HIGHCPU_32на сборке, которая упирается в скачивание npm-пакетов, ускорит её на 10%, а счёт умножит в разы. Сначала чините кеш, потом — железо. - Секреты в
cloudbuild.yaml. Файл лежит в git. Пароли и токены — только через Secret Manager (availableSecrets), никогда вargsили вsubstitutions. - Деплой с любой ветки.
--branch-pattern='.*'означает, что эксперимент коллеги приедет в прод. Ограничивайте паттерн. - Нет
timeout. Дефолтные 10 минут обрывают сборку на середине, и в логах это выглядит как случайный сбой. - Зависшие триггеры на удалённых ветках. Сборка, которая гоняется по кругу из-за ошибки в конфиге, тихо жжёт build-минуты. Держите бюджетный алерт на проекте.
Итоги
- Cloud Build — CI/CD, где каждый шаг сборки является контейнером, а общая память шагов — папка
/workspace. cloudbuild.yamlописывает конвейер: тесты →docker build→pushв Artifact Registry →gcloud run deploy.- Подстановки
$PROJECT_ID,$SHORT_SHA,$BRANCH_NAMEдают воспроизводимые теги образов и возможность отката. - Триггер связывает пуш в ветку со сборкой; PR-триггер гоняет тесты без деплоя.
- Сервисному аккаунту сборки нужны
artifactregistry.writer,run.adminиiam.serviceAccountUser— последняя роль забывается чаще всего. - Бесплатный лимит есть, но кеш сборки и
.gcloudignoreэкономят и минуты, и деньги.