feat: add VM 202 for report-generator PG + MinIO #75

Merged
claude merged 1 commits from feat/vm-202-reportgen into main 2026-02-20 09:54:09 +01:00
Owner

Summary

  • Provision VM 202 (185.47.204.228) — 4 CPU / 8GB RAM / 100GB disk
  • For hosting PostgreSQL 17 and MinIO (moving stateful workloads out of K8s)
  • Module: add extra_firewall_rules variable with dynamic block
  • VM FW: open PostgreSQL (5432) and MinIO (9000) from K8s host (185.47.204.226)

Context

Part of report-generator v2: make K8s pods stateless, move DB/storage to dedicated VM.

## Summary - Provision VM 202 (185.47.204.228) — 4 CPU / 8GB RAM / 100GB disk - For hosting PostgreSQL 17 and MinIO (moving stateful workloads out of K8s) - Module: add `extra_firewall_rules` variable with dynamic block - VM FW: open PostgreSQL (5432) and MinIO (9000) from K8s host (185.47.204.226) ## Context Part of report-generator v2: make K8s pods stateless, move DB/storage to dedicated VM.
claude added 1 commit 2026-02-20 09:27:12 +01:00
feat: add VM 202 for report-generator PostgreSQL + MinIO
Some checks failed
AI Review / AI Code Review (pull_request) Successful in 1s
PR Checks / OpenTofu Validate & Policy (pull_request) Failing after 8s
Security Scan / Security Scan (pull_request) Successful in 10s
0/0 projects policies checked successfully.
011bbf52f4
Provision a dedicated VM (VMID 202, 185.47.204.228) with 4 CPU / 8GB RAM / 100GB disk
for hosting PostgreSQL and MinIO — moving stateful workloads out of K8s.

Module changes:
- Add extra_firewall_rules variable to tenant-vm module (dynamic block)
- VM 202 gets additional FW rules: PostgreSQL (5432) and MinIO (9000) from K8s host

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

Ran Plan for dir: environments/production workspace: default

Plan Error

Show Output
running 'sh -c' 'sops -d --extract '["proxmox_api_token"]' proxmox.secrets.yaml' in '/atlantis-data/repos/claude/infrastructure/75/default/environments/production': exit status 128: running "sops -d --extract '[\"proxmox_api_token\"]' proxmox.secrets.yaml" in "/atlantis-data/repos/claude/infrastructure/75/default/environments/production": 
Failed to get the data key required to decrypt the SOPS file.

Group 0: FAILED
  age1yttnttdpafzn73mf3g8fw4x04444gymwsfrfm99fv9qkcxqzqs7sld8hln: FAILED
    - | failed to load age identities. Did not find keys in
      | locations 'SOPS_AGE_SSH_PRIVATE_KEY_FILE',
      | '/home/atlantis/.ssh/id_ed25519',
      | '/home/atlantis/.ssh/id_rsa', 'SOPS_AGE_KEY',
      | 'SOPS_AGE_KEY_FILE', 'SOPS_AGE_KEY_CMD', and
      | '/home/atlantis/.config/sops/age/keys.txt'.

Recovery failed because no master key was able to decrypt the file. In
order for SOPS to recover the file, at least one key has to be successful,
but none were.


Ran Plan for dir: `environments/production` workspace: `default` **Plan Error** <details><summary>Show Output</summary> ``` running 'sh -c' 'sops -d --extract '["proxmox_api_token"]' proxmox.secrets.yaml' in '/atlantis-data/repos/claude/infrastructure/75/default/environments/production': exit status 128: running "sops -d --extract '[\"proxmox_api_token\"]' proxmox.secrets.yaml" in "/atlantis-data/repos/claude/infrastructure/75/default/environments/production": Failed to get the data key required to decrypt the SOPS file. Group 0: FAILED age1yttnttdpafzn73mf3g8fw4x04444gymwsfrfm99fv9qkcxqzqs7sld8hln: FAILED - | failed to load age identities. Did not find keys in | locations 'SOPS_AGE_SSH_PRIVATE_KEY_FILE', | '/home/atlantis/.ssh/id_ed25519', | '/home/atlantis/.ssh/id_rsa', 'SOPS_AGE_KEY', | 'SOPS_AGE_KEY_FILE', 'SOPS_AGE_KEY_CMD', and | '/home/atlantis/.config/sops/age/keys.txt'. Recovery failed because no master key was able to decrypt the file. In order for SOPS to recover the file, at least one key has to be successful, but none were. ``` </details>
Author
Owner

