Обратите внимание: 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

Структура чарта

ts
src
deployment.ts
helpers.ts
index.ts
service.ts
deno.json
deno.lock
input.example.yaml
Chart.yaml
values.yaml
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 не может быть скачан автоматически:

  1. Опубликуйте чарт:
    werf bundle publish --repo example.org/mycompany/myapp
    

    Все модули npm будут минифицированы и включены в бандл, так что чарт можно установить даже без доступа к интернету.

  2. На целевой машине в изолированном окружении (без доступа к сети) скачайте 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 и включается в итоговый результат рендеринга.