Создавать ресурсы кликами в консоли удобно ровно до второго раза. Дальше начинаются вопросы: как воспроизвести то же самое в другом проекте? как понять, что вообще создано? как откатить изменение? Ответ — инфраструктура как код (IaC): инфраструктуру описывают текстовыми файлами, которые хранят в git, ревьюят и применяют. В GCP основной инструмент для этого — Terraform. Общие принципы IaC, не привязанные к облаку, разбирает статья про основы infrastructure as code; здесь — как это устроено в GCP.
План сравнивает три вещи, а не две: код с состоянием и состояние с реальностью. Поэтому он ловит правку мышью в консоли, и поэтому состояние нельзя терять и нельзя держать локально в команде.
Почему Terraform
У Google был свой инструмент описания инфраструктуры — Deployment Manager, но его не просто перестали развивать: поддержку прекратили 1 апреля 2026 года, а сам сервис отключат после 30 июня 2027-го, и Google предлагает переезжать на Infrastructure Manager или на что-то другое. Стандартом де-факто в GCP стал Terraform с официальным провайдером Google. Он управляет ресурсами десятков платформ на одном языке (HCL), поэтому знание переносимо: тот же Terraform описывает и AWS, и Kubernetes.
Одну вещь стоит знать заранее, потому что слово «открытый» применительно к Terraform сегодня уже неточно. В августе 2023 года HashiCorp сменила лицензию с открытой на BUSL, и сообщество отделило форк — OpenTofu, который остался полностью открытым и живёт под крылом Linux Foundation. По языку и командам он совместим: tofu init, tofu plan, tofu apply делают ровно то же самое. Часть команд перешла на OpenTofu именно из-за лицензии, так что встретить его в чужом проекте — обычное дело.
Идея простая: вы декларативно пишете, что должно существовать, а Terraform сам вычисляет, что для этого создать, изменить или удалить, сравнив ваше описание с текущим состоянием.
Провайдер и ресурсы
Провайдер (provider) — плагин, который учит Terraform работать с платформой. Для GCP его настраивают, указав проект и регион по умолчанию:
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 8.0"
}
}
}
variable "project_id" {
type = string
}
provider "google" {
project = var.project_id
region = "europe-west1"
}
Три детали в этом куске неочевидны, а без них он не заработает или подведёт позже. Переменную нужно объявить: увидев var.project_id без блока variable, Terraform откажется работать — такой переменной он не знает. Версию провайдера закрепляют явно: без этого у вас и у коллеги подтянутся разные версии, а очередное обновление провайдера может сломать конфигурацию, которая вчера применялась. И третье: если нужна возможность GCP, которой в обычном провайдере ещё нет, берут google-beta — тот же провайдер, но с ресурсами в статусе беты. Его подключают рядом с обычным и у нужного ресурса пишут provider = google-beta. Половина примеров из документации Google написана именно под него, и без этой строчки они не применяются.
Ресурс (resource) — единица инфраструктуры: сеть, подсеть, виртуальная машина, база. Каждый описывается блоком:
resource "google_compute_network" "main" {
name = "main-vpc"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "app" {
name = "app"
region = "europe-west1"
network = google_compute_network.main.id
ip_cidr_range = "10.0.1.0/24"
}
После terraform apply в проекте появятся ровно два ресурса — сеть main-vpc и подсеть app в europe-west1: на это уходит меньше минуты, и оба видны в консоли в разделе VPC networks. Ресурсы ссылаются друг на друга (google_compute_network.main.id), и Terraform сам понимает порядок создания — сеть раньше подсети. Команд несколько: terraform plan показывает, что изменится (это стоит читать всегда), terraform apply применяет, terraform destroy удаляет.
С чего начинается работа: init, fmt, validate
Перед plan и apply есть три команды, без которых ничего не поедет.
terraform init — подготовка каталога: скачивает провайдер (тот самый плагин для GCP), подтягивает модули, настраивает хранение состояния. Выполняется первым делом и повторно — каждый раз, когда поменяли версию провайдера, подключили модуль или изменили настройки хранения состояния. Забытый init после правки модуля — самая частая причина непонятных ошибок.
terraform fmt -recursive — приводит форматирование к единому виду. Ценность не в красоте, а в ревью: без него половина изменений в запросе — это переставленные пробелы.
terraform validate — проверяет синтаксис, типы и ссылки, не обращаясь в облако. Работает мгновенно и ловит опечатки в именах переменных и ресурсов до того, как вы потратите минуту на планирование.
terraform init
terraform fmt -recursive
terraform validate
terraform plan -out=tfplan
terraform apply tfplan
Первые три ставят в хук перед коммитом и в конвейер как первый шаг — это секунды, которые экономят минуты.
Переменные, модули, окружения
Список типичных ошибок ругает за жёстко вписанные значения и копирование описаний — вот инструменты, которыми это лечится.
Переменные вынимают значения из кода:
variable "project_id" { type = string }
variable "region" { type = string default = "europe-west4" }
variable "min_instances" { type = number default = 0 }
resource "google_cloud_run_v2_service" "orders" {
name = "orders"
location = var.region
project = var.project_id
template {
scaling { min_instance_count = var.min_instances }
}
}
Значения приходят из файла (terraform.tfvars), из переменных окружения (TF_VAR_project_id) или из аргументов командной строки — то есть один код разворачивается в разные проекты без правок.
Модули убирают копирование. Любая папка с файлами — модуль; вызывается блоком с указанием источника:
module "orders_service" {
source = "./modules/cloud-run-service"
name = "orders"
image = var.image
min_instances = 2
env = { SPRING_PROFILES_ACTIVE = "prod" }
}
Для общих модулей источник указывают с версией — из реестра или по тегу в репозитории; без версии обновление модуля автором меняет поведение у всех сразу.
Окружения делают отдельными каталогами со своим состоянием и общим модулем:
envs/
dev/ main.tf (module "app" { source = "../../modules/app" ... }) backend → dev/state
prod/ main.tf (тот же модуль с другими значениями) backend → prod/state
Почему так, а не рабочими областями (workspace): каталоги нельзя перепутать, у каждого своё состояние и свои права, и в конвейере видно, куда именно применяется изменение. Рабочие области удобны для мелочей и опасны для боевого контура — легко применить не туда.
И правило, которое важнее инструментов: отличия сред — это значения, а не разные описания. Как только у боевого контура появляется свой отдельный код, среды начинают расходиться.
Состояние (state) и где его хранить
Terraform помнит, что создал, в файле состояния (state) — карте соответствия «описание ↔ реальные ресурсы». Этот файл нельзя терять и нельзя держать локально при работе в команде, иначе двое применят изменения поверх друг друга.
Правильно — хранить state в общем месте. В GCP это делают в Cloud Storage: у Terraform есть backend gcs, который держит state в бакете с блокировкой, чтобы двое не применяли одновременно.
terraform {
backend "gcs" {
bucket = "my-tfstate"
prefix = "prod"
}
}
Нативные инструменты
Для большинства команд ответ по умолчанию — обычный Terraform: он переносим между облаками и лучше всего документирован. Но у GCP есть два родных подхода, о которых полезно знать:
- Config Connector — управление ресурсами GCP в стиле Kubernetes: вы описываете облачные ресурсы как объекты кластера. Удобно тем, кто и так живёт в GKE.
- Infrastructure Manager — управляемый сервис, который запускает ваш же Terraform на стороне Google, снимая заботу о хранении state и запуске.
Где это применяется
IaC — стандартный способ управлять инфраструктурой GCP в любом серьёзном проекте: инфраструктура лежит в git рядом с кодом, изменения проходят ревью в pull request, а применяет их конвейер доставки, а не человек руками в консоли. Это убирает «а кто и что там накликал» и делает инфраструктуру воспроизводимой.
Типичные ошибки новичков:
- держат state локально в командной работе — рассинхрон и затёртые изменения;
- применяют
applyне глядя вplan— и удаляют лишнее; - хардкодят значения вместо переменных — копипастят описания вместо переиспользования;
- кладут секреты прямо в
.tf-файлы и коммитят в git — секреты берут из Secret Manager, а не из кода.
Глубже: что на самом деле делает planрасширенное
Обычно говорят «план показывает, что изменится», и это упрощение, из которого не видно главного. План сравнивает три вещи:
- Код — как вы хотите.
- Состояние — как было в прошлый раз по мнению инструмента.
- Реальность — как сейчас в облаке (для этого план опрашивает провайдера).
Отсюда и главная ценность состояния: план обнаруживает изменения, сделанные руками в консоли. Кто-то поменял размер машины мышью — план покажет, что собирается вернуть как в коде. Кто-то удалил ресурс — план покажет, что собирается создать его заново.
terraform plan -refresh-only # только сверить состояние с реальностью, ничего не менять
Эта команда — правильный первый шаг при разборе расхождений: она показывает, чем реальность отличается от записанного, и предлагает обновить состояние.
Что делают с найденным расхождением: либо возвращают реальность к коду (обычный apply), либо принимают изменение в код (описывают то, что сделали руками), либо признают чужое владение — если свойство меняет другой механизм (автомасштабирование меняет число экземпляров), его исключают из управления через ignore_changes. Четвёртого варианта нет, и выбор делают осознанно.
Глубже: в состоянии лежат ваши секретырасширенное
Статья ругает за секреты в описании — и то же самое надо сказать про состояние, потому что там они оказываются неизбежно.
Состояние хранит значения всех атрибутов созданных ресурсов в открытом виде: сгенерированный пароль базы, содержимое созданного секрета, ключ служебной учётной записи, строку подключения. Инструмент обязан их помнить, чтобы сравнивать желаемое с текущим.
Отсюда правила, которые не обходятся:
- Бакет с состоянием закрывают строже остального: доступ только у конвейера и у узкой группы, версионирование включено, шифрование обязательно. Права на чтение этого бакета равны правам на все пароли описанной инфраструктуры.
- Состояние не коммитят в репозиторий и не пересылают.
- Пометка
sensitiveне шифрует — она лишь скрывает значение в выводе плана. Частое заблуждение. - Чувствительное лучше не создавать инфраструктурным кодом: пароль генерируют в хранилище секретов, а в коде ссылаются на него — тогда значение в состояние не попадает.
Глубже: заводим существующие ресурсы: importрасширенное
Статья написана под переход от кликов к коду, и без этого шага переход невозможен: в проекте уже всё накликано.
Импорт не создаёт и не меняет ресурс — он записывает существующий ресурс в состояние, после чего инструмент начинает считать его своим. Дальше вы описываете его в коде так, чтобы план показывал «изменений нет»: это и есть критерий успеха.
import {
to = google_storage_bucket.reports
id = "acme-orders-reports"
}
resource "google_storage_bucket" "reports" {
name = "acme-orders-reports"
location = "EU"
}
Проверка и применение:
terraform plan # покажет, что импортируется и чем описание отличается от реальности
terraform apply
Порядок, который работает на живом проекте: начинать с простого и мало от чего зависящего (бакеты, реестры, секреты), идти снизу вверх по зависимостям (сначала сеть, потом то, что в ней), по одному ресурсу за раз до нулевого плана, важное сразу защищать от удаления (prevent_destroy), а что-то осознанно оставлять вне кода. И общая стратегия: всё новое сразу в коде, старое импортируем по мере того, как к нему приходится прикасаться.
Глубже: блокировка состояния и если apply упал посерединерасширенное
Два практических вопроса про состояние, ответы на которые нужны в неудачный момент.
Блокировка. Пока идёт применение, состояние заблокировано — иначе два одновременных применения его испортят. В хранилище объектов GCP блокировка встроена (отдельная таблица, как в некоторых других облаках, не нужна): инструмент создаёт служебный объект рядом с файлом состояния. Блокировка держится всё время применения — то есть минуты, а для большой конфигурации и десятки минут.
Применение прервалось. Упал исполнитель, оборвалась сеть, нажали отмену. Два следствия. Во-первых, часть ресурсов уже создана: инструмент применяет изменения по одному, и состояние отражает то, что успело пройти. Повторный apply продолжит с того места — ничего страшного в этом нет, и это одна из причин не паниковать.
Во-вторых, блокировка могла остаться. Тогда следующий запуск сообщит об этом и назовёт идентификатор:
terraform force-unlock <lock-id>
Делают это только убедившись, что применение действительно не идёт (посмотреть в журнале конвейера, спросить коллег). Снятая блокировка при работающем применении означает два одновременных процесса и испорченное состояние — а это худшее, что может случиться, потому что состояние нельзя восстановить из кода.
И страховка на этот случай: версионирование бакета с состоянием. Испортили — восстановили предыдущую версию файла. Эта настройка не «хорошая практика», а обязательная.
Коротко
- Deployment Manager закрыт, поэтому в GCP описывают инфраструктуру Terraform или его открытым форком OpenTofu: декларативно, что должно существовать.
- Провайдер google настраивают проектом и регионом, переменные объявляют явно, ресурсы ссылаются друг на друга, и по ссылкам строится порядок создания.
- Перед plan и apply идут init, fmt и validate: их ставят в хук перед коммитом и первым шагом конвейера.
- Переменные вынимают значения из кода, модули убирают копирование и берутся с версией, окружения делают отдельными каталогами со своим состоянием, а не рабочими областями; отличия сред это значения, а не описания.
- Состояние хранят в бакете Cloud Storage через backend gcs с блокировкой и версионированием; локальный файл в команде даёт затёртые изменения.
- План сравнивает код, состояние и реальность, поэтому ловит правки руками; с расхождением либо возвращают реальность к коду, либо принимают в код, либо признают чужое владение.
- В состоянии лежат секреты в открытом виде: бакет закрывают строже остального, состояние не коммитят, sensitive не шифрует, чувствительное создают в хранилище секретов, а не кодом.
- Существующее заводят импортом до пустого плана, снизу вверх по зависимостям; упавший apply оставляет часть ресурсов и иногда блокировку, которую снимают только убедившись, что применение не идёт.
- Config Connector управляет ресурсами из Kubernetes, Infrastructure Manager запускает ваш Terraform на стороне Google.
Что почитать дальше
- Основы инфраструктуры как кода — state, идемпотентность и дрейф без привязки к провайдеру.
- Terraform — тот же инструмент подробно: провайдеры, count и for_each, модули.
- Принципы конвейера доставки — как устроено применение из конвейера.
- Безопасность и наблюдаемость в GCP — хранилище секретов, на которое ссылаются из кода.