commit caf9bf8eb97230f7cc892e44c4a565589509b46d Author: “Naeel” Date: Tue Jul 21 09:05:46 2026 +0400 add CRUD example: PG + Lucee + Flask + Node.js diff --git a/CRUD/README.md b/CRUD/README.md new file mode 100644 index 0000000..9c2f2a9 --- /dev/null +++ b/CRUD/README.md @@ -0,0 +1,62 @@ +# CRUD Test Stand + +Три приложения (Lucee / Flask / Node.js) + общая PostgreSQL. Каждое делает CRUD в одной таблице `crud_items`. + +## Как скачать только эту папку + +```bash +git clone --depth 1 --filter=blob:none --sparse https://gitea.services.ngcloud.ru/Nail/tf_provider.git +cd tf_provider +git sparse-checkout set TEST_STAND/CRUD +``` + +## Что где менять + +### 1. `terraform.tfvars` — заполнить `terraform.tfvars.example` и переименовать в `terraform.tfvars` + +| Параметр | Где брать | +|---|---| +| `api_token` | ЛК → Профиль → Токены → создать «Технический» | +| `realm` | Кластер Kubernetes: `k8s-3-sandbox-nubes-ru` (песочница) или другой | +| `s3_name` | ЛК → S3 → Имя экземпляра | +| `s3_user_uid` | Там же — UUID. **ИЛИ** `s3_name` — любое одно, провайдер сам найдёт пару | + +### 2. `locals.tf` — сменить домены + +Каждое приложение должно иметь **уникальный** домен. Замени: + +``` +lucee_domain = "..." # станет <имя>.luceek8s.dev.nubes.ru +flask_domain = "..." # станет <имя>.pythonk8s.dev.nubes.ru +nodejs_domain = "..." # станет <имя>.<суффикс>.dev.nubes.ru +``` + +### 3. `locals.tf` — git_revision + +При изменении кода в репозиториях (`tfluceecrud`, `tfflaskcrud`, `tfnodejscrud`) — обновить `*_git_revision` на новый хеш коммита. Terraform сам сделает redeploy. + +## Порядок запуска + +При первом создании PG не успевает отдать пароль юзера до того как стартуют приложения — Lucee/Flask/Node.js падают с ошибкой. Проще всего сделать два apply, чем усложнять `.tf` файлы: + +```bash +terraform init +terraform apply # 1. создаст PG + пользователя + БД +terraform apply # 2. создаст Lucee + Flask + Node.js (пароль уже в стейте) +``` + +**Альтернатива:** закомментировать `lucee.tf`, `flask.tf`, `nodejs.tf` → `terraform apply` → раскомментировать → `terraform apply`. + +Дальше — один `terraform apply` при любых изменениях. + +## Состав + +| Файл | Ресурс | +|---|---| +| `main.tf` | Провайдер (`nubes-test/nubes`) + переменные | +| `postgres.tf` | `nubes_postgres.main_pg` | +| `postgres_user_db.tf` | Пользователь + БД | +| `lucee.tf` | `nubes_lucee.applucee` | +| `flask.tf` | `nubes_flask.appflask` | +| `nodejs.tf` | `nubes_nodejs.appnodejs` | +| `locals.tf` | Все настраиваемые значения | diff --git a/CRUD/flask.tf b/CRUD/flask.tf new file mode 100644 index 0000000..70f1b5f --- /dev/null +++ b/CRUD/flask.tf @@ -0,0 +1,48 @@ +# ============================================================================= +# Flask — CRUD (та же PG, та же таблица что у Lucee) +# ============================================================================= +locals { + # Повторно используем pg_host/pg_user/pg_pass/pg_db из lucee.tf locals + flask_pg_host = nubes_postgres.main_pg.state_out_flat["internalMaster"] + flask_pg_user = nubes_postgres_user.crud_user_0.username + flask_pg_pass = nonsensitive(jsondecode(nubes_postgres.main_pg.vault_secrets["users"]).user4crudpg.password) + flask_pg_db = nubes_postgres_database.pg_db.db_name +} + +resource "nubes_flask" "appflask" { + resource_name = local.flask_resource_name + + startup_configuration = { + resource_realm = var.realm + } + + cluster_configuration = { + cpu = local.flask_cpu + memory = local.flask_memory + replicas = local.flask_replicas + } + + access_configuration = { + domain = local.flask_domain + } + + app_configuration = { + version = "3.12" + git_path = local.flask_git_path + health_path = "/" + } + + git_revision = local.flask_git_revision + + json_env = jsonencode({ + TABLE_NAME = local.crud_table_name + PGHOST = local.flask_pg_host + PGPORT = local.pg_port + PGUSER = local.flask_pg_user + PGPASSWORD = local.flask_pg_pass + PGDATABASE = local.flask_pg_db + PGSSLMODE = local.pg_ssl_mode + }) + + depends_on = [nubes_postgres.main_pg] +} diff --git a/CRUD/locals.tf b/CRUD/locals.tf new file mode 100644 index 0000000..2183c9f --- /dev/null +++ b/CRUD/locals.tf @@ -0,0 +1,91 @@ +# ============================================================================= +# locals.tf — все настраиваемые значения модуля CRUD (PG + Lucee + Flask + Node.js) +# Никакого хардкода в ресурсах — всё здесь. +# ============================================================================= + +locals { + + # ═══════════════════════════════════════════════════════════════════════════ + # PostgreSQL — общая БД для всех трёх приложений + # ═══════════════════════════════════════════════════════════════════════════ + + pg_resource_name = "pg4crud" # имя ресурса в Nubes + pg_cpu = 500 # CPU в millicores (500 = 0.5 ядра) + pg_memory = 512 # память в MB + pg_replicas = 1 # количество реплик + pg_disk = 10 # диск в GB + pg_version = "17" # версия PostgreSQL + pg_retain = 14 # дней хранения бэкапов + pg_schedule = "0 0 * * *" # cron расписание бэкапов (ежедневно в полночь) + pg_timeout = "11m" # таймаут операций create/modify + + # ═══════════════════════════════════════════════════════════════════════════ + # PostgreSQL — пользователь и база данных + # ═══════════════════════════════════════════════════════════════════════════ + + pg_username = "user4crudpg" # имя пользователя БД + pg_role = "ddl_user" # роль (ddl_user = может создавать таблицы) + pg_db_name = "db4crudpg" # имя базы данных + + # ═══════════════════════════════════════════════════════════════════════════ + # Lucee — CFML-приложение (сервис 94) + # ═══════════════════════════════════════════════════════════════════════════ + + lucee_git_revision = "82685681" # коммит/тег в git (менять для редеплоя) + lucee_resource_name = "luceecrud" # имя ресурса в Nubes + lucee_domain = "tflucee" # домен (станет tflucee.luceek8s.dev.nubes.ru). Имя должно быть уникальным — заменяйте на своё! + lucee_version = "5.4" # версия Lucee (CFML engine) + lucee_git_path = "https://gitea.services.ngcloud.ru/Nail/tfluceecrud.git" + lucee_cpu = 300 # CPU в millicores + lucee_memory = 512 # память в MB + lucee_replicas = 1 # количество реплик + + # ═══════════════════════════════════════════════════════════════════════════ + # Таблица CRUD — общая для всех трёх приложений + # ═══════════════════════════════════════════════════════════════════════════ + + crud_table_name = "crud_items" # имя таблицы (TABLE_NAME в env) + + # ═══════════════════════════════════════════════════════════════════════════ + # Flask — Python-приложение (сервис 89) + # ═══════════════════════════════════════════════════════════════════════════ + + flask_git_revision = "54746e91" # коммит/тег в git (менять для редеплоя) + flask_resource_name = "flaskcrud" # имя ресурса в Nubes + flask_domain = "tfflask" # домен (станет tfflask.pythonk8s.dev.nubes.ru). Имя должно быть уникальным — заменяйте на своё! + flask_git_path = "https://gitea.services.ngcloud.ru/Nail/tfflaskcrud.git" + flask_cpu = 300 # CPU в millicores + flask_memory = 512 # память в MB + flask_replicas = 1 # количество реплик + + # ═══════════════════════════════════════════════════════════════════════════ + # Node.js — Express-приложение (сервис 95) + # ═══════════════════════════════════════════════════════════════════════════ + + nodejs_git_revision = "809d303b" # коммит/тег в git (менять для редеплоя) + nodejs_resource_name = "nodejscrud" # имя ресурса в Nubes + nodejs_domain = "tfnodejs" # домен (станет tfnodejs.<суффикс>.dev.nubes.ru). Имя должно быть уникальным — заменяйте на своё! + nodejs_git_path = "https://gitea.services.ngcloud.ru/Nail/tfnodejscrud.git" + nodejs_cpu = 300 # CPU в millicores + nodejs_memory = 512 # память в MB + nodejs_replicas = 1 # количество реплик + nodejs_timeout = "15m" # таймаут операций create/modify + + # ═══════════════════════════════════════════════════════════════════════════ + # JDBC — параметры подключения Lucee к PostgreSQL + # ═══════════════════════════════════════════════════════════════════════════ + + jdbc_class = "org.postgresql.Driver" + jdbc_bundle_name = "org.postgresql.jdbc" + jdbc_bundle_version = "42.6.0" + jdbc_conn_limit = "5" # макс. количество соединений + jdbc_live_timeout = "15" # таймаут неактивного соединения (минут) + jdbc_validate = "false" # валидация соединения при выдаче из пула + + # ═══════════════════════════════════════════════════════════════════════════ + # PostgreSQL — общие параметры подключения + # ═══════════════════════════════════════════════════════════════════════════ + + pg_port = "5432" # порт PostgreSQL + pg_ssl_mode = "require" # SSL-режим (require = обязательно TLS) +} diff --git a/CRUD/lucee.tf b/CRUD/lucee.tf new file mode 100644 index 0000000..85d2e25 --- /dev/null +++ b/CRUD/lucee.tf @@ -0,0 +1,59 @@ +locals { + pg_host = nubes_postgres.main_pg.state_out_flat["internalMaster"] + pg_user = nubes_postgres_user.crud_user_0.username + pg_pass = nonsensitive(jsondecode(nubes_postgres.main_pg.vault_secrets["users"]).user4crudpg.password) + pg_db = nubes_postgres_database.pg_db.db_name +} + +resource "nubes_lucee" "applucee" { + resource_name = local.lucee_resource_name + + startup_configuration = { + resource_realm = var.realm + } + + cluster_configuration = { + cpu = local.lucee_cpu + memory = local.lucee_memory + replicas = local.lucee_replicas + } + + access_configuration = { + domain = local.lucee_domain + } + + app_configuration = { + version = local.lucee_version + git_path = local.lucee_git_path + } + + git_revision = local.lucee_git_revision + + json_env = jsonencode({ + TABLE_NAME = local.crud_table_name + testds_class = local.jdbc_class + testds_bundleName = local.jdbc_bundle_name + testds_bundleVersion = local.jdbc_bundle_version + testds_connectionString = "jdbc:postgresql://${local.pg_host}:5432/${local.pg_db}" + testds_username = local.pg_user + testds_password = local.pg_pass + testds_connectionLimit = local.jdbc_conn_limit + testds_liveTimeout = local.jdbc_live_timeout + testds_validate = local.jdbc_validate + + PGHOST = local.pg_host + PGPORT = local.pg_port + PGUSER = local.pg_user + PGPASSWORD = local.pg_pass + PGSSLMODE = local.pg_ssl_mode + DATABASE_URL = format( + "postgresql://%s:%s@%s:5432/%s", + local.pg_user, + local.pg_pass, + local.pg_host, + local.pg_db + ) + }) + + depends_on = [nubes_postgres.main_pg] +} diff --git a/CRUD/main.tf b/CRUD/main.tf new file mode 100644 index 0000000..89fcf56 --- /dev/null +++ b/CRUD/main.tf @@ -0,0 +1,39 @@ +terraform { + required_providers { + nubes = { + source = "registry.kube5s.ru/nubes-test/nubes" + version = "5.1.16" + } + } +} + +variable "api_token" { + type = string + sensitive = true + description = "Nubes API token" +} +# variable "s3_uid" { +# type = string +# sensitive = true +# description = "Nubes S3 UID" +# } +variable "realm" { + type = string + sensitive = true + description = "resource_realm parameter for nubes_postgres resource" +} +variable "s3_user_uid" { + type = string + description = "S3 user UUID" +} +variable "s3_name" { + type = string + description = "S3 user name" +} + +provider "nubes" { + api_token = var.api_token + api_endpoint = "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc" + # log_level = "debug" # none | info | debug, default = "none" +} + diff --git a/CRUD/nodejs.tf b/CRUD/nodejs.tf new file mode 100644 index 0000000..50e7fee --- /dev/null +++ b/CRUD/nodejs.tf @@ -0,0 +1,48 @@ +# ============================================================================= +# Node.js — CRUD (та же PG, та же таблица что у Lucee/Flask) +# ============================================================================= +locals { + nodejs_pg_host = nubes_postgres.main_pg.state_out_flat["internalMaster"] + nodejs_pg_user = nubes_postgres_user.crud_user_0.username + nodejs_pg_pass = nonsensitive(jsondecode(nubes_postgres.main_pg.vault_secrets["users"]).user4crudpg.password) + nodejs_pg_db = nubes_postgres_database.pg_db.db_name +} + +resource "nubes_nodejs" "appnodejs" { + resource_name = local.nodejs_resource_name + + startup_configuration = { + resource_realm = var.realm + } + + cluster_configuration = { + cpu = local.nodejs_cpu + memory = local.nodejs_memory + replicas = local.nodejs_replicas + } + + access_configuration = { + domain = local.nodejs_domain + } + + app_configuration = { + version = "22" + git_path = local.nodejs_git_path + health_path = "/" + } + + git_revision = local.nodejs_git_revision + operation_timeout = local.nodejs_timeout + + json_env = jsonencode({ + TABLE_NAME = local.crud_table_name + PGHOST = local.nodejs_pg_host + PGPORT = local.pg_port + PGUSER = local.nodejs_pg_user + PGPASSWORD = local.nodejs_pg_pass + PGDATABASE = local.nodejs_pg_db + PGSSLMODE = local.pg_ssl_mode + }) + + depends_on = [nubes_postgres.main_pg] +} diff --git a/CRUD/postgres.tf b/CRUD/postgres.tf new file mode 100644 index 0000000..60dddb0 --- /dev/null +++ b/CRUD/postgres.tf @@ -0,0 +1,49 @@ +resource "nubes_postgres" "main_pg" { + resource_name = local.pg_resource_name + + startup_configuration = { + resource_realm = var.realm + } + + cluster_configuration = { + cpu = local.pg_cpu + memory = local.pg_memory + replicas = local.pg_replicas + disk = local.pg_disk + } + + access_configuration = { + master_ip_space = "no-needed" + master_access_list = jsonencode(["10.0.0.0/8"]) + slave_ip_space = "no-needed" + slave_access_list = jsonencode([]) + } + + postgres_configuration = { + version = local.pg_version + ssl_required = true + pooler_master = false + pooler_slave = false + } + + postgres_conf = jsonencode([{ + param_name = "log_connections" + param_value = "" + }]) + + backup_configuration = { + s3_uid = var.s3_name + retain = local.pg_retain + schedule = local.pg_schedule + } + + autoscale_configuration = { + enabled = false + schedule = 0 + percent = 10 + quota = 100 + } + + operation_timeout = local.pg_timeout + adopt_existing_on_create = true +} diff --git a/CRUD/postgres_user_db.tf b/CRUD/postgres_user_db.tf new file mode 100644 index 0000000..cb6c72b --- /dev/null +++ b/CRUD/postgres_user_db.tf @@ -0,0 +1,16 @@ +# ============================================================================= +# PostgreSQL — пользователи и базы данных +# ============================================================================= +resource "nubes_postgres_user" "crud_user_0" { + postgres_id = nubes_postgres.main_pg.id + username = local.pg_username + role = local.pg_role + adopt_existing_on_create = true +} + +resource "nubes_postgres_database" "pg_db" { + postgres_id = nubes_postgres.main_pg.id + db_name = local.pg_db_name + db_owner = nubes_postgres_user.crud_user_0.username + adopt_existing_on_create = true +} diff --git a/CRUD/terraform.tfvars b/CRUD/terraform.tfvars new file mode 100644 index 0000000..1621249 --- /dev/null +++ b/CRUD/terraform.tfvars @@ -0,0 +1,6 @@ +api_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOWNjMjY4LTZjNmEtNzgxZS04NjEzLTRiZWQ0ZWM3Y2QyMCIsImV4cCI6MTc4OTAxNTg2NywiaWF0IjoxNzczNDYzODY3LCJqdGkiOiJmYjRkMGFiNy1lYTI1LTQ5YmItYmQ3OC0zYjBiNmRiZTdhNzkiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJjb21wYW55X2lkIjoiIiwiZ3JvdXBzIjpudWxsLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiIiLCJnaXZlbl9uYW1lIjoiIiwiZmFtaWx5X25hbWUiOiIiLCJlbWFpbCI6InRhemV0ZGlub3ZuQGdtYWlsLmNvbSJ9.xPxWUWA8e_GEUQ8bgpuHOZvoY7Xu2udRtGPrpHZrzP3Z5uhX-NqDvVDDZQRSpuqoAeXjmekTxX0kHFvwEmv-Kd1hnQZJA_nJf3DvSVPYErtu4ePgy4U2N-4uwlMvjtRysxv17SMEPaDP4XAKj6SxDZ8eEtRCnrTJpgxwh-2cz1NRUsH99pIBY0doj7yNkPOQnnrOCQpnzWdX-d1I1ERCwGg10qOa5VpxP3yXoc2PuDMF6gFU_1VxlhCEdL94TTlFQDPdaBmaDk6hDwUq6T7RZk4cRQ8eao10tUWLHucfaMBGtKEutgrbqR8kOmMB6wY3x86TXdZfBS0zr7RO137d-g" +# s3_uid = "332cdb0d-34bf-43bf-864d-4adcc3b556fb" +realm = "k8s-3-sandbox-nubes-ru" +# realm = "naeel-test-3" +s3_user_uid = "332cdb0d-34bf-43bf-864d-4adcc3b556fb" +s3_name = "naeel-s3" diff --git a/CRUD/terraform.tfvars.example b/CRUD/terraform.tfvars.example new file mode 100644 index 0000000..63ae1ea --- /dev/null +++ b/CRUD/terraform.tfvars.example @@ -0,0 +1,16 @@ +# ============================================================================= +# terraform.tfvars.example +# Заполнить своими значениями и переименовать в terraform.tfvars +# ============================================================================= +# +# Где брать: +# api_token — ЛК → Профиль → Токены → создать «Технический» +# realm — ЛК → Кластеры → выбрать (напр. k8s-3-sandbox-nubes-ru) +# s3_name — ЛК → S3 → Имя экземпляра +# s3_user_uid — там же → UUID. Указать ОДНО из s3_name/s3_user_uid +# ============================================================================= + +api_token = "" # JWT-токен +realm = "k8s-3-sandbox-nubes-ru" # кластер Kubernetes +s3_name = "" # имя S3-экземпляра +s3_user_uid = "" # UUID S3-пользователя