14 вересня 2026 р.
Terraform для Azure

Terraform перетворює налаштування Azure з послідовності ручних кліків у відтворюваний код. Замість того щоб щоразу створювати resource group, virtual network, subnet, network security group і virtual machine через Portal, команда описує бажаний стан у HCL, переглядає план і лише після перевірки дозволяє зміни. Це спрощує повторне розгортання середовищ, code review, аудит і пошук конфігураційного drift.
У цій інструкції ми зберемо практичний Terraform-проєкт для Azure: налаштуємо AzureRM provider, безпечну автентифікацію, remote state в Azure Blob Storage, мережу та Linux VM. Наприкінці розберемо plan/apply workflow, імпорт уже наявних ресурсів і типові помилки. Приклад навмисно компактний, але його структура придатна для подальшого поділу на модулі та окремі середовища.
Terraform не робить невдалу архітектуру безпечною. Помилка в CIDR, RBAC або lifecycle може відкрити сервіс назовні, замінити VM чи видалити ресурс із даними. Перед автоматизацією варто визначити subscriptions, management groups, naming, tagging, мережеві межі та правила доступу. Якщо ви вже працювали з Terraform для AWS, загальний цикл залишиться знайомим, але Azure має власну модель subscriptions, resource groups, Microsoft Entra ID та RBAC.
Підготовка Terraform-проєкту та AzureRM provider
Встановіть Terraform із офіційного репозиторію HashiCorp, Azure CLI — з репозиторію Microsoft, а Git — із пакетів вашої ОС. Не копіюйте випадковий binary і не фіксуйте версію лише тому, що вона трапилася в старій інструкції. Перевірте встановлені інструменти:
terraform version
az version
git --versionСтворіть каталог проєкту та базові файли:
mkdir terraform-azure-demo
cd terraform-azure-demo
touch versions.tf providers.tf variables.tf main.tf outputs.tf
touch backend.hcl terraform.tfvars.example .gitignoreУ `.gitignore` виключіть локальний state, plan-файли та файли зі значеннями змінних:
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
crash.log
crash.*.log
terraform.tfvars
*.auto.tfvars
*.auto.tfvars.jsonФайл `.terraform.lock.hcl` не ігноруйте: його потрібно commit у Git, щоб локальне середовище й CI завантажували ту саму перевірену версію provider. У `versions.tf` задайте підтримуваний діапазон Terraform та AzureRM. Constraint `~> 5.0` допускає сумісні оновлення в межах major-версії, але не виконає автоматичний перехід на наступний major:
terraform {
required_version = ">= 1.10, < 2.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 5.0"
}
}
}Перед оновленням provider переглядайте release notes і запускайте plan у тестовій subscription. Команда `terraform init -upgrade` змінює lock-файл, тому її результат має проходити звичайний review.
У `providers.tf` достатньо ввімкнути AzureRM features і явно передати subscription. Явний `subscription_id` зменшує ризик випадкового apply не в тому контурі:
provider "azurerm" {
features {}
subscription_id = var.subscription_id
}Додайте в `variables.tf` керовані параметри та перевірку назви середовища:
variable "subscription_id" {
description = "Azure subscription ID for this stack"
type = string
}
variable "location" {
description = "Azure region for resources"
type = string
default = "westeurope"
}
variable "project_name" {
type = string
default = "web-platform"
}
variable "environment" {
type = string
default = "dev"
validation {
condition = contains(["dev", "stage", "prod"], var.environment)
error_message = "environment must be dev, stage or prod"
}
}
variable "admin_cidr" {
description = "Trusted IPv4 CIDR allowed to reach SSH"
type = string
}
variable "ssh_public_key_path" {
description = "Path to an existing SSH public key"
type = string
default = "~/.ssh/id_ed25519.pub"
}Локальний `terraform.tfvars` не додавайте в Git:
subscription_id = "00000000-0000-0000-0000-000000000000"
admin_cidr = "203.0.113.10/32"Документаційні значення потрібно замінити власними. Для production адміністративний порт краще не відкривати в Internet узагалі: використовуйте Azure Bastion, VPN, private connectivity або керований операційний канал.
Автентифікація без секретів у Terraform-коді
Для локальної роботи увійдіть через Azure CLI і відразу перевірте активну subscription:
az login
az account list --output table
az account set --subscription "00000000-0000-0000-0000-000000000000"
az account show --query '{name:name,id:id,tenantId:tenantId}' --output tableTerraform може використати контекст Azure CLI. Не записуйте client secret у `.tf`, `terraform.tfvars`, backend configuration або shell history. State і saved plan можуть містити чутливі дані навіть тоді, коли output позначений як `sensitive`.
Для CI не використовуйте персональний `az login` або довгостроковий пароль service principal. Краща модель — workload identity federation через OpenID Connect: CI-платформа отримує короткоживучий токен, Microsoft Entra ID перевіряє issuer, subject та audience, після чого Azure видає тимчасовий доступ. Призначайте identity мінімальну роль на конкретну resource group або subscription лише тоді, коли stack справді керує всім її вмістом.
Змінні середовища для автоматизованого запуску мають надходити із захищеного CI context:
export ARM_USE_OIDC=true
export ARM_USE_AZUREAD=true
export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_TENANT_ID="00000000-0000-0000-0000-000000000000"
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"Саме значення не слід друкувати в job log. Розділяйте identity для plan і apply, захищайте production environment ручним approval та не видавайте роль Owner, якщо достатньо Contributor або ще вужчої custom role. Якісний CI/CD-процес також серіалізує apply для одного state і зберігає план лише протягом короткого контрольованого вікна.
Remote state в Azure Blob Storage
Локальний `terraform.tfstate` непридатний для спільної роботи: він легко губиться, не має централізованого контролю доступу і створює ризик паралельних apply. Backend `azurerm` зберігає state як blob та підтримує locking через можливості Azure Blob Storage.
Storage account для backend створюйте окремим bootstrap-процесом. Не керуйте ним із того самого state, який лежить у цьому account: destroy root stack тоді може забрати власне сховище стану. Приклад bootstrap через Azure CLI:
az group create \
--name rg-terraform-state \
--location westeurope
az storage account create \
--name sttfstateexample001 \
--resource-group rg-terraform-state \
--location westeurope \
--sku Standard_LRS \
--kind StorageV2 \
--min-tls-version TLS1_2 \
--allow-blob-public-access false
az storage container create \
--name tfstate \
--account-name sttfstateexample001 \
--auth-mode loginНазва storage account має бути глобально унікальною, складатися з дозволених Azure символів і не містити секретної інформації. Увімкніть blob versioning, soft delete, diagnostic logs і заборону public access відповідно до внутрішньої політики. Доступ до data plane надавайте через Microsoft Entra ID, наприклад роллю Storage Blob Data Contributor лише для automation identity та відповідальних інженерів.
Додайте backend block у `versions.tf`:
terraform {
backend "azurerm" {
use_azuread_auth = true
}
}Несекретні координати backend збережіть у `backend.hcl`:
resource_group_name = "rg-terraform-state"
storage_account_name = "sttfstateexample001"
container_name = "tfstate"
key = "web-platform/dev/terraform.tfstate"Ініціалізуйте каталог і перевірте providers:
terraform init -backend-config=backend.hcl
terraform providersНе передавайте access key або SAS token у командному рядку: аргумент може залишитися в history, process list, `.terraform` або plan artifacts. Для одного environment використовуйте один стабільний backend key; для dev, stage і prod — різні keys, identities та бажано окремі subscriptions. Remote state зменшує ризик, але не є резервною копією сам по собі, тому recovery попередньої blob version потрібно документувати й тестувати.
Resource group, мережа та Linux VM
У `main.tf` сформуйте єдині tags, resource group і мережу. Діапазони адрес плануйте до створення peering або VPN: зміна address space з конфліктами часто потребує складної міграції.
locals {
name_prefix = "${var.project_name}-${var.environment}"
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
}
}
resource "azurerm_resource_group" "main" {
name = "rg-${local.name_prefix}"
location = var.location
tags = local.common_tags
}
resource "azurerm_virtual_network" "main" {
name = "vnet-${local.name_prefix}"
address_space = ["10.50.0.0/16"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tags = local.common_tags
}
resource "azurerm_subnet" "app" {
name = "snet-app"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.50.10.0/24"]
}Для демонстрації створимо public IP і network security group із доступом до SSH лише з одного довіреного CIDR. Правило `0.0.0.0/0` для TCP/22 не використовуйте навіть тимчасово: воно швидко привертає автоматизоване сканування.
resource "azurerm_public_ip" "vm" {
name = "pip-${local.name_prefix}-01"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
allocation_method = "Static"
sku = "Standard"
tags = local.common_tags
}
resource "azurerm_network_security_group" "vm" {
name = "nsg-${local.name_prefix}-vm"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tags = local.common_tags
security_rule {
name = "AllowSshFromTrustedCidr"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = var.admin_cidr
destination_address_prefix = "*"
}
}
resource "azurerm_network_interface" "vm" {
name = "nic-${local.name_prefix}-01"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tags = local.common_tags
ip_configuration {
name = "primary"
subnet_id = azurerm_subnet.app.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.vm.id
}
}
resource "azurerm_network_interface_security_group_association" "vm" {
network_interface_id = azurerm_network_interface.vm.id
network_security_group_id = azurerm_network_security_group.vm.id
}Тепер додайте Linux VM. Пароль не потрібен: вхід дозволений лише за SSH public key. Значення SKU та образу перевіряйте для обраного регіону, а перед apply перегляньте актуальну ціну й quota.
resource "azurerm_linux_virtual_machine" "vm" {
name = "vm-${local.name_prefix}-01"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = "Standard_B2s"
admin_username = "azureadmin"
network_interface_ids = [
azurerm_network_interface.vm.id
]
disable_password_authentication = true
admin_ssh_key {
username = "azureadmin"
public_key = file(pathexpand(var.ssh_public_key_path))
}
os_disk {
name = "osdisk-${local.name_prefix}-01"
caching = "ReadWrite"
storage_account_type = "StandardSSD_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
identity {
type = "SystemAssigned"
}
tags = local.common_tags
}System-assigned managed identity сама по собі не має прав. Додавайте RBAC assignment лише до конкретного ресурсу, який VM повинна читати або змінювати. Не видавайте Contributor на всю subscription «про запас».
В `outputs.tf` залиште тільки потрібні операційні значення:
output "resource_group_name" {
value = azurerm_resource_group.main.name
}
output "vm_id" {
value = azurerm_linux_virtual_machine.vm.id
}
output "public_ip_address" {
value = azurerm_public_ip.vm.ip_address
}Для production-платформи одного public VM недостатньо. Зазвичай потрібні private subnets, zone redundancy, load balancer або Application Gateway, backup, monitoring, patching, Key Vault, private endpoints і контроль egress. Terraform описує ресурси, але не замінює операційне проєктування та DevOps-супровід.
Plan, apply, drift та імпорт ресурсів
Після створення файлів відформатуйте й перевірте конфігурацію:
terraform fmt -check -recursive
terraform init -backend-config=backend.hcl
terraform validate
terraform plan -out=dev.tfplan
terraform show dev.tfplanПрочитайте plan повністю, а не лише останній рядок. Особливу увагу зверніть на `replace`, `destroy`, зміни CIDR, public IP, NSG rules, disk та identity. Застосовуйте саме переглянутий saved plan:
terraform apply dev.tfplan
terraform outputSaved plan може містити sensitive values, тому його не можна публікувати або зберігати як довгоживучий artifact. Якщо після plan змінився HCL, provider lock, credentials чи зовнішня інфраструктура, побудуйте новий plan і проведіть review повторно.
Для пошуку ручних змін використовуйте read-only plan з окремою обробкою exit code:
terraform plan -detailed-exitcodeКод `0` означає відсутність diff, `2` — наявність змін, `1` — помилку. У CI код `2` не слід бездумно трактувати як технічний збій. Знайдений drift спочатку класифікують: аварійна ручна зміна може бути правильною, а автоматичний apply здатен її скасувати.
Наявний Azure resource можна прив’язати до Terraform через import block. Спочатку опишіть ресурс у HCL, потім додайте його повний Azure resource ID:
import {
to = azurerm_resource_group.main
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-web-platform-dev"
}Після `terraform plan` вирівняйте аргументи так, щоб import не призвів до несподіваної заміни чи видалення. Import створює binding у state, але не доводить конфігурацію до правильного стану автоматично. Після успішного apply import block можна видалити окремою reviewed зміною.
Для контрольованого прибирання навчального середовища створіть destroy plan:
terraform plan -destroy -out=destroy.tfplan
terraform show destroy.tfplan
terraform apply destroy.tfplanНе запускайте `terraform destroy -auto-approve` у спільній subscription. Ресурси з даними повинні мати backup, retention, locks і перевірену процедуру відновлення. Azure resource lock може навмисно зупинити Terraform destroy; знімати його автоматично без окремого approval небезпечно.
Типові помилки
Перша помилка — секрети у provider, backend, `.tfvars` або Git history. Якщо credential уже опинився в репозиторії, видалення рядка недостатньо: відкличте секрет, перевірте sign-in та audit logs, оцініть виконані операції й лише потім очищуйте історію за погодженою процедурою.
Друга — одна identity з Owner для всіх subscriptions. Розділяйте права plan і apply, середовища та stacks. Використовуйте OIDC або managed identity, вузький scope й мінімальну RBAC role. Пам’ятайте, що management-plane роль і data-plane доступ до Blob Storage — різні дозволи.
Третя — один state для dev, stage і prod. Такий state збільшує blast radius, сповільнює plan і змушує automation identity мати зайві права. Розділяйте backend keys, а критичні платформи — ще й subscriptions.
Четверта — відкритий NSG, випадкова subscription або неправильний tenant. Перед plan перевіряйте `az account show`, передавайте `subscription_id` явно, обмежуйте CIDR і додавайте policy checks. Автоматизація однаково швидко відтворює правильну конфігурацію та небезпечну помилку.
П’ята — ручне редагування state або примусове розблокування без перевірки. Для state operations використовуйте Terraform CLI, зберігайте versioning і спочатку переконайтеся, що попередній apply справді завершився. Два паралельні pipeline для одного key мають бути заборонені на рівні CI, навіть якщо backend підтримує locking.
Висновок
Надійний Terraform-проєкт для Azure починається не з Linux VM, а з правильних меж: окрема subscription або resource group, мінімальні RBAC-права, короткоживуча identity, віддалений state та review кожного plan. AzureRM provider описує resource group, virtual network, subnet, NSG, NIC і VM як залежний граф, а Terraform створює їх у потрібній послідовності.
Практичний цикл залишається простим: `fmt`, `validate`, `plan`, уважний review і `apply` того самого saved plan. Далі конфігурацію можна розділити на network, compute та data stacks, винести повторювані частини в modules і додати policy scanning. Не поєднуйте всі середовища в один state та не підміняйте code review автоматичним approval.
Починайте з ізольованої dev subscription, перевіряйте tenant і subscription перед кожною зміною, контролюйте витрати та регулярно шукайте drift. Terraform дає відтворюваність, але безпека з’являється лише тоді, коли credentials короткоживучі, доступ мінімальний, state захищений, а видалення й відновлення перевірені заздалегідь.