Создавать ресурсы кликами в консоли удобно ровно до второго раза. Дальше начинаются вопросы: как воспроизвести то же самое в другом проекте? как понять, что вообще создано? как откатить изменение? Ответ — инфраструктура как код (IaC): инфраструктуру описывают текстовыми файлами, которые хранят в git, ревьюят и применяют. В GCP основной инструмент для этого — Terraform. Общие принципы IaC, не привязанные к облаку, разбирает статья про основы infrastructure as code; здесь — как это устроено в GCP.

plan код как вы хотите состояние в бакете как было по мнению инструмента реальность в облаке как сейчас apply после чтения плана

План сравнивает три вещи, а не две: код с состоянием и состояние с реальностью. Поэтому он ловит правку мышью в консоли, и поэтому состояние нельзя терять и нельзя держать локально в команде.

Обязательно

Почему 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расширенное

Обычно говорят «план показывает, что изменится», и это упрощение, из которого не видно главного. План сравнивает три вещи:

  1. Код — как вы хотите.
  2. Состояние — как было в прошлый раз по мнению инструмента.
  3. Реальность — как сейчас в облаке (для этого план опрашивает провайдера).

Отсюда и главная ценность состояния: план обнаруживает изменения, сделанные руками в консоли. Кто-то поменял размер машины мышью — план покажет, что собирается вернуть как в коде. Кто-то удалил ресурс — план покажет, что собирается создать его заново.

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.

Что почитать дальше