This commit is contained in:
“Naeel” 2026-03-09 17:57:29 +04:00
commit 9293985003
33 changed files with 898 additions and 0 deletions

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# S3 конфиги с кредами — не коммитим
.s3cfg*
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin
testbin/*
hack/local.env
Dockerfile.cross
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*
# editor and IDE paraphernalia
.idea
*.swp
*.swo
*~
terraform/provider/build/
# Terraform state и кэш провайдеров (не коммитим)
**/.terraform/
**/.terraform.lock.hcl
**/terraform.tfstate
**/terraform.tfstate.backup
**/terraform.tfstate.*.backup
**/handler.zip
# Локальные артефакты упаковки функций
**/dist/

141
README.md Normal file
View File

@ -0,0 +1,141 @@
# Примеры sless
Примеры показывают различные сценарии использования serverless функций через Terraform провайдер `terra.k8c.ru/naeel/sless`.
## Требования
- Terraform >= 1.0
- Доступ к `https://sless-api.kube5s.ru`
## Провайдер
Во всех примерах `main.tf` содержит:
```hcl
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
}
```
---
## Примеры
### `simple-python` — джоб передаёт результат в HTTP-функцию (Python)
При `apply` запускается джоб, его вывод передаётся в HTTP-функцию через `env_vars`.
```bash
cd simple-python
terraform init
terraform apply -auto-approve
# Что вернул джоб (время на момент деплоя):
terraform output job_result
# Проверить функцию:
curl -s https://sless-api.kube5s.ru/fn/default/simple-py-time-display
```
---
### `simple-node` — то же самое, но на Node.js 20
```bash
cd simple-node
terraform init
terraform apply -auto-approve
terraform output job_result
curl -s https://sless-api.kube5s.ru/fn/default/simple-node-time-display
```
---
### `hello-node` — минимальный пример на Node.js
Две независимые функции: HTTP-функция (возвращает приветствие) и одноразовый джоб (суммирует числа).
```bash
cd hello-node
terraform init
terraform apply -auto-approve
# Проверить HTTP-функцию:
curl -s -X POST https://sless-api.kube5s.ru/fn/default/hello-http \
-H 'Content-Type: application/json' -d '{"name":"World"}'
# Посмотреть результат джоба:
terraform output job_message
```
---
### `notes-python` — CRUD API на Python + PostgreSQL
Полноценное приложение: инициализация схемы БД через джобы, CRUD-функция, read-only функция для списка записей.
**Переменные:**
| Переменная | Описание | Дефолт |
|---|---|---|
| `pg_dsn` | DSN для подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` |
```bash
cd notes-python
terraform init
# Опционально — переопределить DSN:
# export TF_VAR_pg_dsn="postgres://user:pass@host:5432/db?sslmode=disable"
terraform apply -auto-approve
# Проверить инициализацию БД:
terraform output db_init_table_status
terraform output db_init_index_status
# URL функций:
terraform output notes_url # CRUD
terraform output notes_list_url # список всех записей
# Создать запись:
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/add?title=Hello&body=World"
# Список записей:
curl -s https://sless-api.kube5s.ru/fn/default/notes-list
# Обновить (id из предыдущего ответа):
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/update?id=1&title=Updated&body=New+body"
# Удалить:
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/delete?id=1"
```
---
## Общие команды
```bash
# Посмотреть текущее состояние ресурсов:
terraform show
# Пересоздать конкретный ресурс:
terraform apply -replace=sless_function.имя -auto-approve
# Повторно запустить джоб — увеличить run_id в .tf файле, затем:
terraform apply -auto-approve
# Удалить все ресурсы примера:
terraform destroy -auto-approve
```
## Структура каждого примера
```
пример/
├── main.tf — провайдер
├── *.tf — ресурсы (функции, триггеры, джобы)
├── outputs.tf — URLs и статусы после apply
├── variables.tf — входные переменные (если есть)
└── code/ — исходный код функций
```

View File

@ -0,0 +1,8 @@
// 2026-03-08
// handler-http.js — HTTP-функция: возвращает приветствие.
// Используется с sless_trigger (постоянный эндпоинт).
exports.handle = async (event) => {
const name = event.name || 'World';
return { message: `Hello, ${name}! HTTP !!!` };
};

View File

@ -0,0 +1,11 @@
// 2026-03-08
// handler-job.js — batch-функция: суммирует числа и считает среднее.
// Используется с sless_job (одноразовый запуск).
// event.numbers — массив чисел, например [1, 2, 3, 4, 5]
exports.handle = async (event) => {
const numbers = event.numbers || [];
const sum = numbers.reduce((acc, n) => acc + n, 0);
const avg = numbers.length > 0 ? sum / numbers.length : 0;
return { input: numbers, sum, avg, count: numbers.length };
};

24
hello-node/http.tf Normal file
View File

@ -0,0 +1,24 @@
# 2026-03-08 / Изменено: 2026-03-09
# http.tf HTTP-функция: принимает запросы, возвращает приветствие.
# Код: code/handler-http.js
resource "sless_function" "hello_http" {
name = "hello-http"
runtime = "nodejs20"
entrypoint = "handler-http.handle"
memory_mb = 128
timeout_sec = 30
source_dir = "${path.module}/code"
}
resource "sless_trigger" "hello_http" {
name = "hello-http-trigger"
type = "http"
function = sless_function.hello_http.name
enabled = true
}
output "trigger_url" {
value = sless_trigger.hello_http.url
}

34
hello-node/job.tf Normal file
View File

@ -0,0 +1,34 @@
# 2026-03-08 / Изменено: 2026-03-09
# job.tf одноразовая функция: суммирует числа из переданного массива.
# Код: code/handler-job.js
resource "sless_function" "hello_job" {
name = "hello-job"
runtime = "nodejs20"
entrypoint = "handler-job.handle"
memory_mb = 128
timeout_sec = 30
source_dir = "${path.module}/code"
}
# Одноразовый запуск. Для повторного запуска увеличь run_id (123...).
resource "sless_job" "hello_run" {
name = "hello-run"
function = sless_function.hello_job.name
event_json = jsonencode({ numbers = [100, 200, 300] })
wait_timeout_sec = 600
run_id = 9
}
output "job_phase" {
value = sless_job.hello_run.phase
}
output "job_message" {
value = sless_job.hello_run.message
}
output "job_completion_time" {
value = sless_job.hello_run.completion_time
}

20
hello-node/main.tf Normal file
View File

@ -0,0 +1,20 @@
# 2026-03-08
# main.tf провайдеры.
# Функции и их код определены в отдельных файлах:
# http.tf HTTP-триггер (code/handler-http.js)
# job.tf одноразовый запуск (code/handler-job.js)
terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.10"
}
}
}
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = "dev-token-change-me"
}

View File

@ -0,0 +1,2 @@
# Временный файл для негативных тестов — не применяется через terraform
# Тесты запускаются вручную с временным переименованием в .tf

View File

@ -0,0 +1,31 @@
# 2026-03-09
# notes_list.py — чтение всех записей из таблицы notes.
#
# Назначение: отдать полный список заметок одним запросом.
# Принимает GET или POST — тело/query параметры игнорируются.
# Возвращает JSON-массив, сортировка: новые записи первые (ORDER BY created_at DESC).
#
# Пример ответа:
# [
# {"id": 3, "title": "Hello", "body": "World", "created_at": "2026-03-09 ..."},
# {"id": 1, "title": "First", "body": "Note", "created_at": "2026-03-08 ..."}
# ]
import os
import psycopg2
import psycopg2.extras
def list_notes(event):
dsn = os.environ['PG_DSN']
conn = psycopg2.connect(dsn)
try:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute(
"SELECT id, title, body, created_at::text FROM notes ORDER BY created_at DESC"
)
rows = cur.fetchall()
return [dict(r) for r in rows]
except Exception as e:
return {'error': str(e)}
finally:
conn.close()

View File

@ -0,0 +1 @@
psycopg2-binary

View File

@ -0,0 +1,81 @@
# 2026-03-09
# notes_crud.py — CRUD роутер для таблицы notes.
#
# Назначение: единая функция, которая обрабатывает все операции с записями.
# Роутинг осуществляется по sub-path URL (event._path), который runtime
# берёт из входящего HTTP-запроса и добавляет в event автоматически.
#
# Доступные маршруты (все POST):
# /fn/default/notes/add?title=...&body=... → создать запись
# /fn/default/notes/update?id=1&title=...&body=... → обновить запись
# /fn/default/notes/delete?id=1 → удалить запись
#
# Параметры берутся из query string (event._query) или из тела запроса (event).
# event._path и event._query добавляет Python runtime (server.py) автоматически.
import os
import psycopg2
import psycopg2.extras
def crud(event):
dsn = os.environ['PG_DSN']
# sub-path без ведущего слэша: "add", "update", "delete"
action = event.get('_path', '/').strip('/')
q = event.get('_query', {})
conn = psycopg2.connect(dsn)
try:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
if action == 'add':
title = q.get('title') or event.get('title', '')
body = q.get('body') or event.get('body', '')
if not title:
return {'error': 'title is required'}
cur.execute(
"INSERT INTO notes (title, body) VALUES (%s, %s)"
" RETURNING id, title, body, created_at::text",
(title, body)
)
row = cur.fetchone()
conn.commit()
return dict(row)
elif action == 'update':
id_ = q.get('id') or event.get('id')
if not id_:
return {'error': 'id is required'}
title = q.get('title') or event.get('title', '')
body = q.get('body') or event.get('body', '')
cur.execute(
"UPDATE notes SET title=%s, body=%s WHERE id=%s"
" RETURNING id, title, body, created_at::text",
(title, body, int(id_))
)
row = cur.fetchone()
conn.commit()
return dict(row) if row else {'error': 'not found'}
elif action == 'delete':
id_ = q.get('id') or event.get('id')
if not id_:
return {'error': 'id is required'}
cur.execute(
"DELETE FROM notes WHERE id=%s RETURNING id",
(int(id_),)
)
row = cur.fetchone()
conn.commit()
return {'deleted': row['id']} if row else {'error': 'not found'}
else:
return {
'error': f'unknown action: /{action}',
'hint': 'use /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X'
}
except Exception as e:
conn.rollback()
return {'error': str(e)}
finally:
conn.close()

View File

@ -0,0 +1 @@
psycopg2-binary

View File

@ -0,0 +1 @@
psycopg2-binary

View File

@ -0,0 +1,39 @@
# 2026-03-09
# sql_runner.py — универсальный DDL/SQL исполнитель.
#
# Назначение: выполнять произвольные SQL запросы переданные через event.
# Используется ТОЛЬКО через sless_job (init.tf) — HTTP-триггера нет намеренно,
# чтобы никто снаружи не мог выполнить произвольный SQL.
#
# Входящий event:
# {
# "statements": [
# "CREATE TABLE IF NOT EXISTS ...",
# "CREATE INDEX IF NOT EXISTS ..."
# ]
# }
#
# Все statements выполняются последовательно в одной транзакции.
# Если хотя бы один упал — транзакция откатывается целиком.
import os
import psycopg2
def run_sql(event):
dsn = os.environ['PG_DSN']
statements = event.get('statements', [])
if not statements:
return {'error': 'no statements provided'}
conn = psycopg2.connect(dsn)
try:
cur = conn.cursor()
for sql in statements:
cur.execute(sql)
conn.commit()
return {'ok': True, 'executed': len(statements)}
except Exception as e:
conn.rollback()
return {'error': str(e)}
finally:
conn.close()

44
notes-python/init.tf Normal file
View File

@ -0,0 +1,44 @@
# 2026-03-09
# init.tf однократная инициализация схемы БД через sless_job.
#
# Джобы запускаются один раз при terraform apply и ждут завершения.
# Использует функцию sql_runner (без HTTP-триггера) для безопасного DDL.
#
# Порядок выполнения гарантирован через depends_on:
# 1. notes_table_init создаём таблицу
# 2. notes_index_init создаём индекс (требует таблицу)
#
# Для повторного запуска (например, после DROP TABLE) увеличь run_id.
# run_id отслеживается в state: при изменении terraform перезапустит джоб.
# Джоб создания таблицы notes.
# CREATE TABLE IF NOT EXISTS безопасно запускать повторно, таблица не пересоздаётся.
resource "sless_job" "notes_table_init" {
name = "notes-create-table"
function = sless_function.sql_runner.name
wait_timeout_sec = 120
run_id = 1
event_json = jsonencode({
statements = [
"CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, title text NOT NULL, body text, created_at timestamp DEFAULT now())"
]
})
}
# Джоб создания индекса для сортировки по дате.
# depends_on гарантирует, что таблица уже создана до создания индекса.
resource "sless_job" "notes_index_init" {
depends_on = [sless_job.notes_table_init]
name = "notes-create-index"
function = sless_function.sql_runner.name
wait_timeout_sec = 60
run_id = 1
event_json = jsonencode({
statements = [
"CREATE INDEX IF NOT EXISTS notes_created_idx ON notes(created_at DESC)"
]
})
}

27
notes-python/main.tf Normal file
View File

@ -0,0 +1,27 @@
# 2026-03-09
# main.tf конфигурация terraform и провайдеров.
#
# Все ресурсы вынесены в отдельные .tf файлы по назначению:
# variables.tf входные переменные (pg_dsn)
# sql-runner.tf служебная DDL-функция (без HTTP-триггера)
# init.tf однократная инициализация схемы БД
# notes.tf CRUD функция + HTTP-триггер
# notes-list.tf read-only функция + HTTP-триггер
# outputs.tf URLs развёрнутых эндпоинтов
terraform {
required_providers {
# Провайдер для управления serverless функциями через sless API
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.10"
}
}
}
# sless провайдер подключается к API кластера.
# В продакшне token следует передавать через TF_VAR или secrets.
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = "dev-token-change-me"
}

View File

@ -0,0 +1,22 @@
# 2026-03-09
# notes-list.tf read-only эндпоинт: возвращает все заметки, сортировка новые первые.
resource "sless_function" "notes_list" {
name = "notes-list"
runtime = "python3.11"
entrypoint = "notes_list.list_notes"
memory_mb = 128
timeout_sec = 30
env_vars = {
PG_DSN = var.pg_dsn
}
source_dir = "${path.module}/code/notes-list"
}
resource "sless_trigger" "notes_list_http" {
name = "notes-list-http"
type = "http"
function = sless_function.notes_list.name
}

27
notes-python/notes.tf Normal file
View File

@ -0,0 +1,27 @@
# 2026-03-09
# notes.tf CRUD функция для управления заметками (CREATE / UPDATE / DELETE).
#
# Маршруты (рекомендуется POST):
# /fn/default/notes/add?title=...&body=... INSERT
# /fn/default/notes/update?id=1&title=...&body=... UPDATE
# /fn/default/notes/delete?id=1 DELETE
resource "sless_function" "notes_crud" {
name = "notes"
runtime = "python3.11"
entrypoint = "notes_crud.crud"
memory_mb = 128
timeout_sec = 30
env_vars = {
PG_DSN = var.pg_dsn
}
source_dir = "${path.module}/code/notes"
}
resource "sless_trigger" "notes_crud_http" {
name = "notes-http"
type = "http"
function = sless_function.notes_crud.name
}

42
notes-python/outputs.tf Normal file
View File

@ -0,0 +1,42 @@
# 2026-03-09
# outputs.tf публичные URL развёрнутых функций.
#
# После terraform apply используй эти URLs для тестирования:
# terraform output notes_url базовый URL для CRUD
# terraform output notes_list_url URL для получения всех записей
# URL CRUD-функции (notes_crud).
# Базовый URL к нему добавляй sub-path:
# POST $(terraform output -raw notes_url)/add?title=Hello&body=World
# POST $(terraform output -raw notes_url)/update?id=1&title=Updated
# POST $(terraform output -raw notes_url)/delete?id=1
output "notes_url" {
value = sless_trigger.notes_crud_http.url
description = "CRUD: /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X"
}
# URL read-only функции (notes_list).
# Принимает GET или POST, параметры игнорирует, возвращает все записи.
output "notes_list_url" {
value = sless_trigger.notes_list_http.url
description = "Список всех записей (GET или POST)"
}
# Статус init-джобов показывает результат инициализации БД.
# Если phase="Succeeded" таблица и индекс созданы успешно.
# Если phase="Failed" смотри message, исправь и увеличь run_id в init.tf.
output "db_init_table_status" {
value = {
phase = sless_job.notes_table_init.phase
message = sless_job.notes_table_init.message
}
description = "Статус джоба создания таблицы notes"
}
output "db_init_index_status" {
value = {
phase = sless_job.notes_index_init.phase
message = sless_job.notes_index_init.message
}
description = "Статус джоба создания индекса"
}

View File

@ -0,0 +1,20 @@
# 2026-03-09
# sql-runner.tf служебная DDL-функция для инициализации и миграций БД.
#
# ВАЖНО: эта функция не имеет HTTP-триггера только вызов через sless_job.
# Это сделано намеренно: функция выполняет произвольный SQL, и открывать её
# наружу через HTTP было бы небезопасно.
resource "sless_function" "sql_runner" {
name = "sql-runner"
runtime = "python3.11"
entrypoint = "sql_runner.run_sql"
memory_mb = 128
timeout_sec = 30
env_vars = {
PG_DSN = var.pg_dsn
}
source_dir = "${path.module}/code/sql-runner"
}

15
notes-python/variables.tf Normal file
View File

@ -0,0 +1,15 @@
# 2026-03-09
# variables.tf входные переменные для notes-python примера.
#
# PG_DSN передаётся во все функции через env_vars.
# Хранится как sensitive чтобы не светился в terraform output и логах.
# В продакшне не хардкоди DSN здесь, используй TF_VAR_pg_dsn или secrets manager.
# DSN для подключения к PostgreSQL внутри кластера.
# Формат: postgres://user:password@host:port/dbname?sslmode=...
variable "pg_dsn" {
description = "PostgreSQL DSN для подключения к БД внутри кластера"
type = string
default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable"
sensitive = true
}

View File

@ -0,0 +1,20 @@
// Создано: 2026-03-09
// time_display.js — HTTP-функция (постоянный Deployment + Trigger).
// Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message.
// Демонстрирует цепочку: Job вычисляет данные → Function использует их через env.
exports.showTime = function(event) {
// JOB_TIME устанавливается terraform из статуса джоба (JSON строка)
const jobTimeRaw = process.env.JOB_TIME || '{}';
let jobTime;
try {
const parsed = JSON.parse(jobTimeRaw);
jobTime = parsed.time || jobTimeRaw;
} catch (e) {
jobTime = jobTimeRaw;
}
return {
message: `Сервис запустился в: ${jobTime}`,
path: event._path || '/',
};
};

View File

@ -0,0 +1,10 @@
// Создано: 2026-03-09
// time_getter.js — функция запускается как Job (одноразово).
// Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout
// и записывает его в sless_job.run_getter.message — оттуда terraform передаёт
// значение в sless_function.display через env_var JOB_TIME.
exports.getTime = function(event) {
// Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib
return { time: new Date().toISOString() };
};

30
simple-node/main.tf Normal file
View File

@ -0,0 +1,30 @@
# Создано: 2026-03-09
# main.tf пример: запустить один раз скрипт при деплое и передать его результат в функцию.
# То же самое что simple-python, но на Node.js 20.
#
# Как это работает:
# 1. При «terraform apply» запускается скрипт-джоб (time_getter)
# 2. Скрипт возвращает JSON с текущим временем
# 3. Terraform подхватывает этот JSON и передаёт в переменную окружения HTTP-функции (time_display)
# 4. Функция отдаёт время при каждом запросе
#
# Зачем такое нужно:
# Если данные нужны функции, но считаются один раз при деплое
# напишите логику в джоб, а результат передайте через env_vars.
# Например: получить токен, версию схемы БД, время деплоя и т.д.
#
# namespace захардкодирован внутри провайдера, здесь ничего указывать.
terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.10"
}
}
}
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = "dev-token-change-me"
}

14
simple-node/outputs.tf Normal file
View File

@ -0,0 +1,14 @@
# Создано: 2026-03-09
# outputs.tf что выводит terraform после apply.
# Адрес вашей функции откройте в браузере или вставьте в curl
output "display_url" {
description = "URL функции time_display"
value = sless_trigger.display_http.url
}
# Что вернул скрипт-джоб именно это передано в функцию как JOB_TIME
output "job_result" {
description = "Результат выполнения скрипта time_getter"
value = sless_job.run_getter.message
}

View File

@ -0,0 +1,28 @@
# Создано: 2026-03-09 / Изменено: 2026-03-09
# time-display.tf HTTP-функция, доступная по URL после apply.
# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME.
# HTTP-функция отвечает на запросы по URL из outputs.tf
resource "sless_function" "time_display" {
name = "simple-node-time-display" # уникальное имя в namespace
runtime = "nodejs20"
entrypoint = "time_display.showTime" # файл.функция в code/time_display/
memory_mb = 64
# Передаём результат джоба в функцию через переменную окружения.
# В коде функции: process.env.JOB_TIME
env_vars = {
JOB_TIME = sless_job.run_getter.message
}
source_dir = "${path.module}/code/time_display"
depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции
}
# Публикуем функцию по HTTP URL будет в outputs.tf
resource "sless_trigger" "display_http" {
name = "simple-node-display-http"
type = "http"
function = sless_function.time_display.name
}

View File

@ -0,0 +1,27 @@
# Создано: 2026-03-09 / Изменено: 2026-03-09
# time-getter.tf скрипт который запускается ОДИН РАЗ при terraform apply.
# После запуска его результат доступен через: sless_job.run_getter.message
# Смотри time-display.tf там этот результат передаётся в функцию.
# Функция для скрипта без HTTP-триггера, вызывается только через джоб ниже
resource "sless_function" "time_getter" {
name = "simple-node-time-getter" # уникальное имя в namespace
runtime = "nodejs20"
entrypoint = "time_getter.getTime" # файл.функция в code/time_getter/
memory_mb = 64
source_dir = "${path.module}/code/time_getter"
}
# Джоб запускает функцию time_getter один раз прямо при apply.
# run_id = 1 означает «запустить». Если увеличить (2, 3...) запустится снова.
# После завершения: sless_job.run_getter.message = то что вернула функция
resource "sless_job" "run_getter" {
name = "simple-node-getter-run"
function = sless_function.time_getter.name
run_id = 1
wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта
event_json = "{}" # входные данные для скрипта (пусто данные не нужны)
depends_on = [sless_function.time_getter]
}

View File

@ -0,0 +1,20 @@
# Создано: 2026-03-09
# time_display.py — HTTP-функция (постоянный Deployment + Trigger).
# Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message.
# Демонстрирует цепочку: Job вычисляет данные → Function использует их через env.
import json
import os
def show_time(event):
# JOB_TIME устанавливается terraform из статуса джоба (JSON строка)
job_time_raw = os.environ.get("JOB_TIME", "{}")
try:
job_time = json.loads(job_time_raw).get("time", job_time_raw)
except (json.JSONDecodeError, AttributeError):
job_time = job_time_raw
return {
"message": f"Сервис запустился в: {job_time}",
"path": event.get("_path", "/"),
}

View File

@ -0,0 +1,18 @@
# Создано: 2026-03-09
# time_getter.py — функция запускается как Job (одноразово).
# Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout
# и записывает его в sless_job.run_getter.message — оттуда terraform передаёт
# значение в sless_function.display через env_var JOB_TIME.
import json
from datetime import datetime, timezone
def get_time(event):
# Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib
return {"time": datetime.now(timezone.utc).isoformat()}
if __name__ == "__main__":
# Для локального тестирования без оператора
print(json.dumps(get_time({})))

29
simple-python/main.tf Normal file
View File

@ -0,0 +1,29 @@
# Создано: 2026-03-09
# main.tf пример: запустить один раз скрипт при деплое и передать его результат в функцию.
#
# Как это работает:
# 1. При «terraform apply» запускается скрипт-джоб (time_getter)
# 2. Скрипт возвращает JSON с текущим временем
# 3. Terraform подхватывает этот JSON и передаёт в переменную окружения HTTP-функции (time_display)
# 4. Функция отдаёт время при каждом запросе
#
# Зачем такое нужно:
# Если данные нужны функции, но считаются один раз при деплое
# напишите логику в джоб, а результат передайте через env_vars.
# Например: получить токен, версию схемы БД, время деплоя и т.д.
#
# namespace захардкодирован внутри провайдера, здесь ничего указывать.
terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.10"
}
}
}
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = "dev-token-change-me"
}

14
simple-python/outputs.tf Normal file
View File

@ -0,0 +1,14 @@
# Создано: 2026-03-09
# outputs.tf что выводит terraform после apply.
# Адрес вашей функции откройте в браузере или вставьте в curl
output "display_url" {
description = "URL функции time_display"
value = sless_trigger.display_http.url
}
# Что вернул скрипт-джоб именно это передано в функцию как JOB_TIME
output "job_result" {
description = "Результат выполнения скрипта time_getter"
value = sless_job.run_getter.message
}

View File

@ -0,0 +1,28 @@
# Создано: 2026-03-09 / Изменено: 2026-03-09
# time-display.tf HTTP-функция, доступная по URL после apply.
# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME.
# HTTP-функция отвечает на запросы по URL из outputs.tf
resource "sless_function" "time_display" {
name = "simple-py-time-display" # уникальное имя в namespace
runtime = "python3.11"
entrypoint = "time_display.show_time" # файл.функция в code/time_display/
memory_mb = 64
# Передаём результат джоба в функцию через переменную окружения.
# В коде функции: os.environ.get("JOB_TIME")
env_vars = {
JOB_TIME = sless_job.run_getter.message
}
source_dir = "${path.module}/code/time_display"
depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции
}
# Публикуем функцию по HTTP URL будет в outputs.tf
resource "sless_trigger" "display_http" {
name = "simple-py-display-http"
type = "http"
function = sless_function.time_display.name
}

View File

@ -0,0 +1,27 @@
# Создано: 2026-03-09 / Изменено: 2026-03-09
# time-getter.tf скрипт который запускается ОДИН РАЗ при terraform apply.
# После запуска его результат доступен через: sless_job.run_getter.message
# Смотри time-display.tf там этот результат передаётся в функцию.
# Функция для скрипта без HTTP-триггера, вызывается только через джоб ниже
resource "sless_function" "time_getter" {
name = "simple-py-time-getter" # уникальное имя в namespace
runtime = "python3.11"
entrypoint = "time_getter.get_time" # файл.функция в code/time_getter/
memory_mb = 64
source_dir = "${path.module}/code/time_getter"
}
# Джоб запускает функцию time_getter один раз прямо при apply.
# run_id = 1 означает «запустить». Если увеличить (2, 3...) запустится снова.
# После завершения: sless_job.run_getter.message = то что вернула функция
resource "sless_job" "run_getter" {
name = "simple-py-getter-run"
function = sless_function.time_getter.name
run_id = 1
wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта
event_json = "{}" # входные данные для скрипта (пусто данные не нужны)
depends_on = [sless_function.time_getter]
}