Обратите внимание: TypeScript-шаблоны — экспериментальная функция. Для включения установите переменную окружения
NELM_FEAT_TYPESCRIPT=true.
Обзор
Помимо Helm-шаблонов, werf поддерживает генерацию Kubernetes-манифестов с помощью TypeScript. Helm-шаблоны и TypeScript-шаблоны могут сосуществовать в одном чарте — полученные манифесты объединяются в единый мульти-документный YAML.
TypeScript-шаблоны работают из коробки: развёртывание чарта с директорией ts/ не требует дополнительных инструментов или настройки — werf автоматически скачивает рантайм Deno TypeScript и рендерит TypeScript-шаблоны.
Зачем TypeScript
Язык шаблонов Helm хорошо работает для простых случаев, но с ростом сложности чарта становится сложным в поддержке: примитивный язык с большим количеством подводных камней, ограниченная стандартная библиотека, проблемы с производительностью, сложная отладка, слабая поддержка в IDE и редакторах. TypeScript в werf решает эти проблемы, не усложняя процесс развёртывания.
Возможности
- Поддержка IDE — автокомплит, проверка типов, go-to-definition и рефакторинг в любом редакторе с поддержкой Deno/TypeScript (VS Code, JetBrains, Neovim и др.).
- Стандартный синтаксис — обычные функции, циклы и условия вместо неудобных конструкций шаблонизатора.
- Чистый TypeScript — директория
tsявляется обычным Deno TypeScript-проектом и может рендериться без werf, с помощью одного лишь рантайма Deno TypeScript. - Большая экосистема — TypeScript один из самых популярных языков с обширной документацией, ресурсами сообщества и инструментарием.
- Возможность использовать практически любую стороннюю TypeScript/JavaScript-библиотеку, например kubernetes-models, cdk8s или любую другую из экосистем npm/Deno.
- Тестирование — тестируйте код с помощью привычных TypeScript-библиотек и инструментов.
- Никаких дополнительных требований к хосту — для развёртывания TypeScript-чарта достаточно только werf. Не нужно устанавливать Node, Deno, npm, модули npm или что-либо ещё. Мы берём это на себя — просто выполните
werf converge. - Изолированные окружения — модули npm по умолчанию включаются в бандл чарта, а рантайм Deno может предоставляться хостовой системой, так что во время развёртывания не будет сетевых обращений, кроме обращений к самому Kubernetes.
- Безопасность — код выполняется в изолированной песочнице Deno без доступа к сети, переменным окружения и запуску процессов. Доступ к файловой системе ограничен чтением файлов чарта.
Быстрый старт
Инициализация TypeScript-файлов в существующем чарте:
werf chart ts init
Команда создаст директорию .helm/ts/ с готовым скелетом TypeScript-проекта и несколькими файлами с примерами ресурсов. Попробуйте отредактировать ts/src/deployment.ts — например, изменить количество реплик — и проверьте результат:
werf render --dev
Для развёртывания:
werf converge --dev
Структура чарта
apiVersion: v2
name: ts-chart-example
version: 0.1.0
{
"tasks": {
"build": {
"description": "Run deno build",
"command": "deno bundle --output=dist/bundle.js src/index.ts"
},
"dev": {
"description": "Run in development mode",
"command": "deno run --no-remote --deny-read --deny-write --deny-net --deny-env --deny-run --allow-read=input.example.yaml src/index.ts --input-file ./input.example.yaml"
},
"start": {
"description": "Run the bundled dist/bundle.js",
"command": "deno run --no-remote --deny-read --deny-write --deny-net --deny-env --deny-run --allow-read=input.example.yaml dist/bundle.js --input-file ./input.example.yaml"
}
},
"imports": {
"@nelm/chart-ts-sdk": "npm:@nelm/chart-ts-sdk@^0.1.5"
}
}
{
"version": "5",
"specifiers": {
"npm:@nelm/chart-ts-sdk@~0.1.4": "0.1.4"
},
"npm": {
"@jsr/std__yaml@1.0.12": {
"integrity": "sha512-pz/BisWZWH16JvLJBwrNwUwfIsRnf9qniMrmI6Z3vIAcVRVFcA5+i4o6z6QqsMKqFzjlB66WZE+jSyujT/RvRg==",
"tarball": "https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.12.tgz"
},
"@nelm/chart-ts-sdk@0.1.4": {
"integrity": "sha512-NCeflvAuZQxzmGZGpm0lP3Uy5d2xYRq8TKW0MWTKyknnZAe2tSv9+2uCZVA46oIxdm9FsBKocmCHEfqyOwe4lQ==",
"dependencies": [
"@std/yaml@npm:@jsr/std__yaml@1.0.12"
]
}
},
"workspace": {
"dependencies": [
"npm:@nelm/chart-ts-sdk@~0.1.4"
]
}
}
Capabilities:
APIVersions:
- v1
HelmVersion:
go_version: go1.25.0
version: v3.20
KubeVersion:
Major: "1"
Minor: "35"
Version: v1.35.0
Chart:
APIVersion: v2
Annotations:
anno: value
AppVersion: 1.0.0
Condition: ts-chart-example.enabled
Description: ts-chart-example description
Home: https://example.org/home
Icon: https://example.org/icon
Keywords:
- ts-chart-example
Maintainers:
- Email: john@example.com
Name: john
URL: https://example.com/john
Name: ts-chart-example
Sources:
- https://example.org/ts-chart-example
Tags: ts-chart-example
Type: application
Version: 0.1.0
Files:
myfile: "content"
Release:
IsInstall: false
IsUpgrade: true
Name: ts-chart-example
Namespace: ts-chart-example
Revision: 2
Service: Helm
Values:
global:
werf:
name: myapp
version: v2.35.0
repo: example.org/mycompany/myapp
env: production
images:
app:
registry: example.org
namespace: mycompany
name: myapp
tag: a1b2c3d4-1234567890
digest: "sha256:abcdef1234567890"
tag_digest: "a1b2c3d4-1234567890@sha256:abcdef1234567890"
image: example.org/mycompany/myapp
repository: mycompany/myapp
ref: "example.org/mycompany/myapp:a1b2c3d4-1234567890@sha256:abcdef1234567890"
ref_tag: "example.org/mycompany/myapp:a1b2c3d4-1234567890"
repository_ref: "mycompany/myapp:a1b2c3d4-1234567890@sha256:abcdef1234567890"
repository_tag: "mycompany/myapp:a1b2c3d4-1234567890"
name_ref: "myapp:a1b2c3d4-1234567890@sha256:abcdef1234567890"
name_tag: "myapp:a1b2c3d4-1234567890"
commit:
date:
human: "2025-01-15 12:00:00 +0000"
unix: 1736942400
hash: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
image:
repository: nginx
tag: latest
replicaCount: 1
service:
enabled: true
port: 80
type: ClusterIP
import type { WerfRenderContext } from '@nelm/chart-ts-sdk';
import { getFullname, getLabels, getSelectorLabels } from './helpers.ts';
export function newDeployment($: WerfRenderContext): object {
const name = getFullname($);
return {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
name,
labels: getLabels($),
},
spec: {
replicas: $.Values.replicaCount ?? 1,
selector: {
matchLabels: getSelectorLabels($),
},
template: {
metadata: {
labels: getSelectorLabels($),
},
spec: {
containers: [
{
name: name,
image: ($.Values.image?.repository ?? 'nginx') + ':' + ($.Values.image?.tag ?? 'latest'),
ports: [
{
name: 'http',
containerPort: $.Values.service?.port ?? 80,
},
],
},
],
},
},
},
};
}
import type { WerfRenderContext } from '@nelm/chart-ts-sdk';
/**
* Truncate string to max length, removing trailing hyphens.
*/
export function trunc(str: string, max: number): string {
if (str.length <= max) return str;
return str.slice(0, max).replace(/-+$/, '');
}
/**
* Get the fully qualified app name.
* Truncated at 63 chars (DNS naming spec limit).
*/
export function getFullname($: WerfRenderContext): string {
if ($.Values.fullnameOverride) {
return trunc($.Values.fullnameOverride, 63);
}
const chartName = $.Values.nameOverride || $.Chart.Name;
if ($.Release.Name.includes(chartName)) {
return trunc($.Release.Name, 63);
}
return trunc(`${$.Release.Name}-${chartName}`, 63);
}
export function getLabels($: WerfRenderContext): Record<string, string> {
return {
'app.kubernetes.io/name': $.Chart.Name,
'app.kubernetes.io/instance': $.Release.Name,
};
}
export function getSelectorLabels($: WerfRenderContext): Record<string, string> {
return {
'app.kubernetes.io/name': $.Chart.Name,
'app.kubernetes.io/instance': $.Release.Name,
};
}
import { WerfRenderContext, RenderResult, render } from '@nelm/chart-ts-sdk';
import { newDeployment } from './deployment.ts';
import { newService } from './service.ts';
function generate($: WerfRenderContext): RenderResult {
const manifests: object[] = [];
manifests.push(newDeployment($));
if ($.Values.service?.enabled !== false) {
manifests.push(newService($));
}
return { manifests };
}
await render(generate);
import type { WerfRenderContext } from '@nelm/chart-ts-sdk';
import { getFullname, getLabels, getSelectorLabels } from './helpers.ts';
export function newService($: WerfRenderContext): object {
return {
apiVersion: 'v1',
kind: 'Service',
metadata: {
name: getFullname($),
labels: getLabels($),
},
spec: {
type: $.Values.service?.type ?? 'ClusterIP',
ports: [
{
port: $.Values.service?.port ?? 80,
targetPort: 'http',
},
],
selector: getSelectorLabels($),
},
};
}
replicaCount: 1
image:
repository: nginx
tag: latest
service:
enabled: true
type: ClusterIP
port: 80
Разработка чарта с TypeScript-шаблонами
Установите Deno и следуйте руководству по настройке для вашей IDE или редактора (VS Code, JetBrains, Neovim и др.).
Инициализируйте TypeScript-файлы в чарте, если они ещё не инициализированы:
werf chart ts init
Откройте директорию ts/ в редакторе как обычный Deno/TypeScript-проект. Работайте с ним так же, как с любой TypeScript-кодовой базой — запускайте скрипты, пишите тесты, используйте отладчик. Deno предоставляет богатый набор инструментов для тестирования, линтинга, форматирования и многого другого. Подробнее см. документацию Deno.
Структуру кодовой базы можно организовать по своему усмотрению. Единственное требование — файл ts/src/index.ts должен существовать, и функция render из @nelm/chart-ts-sdk обязательно должна быть вызвана. Иначе рендеринг TypeScript не произойдёт.
Для отладки рендеринга шаблонов в окружении, максимально близком к тому, как werf запускает Deno, используйте задачу dev из ts/deno.json:
cd .helm/ts
deno task dev
TypeScript-движок вызовет функцию render из ts/src/index.ts с примером контекста из ts/input.example.yaml. Полученный YAML будет выведен в консоль после сообщения Rendered manifests:.
Устанавливайте библиотеки с помощью deno add, например попробуйте установить kubernetes-models — библиотеку для строгой типизации Kubernetes-ресурсов:
deno add npm:kubernetes-models
Зависимость добавится в deno.json автоматически. Теперь можно импортировать и использовать её:
// .helm/ts/src/deployment.ts:
import { Deployment } from 'kubernetes-models/apps/v1';
export function newDeployment($: WerfRenderContext): object {
return new Deployment({
metadata: { name: 'myapp' },
spec: {
// other fields
},
}).toJSON();
}
Чтобы убедиться, что всё работает с рантаймом Deno из werf, выполните:
werf lint --dev
werf render --dev
Как развернуть чарт с TypeScript-шаблонами
Просто запустите werf converge: бинарный файл Deno будет скачан в кеш, TypeScript-шаблоны будут отрендерены и развёрнуты.
Обратите внимание: Согласно политикам гиттерминизма, все изменённые файлы должны быть закоммичены.
Развёртывание в изолированных окружениях
Для изолированных окружений, где Deno не может быть скачан автоматически:
- Опубликуйте чарт:
werf bundle publish --repo example.org/mycompany/myappВсе модули npm будут минифицированы и включены в бандл, так что чарт можно установить даже без доступа к интернету.
- На целевой машине в изолированном окружении (без доступа к сети) скачайте Deno вручную и выполните:
werf bundle apply --repo example.org/mycompany/myapp --deno-binary-path /usr/local/bin/denoГде
/usr/local/bin/deno— путь к локальному бинарному файлу Deno. TypeScript-шаблоны будут отрендерены и развёрнуты с использованием предварительно скомпилированных файлов из бандла чарта.
Обзор SDK API
TypeScript-движок использует пакет @nelm/chart-ts-sdk.
Функции “render” и “generate”
index.ts обязан вызвать функцию render(). Функция generate(), которая непосредственно генерирует манифесты, должна быть передана в render() в качестве аргумента, например:
// .helm/ts/src/index.ts:
await render(generate);
Объект “WerfRenderContext”
Функция generate получает корневой контекст в переменной $ типа WerfRenderContext — тот же контекст, что и в Helm-шаблонах:
| Поле | Тип | Описание |
|---|---|---|
$.Values |
WerfServiceValues |
Параметры чарта + сервисные значения в $.Values.global.werf |
$.Release |
Release |
Информация о релизе |
$.Chart |
ChartMetadata |
Метаданные из Chart.yaml |
$.Capabilities |
Capabilities |
Возможности кластера (API-версии, версия Kubernetes) |
$.Files |
Record<string, Uint8Array> |
Исходные файлы чарта (кроме templates/ и ts/) |
Пример контекста — в файле ts/input.example.yaml. Подробнее о параметрах и их формировании — в разделе Параметризация шаблонов.
Объект “RenderResult”
Функция generate возвращает RenderResult — объект с массивом manifests. Каждый элемент — обычный JavaScript-объект, представляющий Kubernetes-ресурс. Пример вывода:
{
"manifests": [
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": { "name": "myapp" },
"spec": { "..." }
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": { "name": "myapp" },
"spec": { "..." }
}
]
}
Каждый объект сериализуется в YAML и включается в итоговый результат рендеринга.