AI Code Review

Обзор PR: VM 202 для report-generator

🔴 Критические проблемы

  1. Открытый пароль в коде (SECURITY)

    • password = "ir61mXhg7czPsdgf/ZHOom3j" в tenant-vms.tf — коммитится в Git
    • Решение: переместить в OpenBao, использовать terraform_remote_state data source или var.tenant_passwords из .tfvars
    • Пароль должен быть ротирован — он уже скомпрометирован в VCS истории
  2. Отсутствует валидация firewall rules (BLAST RADIUS)

    • dport и source помечены optional(), но обязательны для type=in правил
    • Если кто-то пропустит source — откреется доступ со всех IP (0.0.0.0/0)
    • Решение: добавить validation блок в переменную или явные required = true для TCP входящих
  3. Динамический блок без safeguards (BEST PRACTICES)

    • Нет проверки на дублирование правил — можно случайно открыть один порт дважды
    • Нет ограничения на количество правил
    • Рекомендация: добавить sensitive = true для пароля, если он останется в коде временно

🟡 Средние проблемы

  1. Firewall правила не учитывают EGRESS (COMPLETENESS)

    • Правила только type = "in", но нет явного ACCEPT для outbound (например, для PG client connections из K8s)
    • Проверить стандартные правила Proxmox — есть ли default ACCEPT egress или нужно открывать явно
  2. Недостаточно информации о network config (DOCUMENTATION)

    • 185.47.204.228 — в subnet 185.47.204.224/28 это IP .228, что за границами .227-.230 из памяти
    • Вопрос: это намеренное расширение subnet? Обновлена ли bare_srv_1/network.yaml и ROADMAP.md?

🟢 Хорошие моменты

  • Scope правильный (только K8s host как source)
  • Модульный подход с extra_firewall_rules — гибко для будущих VMs
  • Комментарии к правилам ясные

Рекомендации перед merge

# Действие Приоритет
1 Переместить пароль в OpenBao, убрать из .tf БЛОКИРУЕТ
2 Добавить validation блок для dport / source в extra_firewall_rules Высокий
3 Проверить subnet расширение (IP .228 vs .227-.230) Средний
4 Подтвердить EGRESS — нужны ли outbound rules для PG/MinIO Средний
5 Обновить state/fleet/bare_srv_1/network.yaml с VM 202 Средний

Вопрос: есть ли уже скрипт ротации пароля или это ручной процесс? Нужно задокументировать в ROADMAP.md.


Automated review by Claude Haiku | advisory, does not block merge

