Resolve the default security group UUID dynamically instead of hardcoding it
The "default" security group VK Cloud auto-creates in every project has a UUID unique to that project, but main.tf hardcoded one project's UUID across all 4 instance resources (router, priv_srv_01/02/03) - not portable to another project. Replaced with data.vkcs_networking_secgroup (matched by name = "default", verified against the real provider schema via terraform providers schema -json on the local provider mirror) behind local.default_security_group_id. The new default_security_group_id variable is a last-resort manual override via coalesce() for projects where the lookup doesn't fit (non-standard name/SDN) - not the normal path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hXR2ftXZZhJ4Y3XuSoR8r
This commit is contained in:
1 parent
1ef4b12143
commit
b5d6367fd8
7 files changed
+174
-6
No files matched your search
@@ -35,6 +35,10 @@ Each entry in `private_network_cidrs` is one network shared by *all* routers - e
|
||||
|
||||
New Terraform variables: `router_count`, `private_network_cidrs`, `router_availability_zones` (see `terraform/variables.tf`). The post-install script is a Terraform template (`terraform/scripts/network-init.sh.tpl`) rendered per-router via `templatefile()`, matching each private interface to its expected subnet deterministically instead of guessing - it already handles any interface count, no hardcoded assumption of 2. `terraform/versions.tf` now pins the provider source (`vk-cs/vkcs`, `~> 0.17`), which was previously undeclared.
|
||||
|
||||
**Default security group**
|
||||
|
||||
Every VK Cloud project auto-creates a `default` security group with a UUID unique to that project. Rather than hardcoding one project's UUID, it's resolved dynamically via `data.vkcs_networking_secgroup` (matched by `name = "default"`) and attached to every VM through `local.default_security_group_id`. `default_security_group_id` is a Terraform variable for the rare case that lookup doesn't fit a given project (non-standard name/SDN) - treat setting it explicitly (via `terraform.tfvars` or `TF_VAR_default_security_group_id`) as a last resort, not the normal path.
|
||||
|
||||
**Horizontal scaling via environment variables**
|
||||
|
||||
`router_count` has a default and isn't set in `terraform.tfvars`, so it scales purely through `TF_VAR_router_count` using Terraform's standard `TF_VAR_<name>` convention. `private_network_cidrs` has no default and must be set somewhere - either in `terraform.tfvars` (as shipped) or overridden via `TF_VAR_private_network_cidrs` as a JSON-encoded list:
|
||||
|
||||
@@ -26,6 +26,8 @@ private_network_cidrs = [
|
||||
|
||||
`private_network_cidrs` — обязательная переменная без значения по умолчанию: один CIDR-префикс на каждую приватную сеть, к которой будут подключены интерфейсы роутеров (один префикс = одна общая сеть = один приватный интерфейс на роутер). Автоматической нарезки нет — префиксы не должны пересекаться и должны вмещать минимум `router_count + 2` адреса.
|
||||
|
||||
UUID системной Security Group `default` (уникален для каждого проекта VK Cloud) вычисляется автоматически через `data.vkcs_networking_secgroup`, вручную задавать не нужно. Переменная `default_security_group_id` — override только на крайний случай (нестандартное имя/SDN группы в проекте), не для обычного использования.
|
||||
|
||||
## 3. (Опционально) масштабирование
|
||||
|
||||
По умолчанию: 2 роутера × 3 интерфейса (1 публичный + 2 приватных, по числу префиксов в `private_network_cidrs`). Меняется без правки кода — либо через `terraform.tfvars`, либо через переменные окружения:
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# План внедрения: динамическое вычисление UUID Default Security Group
|
||||
|
||||
Дата: 2026-09-04
|
||||
|
||||
## Проблема
|
||||
|
||||
В `terraform/main.tf` в 4 местах (router, priv_srv_01/02/03) захардкожен литерал `"d479b4d7-55b3-4ff1-bf8d-24d826a38f11"` — UUID системной Security Group `default`, автоматически создаваемой в каждом проекте VK Cloud, но **уникальной по UUID для каждого проекта**. Код не переносим между проектами без ручной правки.
|
||||
|
||||
## Решение
|
||||
|
||||
Провайдер `vkcs` предоставляет data source `vkcs_networking_secgroup` (проверено локально через `terraform providers schema -json` на уже установленном provider-mirror v0.17.2) с фильтром по `name`/`sdn` и computed-атрибутом `id`. Автосозданная группа по умолчанию называется `default` (подтверждено пользователем для целевого проекта).
|
||||
|
||||
Явная передача UUID через переменную (в т.ч. `TF_VAR_default_security_group_id`) остаётся только как fallback на крайний случай — через `coalesce()`.
|
||||
|
||||
## Шаги
|
||||
|
||||
1. `terraform/main.tf`: добавить `data "vkcs_networking_secgroup" "default" { name = "default"; sdn = "sprut" }` и `locals.default_security_group_id = coalesce(var.default_security_group_id, data.vkcs_networking_secgroup.default.id)`; заменить 4 вхождения литерала на `local.default_security_group_id`.
|
||||
2. `terraform/variables.tf`: добавить `default_security_group_id` (string, default `null`, без validation — это осознанный override).
|
||||
3. `terraform/tests/test_terraform_delivery.py`: regression-guard на отсутствие старого UUID-литерала; проверка, что data source и локаль существуют и используются; расширить `variables.tf`-only plan-тест на новую переменную.
|
||||
4. `README.md`/`docs/QUICKSTART.md`: кратко описать механизм и его fallback-статус.
|
||||
5. Summary-документ по завершении.
|
||||
|
||||
## Верификация
|
||||
|
||||
- `terraform fmt -check -recursive`.
|
||||
- Реальные `terraform init`/`validate` через локальный provider-mirror.
|
||||
- `venv/bin/pytest terraform/tests -v`.
|
||||
- Реальный lookup (`plan`/`apply` с настоящими credentials) проверяется пользователем самостоятельно в VK Cloud — вне офлайн-песочницы.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Summary: динамическое вычисление UUID Default Security Group
|
||||
|
||||
Дата: 2026-09-04
|
||||
План: [2026-09-04-dynamic-default-secgroup-plan.md](2026-09-04-dynamic-default-secgroup-plan.md)
|
||||
|
||||
## Что сделано
|
||||
|
||||
### `terraform/main.tf`
|
||||
Добавлен `data "vkcs_networking_secgroup" "default" { name = "default"; sdn = "sprut" }` и `locals.default_security_group_id = coalesce(var.default_security_group_id, data.vkcs_networking_secgroup.default.id)`. Все 4 вхождения захардкоженного литерала `"d479b4d7-55b3-4ff1-bf8d-24d826a38f11"` (в `security_group_ids` роутера и `priv_srv_01/02/03`) заменены на `local.default_security_group_id`.
|
||||
|
||||
Точное имя автосозданной группы (`default`, нижний регистр) подтверждено пользователем для целевого проекта. Data source и его атрибуты (`id` актуален, `secgroup_id` deprecated) проверены локально через `terraform providers schema -json` на уже установленном filesystem-mirror провайдера v0.17.2 — без обращения к реальному облаку.
|
||||
|
||||
### `terraform/variables.tf`
|
||||
Добавлена `default_security_group_id` (string, default `null`, без validation) — explicit override как крайняя мера; при `null` (значение по умолчанию) используется динамический lookup.
|
||||
|
||||
### `terraform/tests/test_terraform_delivery.py`
|
||||
Добавлены хелперы `find_local()` (main.tf теперь содержит два отдельных блока `locals {}`, поэтому старое обращение по индексу `main["locals"][0]` было неверным — исправлено) и `find_data_sources()`. Новые тесты:
|
||||
- `test_default_security_group_data_source_exists` — data source объявлен с `name = "default"`;
|
||||
- `test_default_security_group_id_local_prefers_override_then_lookup` — точная проверка выражения `coalesce(...)`;
|
||||
- `test_no_hardcoded_security_group_uuid_in_main` — regression-guard: в `main.tf` не должно быть ни одного литерала в форме UUID;
|
||||
- `test_all_instances_use_default_security_group_local` — все `vkcs_compute_instance` ссылаются на `local.default_security_group_id`, а не на литерал;
|
||||
- `test_default_security_group_id_override_accepted` — реальный `terraform plan` на изолированном `variables.tf` принимает и `null` (по умолчанию), и явный override.
|
||||
|
||||
Итог сьюта: **38 passed** (было 32).
|
||||
|
||||
### `README.md` / `docs/QUICKSTART.md`
|
||||
Добавлено краткое описание механизма и явная пометка override-переменной как "крайняя мера".
|
||||
|
||||
## Верификация
|
||||
|
||||
- `terraform fmt -check -recursive` → чисто.
|
||||
- Реальные `terraform init`/`validate` (локальный provider-mirror) — с override и без → оба успешны.
|
||||
- `venv/bin/pytest terraform/tests -v` → **38 passed**.
|
||||
- Реальный lookup данных (`vkcs_networking_secgroup.default` резолвится в существующую группу проекта) можно проверить только `terraform plan`/`apply` с настоящими credentials в самом VK Cloud — вне рамок этой офлайн-песочницы; это отдельный шаг для пользователя при первом реальном деплое.
|
||||
+20
-4
@@ -12,6 +12,22 @@ data "vkcs_networking_network" "extnet" {
|
||||
sdn = "sprut"
|
||||
}
|
||||
|
||||
# Every VK Cloud project auto-creates a "default" security group with a
|
||||
# UUID unique to that project - resolve it dynamically instead of
|
||||
# hardcoding one project's UUID. var.default_security_group_id is a
|
||||
# last-resort manual override (e.g. non-standard name/SDN in a project).
|
||||
data "vkcs_networking_secgroup" "default" {
|
||||
name = "default"
|
||||
sdn = "sprut"
|
||||
}
|
||||
|
||||
locals {
|
||||
default_security_group_id = coalesce(
|
||||
var.default_security_group_id,
|
||||
data.vkcs_networking_secgroup.default.id
|
||||
)
|
||||
}
|
||||
|
||||
# LAN Network
|
||||
resource "vkcs_networking_network" "lan_net" {
|
||||
name = "router-lan-net"
|
||||
@@ -164,7 +180,7 @@ resource "vkcs_compute_instance" "router" {
|
||||
|
||||
security_group_ids = [
|
||||
vkcs_networking_secgroup.router_sg.id,
|
||||
"d479b4d7-55b3-4ff1-bf8d-24d826a38f11"
|
||||
local.default_security_group_id
|
||||
]
|
||||
|
||||
config_drive = true
|
||||
@@ -213,7 +229,7 @@ resource "vkcs_compute_instance" "priv_srv_01" {
|
||||
|
||||
security_group_ids = [
|
||||
vkcs_networking_secgroup.private_sg.id,
|
||||
"d479b4d7-55b3-4ff1-bf8d-24d826a38f11"
|
||||
local.default_security_group_id
|
||||
]
|
||||
|
||||
network {
|
||||
@@ -241,7 +257,7 @@ resource "vkcs_compute_instance" "priv_srv_02" {
|
||||
|
||||
security_group_ids = [
|
||||
vkcs_networking_secgroup.private_sg.id,
|
||||
"d479b4d7-55b3-4ff1-bf8d-24d826a38f11"
|
||||
local.default_security_group_id
|
||||
]
|
||||
|
||||
network {
|
||||
@@ -268,7 +284,7 @@ resource "vkcs_compute_instance" "priv_srv_03" {
|
||||
|
||||
security_group_ids = [
|
||||
vkcs_networking_secgroup.private_sg.id,
|
||||
"d479b4d7-55b3-4ff1-bf8d-24d826a38f11"
|
||||
local.default_security_group_id
|
||||
]
|
||||
|
||||
network {
|
||||
|
||||
@@ -75,6 +75,21 @@ def find_variable(doc, name):
|
||||
return None
|
||||
|
||||
|
||||
def find_local(doc, name):
|
||||
"""main.tf has more than one `locals { ... }` block - search all of them."""
|
||||
for block in doc.get("locals", []):
|
||||
if name in block:
|
||||
return block[name]
|
||||
return None
|
||||
|
||||
|
||||
def find_data_sources(doc, dtype):
|
||||
"""Yield (name, attrs) for every data source of a given type in a parsed doc."""
|
||||
for block in doc.get("data", []):
|
||||
if dtype in block:
|
||||
yield from block[dtype].items()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Delivery layout
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -251,6 +266,21 @@ def test_router_count_validation_is_enforced(count, should_pass):
|
||||
assert ok == should_pass, f"unexpected result for router_count={count}:\n{output}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"override,should_pass",
|
||||
[
|
||||
(None, True), # default null - no override, dynamic lookup is used
|
||||
("11111111-1111-1111-1111-111111111111", True), # explicit override accepted
|
||||
],
|
||||
)
|
||||
def test_default_security_group_id_override_accepted(override, should_pass):
|
||||
overrides = {}
|
||||
if override is not None:
|
||||
overrides["TF_VAR_default_security_group_id"] = override
|
||||
ok, output = _plan_variables_only(overrides)
|
||||
assert ok == should_pass, f"unexpected result for default_security_group_id={override!r}:\n{output}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HCL parses cleanly
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -343,8 +373,8 @@ def test_no_legacy_hardcoded_router_resources():
|
||||
|
||||
def test_private_roles_local_driven_by_variable():
|
||||
main = load_tf("main.tf")
|
||||
locals_block = main["locals"][0]
|
||||
assert locals_block["private_roles"] == [
|
||||
private_roles = find_local(main, "private_roles")
|
||||
assert private_roles == [
|
||||
'${[for idx in range(length(var.private_network_cidrs)) : "priv${idx + 1}"]}'
|
||||
], "locals.private_roles must be generated from length(var.private_network_cidrs)"
|
||||
|
||||
@@ -359,6 +389,54 @@ def test_router_network_blocks_scale_with_private_roles():
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# main.tf: the "default" security group UUID is resolved dynamically, not
|
||||
# hardcoded (it's unique per VK Cloud project)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_default_security_group_data_source_exists():
|
||||
main = load_tf("main.tf")
|
||||
secgroups = dict(find_data_sources(main, "vkcs_networking_secgroup"))
|
||||
assert "default" in secgroups, "expected data.vkcs_networking_secgroup.default"
|
||||
assert secgroups["default"]["name"] == ["default"]
|
||||
|
||||
|
||||
def test_default_security_group_id_local_prefers_override_then_lookup():
|
||||
main = load_tf("main.tf")
|
||||
value = find_local(main, "default_security_group_id")
|
||||
assert value == [
|
||||
"${coalesce(var.default_security_group_id,"
|
||||
"data.vkcs_networking_secgroup.default.id)}"
|
||||
], (
|
||||
"locals.default_security_group_id must fall back to the dynamic "
|
||||
"lookup unless var.default_security_group_id is explicitly overridden"
|
||||
)
|
||||
|
||||
|
||||
def test_no_hardcoded_security_group_uuid_in_main():
|
||||
text = (TERRAFORM_DIR / "main.tf").read_text()
|
||||
uuid_pattern = re.compile(
|
||||
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.I
|
||||
)
|
||||
assert not uuid_pattern.search(text), (
|
||||
"main.tf must not contain a literal UUID (e.g. a hardcoded security "
|
||||
"group ID) - it's unique per project and must be resolved dynamically "
|
||||
"via data.vkcs_networking_secgroup or overridden via "
|
||||
"var.default_security_group_id"
|
||||
)
|
||||
|
||||
|
||||
def test_all_instances_use_default_security_group_local():
|
||||
main = load_tf("main.tf")
|
||||
for name, attrs in find_resources(main, "vkcs_compute_instance"):
|
||||
sg_ids = attrs["security_group_ids"][0]
|
||||
assert "${local.default_security_group_id}" in sg_ids, (
|
||||
f"vkcs_compute_instance.{name} does not reference "
|
||||
f"local.default_security_group_id in security_group_ids"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# terraform.tfvars example CIDRs: sanity-check the values actually shipped
|
||||
# (no auto-carving anymore - these come straight from the admin/example)
|
||||
|
||||
@@ -61,3 +61,9 @@ variable "private_network_cidrs" {
|
||||
error_message = "private_network_cidrs entries must be unique."
|
||||
}
|
||||
}
|
||||
|
||||
variable "default_security_group_id" {
|
||||
description = "Explicit override for the project's 'default' security group UUID. Last resort only - by default it's resolved dynamically via data.vkcs_networking_secgroup (see main.tf), since this UUID is unique per project and must not be hardcoded."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
Reference in new issue
Block a user