Terraform для GCP
Почему инфраструктуру в облаке пишут кодом, а не собирают мышкой, — и как выглядит первый рабочий проект Terraform для GCP.
Terraform — инструмент «инфраструктура как код» (IaC, Infrastructure as Code): вы описываете желаемое состояние облака в текстовых файлах, а Terraform сам вычисляет, что нужно создать, изменить или удалить, чтобы реальность совпала с описанием.
Зачем это на практике: три беды кликов
Пока проект маленький, консоль Google Cloud кажется удобной: нажал «Создать бакет», выбрал регион, поставил галочку — готово. Проблемы начинаются позже, и всегда одни и те же.
Беда первая — невоспроизводимость. Через полгода вам нужен второй такой же контур (staging рядом с prod). Вы открываете консоль и понимаете, что не помните: какой класс хранилища был у бакета? включён ли uniform access? сколько инстансов у Cloud Run минимум? Приходится сравнивать вручную, и staging всё равно получается «почти таким же». А «почти такой же» стенд — это стенд, на котором баг не воспроизводится.
Беда вторая — нет ревью и истории. Коллега открыл настройки сервиса, поменял переменную окружения, ушёл в отпуск. Прод лёг. Кто менял? Что именно? Audit Logs, конечно, ответят — но это расследование, а не защита. Код в git даёт и то и другое: изменение приходит пул-реквестом, его читают до применения.
Беда третья — дрейф. Реальная конфигурация постепенно расходится с тем, что все думают о ней. Terraform умеет этот дрейф ловить: он сравнивает описание с фактом и показывает разницу.
Terraform бесплатен (open source), платите вы только за сами ресурсы, которые он создаёт. То есть это редкий случай, когда инструмент ничего не стоит, а экономит — много: главным образом на «забытых» ресурсах, о которых речь ниже.
Из чего состоит проект Terraform
Проект — это просто папка с файлами .tf. Terraform читает их все скопом, порядок и имена файлов роли не играют, поэтому раскладку выбирают по смыслу:
| Файл | Что обычно внутри |
main.tf | настройки Terraform, провайдер, ресурсы |
variables.tf | входные параметры (project_id, регион, размер машины) |
outputs.tf | что вернуть наружу: URL сервиса, имя бакета |
terraform.tfvars | значения переменных для конкретного контура |
Язык называется HCL. Он декларативный: вы не пишете «сначала создай бакет, потом сервис», вы описываете, что должно существовать.
Провайдер google и аутентификация
Провайдер — плагин, который знает, как разговаривать с API конкретного облака. Для GCP это hashicorp/google. Блок terraform фиксирует его версию, чтобы у вас и у коллеги подтянулся один и тот же плагин.
terraform {
required_version = ">= 1.5"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 6.0"
}
}
backend "gcs" {
bucket = "tfstate-my-shop"
prefix = "prod"
}
}
provider "google" {
project = var.project_id
region = "europe-west1"
}
Откуда провайдер берёт доступ к вашему проекту? Из ADC — Application Default Credentials. Локально их выдаёт одна команда:
gcloud auth application-default login
# проверяем, что Terraform видит нужный проект
gcloud config get-value project
В CI логиниться человеком нельзя. Там используют сервисный аккаунт: либо Workload Identity Federation (без файла ключа — правильный путь), либо, если совсем никак, JSON-ключ в секрете. Скачанный ключ сервисного аккаунта в репозитории — классическая утечка, за которую потом платят чужие майнеры.
Ресурсы и ссылки между ними
Каждый ресурс — блок resource "тип" "локальное_имя". Локальное имя нужно только внутри Terraform, чтобы ссылаться на ресурс из других мест.
variable "project_id" {
type = string
description = "ID проекта GCP"
}
resource "google_storage_bucket" "uploads" {
name = "${var.project_id}-uploads"
location = "EUROPE-WEST1"
uniform_bucket_level_access = true
force_destroy = false
lifecycle_rule {
condition {
age = 30
}
action {
type = "Delete"
}
}
}
resource "google_cloud_run_v2_service" "api" {
name = "shop-api"
location = "europe-west1"
template {
containers {
image = "europe-west1-docker.pkg.dev/${var.project_id}/apps/shop-api:latest"
env {
name = "UPLOADS_BUCKET"
value = google_storage_bucket.uploads.name
}
}
}
}
output "bucket_name" {
value = google_storage_bucket.uploads.name
}
Обратите внимание на строку value = google_storage_bucket.uploads.name. Мы не вписали имя бакета руками — мы сослались на ресурс. Из таких ссылок Terraform строит граф зависимостей и сам понимает: бакет надо создать раньше сервиса. Никаких «сначала», «потом» писать не нужно.
State: файл, который нельзя потерять
State — файл, в котором Terraform хранит соответствие «мой блок
google_storage_bucket.uploads= вот этот реальный бакет в облаке».
Без state Terraform не отличил бы «создать новый бакет» от «этот бакет я уже создавал, надо его изменить». Отсюда два железных правила.
Правило первое: state не хранится в git. В нём лежат plain-text значения — в том числе пароли к базе, если вы их создавали Terraform'ом. Плюс два человека, работающие с локальными копиями state, гарантированно передерутся.
Правило второе: state лежит в удалённом backend. Для GCP это бакет GCS — блок backend "gcs" из примера выше. Бонусом GCS даёт блокировку: пока один apply идёт, второй не стартует. Бакету со state обязательно включают версионирование — это ваша страховка.
gsutil mb -l europe-west1 gs://tfstate-my-shop
gsutil versioning set on gs://tfstate-my-shop
Цикл работы: init, plan, apply
# 1. скачать провайдер и подключить backend (один раз на машине)
terraform init
# 2. посмотреть, что будет сделано, и сохранить план в файл
terraform plan -out=tfplan
# 3. применить ровно этот план
terraform apply tfplan
# 4. снести всё, что описано в этой папке (осторожно!)
terraform destroy
terraform plan — самая ценная команда во всём инструменте. Это сухой прогон: ничего не меняется, но вы видите будущее.
Результат:
Terraform will perform the following actions:
# google_storage_bucket.uploads will be created
+ resource "google_storage_bucket" "uploads" {
+ name = "my-shop-uploads"
+ location = "EUROPE-WEST1"
}
# google_cloud_run_v2_service.api will be created
+ resource "google_cloud_run_v2_service" "api" {
+ name = "shop-api"
+ location = "europe-west1"
}
Plan: 2 to add, 0 to change, 0 to destroy.
Читать план нужно с конца — со строки Plan: X to add, Y to change, Z to destroy. Если вы правили одну переменную окружения, а Terraform собирается что-то уничтожить — стоп, не подтверждайте. Разберитесь, что именно.
Символы в плане: + создать, - удалить, ~ изменить на месте, а самый опасный — -/+, «удалить и создать заново» (в выводе это подписано как forces replacement).
Как это работает
Под капотом каждый plan проходит три шага.
- Refresh. Terraform берёт state и по каждому ресурсу дёргает GET-запрос к API Google: «а ты вообще ещё существуешь, и какой ты сейчас?». Так он видит дрейф — например, что кто-то руками поменял регион.
- Diff. Сравниваются три вещи: ваш код (желаемое), state (что Terraform создавал) и ответ API (что есть на самом деле). Разница и есть план.
- Apply. Terraform обходит граф зависимостей и вызывает CREATE/UPDATE/DELETE в API. Независимые ресурсы создаются параллельно (по умолчанию до 10 штук одновременно).
Ключевая деталь — ForceNew-атрибуты. У части полей нет операции «изменить» в самом API Google: нельзя переименовать бакет или сменить регион Cloud SQL. Если вы правите такое поле, у Terraform остаётся ровно один способ добиться описанного состояния — снести ресурс и создать новый. Для бакета это потеря файлов, для базы — потеря данных. Именно поэтому план читают глазами, а не жмут «да» на автомате.
Отсюда же вытекает идемпотентность: запустите apply десять раз подряд без изменений в коде — девять раз он честно скажет No changes. Terraform применяет не «действия», а разницу.
Частые ошибки
- Забыть
terraform destroyна тестовом стенде — и платить. Самая дорогая ошибка новичка. Подняли Cloud SQL «на посмотреть», закрыли ноутбук, вспомнили через месяц. Инстанс базы тикает круглосуточно, даже если в него никто не ходит. Правило: тестовый контур поднимается и сносится одной командой, а на биллинге стоит бюджетный алерт. terraform apply -auto-approveгде попало. В CI это допустимо только после того, как план был сохранён в файл и просмотрен человеком в пул-реквесте. «Автоапрув» на ветке разработчика однажды снесёт прод-базу, и это будет обидно.- state в git. Секреты утекут, история конфликтов замучает. Сразу заводите backend в GCS.
- Правки мышкой поверх Terraform. Один раз «быстренько поправил в консоли» — и следующий
applyмолча вернёт всё обратно. Либо всё через код, либо ресурс явно исключён из Terraform. - Нет
deletion_protectionна важных ресурсах. Для Cloud SQL и продовых Cloud Run это дешёвая страховка от опечатки в имени ресурса. - Один общий state на всё. Прод и dev в одном state — значит, любой эксперимент требует плана по проду. Разделяйте контуры хотя бы разными
prefixв backend.
Итоги
- Terraform описывает желаемое состояние облака; что делать для его достижения, он решает сам.
- Провайдер
hashicorp/google+ ADC (gcloud auth application-default login) — минимум для старта. - Ссылки между ресурсами (
google_storage_bucket.uploads.name) автоматически строят граф зависимостей. - State — карта «код ↔ реальность». Только в GCS, только с версионированием, никогда в git.
planпередapply— обязательный ритуал. Ищите глазамиdestroyиforces replacement.- Забытый
destroyна тестовом стенде — самая частая строчка в неожиданном счёте от Google.