## AI Code Review ## Обзор PR: VM 202 для report-generator ### 🔴 Критические проблемы 1. **Открытый пароль в коде (SECURITY)** - `password = "ir61mXhg7czPsdgf/ZHOom3j"` в `tenant-vms.tf` — коммитится в Git - **Решение:** переместить в OpenBao, использовать `terraform_remote_state` data source или `var.tenant_passwords` из `.tfvars` - Пароль должен быть ротирован — он уже скомпрометирован в VCS истории 2. **Отсутствует валидация firewall rules (BLAST RADIUS)** - `dport` и `source` помечены `optional()`, но обязательны для `type=in` правил - Если кто-то пропустит `source` — откреется доступ со всех IP (0.0.0.0/0) - **Решение:** добавить `validation` блок в переменную или явные `required = true` для TCP входящих 3. **Динамический блок без safeguards (BEST PRACTICES)** - Нет проверки на дублирование правил — можно случайно открыть один порт дважды - Нет ограничения на количество правил - Рекомендация: добавить `sensitive = true` для пароля, если он останется в коде временно ### 🟡 Средние проблемы 4. **Firewall правила не учитывают EGRESS (COMPLETENESS)** - Правила только `type = "in"`, но нет явного `ACCEPT` для outbound (например, для PG client connections из K8s) - Проверить стандартные правила Proxmox — есть ли default ACCEPT egress или нужно открывать явно 5. **Недостаточно информации о network config (DOCUMENTATION)** - `185.47.204.228` — в subnet `185.47.204.224/28` это IP `.228`, что за границами `.227-.230` из памяти - **Вопрос:** это намеренное расширение subnet? Обновлена ли `bare_srv_1/network.yaml` и `ROADMAP.md`? ### 🟢 Хорошие моменты - Scope правильный (только K8s host как source) - Модульный подход с `extra_firewall_rules` — гибко для будущих VMs - Комментарии к правилам ясные --- ## Рекомендации перед merge | # | Действие | Приоритет | |---|----------|----------| | 1 | Переместить пароль в OpenBao, убрать из .tf | **БЛОКИРУЕТ** | | 2 | Добавить `validation` блок для `dport` / `source` в extra_firewall_rules | Высокий | | 3 | Проверить subnet расширение (IP .228 vs .227-.230) | Средний | | 4 | Подтвердить EGRESS — нужны ли outbound rules для PG/MinIO | Средний | | 5 | Обновить `state/fleet/bare_srv_1/network.yaml` с VM 202 | Средний | **Вопрос:** есть ли уже скрипт ротации пароля или это ручной процесс? Нужно задокументировать в ROADMAP.md. --- _Automated review by Claude Haiku | advisory, does not block merge_
Author
Owner

atlantis plan

atlantis plan
Author
Owner

Ran Plan for project: production dir: environments/production workspace: default

Show Output
OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create

OpenTofu will perform the following actions:

  # module.tenant_vm["vm-202-reportgen"].proxmox_virtual_environment_file.cloud_init will be created
+ resource "proxmox_virtual_environment_file" "cloud_init" {
      + content_type           = "snippets"
      + datastore_id           = "local"
      + file_modification_date = (known after apply)
      + file_name              = (known after apply)
      + file_size              = (known after apply)
      + file_tag               = (known after apply)
      + id                     = (known after apply)
      + node_name              = "georgeops"
      + overwrite              = true
      + timeout_upload         = 1800

      + source_raw {
          + data      = (sensitive value)
          + file_name = "ci-vm-202-reportgen.yaml"
          + resize    = 0
        }
    }

  # module.tenant_vm["vm-202-reportgen"].proxmox_virtual_environment_firewall_options.tenant will be created
+ resource "proxmox_virtual_environment_firewall_options" "tenant" {
      + dhcp          = false
      + enabled       = true
      + id            = (known after apply)
      + input_policy  = "DROP"
      + log_level_in  = "nolog"
      + log_level_out = "nolog"
      + macfilter     = true
      + ndp           = false
      + node_name     = "georgeops"
      + output_policy = "ACCEPT"
      + radv          = true
      + vm_id         = 202
    }

  # module.tenant_vm["vm-202-reportgen"].proxmox_virtual_environment_firewall_rules.tenant will be created
