9.1 KiB
Natural Language Infrastructure (NLI) Pattern
Статус: ✅ Реализовано
Компонент: Tubulus Resource + Gemini AI
Файлы: internal/provider/tubulus_ai.go, internal/provider/tubulus_resource.go
Концепция
Вместо того, чтобы пользователь писал технические параметры в HCL, он может указать инструкцию на естественном языке, которую AI (Gemini) преобразует в конфигурацию на этапе terraform plan.
Архитектура
┌──────────────────┐
│ User Instruction│ "создай на 30 секунд,
│ (Natural Lang) │ запиши пароль_123"
└────────┬─────────┘
│
▼
┌──────────────────┐
│ ModifyPlan() │ Terraform Plugin Framework
│ (Hook) │ Перехватывает план до показа
└────────┬─────────┘
│
▼
┌──────────────────┐
│ askGemini() │ Gemini 2.0 Flash API
│ (AI Parser) │ Возвращает структурированный JSON
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Plan Injection │ plan.DurationMs = 30000
│ (Computed) │ plan.BodyMessage = "пароль_123"
└──────────────────┘
Пример использования
resource "nubes_tubulus_instance" "test" {
display_name = "My Test"
description = "AI-driven resource"
# Естественный язык вместо технических параметров
instruction = "поработай секунд 20, положи 'hello world' и вали на 1 этапе"
# Эти поля рассчитаются автоматически через AI:
# duration_ms = 20000
# body_message = "hello world"
# where_fail = 1
# fail_in_progress = true
}
Системный промпт (ключевые элементы)
-
Типизация с примерами:
"duration_ms": (integer) Длительность в миллисекундах Примеры: "5 секунд" → 5000, "минута" → 60000, "быстро" → 1000 -
Строгие ограничения:
"where_fail": ТОЛЬКО [0, 1, 2, 3] 0 = не ломается, 1 = prepare, 2 = data_fill, 3 = after_vault -
Логические зависимости:
Если fail_in_progress=true, но этап не указан — ставь where_fail=2 -
Дефолты для null:
Если НЕ указан текст явно для body_message — оставь null (не "ai_generated") -
Понимание разговорного языка:
"долго" = 120000, "очень долго" = 300000, "полминутки" = 30000
Технические детали
ModifyPlan Lifecycle
func (r *TubulusResource) ModifyPlan(ctx, req, resp) {
// 1. Проверка: это удаление?
if req.Plan.Raw.IsNull() { return }
// 2. Извлечение плана
var plan TubulusResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
// 3. Проверка: есть инструкция?
if plan.Instruction.IsNull() { return }
// 4. СТАБИЛИЗАЦИЯ: уже вызывали AI?
// (Предотвращает "plan inconsistency" между plan/apply)
if !plan.DurationMs.IsUnknown() &&
!plan.BodyMessage.IsUnknown() {
return
}
// 5. Вызов Gemini
aiConfig, err := r.askGemini(ctx, plan.Instruction.ValueString())
// 6. Инъекция значений
if aiConfig.DurationMs != nil {
plan.DurationMs = types.Int64Value(*aiConfig.DurationMs)
}
// 7. Обновление плана
resp.Plan.Set(ctx, &plan)
}
Стабилизация (Critical!)
Проблема: Terraform вызывает ModifyPlan() дважды:
- 1-й раз: при
terraform plan(показ пользователю) - 2-й раз: при
terraform apply(финальная проверка)
Если Gemini возвращает разные значения → ошибка:
Error: Provider produced inconsistent final plan
Решение: Проверяем, что значения ещё Unknown:
if !plan.BodyMessage.IsUnknown() {
return // Уже вызывали AI, не повторяем
}
Null vs Empty String
Важно: Если AI не вернул body_message, устанавливаем null, а не пустую строку:
if aiConfig.BodyMessage != nil {
plan.BodyMessage = types.StringValue(*aiConfig.BodyMessage)
} else {
plan.BodyMessage = types.StringNull() // ← Критично!
}
Иначе: план при apply будет видеть "" вместо null и выдаст inconsistency.
Тестовые кейсы (100% Success Rate)
| Инструкция | Duration | BodyMessage | WhereFail | FailInProgress |
|---|---|---|---|---|
| "сделай быстро" | 1000 | null | 0 | false |
| "долго долго" | 300000 | null | 0 | false |
| "напиши привет" | 5000 | "привет" | 0 | false |
| "сломай сразу" | 5000 | null | 0 | true (at_start) |
| "зделай нормална на 10 сикунд" | 10000 | null | 0 | false |
| "30 сек, упади в середине, пароль_123" | 30000 | "пароль_123" | 2 | true |
| "duration 15000ms, fail at stage 3" | 15000 | null | 3 | true |
| "просто сделай что-нибудь" | 5000 | null | 0 | false |
Расширяемость
Этот паттерн можно применить к любому ресурсу провайдера:
PostgreSQL NLI
# resource "nubes_postgres_instance" "db" {
instruction = "создай базу на 50 гигов с репликой в двух зонах"
# AI → cpu=2, ram=4096, disk=50000, replicas=2
}
VM NLI
# resource "nubes_vm_instance" "server" {
instruction = "подними ubuntu 22.04 с 8 ядрами и 16 гигами памяти"
# AI → os="ubuntu-22.04", cpu=8, ram=16384
}
Edge Gateway NLI
# resource "nubes_edge_gateway" "firewall" {
instruction = "настрой файрвол: блокируй китай и открой 80, 443"
# AI → firewall_rules=[...], allowed_ports=[80,443]
}
Преимущества
- Снижение порога входа: Новички могут писать конфигурации без изучения схемы ресурсов
- Скорость разработки: "Создай Postgres" вместо 30 строк HCL
- Читаемость:
instruction = "быстрая база"понятнее, чемcpu=1, ram=2048, disk=10000 - Прозрачность: Результат виден в
terraform plan(не чёрный ящик) - Аудит: Все AI вызовы логируются → можно отследить "что просил vs что получил"
Ограничения
- Latency: Каждый
terraform planвызывает Gemini (~1-2 секунды) - Cost: Free Tier Gemini → 15 requests/min. Для крупных проектов нужен Billing
- Determinism: AI может интерпретировать один запрос по-разному (минимизировано через примеры в промпте)
- Offline: Требует интернет для доступа к Gemini API
Улучшения (TODO)
- Caching: Кэшировать
(instruction → JSON)локально, чтобы повторный plan не вызывал API - Offline Mode: Fallback на дефолтные значения, если Gemini недоступен
- Multi-language: Поддержка английского, немецкого и т.д.
- Validation: Предупреждение, если AI вернул некорректные значения (например,
where_fail=99)
Выводы
Natural Language Infrastructure — это первый шаг к "инфраструктуре как разговор". Вместо того, чтобы быть промежуточным звеном между человеком и API, Terraform становится интерфейсом, а AI — компилятором.
Эта реализация доказывает, что Plugin Framework достаточно гибок для таких экспериментов, и открывает дверь к новому способу управления облаком.