Terraform для MikroTik — Infrastructure as Code
Terraform — это инструмент декларативного управления инфраструктурой от HashiCorp. Вы описываете желаемое состояние в .tf-файлах, а Terraform вычисляет разницу между текущим и целевым состоянием и применяет только необходимые изменения. Для MikroTik существует community-провайдер terraform-provider-routeros, поддерживающий десятки ресурсов RouterOS: IP-адреса, firewall, DNS, WireGuard, VLAN, маршруты и другие. В этом руководстве настроим Terraform-провайдер, опишем полную конфигурацию роутера в коде, разберём рабочий процесс Plan → Apply → State, импорт существующей конфигурации и лучшие практики для управления несколькими устройствами.
Описание
Зачем Terraform для MikroTik
| Преимущество | Описание |
|---|---|
| Декларативность | Описываете "что хотите", а не "как сделать" |
| Version control | Конфигурация в Git — история изменений, code review, rollback |
| Plan перед Apply | Видите все изменения до применения (diff) |
| Идемпотентность | Повторный apply ничего не сломает |
| Модули | Переиспользование конфигураций для разных роутеров |
| State | Terraform знает текущее состояние — может удалять ненужное |
| CI/CD | Автоматическое применение через GitLab CI / GitHub Actions |
Terraform vs Ansible: когда что использовать
| Параметр | Terraform | Ansible |
|---|---|---|
| Парадигма | Декларативная (описание состояния) | Процедурная (описание шагов) |
| State | Хранит state-файл | Нет state |
| Идемпотентность | Встроенная | Зависит от модуля |
| Удаление ресурсов | Автоматическое (убрали из кода → удалится) | Нужен явный шаг удаления |
| Plan/Preview | terraform plan | --check --diff (ограничено) |
| MikroTik-провайдер | community.routeros (активно развивается) | community.routeros (стабильный) |
| Подходит для | Управление конфигурацией (постоянное состояние) | Одноразовые задачи, бэкапы, обновления |
| Кривая обучения | Средняя (HCL-язык) | Низкая (YAML) |
Рекомендация: Terraform для постоянного управления конфигурацией (firewall, IP, routing, VPN). Ansible для операционных задач (бэкапы, обновления, сбор информации).
Провайдер terraform-provider-routeros
Провайдер terraform-provider-routeros (github.com/terraform-routeros/terraform-provider-routeros) — это community-проект, активно поддерживаемый сообществом. Он подключается к MikroTik через REST API (HTTPS) и управляет ресурсами RouterOS.
Поддерживаемые ресурсы (неполный список):
| Категория | Ресурсы Terraform |
|---|---|
| System | routeros_system_identity, routeros_system_ntp_client |
| IP | routeros_ip_address, routeros_ip_pool, routeros_ip_dns |
| Firewall | routeros_ip_firewall_filter, routeros_ip_firewall_nat, routeros_ip_firewall_mangle |
| Interfaces | routeros_interface_bridge, routeros_interface_vlan, routeros_interface_bonding |
| WireGuard | routeros_interface_wireguard, routeros_interface_wireguard_peer |
| Routing | routeros_routing_table, routeros_ip_route |
| DHCP | routeros_ip_dhcp_server, routeros_ip_dhcp_server_network |
| Queue | routeros_queue_simple |
| User | routeros_system_user, routeros_system_user_group |
Настройка
Шаг 1: подготовка MikroTik (REST API)
Terraform-провайдер работает через REST API, поэтому на роутере нужно включить HTTPS:
[admin@MikroTik] ># Включаем HTTPS-сервис /ip/service/set www-ssl disabled=no # Отключаем HTTP (небезопасный) /ip/service/set www disabled=yes # Создаём самоподписанный сертификат (или используйте Let's Encrypt) /certificate/add name=terraform-cert \ common-name=router.local \ key-size=2048 \ days-valid=3650 \ key-usage=tls-server /certificate/sign terraform-cert # Назначаем сертификат /ip/service/set www-ssl certificate=terraform-cert # Создаём пользователя для Terraform /user/group/add name=terraform-group \ policy=api,read,write,test,winbox \ comment="Terraform automation group" /user/add name=terraform \ group=terraform-group \ password="TerraformPass!2025" \ address=10.0.0.100/32 \ comment="Terraform automation user" # Ограничиваем доступ к HTTPS /ip/service/set www-ssl address=10.0.0.100/32 # Проверяем /ip/service/print where name=www-ssl /user/print where name=terraform
Шаг 2: установка Terraform и провайдера
[admin@MikroTik] ># Установка Terraform (Linux/macOS) # Скачайте с https://developer.hashicorp.com/terraform/install # Проверка terraform version # Создаём рабочую директорию mkdir mikrotik-terraform && cd mikrotik-terraform
Создаём файл versions.tf с описанием провайдера:
hcl# versions.tf terraform { required_version = ">= 1.5.0" required_providers { routeros = { source = "terraform-routeros/routeros" version = ">= 1.30.0" } } }
Создаём provider.tf с настройками подключения:
hcl# provider.tf provider "routeros" { hosturl = "https://192.168.88.1" username = var.routeros_username password = var.routeros_password insecure = true # Для самоподписанного сертификата (false для production) }
Переменные в variables.tf:
hcl# variables.tf variable "routeros_username" { description = "RouterOS username" type = string default = "terraform" } variable "routeros_password" { description = "RouterOS password" type = string sensitive = true } variable "lan_subnet" { description = "LAN subnet" type = string default = "192.168.88.0/24" } variable "lan_gateway" { description = "LAN gateway IP" type = string default = "192.168.88.1" }
Файл с секретами terraform.tfvars (не коммитить в Git!):
hcl# terraform.tfvars routeros_username = "terraform" routeros_password = "TerraformPass!2025"
Инициализация:
[admin@MikroTik] ># Скачиваем провайдер terraform init # Проверяем terraform providers
Шаг 3: описание ресурсов — полная конфигурация роутера
System identity:
hcl# system.tf resource "routeros_system_identity" "router" { name = "office-router-01" } resource "routeros_system_ntp_client" "ntp" { enabled = true }
IP-адреса:
hcl# ip.tf resource "routeros_ip_address" "lan" { address = "${var.lan_gateway}/24" interface = "bridge" comment = "LAN - managed by Terraform" } resource "routeros_ip_address" "management" { address = "10.0.0.1/24" interface = "ether1" comment = "Management - managed by Terraform" }
DNS:
hcl# dns.tf resource "routeros_ip_dns" "dns" { servers = "8.8.8.8,1.1.1.1" allow_remote_requests = true }
DHCP-сервер:
hcl# dhcp.tf resource "routeros_ip_pool" "dhcp_pool" { name = "dhcp-pool" ranges = ["192.168.88.100-192.168.88.200"] } resource "routeros_ip_dhcp_server" "lan_dhcp" { name = "dhcp-lan" interface = "bridge" address_pool = routeros_ip_pool.dhcp_pool.name lease_time = "1d" comment = "Managed by Terraform" } resource "routeros_ip_dhcp_server_network" "lan_network" { address = var.lan_subnet gateway = var.lan_gateway dns_server = var.lan_gateway comment = "Managed by Terraform" }
Firewall:
hcl# firewall.tf # --- Input chain --- resource "routeros_ip_firewall_filter" "input_established" { chain = "input" action = "accept" connection_state = "established,related" comment = "TF: accept established,related" } resource "routeros_ip_firewall_filter" "input_drop_invalid" { chain = "input" action = "drop" connection_state = "invalid" comment = "TF: drop invalid" } resource "routeros_ip_firewall_filter" "input_icmp" { chain = "input" action = "accept" protocol = "icmp" comment = "TF: accept ICMP" } resource "routeros_ip_firewall_filter" "input_winbox" { chain = "input" action = "accept" protocol = "tcp" dst_port = "8291" src_address = "192.168.88.0/24" comment = "TF: Winbox from LAN" } resource "routeros_ip_firewall_filter" "input_ssh" { chain = "input" action = "accept" protocol = "tcp" dst_port = "22" src_address = "10.0.0.0/24" comment = "TF: SSH from management" } resource "routeros_ip_firewall_filter" "input_dns" { chain = "input" action = "accept" protocol = "udp" dst_port = "53" in_interface_list = "LAN" comment = "TF: DNS from LAN" } resource "routeros_ip_firewall_filter" "input_drop_wan" { chain = "input" action = "drop" in_interface_list = "WAN" comment = "TF: drop all from WAN" } # --- NAT --- resource "routeros_ip_firewall_nat" "masquerade" { chain = "srcnat" action = "masquerade" out_interface_list = "WAN" comment = "TF: masquerade to WAN" }
WireGuard VPN:
hcl# wireguard.tf resource "routeros_interface_wireguard" "wg0" { name = "wireguard1" listen_port = 13231 comment = "Managed by Terraform" } resource "routeros_ip_address" "wg_address" { address = "10.10.10.1/24" interface = routeros_interface_wireguard.wg0.name comment = "WireGuard - managed by Terraform" } resource "routeros_interface_wireguard_peer" "peer_branch1" { interface = routeros_interface_wireguard.wg0.name public_key = "aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0uV1wX2yZ3=" allowed_address = "10.10.10.2/32" endpoint_address = "branch1.example.com" endpoint_port = 13231 persistent_keepalive = "25s" comment = "TF: Branch 1" } resource "routeros_interface_wireguard_peer" "peer_remote" { interface = routeros_interface_wireguard.wg0.name public_key = "qW1eR2tY3uI4oP5aS6dF7gH8jK9lZ0xC1vB2nM3=" allowed_address = "10.10.10.10/32" persistent_keepalive = "25s" comment = "TF: Remote worker" } # Firewall для WireGuard resource "routeros_ip_firewall_filter" "wg_input" { chain = "input" action = "accept" protocol = "udp" dst_port = tostring(routeros_interface_wireguard.wg0.listen_port) comment = "TF: allow WireGuard" }
Шаг 4: рабочий процесс Plan → Apply → State
[admin@MikroTik] ># 1. Plan — смотрим что будет изменено terraform plan # Вывод: # routeros_system_identity.router: will be created # routeros_ip_address.lan: will be created # routeros_ip_firewall_filter.input_established: will be created # ... # Plan: 15 to add, 0 to change, 0 to destroy. # 2. Apply — применяем изменения terraform apply # Terraform запросит подтверждение: "Do you want to perform these actions?" # Введите "yes" # Для автоматического применения (CI/CD) terraform apply -auto-approve # 3. State — просмотр текущего состояния terraform state list terraform state show routeros_ip_address.lan # 4. Повторный plan — ничего не изменится (идемпотентность) terraform plan # No changes. Your infrastructure matches the configuration.
Шаг 5: импорт существующей конфигурации
Если роутер уже настроен, можно импортировать существующие ресурсы в Terraform state:
[admin@MikroTik] ># Импорт IP-адреса по его .id terraform import routeros_ip_address.lan "*1" # Импорт identity terraform import routeros_system_identity.router "." # Импорт firewall-правила terraform import routeros_ip_firewall_filter.input_established "*1" # После импорта — сгенерируйте конфигурацию terraform plan # Terraform покажет различия между state и .tf-файлами # Скорректируйте .tf-файлы чтобы plan показал "No changes"
Начиная с Terraform 1.5 можно использовать import блоки:
hcl# imports.tf import { to = routeros_ip_address.lan id = "*1" } import { to = routeros_system_identity.router id = "." }
[admin@MikroTik] ># Генерация конфигурации из импортированных ресурсов terraform plan -generate-config-out=generated.tf
Модули: переиспользование конфигураций
Создаём модуль для стандартного firewall:
hcl# modules/firewall/main.tf variable "lan_networks" { type = list(string) default = ["192.168.88.0/24"] } variable "management_networks" { type = list(string) default = ["10.0.0.0/24"] } resource "routeros_ip_firewall_filter" "input_established" { chain = "input" action = "accept" connection_state = "established,related" comment = "TF-module: established,related" } resource "routeros_ip_firewall_filter" "input_drop_invalid" { chain = "input" action = "drop" connection_state = "invalid" comment = "TF-module: drop invalid" } resource "routeros_ip_firewall_filter" "input_icmp" { chain = "input" action = "accept" protocol = "icmp" comment = "TF-module: accept ICMP" } resource "routeros_ip_firewall_filter" "input_winbox" { for_each = toset(var.management_networks) chain = "input" action = "accept" protocol = "tcp" dst_port = "8291" src_address = each.value comment = "TF-module: Winbox from ${each.value}" } resource "routeros_ip_firewall_nat" "masquerade" { chain = "srcnat" action = "masquerade" out_interface_list = "WAN" comment = "TF-module: masquerade" }
Использование модуля:
hcl# main.tf module "firewall" { source = "./modules/firewall" lan_networks = ["192.168.88.0/24", "10.20.0.0/24"] management_networks = ["10.0.0.0/24"] }
Управление несколькими роутерами (Workspaces / Multiple Providers)
Вариант 1: Terraform workspaces:
[admin@MikroTik] ># Workspace для каждого роутера terraform workspace new office-main terraform workspace new office-branch1 terraform workspace new office-branch2 # Переключение terraform workspace select office-main terraform apply -var-file="vars/office-main.tfvars"
Вариант 2: multiple providers с alias:
hcl# providers.tf provider "routeros" { alias = "office_main" hosturl = "https://10.0.0.1" username = var.routeros_username password = var.routeros_password insecure = true } provider "routeros" { alias = "branch1" hosturl = "https://10.0.1.1" username = var.routeros_username password = var.routeros_password insecure = true } # Ресурсы для office-main resource "routeros_system_identity" "office_main" { provider = routeros.office_main name = "office-main" } # Ресурсы для branch1 resource "routeros_system_identity" "branch1" { provider = routeros.branch1 name = "office-branch1" }
Best practices: структура проекта
codemikrotik-terraform/ ├── environments/ │ ├── production/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── terraform.tfvars # .gitignore! │ │ └── backend.tf │ └── staging/ │ ├── main.tf │ └── ... ├── modules/ │ ├── firewall/ │ │ ├── main.tf │ │ ├── variables.tf │ │ └── outputs.tf │ ├── wireguard/ │ │ └── ... │ └── dhcp/ │ └── ... ├── .gitignore └── README.md
.gitignore для Terraform:
[admin@MikroTik] ># .gitignore *.tfstate *.tfstate.backup .terraform/ terraform.tfvars *.auto.tfvars .terraform.lock.hcl
Проверка
Валидация конфигурации
[admin@MikroTik] ># Проверка синтаксиса terraform validate # Форматирование кода terraform fmt -recursive # Plan — предпросмотр изменений terraform plan # Plan с сохранением в файл (для CI/CD) terraform plan -out=tfplan terraform apply tfplan
На роутере после apply:
[admin@MikroTik] ># Проверяем identity /system/identity/print # Проверяем IP-адреса /ip/address/print where comment~"Terraform" # Проверяем firewall /ip/firewall/filter/print where comment~"TF" # Проверяем NAT /ip/firewall/nat/print where comment~"TF" # Проверяем WireGuard /interface/wireguard/print /interface/wireguard/peers/print # Проверяем DHCP /ip/dhcp-server/print /ip/pool/print
Просмотр state
[admin@MikroTik] ># Список всех управляемых ресурсов terraform state list # Детали конкретного ресурса terraform state show routeros_ip_firewall_filter.input_established # Полный state в JSON terraform show -json | python3 -m json.tool
Проверка drift (расхождение state и реальности)
[admin@MikroTik] ># Если кто-то изменил конфигурацию вручную через Winbox/CLI, # terraform plan покажет drift: terraform plan # Если plan показывает изменения, которых вы не делали — # значит кто-то изменил конфигурацию вручную. # terraform apply вернёт конфигурацию к описанной в .tf-файлах. # Если нужно принять ручные изменения — обновите .tf-файлы # и выполните refresh: terraform apply -refresh-only
Типичные ошибки
1. Ошибка подключения к REST API
codeError: failed to connect to RouterOS: Get "https://192.168.88.1/rest/system/resource": dial tcp 192.168.88.1:443: connect: connection refused
[admin@MikroTik] ># Проверяем что www-ssl включён /ip/service/print where name=www-ssl # Проверяем сертификат /ip/service/print detail where name=www-ssl # certificate= должен быть заполнен # Проверяем разрешённые адреса /ip/service/set www-ssl address=10.0.0.100/32 # IP Terraform-хоста должен быть в списке
2. Ошибка "401 Unauthorized"
codeError: failed to authenticate: 401 Unauthorized
[admin@MikroTik] ># Проверяем пользователя /user/print where name=terraform # Проверяем что группа имеет policy=api /user/group/print where name=terraform-group # Проверяем ограничение по IP /user/print detail where name=terraform
3. Порядок firewall-правил
Terraform не гарантирует порядок создания ресурсов. Для firewall порядок правил критичен:
hcl# Используйте depends_on для управления порядком resource "routeros_ip_firewall_filter" "input_established" { chain = "input" action = "accept" connection_state = "established,related" comment = "TF: 01 - established" } resource "routeros_ip_firewall_filter" "input_drop_all" { chain = "input" action = "drop" comment = "TF: 99 - drop all" depends_on = [ routeros_ip_firewall_filter.input_established, routeros_ip_firewall_filter.input_icmp, routeros_ip_firewall_filter.input_winbox, ] } # Альтернатива: используйте place_before в некоторых ресурсах # (если провайдер поддерживает)
4. Не все ресурсы RouterOS поддерживаются
Провайдер не покрывает 100% RouterOS. Если нужный ресурс отсутствует:
hcl# Используйте routeros_system_script для выполнения произвольных команд resource "routeros_system_script" "custom_config" { name = "terraform-custom" source = <<-EOT /interface/bridge/port/add interface=ether2 bridge=bridge /interface/bridge/port/add interface=ether3 bridge=bridge EOT } # ВНИМАНИЕ: такие ресурсы не идемпотентны # Terraform не может отслеживать их состояние
Проверьте документацию провайдера на наличие нужного ресурса:
[admin@MikroTik] ># Список всех поддерживаемых ресурсов terraform providers schema -json | python3 -m json.tool | grep "routeros_"
5. Конфликт при одновременном управлении Terraform и Winbox
Если администратор изменит конфигурацию через Winbox, следующий terraform apply перезатрёт эти изменения. Решения:
- Правило: всё что в Terraform — меняется только через Terraform
- Маркировка: все Terraform-ресурсы имеют comment с "TF:" — не трогайте их в Winbox
- Locking: используйте remote state с locking (S3 + DynamoDB)
- CI/CD: применяйте Terraform только через CI/CD pipeline
6. Потеря state-файла
State-файл (terraform.tfstate) содержит маппинг между .tf-ресурсами и реальными объектами на роутере. Потеря state означает, что Terraform "забудет" обо всех управляемых ресурсах.
Используйте remote state:
hcl# backend.tf — хранение state в S3 terraform { backend "s3" { bucket = "mikrotik-terraform-state" key = "office-main/terraform.tfstate" region = "eu-central-1" dynamodb_table = "terraform-locks" encrypt = true } }
Или для небольших проектов — коммитьте state в приватный Git-репозиторий (не идеально, но лучше чем потерять).
7. Ошибка при destroy — роутер становится недоступным
[admin@MikroTik] ># ОПАСНО: terraform destroy удалит ВСЕ управляемые ресурсы # Включая IP-адреса и firewall — роутер может стать недоступным! # Защита: используйте lifecycle prevent_destroy
hclresource "routeros_ip_address" "management" { address = "10.0.0.1/24" interface = "ether1" comment = "Management - DO NOT DELETE" lifecycle { prevent_destroy = true } } resource "routeros_ip_service" "ssh" { numbers = "ssh" disabled = false lifecycle { prevent_destroy = true } }
8. Ограничения провайдера
| Ограничение | Описание | Workaround |
|---|---|---|
| Порядок firewall-правил | Нет встроенной поддержки порядка | depends_on, нумерация в comment |
| Не все ресурсы | ~70% RouterOS покрыто | routeros_system_script для остального |
| Только REST API | Нужен HTTPS на роутере | Включите www-ssl |
| RouterOS 7 only | REST API недоступен в RouterOS 6 | Обновитесь до RouterOS 7 |
| Один роутер на провайдер | Для N роутеров нужно N провайдеров | Используйте alias или workspaces |
# Включаем HTTPS-сервис /ip/service/set www-ssl disabled=no # Отключаем HTTP (небезопасный) /ip/service/set www disabled=yes # Создаём самоподписанный сертификат (или используйте Let's Encrypt) /certificate/add name=terraform-cert \ common-name=router.local \ key-size=2048 \ days-valid=3650 \ key-usage=tls-server /certificate/sign terraform-cert # Назначаем сертификат /ip/service/set www-ssl certificate=terraform-cert # Создаём пользователя для Terraform /user/group/add name=terraform-group \ policy=api,read,write,test,winbox \ comment="Terraform automation group" /user/add name=terraform \ group=terraform-group \ password="TerraformPass!2025" \ address=10.0.0.100/32 \ comment="Terraform automation user" # Ограничиваем доступ к HTTPS /ip/service/set www-ssl address=10.0.0.100/32 # Проверяем /ip/service/print where name=www-ssl /user/print where name=terraform # Установка Terraform (Linux/macOS) # Скачайте с https://developer.hashicorp.com/terraform/install # Проверка terraform version # Создаём рабочую директорию mkdir mikrotik-terraform && cd mikrotik-terraform Создаём `provider.tf` с настройками подключения: Переменные в `variables.tf`: Файл с секретами `terraform.tfvars` (не коммитить в Git!): Инициализация: #### Шаг 3: описание ресурсов — полная конфигурация роутера **System identity:** **IP-адреса:** **DNS:** **DHCP-сервер:** **Firewall:** **WireGuard VPN:** #### Шаг 4: рабочий процесс Plan → Apply → State #### Шаг 5: импорт существующей конфигурации Если роутер уже настроен, можно импортировать существующие ресурсы в Terraform state: Начиная с Terraform 1.5 можно использовать `import` блоки: #### Модули: переиспользование конфигураций Создаём модуль для стандартного firewall: Использование модуля: #### Управление несколькими роутерами (Workspaces / Multiple Providers) Вариант 1: Terraform workspaces: Вариант 2: multiple providers с alias: #### Best practices: структура проекта `.gitignore` для Terraform: ### Проверка #### Валидация конфигурации На роутере после apply: #### Просмотр state #### Проверка drift (расхождение state и реальности) ### Типичные ошибки #### 1. Ошибка подключения к REST API #### 2. Ошибка "401 Unauthorized" #### 3. Порядок firewall-правил Terraform не гарантирует порядок создания ресурсов. Для firewall порядок правил критичен: #### 4. Не все ресурсы RouterOS поддерживаются Провайдер не покрывает 100% RouterOS. Если нужный ресурс отсутствует: Проверьте документацию провайдера на наличие нужного ресурса: #### 5. Конфликт при одновременном управлении Terraform и Winbox Если администратор изменит конфигурацию через Winbox, следующий `terraform apply` перезатрёт эти изменения. Решения: 1. **Правило**: всё что в Terraform — меняется только через Terraform 2. **Маркировка**: все Terraform-ресурсы имеют comment с "TF:" — не трогайте их в Winbox 3. **Locking**: используйте remote state с locking (S3 + DynamoDB) 4. **CI/CD**: применяйте Terraform только через CI/CD pipeline #### 6. Потеря state-файла State-файл (`terraform.tfstate`) содержит маппинг между .tf-ресурсами и реальными объектами на роутере. Потеря state означает, что Terraform "забудет" обо всех управляемых ресурсах. Используйте remote state: Или для небольших проектов — коммитьте state в приватный Git-репозиторий (не идеально, но лучше чем потерять). #### 7. Ошибка при destroy — роутер становится недоступным