+ resource "proxmox_virtual_environment_firewall_rules" "tenant" {
      + id        = (known after apply)
      + node_name = "georgeops"
      + vm_id     = 202

      + rule {
          + action  = "ACCEPT"
          + comment = "Allow SSH"
          + dport   = "22"
          + enabled = true
          + pos     = (known after apply)
          + proto   = "tcp"
          + type    = "in"
        }
      + rule {
          + action  = "ACCEPT"
          + comment = "Allow ICMP"
          + enabled = true
          + pos     = (known after apply)
          + proto   = "icmp"
          + type    = "in"
        }
      + rule {
          + action  = "ACCEPT"
          + comment = "Allow node_exporter from control plane"
          + dport   = "9100"
          + enabled = true
          + pos     = (known after apply)
          + proto   = "tcp"
          + source  = "78.109.17.180"
          + type    = "in"
        }
      + rule {
          + action  = "DROP"
          + comment = "Block SMTP (anti-spam)"
          + dport   = "25"
          + enabled = true
          + pos     = (known after apply)
          + proto   = "tcp"
          + type    = "out"
        }
      + rule {
          + action  = "ACCEPT"
          + comment = "PostgreSQL from K8s (bare_srv_1)"
          + dport   = "5432"
          + enabled = true
          + pos     = (known after apply)
          + proto   = "tcp"
          + source  = "185.47.204.226"
          + type    = "in"
        }
      + rule {
          + action  = "ACCEPT"
          + comment = "MinIO from K8s (bare_srv_1)"
          + dport   = "9000"
          + enabled = true
          + pos     = (known after apply)
          + proto   = "tcp"
          + source  = "185.47.204.226"
          + type    = "in"
        }
    }

  # module.tenant_vm["vm-202-reportgen"].proxmox_virtual_environment_vm.tenant will be created
