17 вересня 2026 р.
Terraform для GCP

Terraform дає змогу описати ресурси Google Cloud декларативним кодом, переглядати майбутні зміни до їх застосування та відтворювати однакові середовища без ручних операцій у консолі. У цьому матеріалі побудуємо невеликий, але повний проєкт: налаштуємо провайдер Google, винесемо стан у Cloud Storage, створимо VPC, підмережу, правило firewall і віртуальну машину Compute Engine.
Приклад підходить як основа для dev або test середовища. Для production його варто доповнити окремими модулями, централізованим IAM, журналюванням, резервним копіюванням та перевірками в CI/CD. Якщо потрібне впровадження під ключ, команда ITheal надає DevOps-послуги, а автоматизацію перевірок і розгортань можна побудувати в межах налаштування CI/CD.
Підготовка проєкту та провайдера Google
Перед початком потрібні Terraform, Google Cloud CLI, проєкт GCP з підключеним білінгом і права на створення мережевих ресурсів та Compute Engine. Перевіримо інструменти:
terraform version
gcloud version
gcloud auth list
gcloud config get-value projectСтворимо каталог із файлами, які розділяють конфігурацію за призначенням:
terraform-gcp/
├── backend.tf
├── main.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
└── variables.tfУ `providers.tf` зафіксуємо сумісні версії Terraform і провайдера. Обмеження версії через `~>` дозволяє отримувати виправлення в межах основної версії, але захищає від неочікуваного переходу на наступну:
terraform {
required_version = ">= 1.7.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 7.0"
}
}
}
provider "google" {
project = var.project_id
region = var.region
zone = var.zone
}Оголосимо змінні у `variables.tf`:
variable "project_id" {
description = "Google Cloud project ID"
type = string
}
variable "region" {
description = "Region for regional resources"
type = string
default = "europe-west1"
}
variable "zone" {
description = "Zone for the Compute Engine instance"
type = string
default = "europe-west1-b"
}
variable "environment" {
description = "Environment name used in resource labels"
type = string
default = "dev"
validation {
condition = contains(["dev", "stage", "prod"], var.environment)
error_message = "environment must be dev, stage or prod."
}
}
variable "ssh_source_ranges" {
description = "CIDR ranges allowed to connect over SSH"
type = list(string)
}Значення, які відрізняються між середовищами, запишемо в `terraform.tfvars`:
project_id = "my-gcp-project"
region = "europe-west1"
zone = "europe-west1-b"
environment = "dev"
ssh_source_ranges = ["203.0.113.10/32"]Не використовуйте `0.0.0.0/0` для SSH. Вкажіть зовнішню адресу адміністратора або корпоративного VPN. Сам файл `terraform.tfvars` можна зберігати в репозиторії лише тоді, коли він не містить паролів, токенів чи інших секретів.
Автентифікація, API та принцип мінімальних прав
Для локальної роботи Google рекомендує Application Default Credentials. Звичайний вхід `gcloud auth login` авторизує CLI, але Terraform шукає облікові дані через ADC, тому виконаємо окрему команду:
gcloud auth application-default login
gcloud config set project my-gcp-project
gcloud auth application-default set-quota-project my-gcp-projectJSON-ключ сервісного облікового запису не слід додавати до HCL, `.tfvars`, Git або образу контейнера. Для локального доступу до production краще застосовувати impersonation:
gcloud auth application-default login \
--impersonate-service-account=terraform-deployer@my-gcp-project.iam.gserviceaccount.comКористувачеві для цього потрібна роль `roles/iam.serviceAccountTokenCreator` на цільовому сервісному обліковому записі. У CI поза Google Cloud доцільно використовувати Workload Identity Federation: зовнішня система отримує короткоживучі облікові дані без постійного приватного ключа. Усередині Google Cloud краще прив'язати окремий сервісний обліковий запис до runner і надати йому лише необхідні ролі.
Увімкнемо API, потрібні для прикладу:
gcloud services enable \
compute.googleapis.com \
iam.googleapis.com \
storage.googleapis.com \
--project=my-gcp-projectНе призначайте автоматизації широкі базові ролі `Owner` або `Editor`. Складіть набір predefined або custom roles відповідно до ресурсів, якими реально керує Terraform. Права на state bucket також мають бути окремими від прав на всю інфраструктуру.
Після створення файлів ініціалізуємо каталог і перевіримо синтаксис:
terraform init
terraform fmt -check -recursive
terraform validateФайл `.terraform.lock.hcl`, створений командою `init`, потрібно закомітити: він фіксує вибрану версію провайдера та його контрольні суми. Каталог `.terraform/`, plan-файли й локальні state-файли до Git не додають.
Віддалений state у Cloud Storage
Terraform state містить відповідність між HCL-ресурсами та реальними об'єктами GCP. Локальний `terraform.tfstate` незручний для команди, легко втрачається і може містити чутливі значення. Для спільної роботи перенесемо його до окремого bucket у Cloud Storage.
Backend не може створити власний bucket під час `terraform init`, тому bucket треба підготувати один раз окремим bootstrap-процесом:
gcloud storage buckets create gs://my-gcp-project-tfstate \
--project=my-gcp-project \
--location=europe-west1 \
--uniform-bucket-level-access
gcloud storage buckets update gs://my-gcp-project-tfstate \
--versioningВерсіонування допомагає відновити попередній об'єкт state після помилкового видалення або невдалої операції. Для production також варто налаштувати retention policy, аудит доступу й, за вимогами безпеки, шифрування ключем Cloud KMS.
Додамо `backend.tf`:
terraform {
backend "gcs" {
bucket = "my-gcp-project-tfstate"
prefix = "server/dev"
}
}Назва bucket у Cloud Storage глобально унікальна. Префікс має розділяти стани різних компонентів і середовищ: наприклад, `network/prod`, `gke/prod` або `server/dev`. Не зберігайте облікові дані в backend-конфігурації: Terraform може записати їх до `.terraform/` і plan-файлів.
Повторно ініціалізуємо проєкт. Якщо локальний state уже існував, Terraform запропонує міграцію:
terraform init -migrate-stateGCS backend підтримує блокування state, але це не замінює дисципліну запусків. У CI для одного state не повинні паралельно виконуватися два `apply`.
VPC, підмережа, firewall і Compute Engine
У `main.tf` створимо custom-mode VPC. На відміну від auto-mode мережі, вона не створює підмережі автоматично в кожному регіоні, тому адресний простір залишається контрольованим:
locals {
name = "${var.environment}-web"
labels = {
environment = var.environment
managed_by = "terraform"
}
}
resource "google_compute_network" "main" {
name = "${local.name}-vpc"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "main" {
name = "${local.name}-subnet"
region = var.region
network = google_compute_network.main.id
ip_cidr_range = "10.20.0.0/24"
}Правила firewall у GCP застосовуються до instance за target tags. Відкриємо HTTP для демонстраційного вебсервера, а SSH — лише з дозволених CIDR:
resource "google_compute_firewall" "http" {
name = "${local.name}-allow-http"
network = google_compute_network.main.name
direction = "INGRESS"
source_ranges = ["0.0.0.0/0"]
target_tags = ["web"]
allow {
protocol = "tcp"
ports = ["80"]
}
}
resource "google_compute_firewall" "ssh" {
name = "${local.name}-allow-ssh"
network = google_compute_network.main.name
direction = "INGRESS"
source_ranges = var.ssh_source_ranges
target_tags = ["ssh"]
allow {
protocol = "tcp"
ports = ["22"]
}
}Тепер опишемо VM. Образ беремо із сімейства Debian, тому Google Cloud поверне актуальний образ цієї сім'ї. Startup script встановлює Nginx і створює просту сторінку для перевірки:
data "google_compute_image" "debian" {
family = "debian-12"
project = "debian-cloud"
}
resource "google_compute_instance" "web" {
name = "${local.name}-vm"
zone = var.zone
machine_type = "e2-micro"
tags = ["web", "ssh"]
labels = local.labels
allow_stopping_for_update = true
boot_disk {
initialize_params {
image = data.google_compute_image.debian.self_link
size = 10
type = "pd-balanced"
}
}
network_interface {
subnetwork = google_compute_subnetwork.main.id
access_config {}
}
metadata = {
enable-oslogin = "TRUE"
}
metadata_startup_script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
apt-get update
apt-get install -y nginx
echo "Terraform on GCP: ${var.environment}" > /var/www/html/index.html
systemctl enable --now nginx
EOT
service_account {
scopes = ["cloud-platform"]
}
}Порожній `access_config` надає VM тимчасову зовнішню IPv4-адресу. Для production навантаження зазвичай краще прибрати публічну адресу, використовувати load balancer та Cloud NAT, а адміністративний доступ організувати через IAP або VPN. Також створіть окремий user-managed service account для VM і призначте йому мінімальні ролі; default service account не є добрим production-вибором.
В `outputs.tf` повернемо ідентифікатори та адресу:
output "instance_name" {
value = google_compute_instance.web.name
}
output "external_ip" {
value = google_compute_instance.web.network_interface[0].access_config[0].nat_ip
}
output "web_url" {
value = "http://${google_compute_instance.web.network_interface[0].access_config[0].nat_ip}"
}Безпечний цикл plan, apply та супровід ресурсів
Перед зміною інфраструктури завжди сформуйте план і перегляньте не лише кількість операцій, а й конкретні атрибути. Символ `~` означає оновлення, `+` — створення, `-` — видалення, а `-/+` попереджає про заміну ресурсу:
terraform fmt -recursive
terraform validate
terraform plan -out=tfplan
terraform show tfplan
terraform apply tfplanЗастосування саме збереженого `tfplan` гарантує, що Terraform виконає переглянутий набір змін, якщо від моменту планування стан не застарів. Plan-файл може містити значення конфігурації, тому його не слід публікувати як відкритий CI-артефакт.
Після створення отримаємо URL і перевіримо відповідь:
terraform output -raw web_url
curl "$(terraform output -raw web_url)"Якщо ресурс уже створений вручну, не дублюйте його. Спочатку додайте відповідний resource block, потім імпортуйте об'єкт у state:
terraform import \
google_compute_instance.web \
projects/my-gcp-project/zones/europe-west1-b/instances/dev-web-vm
terraform planImport лише встановлює зв'язок зі state; він не гарантує, що HCL повністю відповідає реальному ресурсу. Після імпорту доведіть `terraform plan` до очікуваного результату без випадкових змін. Для регулярного пошуку drift запускайте read-only plan у CI за розкладом. Схожий підхід до змінних, state і перевірки плану описаний у матеріалі Terraform для Azure, але ресурси та модель автентифікації хмари відрізняються.
Для CI корисна така послідовність: `fmt -check`, `init`, `validate`, статичний аналіз, `plan`, ручне погодження і лише потім `apply`. Різні середовища повинні мати окремі state та окремі ідентичності. Не запускайте production `apply` з ноутбука, якщо вже є керований pipeline з аудитом.
Типові помилки
- `403 Permission denied` означає не лише відсутність ролі: перевірте активний project, ADC, quota project, увімкнений API та IAM саме для потрібного ресурсу. Після зміни доступу до bucket іноді потрібен короткий час для поширення політики. - Помилка `default credentials were not found` часто виникає, коли виконано `gcloud auth login`, але не створено ADC через `gcloud auth application-default login`. - Один state для dev, stage і prod створює небезпечне зв'язування середовищ. Розділяйте їх backend-префіксами або незалежними root modules. - Ручна зміна ресурсу в Cloud Console породжує drift. Якщо зміна потрібна постійно, перенесіть її до HCL і перевірте план. - Незафіксована версія провайдера може змінити поведінку після нового `init`. Задавайте version constraint і комітьте `.terraform.lock.hcl`. - Відкритий SSH з `0.0.0.0/0`, широкі ролі та довгоживучі JSON-ключі збільшують ризик компрометації. Використовуйте вузькі CIDR, OS Login, мінімальні IAM-права та короткоживучі облікові дані. - `terraform destroy` видаляє всі керовані ресурси поточного state. Перед запуском перевірте workspace, backend, project і повний план видалення.
Тестове середовище після завершення можна прибрати:
terraform plan -destroy -out=destroy.tfplan
terraform show destroy.tfplan
terraform apply destroy.tfplanState bucket зазвичай не видаляють разом із середовищем: він належить bootstrap-рівню та зберігає історію станів.
Висновок
Terraform для GCP стає передбачуваним, коли проєкт має чіткі межі: версії провайдерів зафіксовані, автентифікація не залежить від постійних ключів, state зберігається у версіонованому GCS bucket, а кожна зміна проходить через plan. Навіть невеликий приклад із VPC, підмережею, firewall і Compute Engine демонструє головну перевагу Infrastructure as Code: конфігурацію можна переглянути, повторити та перевірити до застосування.
Для production розділяйте мережу й обчислювальні ресурси на модулі, ізолюйте стани середовищ, додайте policy checks і запускайте apply лише через контрольований pipeline. Такий фундамент полегшує подальше підключення GKE, Cloud SQL, load balancer, Secret Manager та інших сервісів без повернення до неперевірених ручних операцій.