+ resource "proxmox_virtual_environment_vm" "tenant" {
      + acpi                                 = true
      + bios                                 = "seabios"
      + boot_order                           = (known after apply)
      + delete_unreferenced_disks_on_destroy = true
      + hotplug                              = (known after apply)
      + id                                   = (known after apply)
      + ipv4_addresses                       = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + keyboard_layout                      = "en-us"
      + mac_addresses                        = (known after apply)
      + migrate                              = false
      + name                                 = "vm-202-reportgen"
      + network_interface_names              = (known after apply)
      + node_name                            = "georgeops"
      + on_boot                              = true
      + protection                           = false
      + purge_on_destroy                     = true
      + reboot                               = false
      + reboot_after_update                  = true
      + scsi_hardware                        = "virtio-scsi-pci"
      + started                              = true
      + stop_on_destroy                      = true
      + tablet_device                        = true
      + tags                                 = [
          + "tenant",
          + "tofu",
          + "ubuntu",
        ]
      + template                             = false
      + timeout_clone                        = 1800
      + timeout_create                       = 1800
      + timeout_migrate                      = 1800
      + timeout_move_disk                    = 1800
      + timeout_reboot                       = 1800
      + timeout_shutdown_vm                  = 1800
      + timeout_start_vm                     = 1800
      + timeout_stop_vm                      = 300
      + vm_id                                = 202

      + cpu {
          + cores      = 4
          + hotplugged = 0
          + limit      = 4
          + numa       = false
          + sockets    = 1
          + type       = "x86-64-v2-AES"
          + units      = (known after apply)
        }

      + disk {
          + aio               = "io_uring"
          + backup            = true
          + cache             = "none"
          + datastore_id      = "local"
          + discard           = "on"
          + file_format       = "qcow2"
          + file_id           = "local:iso/ubuntu-24.04-cloudimg-amd64.img"
          + interface         = "virtio0"
          + iothread          = true
          + path_in_datastore = (known after apply)
          + replicate         = true
          + size              = 100
          + ssd               = false

          + speed {
              + iops_read            = 5000
              + iops_read_burstable  = 8000
              + iops_write           = 3000
              + iops_write_burstable = 5000
              + read                 = 200
              + read_burstable       = 400
              + write                = 100
              + write_burstable      = 200
            }
        }

      + initialization {
          + datastore_id         = "local"
          + file_format          = (known after apply)
          + meta_data_file_id    = (known after apply)
          + network_data_file_id = (known after apply)
          + type                 = (known after apply)
          + user_data_file_id    = (known after apply)
          + vendor_data_file_id  = (known after apply)

          + dns {
              + servers = [
                  + "188.93.16.19",
                  + "188.93.17.19",
                ]
            }

          + ip_config {
              + ipv4 {
                  + address = "185.47.204.228/28"
                  + gateway = "185.47.204.225"
                }
            }
        }

      + memory {
          + dedicated      = 8192
          + floating       = 0
          + keep_hugepages = false
          + shared         = 0
        }

      + network_device {
          + bridge      = "vmbr1"
          + enabled     = true
          + firewall    = true
          + mac_address = (known after apply)
          + model       = "virtio"
          + mtu         = 0
          + queues      = 0
          + rate_limit  = 12
          + vlan_id     = 0
        }

      + vga (known after apply)
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Changes to Outputs:
~ tenant_vms    = {
      + vm-202-reportgen = {
          + public_ip = "185.47.204.228"
          + username  = "root"
          + vm_id     = 202
        }
    }

=== Checkov IaC Security Scan ===
  • ▶️ To apply this plan, comment:
    atlantis apply -p production
    
  • 🚮 To delete this plan and lock, click here
  • 🔁 To plan this project again, comment:
    atlantis plan -p production
    

Plan: 4 to add, 0 to change, 0 to destroy.


  • To apply all unapplied plans from this Pull Request, comment:
    atlantis apply
    
  • 🚮 To delete all plans and locks from this Pull Request, comment:
    atlantis unlock
    
Ran Plan for project: `production` dir: `environments/production` workspace: `default` <details><summary>Show Output</summary> ```diff OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create OpenTofu will perform the following actions: # module.tenant_vm["vm-202-reportgen"].proxmox_virtual_environment_file.cloud_init will be created + resource "proxmox_virtual_environment_file" "cloud_init" { + content_type = "snippets" + datastore_id = "local" + file_modification_date = (known after apply) + file_name = (known after apply) + file_size = (known after apply) + file_tag = (known after apply) + id = (known after apply) + node_name = "georgeops" + overwrite = true + timeout_upload = 1800 + source_raw { + data = (sensitive value) + file_name = "ci-vm-202-reportgen.yaml" + resize = 0 } } # module.tenant_vm["vm-202-reportgen"].proxmox_virtual_environment_firewall_options.tenant will be created + resource "proxmox_virtual_environment_firewall_options" "tenant" { + dhcp = false + enabled = true + id = (known after apply) + input_policy = "DROP" + log_level_in = "nolog" + log_level_out = "nolog" + macfilter = true + ndp = false + node_name = "georgeops" + output_policy = "ACCEPT" + radv = true + vm_id = 202 } # module.tenant_vm["vm-202-reportgen"].proxmox_virtual_environment_firewall_rules.tenant will be created + resource "proxmox_virtual_environment_firewall_rules" "tenant" { + id = (known after apply) + node_name = "georgeops" + vm_id = 202 + rule { + action = "ACCEPT" + comment = "Allow SSH" + dport = "22" + enabled = true + pos = (known after apply) + proto = "tcp" + type = "in" } + rule { + action = "ACCEPT" + comment = "Allow ICMP" + enabled = true + pos = (known after apply) + proto = "icmp" + type = "in" } + rule { + action = "ACCEPT" + comment = "Allow node_exporter from control plane" + dport = "9100" + enabled = true + pos = (known after apply) + proto = "tcp" + source = "78.109.17.180" + type = "in" } + rule { + action = "DROP" + comment = "Block SMTP (anti-spam)" + dport = "25" + enabled = true + pos = (known after apply) + proto = "tcp" + type = "out" } + rule { + action = "ACCEPT" + comment = "PostgreSQL from K8s (bare_srv_1)" + dport = "5432" + enabled = true + pos = (known after apply) + proto = "tcp" + source = "185.47.204.226" + type = "in" } + rule { + action = "ACCEPT" + comment = "MinIO from K8s (bare_srv_1)" + dport = "9000" + enabled = true + pos = (known after apply) + proto = "tcp" + source = "185.47.204.226" + type = "in" } } # module.tenant_vm["vm-202-reportgen"].proxmox_virtual_environment_vm.tenant will be created + resource "proxmox_virtual_environment_vm" "tenant" { + acpi = true + bios = "seabios" + boot_order = (known after apply) + delete_unreferenced_disks_on_destroy = true + hotplug = (known after apply) + id = (known after apply) + ipv4_addresses = (known after apply) + ipv6_addresses = (known after apply) + keyboard_layout = "en-us" + mac_addresses = (known after apply) + migrate = false + name = "vm-202-reportgen" + network_interface_names = (known after apply) + node_name = "georgeops" + on_boot = true + protection = false + purge_on_destroy = true + reboot = false + reboot_after_update = true + scsi_hardware = "virtio-scsi-pci" + started = true + stop_on_destroy = true + tablet_device = true + tags = [ + "tenant", + "tofu", + "ubuntu", ] + template = false + timeout_clone = 1800 + timeout_create = 1800 + timeout_migrate = 1800 + timeout_move_disk = 1800 + timeout_reboot = 1800 + timeout_shutdown_vm = 1800 + timeout_start_vm = 1800 + timeout_stop_vm = 300 + vm_id = 202 + cpu { + cores = 4 + hotplugged = 0 + limit = 4 + numa = false + sockets = 1 + type = "x86-64-v2-AES" + units = (known after apply) } + disk { + aio = "io_uring" + backup = true + cache = "none" + datastore_id = "local" + discard = "on" + file_format = "qcow2" + file_id = "local:iso/ubuntu-24.04-cloudimg-amd64.img" + interface = "virtio0" + iothread = true + path_in_datastore = (known after apply) + replicate = true + size = 100 + ssd = false + speed { + iops_read = 5000 + iops_read_burstable = 8000 + iops_write = 3000 + iops_write_burstable = 5000 + read = 200 + read_burstable = 400 + write = 100 + write_burstable = 200 } } + initialization { + datastore_id = "local" + file_format = (known after apply) + meta_data_file_id = (known after apply) + network_data_file_id = (known after apply) + type = (known after apply) + user_data_file_id = (known after apply) + vendor_data_file_id = (known after apply) + dns { + servers = [ + "188.93.16.19", + "188.93.17.19", ] } + ip_config { + ipv4 { + address = "185.47.204.228/28" + gateway = "185.47.204.225" } } } + memory { + dedicated = 8192 + floating = 0 + keep_hugepages = false + shared = 0 } + network_device { + bridge = "vmbr1" + enabled = true + firewall = true + mac_address = (known after apply) + model = "virtio" + mtu = 0 + queues = 0 + rate_limit = 12 + vlan_id = 0 } + vga (known after apply) } Plan: 4 to add, 0 to change, 0 to destroy. Changes to Outputs: ~ tenant_vms = { + vm-202-reportgen = { + public_ip = "185.47.204.228" + username = "root" + vm_id = 202 } } === Checkov IaC Security Scan === ``` </details> * :arrow_forward: To **apply** this plan, comment: ```shell atlantis apply -p production ``` * :put_litter_in_its_place: To **delete** this plan and lock, click [here](http://atlantis:4141/lock?id=claude%252Finfrastructure%252Fenvironments%252Fproduction%252Fdefault%252Fproduction) * :repeat: To **plan** this project again, comment: ```shell atlantis plan -p production ``` Plan: 4 to add, 0 to change, 0 to destroy. --- * :fast_forward: To **apply** all unapplied plans from this Pull Request, comment: ```shell atlantis apply ``` * :put_litter_in_its_place: To **delete** all plans and locks from this Pull Request, comment: ```shell atlantis unlock ```
claude merged commit 49372454f2 into main 2026-02-20 09:54:09 +01:00
Author
Owner

Locks and plans deleted for the projects and workspaces modified in this pull request:

  • dir: environments/production workspace: default
Locks and plans deleted for the projects and workspaces modified in this pull request: - dir: `environments/production` workspace: `default`
Author
Owner

atlantis apply

atlantis apply
Author
Owner

Atlantis commands can't be run on closed pull requests

Atlantis commands can't be run on closed pull requests
Author
Owner
Error parsing command: EOF found when expecting closing quote
``` Error parsing command: EOF found when expecting closing quote ```
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: claude/infrastructure#75
No description provided.