
Я Игорь Юрченко, backend-разработчик Сбера, в этой статье расскажу о нашем опыте автоматизации деплоя потоков Apache NiFi.
Apache NiFi — инструмент для управления потоками данных между автоматизированными системами (реализует подход ETL — extract, transform, load). Документация: https://nifi.apache.org/documentation/v1 (на момент написания статьи актуальна версия 2.x, но тут речь про 1.x). Физически это Java-приложение с графическим web-интерфейсом, в котором настраивается поток — в общем случае набор процессоров, которые получают на вход какие-то данные от предыдущего процессора или из внешней системы, обрабатывают их определённым образом и передают следующему процессору или во внешнюю систему. Процессор — готовый модуль с параметрами интеграции и/или обработки данных (например, строка подключения к БД, или схема трансформации данных). То есть ETL настраивается графически, без написания кода. NiFi обладает возможностями горизонтального масштабирования (ноды кластера имеют одинаковую копию настроек потока, обрабатывают данные параллельно), и расширения (пользователь может писать custom процессоры и использовать их в потоках наравне со штатными). Из коробки поддерживается множество внешних систем и протоколов передачи данных.
Apache NiFi Registry — инструмент версионирования потоков, Java-приложение с web-интерфейсом, интегрировано с NiFi. Что-то вроде системы контроля версий исходного кода, но проще. Пользователь может сохранять в Registry, просматривать и восстанавливать старые версии потока. Документация: https://nifi.apache.org/docs/nifi-registry-docs.
Оба инструмента имеют REST API.
Проблема большой автоматизированной системы, использующей NiFi: среднестатистическая группа разработки не может с разумными трудозатратами дорабатывать и деплоить вручную стенд из нескольких кластеров по несколько сотен потоков. Пример одного кластера:

Возможное решение: изменить владельцев — пусть пользователи самостоятельно меняют параметры своих потоков, а группа разработки делает только шаблоны (прототипы) потоков и поддерживает механизм их автоматизированного деплоя, о чём и пойдет речь дальше.
NiFi сам по себе уже является low-code платформой, а описанный в статье подход ещё больше упрощает его использование. Он основан на Scenario 2, Case 4 (Two NiFi Registries using the NiPyAPI) из блога https://pierrevillard.com/2018/2018-04-09-automate-workflow-deployment-in-apache-nifi-with-the-nifi-registry/ для NiFi и Registry версии 1.24 (на момент написания статьи). Реализован в виде библиотеки nifi-deployer.jar (далее коротко - «джарник») и является частью Jenkins pipeline деплоя (сам pipeline тут не описан, но в общих чертах это — git checkout и Jinja2 templating шаблона и метаданных потока, запуск джарника с параметрами — их именами). Можно реализовать на любом языке с поддержкой JSON и HTTP, смысл — вызываем в определённом порядке методы NiFi REST API.
Термины:
поток — версионированная (сохранённая в Registry) process group в NiFi
шаблон — файл описания потока в формате JSON, совместимый с NiFi REST API (содержит процессоры и controller services, их свойства, связи, вложенные process groups и т.д.). Шаблон кастомизируется метаданными и превращается в поток при деплое (1 шаблон * N метаданных = N потоков). Не путать с template в NiFi, ниже описана разница
метаданные — файл параметров потока в формате JSON. Параметры могут быть бизнесовые (фильтры загружаемых сущностей, даты, Avro-схемы и т.д. — статические значения, одинаковые между стендами) и технические (HTTP URL, JDBC connection strings, Kafka brokers — отличаются между стендами, дополнительно кастомизируются через Ansible). Также метаданные делятся на variables (которые в NiFi можно заполнять в отдельном окне, а ссылки на них писать в свойства компонентов выражениями Expression Language — ${variable}, они разрешаются самим NiFi при работе потока), и так называемые api-data (разрешаются не в NiFi, а при деплое — значения заранее подставляются в свойства процессоров, не поддерживающих Expression Language)
В статье не описаны установка и настройка NiFi + Registry (в т.ч. настройки аутентификации пользователей). В интернете можно найти материалы, а тут предполагаем, что это уже сделано.
Общее описание подхода

Процесс разработки и деплоя:
Создаем/меняем process group в Dev NiFi (или на localhost); группа может быть на любом уровне вложенности
Коммитим изменения в Dev Registry (при необходимости создаем bucket, https://nifi.apache.org/docs/nifi-registry-docs/html/user-guide.html#create-a-bucket) — ручное действие
Экспортируем шаблон из Registry в локальный файл, вместе с набором переменных (метаданными) этого потока в отдельном файле для удобства изменения — автоматизировано. Теоретически это можно делать и вручную (https://nifi.apache.org/docs/nifi-registry-docs/html/user-guide.html#export-a-flow-version), но Registry экспортирует процессоры в случайном порядке, автоматизация позволяет отсортировать процессоры в файле, а это минимизирует Git diff
Вручную правим шаблон — подставляем элементы api-data в нужные свойства процессоров (отсюда следует важная рекомендация, подробно рассмотренная в примере ниже — свойства, поддерживающие Expression Language в NiFi, лучше реализовать через variables, п.ч. они экспортируются в готовом виде ${variable}, их не надо вручную менять после экспорта; а свойства, не поддерживающие Expression Language, экспортируются с конкретными значениями, их нужно вручную менять на конструкцию вида [[ api-data ]]; также в файле шаблона нужно восстановить удаляемые при коммите в Registry sensitive-свойства (пароли и т.д.), подставить в них элементы [[ api-data ]] или ссылки вида #{param} на parameter context, в зависимости от принятого в команде способа управления секретами — про parameter context/provider будет следующая статья; перечень sensitive свойств можно установить по элементам propertyDescriptors — даже если свойство не экспортировано в элементе properties, в propertyDescriptors оно будет описано, как "sensitive": true, тогда его "name" должно стать ключом в properties)
Вручную правим файл метаданных (в технических variables подставляем Ansible-переменные вида {{ var }} - HTTP URL-ы, строки подключения к БД и т.д.; добавляем массив api-data с необходимыми элементами api — то, что в шаблоне описано в виде [[ api-data ]])
Коммитим шаблон и метаданные в Git — ручное действие. Шаблон и метаданные могут храниться в разных репозиториях и ветках, любой Git flow на ваш вкус, можно merge, revert, diff, blame
Импортируем файл шаблона в Prod Registry, при этом создаются bucket и flow — автоматизировано (тут же элементы метаданных api-data подставляются в файл шаблона — текстовая замена по конструкциям вида [[ api-data ]])
Импортируем flow из Prod Registry в Prod NiFi (в т.ч. если потока еще нет в NiFi, он создается) — автоматизировано
Обновляем значения переменных потока из файла метаданных — автоматизировано
Пункты до «коммитим в Git» включительно выполняются традиционно разработчиком, но могут и другими ролями, технических стоп-факторов нет. Дальше — Jenkins pipeline-ом, то есть любым человеком, кто хочет просто и быстро задеплоить на прод поток по определённому шаблону (с нужным функционалом), с определёнными параметрами — аналитики, devops-инженеры, сотрудники бизнес-подразделений. Для достижения своих целей им достаточно иметь только описание доступных шаблонов, и по каждому шаблону описание его параметров.
Почему именно такой процесс, а не через templates? На первый взгляд это проще, без дополнительного компонента Registry. Но обновление process group в NiFi может быть комплексным — процессоры не только меняют конфигурацию, но и добавляются/удаляются (а при этом останавливаются), меняются connections между ними и т.д. Чтобы все это делать консистентно, как раз и существует Registry — это его прямая обязанность. А templates при импорте на канву не останавливают процессоры. Как мне кажется, они больше предназначены для создания "snippets", переиспользования одного набора процессоров в разных потоках в одном инстансе NiFi (ускорение разработки — делаем один поток, его часть сохраняем в виде template, используем в другом потоке в этом же NiFi).
Template в NiFi (НЕ «шаблон» в терминах этой статьи):
Скрытый текст
На любом содержимом канвы (один или несколько процессоров или process groups, не обязательно связанных) в контекстном меню пункт Create template:

Созданный шаблон отображается в главном меню в пункте Templates:


Элемент Template верхней панели инструментов позволяет импортировать его на канву:


Пример создания потока
Предположим, группе разработки пришло требование — загрузить данные из REST API в БД. На Dev-стенде создаём поток:

Здесь и далее под dev/prod стендами для демонстрации понимаются группы с соответствующими именами на одном физическом стенде.
Пользователь источника (REST API) указан в свойстве процессора GetHTTP, не поддерживающем Expression Language:

Секретные свойства (пароли) указаны в явном виде, без ссылок на parameter context (защита стандартной маскировкой в Web-интерфейсе — отображается «Sensitive value set» после ввода секрета):


Адрес источника (REST API) и путь к файлу truststore указаны в variables (ссылки на них из свойств компонентов с помощью Expression Language; конкретные значения на скриншотах замаскированы):

Сохраняем поток в Dev Registry:



Экспортируем шаблон из Dev Registry:
java -jar nifi-deployer-0.36-SNAPSHOT.jar -e bucket.name=default flow.name=http-2-db-dev export.path=./http-2-db-1.24.json parent.group.id=cba7fcb9-0186-1000-0000-0000451bf611 nifi.url=https://.../nifi-api registry.url=https://.../nifi-registry-api user=... password=... ssl.keystore.filename=./nifi.p12
Лог экспорта:
Скрытый текст
19.05.2026 14:39:02.142 [main] INFO ApiRegistry - get bucket ID for bucket name default 19.05.2026 14:39:02.142 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets 19.05.2026 14:39:03.384 [main] INFO ApiRegistry - using bucket 2cf0ac20-3ccd-41ed-97c1-97baecf45dfc 19.05.2026 14:39:03.384 [main] INFO ApiRegistry - get flow ID for flow name http-2-db-dev and bucketId 2cf0ac20-3ccd-41ed-97c1-97baecf45dfc 19.05.2026 14:39:03.384 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows 19.05.2026 14:39:03.699 [main] INFO ApiRegistry - using flow 20be5260-9249-4626-99d8-e95b7286836e 19.05.2026 14:39:03.700 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows/20be5260-9249-4626-99d8-e95b7286836e/versions/latest 19.05.2026 14:39:04.275 [Thread-21] INFO getVersionControlInfo$ - get version control info for http-2-db-dev recursively from cba7fcb9-0186-1000-0000-0000451bf611 19.05.2026 14:39:20.650 [main] INFO ApiNiFiGeneric - get variable registry from process group c9143fc0-c492-3fa7-95ae-81c7de3245af 19.05.2026 14:39:20.650 [main] DEBUG HttpClient - GET https://.../nifi-api/process-groups/c9143fc0-c492-3fa7-95ae-81c7de3245af/variable-registry 19.05.2026 14:39:20.854 [main] INFO getMetadataFromNiFi$ - get nested process groups for c9143fc0-c492-3fa7-95ae-81c7de3245af 19.05.2026 14:39:20.855 [main] DEBUG HttpClient - GET https://.../nifi-api/flow/process-groups/c9143fc0-c492-3fa7-95ae-81c7de3245af 19.05.2026 14:39:20.990 [main] INFO writeTemplateAndMetadata$ - write template http-2-db-1.24.json 19.05.2026 14:39:21.003 [main] INFO writeMetadata$ - write metadata http-2-db-1.24-var.json 19.05.2026 14:39:21.062 [main] INFO exportTemplateAndMetadataFromRegistry$ - version 1 exported
Исправим файл шаблона:
Для доступа к REST API на Dev-стенде в потоке используется имя пользователя «rest_api_test_user», но на других стендах оно может быть другим — это параметр. Свойство Username процессора GetHTTP не поддерживает Expression Language (задано в NiFi явно, не через variable), так что в файле шаблона заменим его на конструкцию вида [[ restApiUser ]] (строка 685 — "Username" : "[[ restApiUser ]]"). Ключ restApiUser может быть любым, задаётся автором шаблона (по-хорошему должен адекватно описывать суть параметра). Две квадратные скобки — специальный синтаксис, по которому выполняется текстовая замена этих параметров при деплое
Секретные свойства компонентов не экспортируются из NiFi в Registry, соответственно и в шаблон не попадают. В данном потоке это пароль базовой HTTP-аутентификации в REST API, пароль БД и пароль truststore для установления SSL-соединения с REST API. Используются соответственно в процессоре GetHTTP, в controller service DBCPConnectionPool и StandardSSLContextService. Найдём их в файле шаблона по строке "sensitive" : true. Они описаны в элементе propertyDescriptors соответствующего компонента. Из элемента name возьмём ключ, добавим его в элемент properties, а в качестве значения укажем ту же конструкцию [[ … ]] с адекватным именем параметра, например для пароля REST API это может быть restApiPassword. Таким образом, в properties процессора GetHTTP добавим "Password" : "[[ restApiPassword ]]"
Исправим файл метаданных:
все параметры шаблона (name из [[ name ]]) добавим в массив api-data на верхнем уровне метаданных, для каждого укажем value — реальное значение, которое при деплое подставится в шаблон вместо [[ name ]]
для самого процесса деплоя нужны общие параметры, независимые от шаблона — имя потока, с которым он будет импортирован в NiFi Registry, и целевой путь потока в NiFi. Эти параметры не используются в файле шаблона, но зарезервированы для использования джарником, то есть их имена должны быть одинаковыми во всех файлах метаданных для всех шаблонов. Добавим их с нужными значениями: registryFlowName = http-2-db, processGroupPath = tmp>>prod>>http-2-db. Эти значения должны быть уникальны в рамках одного стенда NiFi, чтобы при деплое не перепутались два логически разных потока.
Исправленные файлы шаблона и метаданных:
Скрытый текст
{ "bucket" : { "allowBundleRedeploy" : false, "allowPublicRead" : false, "createdTimestamp" : 1690978653562, "description" : "", "identifier" : "2cf0ac20-3ccd-41ed-97c1-97baecf45dfc", "link" : { "href" : "buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc", "params" : { "rel" : "self" } }, "name" : "default", "permissions" : { "canDelete" : true, "canRead" : true, "canWrite" : true } }, "externalControllerServices" : { }, "flow" : { "bucketIdentifier" : "2cf0ac20-3ccd-41ed-97c1-97baecf45dfc", "bucketName" : "default", "createdTimestamp" : 1779182540682, "description" : "", "identifier" : "20be5260-9249-4626-99d8-e95b7286836e", "link" : { "href" : "buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows/20be5260-9249-4626-99d8-e95b7286836e", "params" : { "rel" : "self" } }, "modifiedTimestamp" : 1779182541552, "name" : "http-2-db-dev", "type" : "Flow", "versionCount" : 1 }, "flowContents" : { "comments" : "", "componentType" : "PROCESS_GROUP", "connections" : [ { "backPressureDataSizeThreshold" : "1 GB", "backPressureObjectThreshold" : 10000, "bends" : [ { "x" : 1152.0, "y" : 480.0 } ], "componentType" : "CONNECTION", "destination" : { "comments" : "", "groupId" : "ad86f407-8631-318b-8230-76d1d7164043", "id" : "b0cf515e-e112-3ed4-8ebf-cabcb17e004c", "instanceIdentifier" : "b0cf515e-e112-3ed4-8c40-e0d8585aef1b", "name" : "Funnel", "type" : "FUNNEL" }, "flowFileExpiration" : "0 sec", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "c773dc4f-2492-3c20-875c-574bc3782390", "instanceIdentifier" : "c773dc4f-2492-3c20-8278-9856e76bcb46", "labelIndex" : 1, "loadBalanceCompression" : "DO_NOT_COMPRESS", "loadBalanceStrategy" : "DO_NOT_LOAD_BALANCE", "name" : "", "partitioningAttribute" : "", "prioritizers" : [ ], "selectedRelationships" : [ "failure", "retry" ], "source" : { "comments" : "", "groupId" : "ad86f407-8631-318b-8230-76d1d7164043", "id" : "32d35b0d-daab-3189-9f86-71fbe584a6c7", "instanceIdentifier" : "32d35b0d-daab-3189-b8ae-148fcd2e1c26", "name" : "PutSQL", "type" : "PROCESSOR" }, "zIndex" : 0 }, { "backPressureDataSizeThreshold" : "1 GB", "backPressureObjectThreshold" : 10000, "bends" : [ { "x" : 848.0, "y" : 408.0 } ], "componentType" : "CONNECTION", "destination" : { "comments" : "", "groupId" : "ad86f407-8631-318b-8230-76d1d7164043", "id" : "b0cf515e-e112-3ed4-8ebf-cabcb17e004c", "instanceIdentifier" : "b0cf515e-e112-3ed4-8c40-e0d8585aef1b", "name" : "Funnel", "type" : "FUNNEL" }, "flowFileExpiration" : "0 sec", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "fd782ed4-aef7-3e38-8248-01640a2604fa", "instanceIdentifier" : "fd782ed4-aef7-3e38-bfa8-58ab86467cb2", "labelIndex" : 1, "loadBalanceCompression" : "DO_NOT_COMPRESS", "loadBalanceStrategy" : "DO_NOT_LOAD_BALANCE", "name" : "", "partitioningAttribute" : "", "prioritizers" : [ ], "selectedRelationships" : [ "unmatched" ], "source" : { "comments" : "", "groupId" : "ad86f407-8631-318b-8230-76d1d7164043", "id" : "32b96eda-8796-3cbd-8845-9c13d368f451", "instanceIdentifier" : "32b96eda-8796-3cbd-8320-bb8c728aeaf1", "name" : "ExtractText", "type" : "PROCESSOR" }, "zIndex" : 0 }, { "backPressureDataSizeThreshold" : "1 GB", "backPressureObjectThreshold" : 10000, "bends" : [ { "x" : 648.0, "y" : 336.0 } ], "componentType" : "CONNECTION", "destination" : { "comments" : "", "groupId" : "ad86f407-8631-318b-8230-76d1d7164043", "id" : "32b96eda-8796-3cbd-8845-9c13d368f451", "instanceIdentifier" : "32b96eda-8796-3cbd-8320-bb8c728aeaf1", "name" : "ExtractText", "type" : "PROCESSOR" }, "flowFileExpiration" : "0 sec", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "aad4cb51-0cdd-3e21-aba6-16b3a82cb98f", "instanceIdentifier" : "aad4cb51-0cdd-3e21-9176-6b0862368880", "labelIndex" : 1, "loadBalanceCompression" : "DO_NOT_COMPRESS", "loadBalanceStrategy" : "DO_NOT_LOAD_BALANCE", "name" : "", "partitioningAttribute" : "", "prioritizers" : [ ], "selectedRelationships" : [ "success" ], "source" : { "comments" : "", "groupId" : "ad86f407-8631-318b-8230-76d1d7164043", "id" : "e70f7c86-42f6-352a-b9b4-f02c9678ab24", "instanceIdentifier" : "e70f7c86-42f6-352a-866a-17792a25c0bb", "name" : "GetHTTP", "type" : "PROCESSOR" }, "zIndex" : 0 }, { "backPressureDataSizeThreshold" : "1 GB", "backPressureObjectThreshold" : 10000, "bends" : [ { "x" : 1088.0, "y" : 336.0 } ], "componentType" : "CONNECTION", "destination" : { "comments" : "", "groupId" : "ad86f407-8631-318b-8230-76d1d7164043", "id" : "32d35b0d-daab-3189-9f86-71fbe584a6c7", "instanceIdentifier" : "32d35b0d-daab-3189-b8ae-148fcd2e1c26", "name" : "PutSQL", "type" : "PROCESSOR" }, "flowFileExpiration" : "0 sec", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "3bff9b4b-de39-38cd-9d35-684df2049fd3", "instanceIdentifier" : "3bff9b4b-de39-38cd-b18f-cb7e8c81d733", "labelIndex" : 1, "loadBalanceCompression" : "DO_NOT_COMPRESS", "loadBalanceStrategy" : "DO_NOT_LOAD_BALANCE", "name" : "", "partitioningAttribute" : "", "prioritizers" : [ ], "selectedRelationships" : [ "matched" ], "source" : { "comments" : "", "groupId" : "ad86f407-8631-318b-8230-76d1d7164043", "id" : "32b96eda-8796-3cbd-8845-9c13d368f451", "instanceIdentifier" : "32b96eda-8796-3cbd-8320-bb8c728aeaf1", "name" : "ExtractText", "type" : "PROCESSOR" }, "zIndex" : 0 } ], "controllerServices" : [ { "bulletinLevel" : "WARN", "bundle" : { "artifact" : "nifi-dbcp-service-nar", "group" : "org.apache.nifi", "version" : "1.24.0" }, "comments" : "", "componentType" : "CONTROLLER_SERVICE", "controllerServiceApis" : [ { "bundle" : { "artifact" : "nifi-standard-services-api-nar", "group" : "org.apache.nifi", "version" : "1.24.0" }, "type" : "org.apache.nifi.dbcp.DBCPService" } ], "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "bed8843c-c85e-3b34-844d-0804c6d35999", "instanceIdentifier" : "bed8843c-c85e-3b34-887a-548f4db09724", "name" : "DBCPConnectionPool", "properties" : { "dbcp-min-idle-conns" : "0", "Max Wait Time" : "500 millis", "Database Driver Class Name" : "org.postgresql.Driver", "dbcp-min-evictable-idle-time" : "30 mins", "Max Total Connections" : "8", "dbcp-max-conn-lifetime" : "-1", "Database Connection URL" : "${jdbcUrl}", "dbcp-time-between-eviction-runs" : "-1", "Database User" : "${dbUser}", "dbcp-soft-min-evictable-idle-time" : "-1", "database-driver-locations" : "/opt/drivers/postgres", "dbcp-max-idle-conns" : "8", "Password" : "[[ dbPassword ]]" }, "propertyDescriptors" : { "kerberos-password" : { "displayName" : "Kerberos Password", "identifiesControllerService" : false, "name" : "kerberos-password", "sensitive" : true }, "dbcp-min-idle-conns" : { "displayName" : "Minimum Idle Connections", "identifiesControllerService" : false, "name" : "dbcp-min-idle-conns", "sensitive" : false }, "Max Wait Time" : { "displayName" : "Max Wait Time", "identifiesControllerService" : false, "name" : "Max Wait Time", "sensitive" : false }, "Database Driver Class Name" : { "displayName" : "Database Driver Class Name", "identifiesControllerService" : false, "name" : "Database Driver Class Name", "sensitive" : false }, "dbcp-min-evictable-idle-time" : { "displayName" : "Minimum Evictable Idle Time", "identifiesControllerService" : false, "name" : "dbcp-min-evictable-idle-time", "sensitive" : false }, "kerberos-principal" : { "displayName" : "Kerberos Principal", "identifiesControllerService" : false, "name" : "kerberos-principal", "sensitive" : false }, "Max Total Connections" : { "displayName" : "Max Total Connections", "identifiesControllerService" : false, "name" : "Max Total Connections", "sensitive" : false }, "kerberos-credentials-service" : { "displayName" : "Kerberos Credentials Service", "identifiesControllerService" : true, "name" : "kerberos-credentials-service", "sensitive" : false }, "dbcp-max-conn-lifetime" : { "displayName" : "Max Connection Lifetime", "identifiesControllerService" : false, "name" : "dbcp-max-conn-lifetime", "sensitive" : false }, "Validation-query" : { "displayName" : "Validation query", "identifiesControllerService" : false, "name" : "Validation-query", "sensitive" : false }, "Database Connection URL" : { "displayName" : "Database Connection URL", "identifiesControllerService" : false, "name" : "Database Connection URL", "sensitive" : false }, "dbcp-time-between-eviction-runs" : { "displayName" : "Time Between Eviction Runs", "identifiesControllerService" : false, "name" : "dbcp-time-between-eviction-runs", "sensitive" : false }, "Database User" : { "displayName" : "Database User", "identifiesControllerService" : false, "name" : "Database User", "sensitive" : false }, "kerberos-user-service" : { "displayName" : "Kerberos User Service", "identifiesControllerService" : true, "name" : "kerberos-user-service", "sensitive" : false }, "dbcp-soft-min-evictable-idle-time" : { "displayName" : "Soft Minimum Evictable Idle Time", "identifiesControllerService" : false, "name" : "dbcp-soft-min-evictable-idle-time", "sensitive" : false }, "database-driver-locations" : { "displayName" : "Database Driver Location(s)", "identifiesControllerService" : false, "name" : "database-driver-locations", "resourceDefinition" : { "cardinality" : "MULTIPLE", "resourceTypes" : [ "URL", "FILE", "DIRECTORY" ] }, "sensitive" : false }, "dbcp-max-idle-conns" : { "displayName" : "Max Idle Connections", "identifiesControllerService" : false, "name" : "dbcp-max-idle-conns", "sensitive" : false }, "Password" : { "displayName" : "Password", "identifiesControllerService" : false, "name" : "Password", "sensitive" : true } }, "scheduledState" : "DISABLED", "type" : "org.apache.nifi.dbcp.DBCPConnectionPool" }, { "bulletinLevel" : "WARN", "bundle" : { "artifact" : "nifi-ssl-context-service-nar", "group" : "org.apache.nifi", "version" : "1.24.0" }, "comments" : "", "componentType" : "CONTROLLER_SERVICE", "controllerServiceApis" : [ { "bundle" : { "artifact" : "nifi-standard-services-api-nar", "group" : "org.apache.nifi", "version" : "1.24.0" }, "type" : "org.apache.nifi.ssl.SSLContextService" } ], "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "d1c57bde-4f3b-3ad9-ae74-5b44fb96cd0a", "instanceIdentifier" : "d1c57bde-4f3b-3ad9-9800-79565a41624c", "name" : "StandardSSLContextService", "properties" : { "Truststore Type" : "JKS", "SSL Protocol" : "TLSv1.3", "Truststore Filename" : "${truststoreFilename}", "Truststore Password" : "[[ truststorePassword ]]" }, "propertyDescriptors" : { "Truststore Type" : { "displayName" : "Truststore Type", "identifiesControllerService" : false, "name" : "Truststore Type", "sensitive" : false }, "SSL Protocol" : { "displayName" : "TLS Protocol", "identifiesControllerService" : false, "name" : "SSL Protocol", "sensitive" : false }, "Keystore Type" : { "displayName" : "Keystore Type", "identifiesControllerService" : false, "name" : "Keystore Type", "sensitive" : false }, "Truststore Filename" : { "displayName" : "Truststore Filename", "identifiesControllerService" : false, "name" : "Truststore Filename", "resourceDefinition" : { "cardinality" : "SINGLE", "resourceTypes" : [ "FILE" ] }, "sensitive" : false }, "Keystore Password" : { "displayName" : "Keystore Password", "identifiesControllerService" : false, "name" : "Keystore Password", "sensitive" : true }, "key-password" : { "displayName" : "Key Password", "identifiesControllerService" : false, "name" : "key-password", "sensitive" : true }, "Truststore Password" : { "displayName" : "Truststore Password", "identifiesControllerService" : false, "name" : "Truststore Password", "sensitive" : true }, "Keystore Filename" : { "displayName" : "Keystore Filename", "identifiesControllerService" : false, "name" : "Keystore Filename", "resourceDefinition" : { "cardinality" : "SINGLE", "resourceTypes" : [ "FILE" ] }, "sensitive" : false } }, "scheduledState" : "DISABLED", "type" : "org.apache.nifi.ssl.StandardSSLContextService" } ], "defaultBackPressureDataSizeThreshold" : "1 GB", "defaultBackPressureObjectThreshold" : 10000, "defaultFlowFileExpiration" : "0 sec", "flowFileConcurrency" : "UNBOUNDED", "flowFileOutboundPolicy" : "STREAM_WHEN_AVAILABLE", "funnels" : [ { "componentType" : "FUNNEL", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "b0cf515e-e112-3ed4-8ebf-cabcb17e004c", "instanceIdentifier" : "b0cf515e-e112-3ed4-8c40-e0d8585aef1b", "position" : { "x" : 824.0, "y" : 456.0 } } ], "identifier" : "ad86f407-8631-318b-8230-76d1d7164043", "inputPorts" : [ ], "instanceIdentifier" : "c9143fc0-c492-3fa7-95ae-81c7de3245af", "labels" : [ { "componentType" : "LABEL", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "height" : 32.0, "identifier" : "46bed543-1d68-3b65-ab16-31d5f1da3581", "instanceIdentifier" : "46bed543-1d68-3b65-818e-320e3ed71636", "label" : "1. получить даные из REST API", "position" : { "x" : 248.0, "y" : 120.0 }, "style" : { "font-size" : "16px" }, "width" : 256.0, "zIndex" : 0 }, { "componentType" : "LABEL", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "height" : 32.0, "identifier" : "dec730c0-3afc-3d8d-9146-9b6835417fb4", "instanceIdentifier" : "dec730c0-3afc-3d8d-a59b-abdef726e8cf", "label" : "3. сохранить полученные данные в БД", "position" : { "x" : 1136.0, "y" : 120.0 }, "style" : { "font-size" : "16px" }, "width" : 312.0, "zIndex" : 0 }, { "componentType" : "LABEL", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "height" : 48.0, "identifier" : "c5f3ebbc-ac78-3f57-9a08-167295392e1d", "instanceIdentifier" : "c5f3ebbc-ac78-3f57-aa21-052f9e0e4cfd", "label" : "2. перенести содержимое \nв атрибут flow file-а (для PutSQL)", "position" : { "x" : 672.0, "y" : 104.0 }, "style" : { "font-size" : "16px" }, "width" : 272.0, "zIndex" : 0 } ], "logFileSuffix" : "", "name" : "http-2-db-dev", "outputPorts" : [ ], "position" : { "x" : 659.999887235238, "y" : 320.00000615823 }, "processGroups" : [ ], "processors" : [ { "autoTerminatedRelationships" : [ ], "backoffMechanism" : "PENALIZE_FLOWFILE", "bulletinLevel" : "WARN", "bundle" : { "artifact" : "nifi-standard-nar", "group" : "org.apache.nifi", "version" : "1.24.0" }, "comments" : "", "componentType" : "PROCESSOR", "concurrentlySchedulableTaskCount" : 0, "executionNode" : "ALL", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "32b96eda-8796-3cbd-8845-9c13d368f451", "instanceIdentifier" : "32b96eda-8796-3cbd-8320-bb8c728aeaf1", "maxBackoffPeriod" : "10 mins", "name" : "ExtractText", "penaltyDuration" : "30 sec", "position" : { "x" : 672.0, "y" : 152.0 }, "properties" : { "Enable Unicode Predefined Character Classes" : "false", "Permit Whitespace and Comments in Pattern" : "false", "Enable Unicode-aware Case Folding" : "false", "sql.args.1.value" : "(?s)(^.*$)", "Enable DOTALL Mode" : "false", "Enable Unix Lines Mode" : "false", "extract-text-enable-named-groups" : "false", "Maximum Buffer Size" : "10 MB", "Enable Canonical Equivalence" : "false", "Enable Case-insensitive Matching" : "false", "Enable Multiline Mode" : "false", "Maximum Capture Group Length" : "1024", "sql.args.1.type" : "12", "Enable Literal Parsing of the Pattern" : "false", "Character Set" : "UTF-8", "Include Capture Group 0" : "true", "extract-text-enable-repeating-capture-group" : "false" }, "propertyDescriptors" : { "Enable Unicode Predefined Character Classes" : { "displayName" : "Enable Unicode Predefined Character Classes", "identifiesControllerService" : false, "name" : "Enable Unicode Predefined Character Classes", "sensitive" : false }, "Permit Whitespace and Comments in Pattern" : { "displayName" : "Permit Whitespace and Comments in Pattern", "identifiesControllerService" : false, "name" : "Permit Whitespace and Comments in Pattern", "sensitive" : false }, "Enable Unicode-aware Case Folding" : { "displayName" : "Enable Unicode-aware Case Folding", "identifiesControllerService" : false, "name" : "Enable Unicode-aware Case Folding", "sensitive" : false }, "sql.args.1.value" : { "displayName" : "sql.args.1.value", "identifiesControllerService" : false, "name" : "sql.args.1.value", "sensitive" : false }, "Enable DOTALL Mode" : { "displayName" : "Enable DOTALL Mode", "identifiesControllerService" : false, "name" : "Enable DOTALL Mode", "sensitive" : false }, "Enable Unix Lines Mode" : { "displayName" : "Enable Unix Lines Mode", "identifiesControllerService" : false, "name" : "Enable Unix Lines Mode", "sensitive" : false }, "extract-text-enable-named-groups" : { "displayName" : "Enable named group support", "identifiesControllerService" : false, "name" : "extract-text-enable-named-groups", "sensitive" : false }, "Maximum Buffer Size" : { "displayName" : "Maximum Buffer Size", "identifiesControllerService" : false, "name" : "Maximum Buffer Size", "sensitive" : false }, "Enable Canonical Equivalence" : { "displayName" : "Enable Canonical Equivalence", "identifiesControllerService" : false, "name" : "Enable Canonical Equivalence", "sensitive" : false }, "Enable Case-insensitive Matching" : { "displayName" : "Enable Case-insensitive Matching", "identifiesControllerService" : false, "name" : "Enable Case-insensitive Matching", "sensitive" : false }, "Enable Multiline Mode" : { "displayName" : "Enable Multiline Mode", "identifiesControllerService" : false, "name" : "Enable Multiline Mode", "sensitive" : false }, "Maximum Capture Group Length" : { "displayName" : "Maximum Capture Group Length", "identifiesControllerService" : false, "name" : "Maximum Capture Group Length", "sensitive" : false }, "sql.args.1.type" : { "displayName" : "sql.args.1.type", "identifiesControllerService" : false, "name" : "sql.args.1.type", "sensitive" : false }, "Enable Literal Parsing of the Pattern" : { "displayName" : "Enable Literal Parsing of the Pattern", "identifiesControllerService" : false, "name" : "Enable Literal Parsing of the Pattern", "sensitive" : false }, "Character Set" : { "displayName" : "Character Set", "identifiesControllerService" : false, "name" : "Character Set", "sensitive" : false }, "Include Capture Group 0" : { "displayName" : "Include Capture Group 0", "identifiesControllerService" : false, "name" : "Include Capture Group 0", "sensitive" : false }, "extract-text-enable-repeating-capture-group" : { "displayName" : "Enable repeating capture group", "identifiesControllerService" : false, "name" : "extract-text-enable-repeating-capture-group", "sensitive" : false } }, "retriedRelationships" : [ ], "retryCount" : 10, "runDurationMillis" : 0, "scheduledState" : "ENABLED", "schedulingPeriod" : "0 sec", "schedulingStrategy" : "EVENT_DRIVEN", "style" : { }, "type" : "org.apache.nifi.processors.standard.ExtractText", "yieldDuration" : "1 sec" }, { "autoTerminatedRelationships" : [ ], "backoffMechanism" : "PENALIZE_FLOWFILE", "bulletinLevel" : "WARN", "bundle" : { "artifact" : "nifi-standard-nar", "group" : "org.apache.nifi", "version" : "1.24.0" }, "comments" : "", "componentType" : "PROCESSOR", "concurrentlySchedulableTaskCount" : 1, "executionNode" : "PRIMARY", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "e70f7c86-42f6-352a-b9b4-f02c9678ab24", "instanceIdentifier" : "e70f7c86-42f6-352a-866a-17792a25c0bb", "maxBackoffPeriod" : "10 mins", "name" : "GetHTTP", "penaltyDuration" : "30 sec", "position" : { "x" : 248.0, "y" : 152.0 }, "properties" : { "redirect-cookie-policy" : "default", "Filename" : "file", "URL" : "${url}", "Connection Timeout" : "30 sec", "Data Timeout" : "30 sec", "SSL Context Service" : "d1c57bde-4f3b-3ad9-ae74-5b44fb96cd0a", "Username" : "[[ restApiUser ]]", "Follow Redirects" : "false", "Password" : "[[ restApiPassword ]]" }, "propertyDescriptors" : { "Proxy Host" : { "displayName" : "Proxy Host", "identifiesControllerService" : false, "name" : "Proxy Host", "sensitive" : false }, "redirect-cookie-policy" : { "displayName" : "Redirect Cookie Policy", "identifiesControllerService" : false, "name" : "redirect-cookie-policy", "sensitive" : false }, "proxy-configuration-service" : { "displayName" : "Proxy Configuration Service", "identifiesControllerService" : true, "name" : "proxy-configuration-service", "sensitive" : false }, "Filename" : { "displayName" : "Filename", "identifiesControllerService" : false, "name" : "Filename", "sensitive" : false }, "User Agent" : { "displayName" : "User Agent", "identifiesControllerService" : false, "name" : "User Agent", "sensitive" : false }, "Proxy Port" : { "displayName" : "Proxy Port", "identifiesControllerService" : false, "name" : "Proxy Port", "sensitive" : false }, "URL" : { "displayName" : "URL", "identifiesControllerService" : false, "name" : "URL", "sensitive" : false }, "Connection Timeout" : { "displayName" : "Connection Timeout", "identifiesControllerService" : false, "name" : "Connection Timeout", "sensitive" : false }, "Data Timeout" : { "displayName" : "Data Timeout", "identifiesControllerService" : false, "name" : "Data Timeout", "sensitive" : false }, "SSL Context Service" : { "displayName" : "SSL Context Service", "identifiesControllerService" : true, "name" : "SSL Context Service", "sensitive" : false }, "Username" : { "displayName" : "Username", "identifiesControllerService" : false, "name" : "Username", "sensitive" : false }, "Accept Content-Type" : { "displayName" : "Accept Content-Type", "identifiesControllerService" : false, "name" : "Accept Content-Type", "sensitive" : false }, "Follow Redirects" : { "displayName" : "Follow Redirects", "identifiesControllerService" : false, "name" : "Follow Redirects", "sensitive" : false }, "Password" : { "displayName" : "Password", "identifiesControllerService" : false, "name" : "Password", "sensitive" : true } }, "retriedRelationships" : [ ], "retryCount" : 10, "runDurationMillis" : 0, "scheduledState" : "ENABLED", "schedulingPeriod" : "10 sec", "schedulingStrategy" : "TIMER_DRIVEN", "style" : { }, "type" : "org.apache.nifi.processors.standard.GetHTTP", "yieldDuration" : "1 sec" }, { "autoTerminatedRelationships" : [ "success" ], "backoffMechanism" : "PENALIZE_FLOWFILE", "bulletinLevel" : "WARN", "bundle" : { "artifact" : "nifi-standard-nar", "group" : "org.apache.nifi", "version" : "1.24.0" }, "comments" : "", "componentType" : "PROCESSOR", "concurrentlySchedulableTaskCount" : 1, "executionNode" : "ALL", "groupIdentifier" : "ad86f407-8631-318b-8230-76d1d7164043", "identifier" : "32d35b0d-daab-3189-9f86-71fbe584a6c7", "instanceIdentifier" : "32d35b0d-daab-3189-b8ae-148fcd2e1c26", "maxBackoffPeriod" : "10 mins", "name" : "PutSQL", "penaltyDuration" : "30 sec", "position" : { "x" : 1136.0, "y" : 152.0 }, "properties" : { "Support Fragmented Transactions" : "true", "putsql-sql-statement" : "insert into table values (?)", "Batch Size" : "100", "Obtain Generated Keys" : "false", "JDBC Connection Pool" : "bed8843c-c85e-3b34-844d-0804c6d35999", "database-session-autocommit" : "false", "rollback-on-failure" : "false" }, "propertyDescriptors" : { "Support Fragmented Transactions" : { "displayName" : "Support Fragmented Transactions", "identifiesControllerService" : false, "name" : "Support Fragmented Transactions", "sensitive" : false }, "putsql-sql-statement" : { "displayName" : "SQL Statement", "identifiesControllerService" : false, "name" : "putsql-sql-statement", "sensitive" : false }, "Transaction Timeout" : { "displayName" : "Transaction Timeout", "identifiesControllerService" : false, "name" : "Transaction Timeout", "sensitive" : false }, "Batch Size" : { "displayName" : "Batch Size", "identifiesControllerService" : false, "name" : "Batch Size", "sensitive" : false }, "Obtain Generated Keys" : { "displayName" : "Obtain Generated Keys", "identifiesControllerService" : false, "name" : "Obtain Generated Keys", "sensitive" : false }, "JDBC Connection Pool" : { "displayName" : "JDBC Connection Pool", "identifiesControllerService" : true, "name" : "JDBC Connection Pool", "sensitive" : false }, "database-session-autocommit" : { "displayName" : "Database Session AutoCommit", "identifiesControllerService" : false, "name" : "database-session-autocommit", "sensitive" : false }, "rollback-on-failure" : { "displayName" : "Rollback On Failure", "identifiesControllerService" : false, "name" : "rollback-on-failure", "sensitive" : false } }, "retriedRelationships" : [ ], "retryCount" : 10, "runDurationMillis" : 0, "scheduledState" : "ENABLED", "schedulingPeriod" : "10 sec", "schedulingStrategy" : "TIMER_DRIVEN", "style" : { }, "type" : "org.apache.nifi.processors.standard.PutSQL", "yieldDuration" : "1 sec" } ], "remoteProcessGroups" : [ ] }, "flowEncodingVersion" : "1.0", "snapshotMetadata" : { "author" : "nifi", "bucketIdentifier" : null, "comments" : "", "flowIdentifier" : null, "link" : { "href" : "buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows/20be5260-9249-4626-99d8-e95b7286836e/versions/1", "params" : { "rel" : "content" } }, "timestamp" : 1779182541044, "version" : 0 } }
Скрытый текст
{ "name": "http-2-db", "variables": [ { "variable": { "name": "dbUser", "value": "db_prod_user" } }, { "variable": { "name": "jdbcUrl", "value": "jdbc:postgresql://db_host:db_port/db?prepareThreshold=0&searchpath=schema" } }, { "variable": { "name": "truststoreFilename", "value": "/home/user/path/to/file.jks" } }, { "variable": { "name": "url", "value": "https://rest_api_host:rest_api_port/api/v1/some/resource" } } ], "api-data": [ { "api": { "name": "registryFlowName", "value": "http-2-db" } }, { "api": { "name": "processGroupPath", "value": "tmp>>prod>>http-2-db" } }, { "api": { "name": "restApiUser", "value": "rest_api_prod_user" } }, { "api": { "name": "restApiPassword", "value": "rest_api_prod_password" } }, { "api": { "name": "dbPassword", "value": "db_prod_password" } }, { "api": { "name": "truststorePassword", "value": "truststore_prod_password" } } ] }
Деплоим в прод:
java -jar nifi-deployer-0.36-SNAPSHOT.jar -i bucket.name=default import.version.file=./http-2-db-1.24.json import.variable.file=./http-2-db-1.24-var.json nifi.url=https://.../nifi-api registry.url=https://.../nifi-registry-api user=... password=... ssl.keystore.filename=./nifi.p12
Лог, по которому примерно можно понять, какие конкретно действия выполняет джарник:
Скрытый текст
31.05.2026 12:43:59.914 [main] INFO deployTemplateAndMetadata2RegistryAndNiFi$ - read template from file ./http-2-db-1.24.json 31.05.2026 12:43:59.925 [main] INFO deployTemplateAndMetadata2RegistryAndNiFi$ - read metadata from file ./http-2-db-1.24-var.json 31.05.2026 12:44:00.138 [main] INFO deployTemplateAndMetadata2RegistryAndNiFi$ - parse template from file ./http-2-db-1.24.json 31.05.2026 12:44:00.188 [main] INFO ApiRegistry - get all flow versions from Registry 31.05.2026 12:44:00.189 [main] INFO ApiRegistry - get bucket ID for bucket name default 31.05.2026 12:44:00.189 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets 31.05.2026 12:44:00.196 [main] INFO AuthHandler - getting token from https://... 31.05.2026 12:44:00.698 [main] INFO ApiRegistry - using bucket 2cf0ac20-3ccd-41ed-97c1-97baecf45dfc 31.05.2026 12:44:00.698 [main] INFO ApiRegistry - get flow ID for flow name http-2-db and bucketId 2cf0ac20-3ccd-41ed-97c1-97baecf45dfc 31.05.2026 12:44:00.698 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows 31.05.2026 12:44:01.097 [main] INFO ApiRegistry - create flow http-2-db 31.05.2026 12:44:01.105 [main] DEBUG HttpClient - POST https://.../nifi-registry-api/buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows 31.05.2026 12:44:01.451 [main] INFO ApiRegistry - using flow d46dcc26-9ed3-4b9c-9f73-1f36738be47c 31.05.2026 12:44:01.451 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows/d46dcc26-9ed3-4b9c-9f73-1f36738be47c/versions 31.05.2026 12:44:01.579 [main] INFO importFlow2Registry$ - update flow http-2-db in Registry 31.05.2026 12:44:01.580 [main] DEBUG HttpClient - POST https://.../nifi-registry-api/buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows/d46dcc26-9ed3-4b9c-9f73-1f36738be47c/versions 31.05.2026 12:44:01.969 [main] INFO importFlow2Registry$ - ./http-2-db-1.24.json imported to Registry 31.05.2026 12:44:01.970 [main] INFO importGroupFromRegistry2NiFi$ - search/create recursively tmp>>prod>>http-2-db 31.05.2026 12:44:01.971 [main] DEBUG HttpClient - GET https://.../nifi-api/flow/process-groups/root 31.05.2026 12:44:01.971 [main] INFO AuthHandler - getting token from https://... 31.05.2026 12:44:02.595 [main] DEBUG HttpClient - GET https://.../nifi-api/flow/process-groups/cba7fcb9-0186-1000-0000-0000451bf611 31.05.2026 12:44:02.896 [Thread-21] INFO vci.getVersionControlInfo$ - get version control info for http-2-db recursively from root 31.05.2026 12:50:15.502 [main] DEBUG HttpClient - GET https://.../nifi-api/controller/registry-clients 31.05.2026 12:50:15.826 [main] INFO importGroupFromRegistry2NiFi$ - registry b463120f-018c-1000-0000-00006c563b27 31.05.2026 12:50:15.830 [main] DEBUG HttpClient - POST https://.../nifi-api/process-groups/3f508567-019e-1000-ffff-ffffc155c0cd/process-groups 31.05.2026 12:50:16.342 [main] INFO importGroupFromRegistry2NiFi$ - imported https://.../nifi?processGroupId=7d70f7a3-019e-1000-ffff-ffffdde8afc7 from Registry to NiFi
Поток создан в Registry:

Поток создан в NIFi:

В свойство Username процессора GetHTTP установлено значение из файла метаданных:

Изменение шаблона:
Скрытый текст
Для изменения существующего шаблона порядок действий в целом аналогичен созданию, только после выгрузки файла шаблона у разработчика есть его предыдущая версия — с ней можно сравнить новую. В свойствах компонентов, не поддерживающих Expression Language, в новой версии экспортированы конкретные значения, а в старой прописаны элементы api-data. Sensitive-свойства компонентов в новой версии не экспортированы (не сохраняются из NiFi в Registry), а в старой версии они также прописаны элементами api-data. Всё это видно в git diff и поддаётся относительно лёгкому восстановлению:

Возможные проблемы при деплое
В потоке сделаны изменения, но не закоммичены в Registry (в интерфейсе это отображается серой звездочкой и сообщением «Locally modified Versioned Process Group») — NiFi в этом состоянии не разрешает менять поток, чтобы не потерять потенциально полезные изменения в нём, вызов REST API возвращает 409 Conflict. Это невозможно разрулить автоматизированно. Нужно вручную откатить или закоммитить изменения потока в Registry перед деплоем (пункт Version контекстного меню). Пример:

Кроме того, NiFi не позволяет удалять очереди, в которых есть файлы (и соответственно процессоры, соединённые с этими очередями). Это также нужно разруливать вручную — запустить процессоры и прокачать все данные в потоке перед деплоем, если при обновлении должны удалиться процессоры или измениться их связи.
Заключение
Дополнительные возможности джарника:
режим импорта только метаданных. Цель — быстрое обновление параметров потока, без пересоздания процессоров и связей между ними. Для этого не требуется файл шаблона, только файл метаданных и имя потока (параметр джарника parent.group.id)
подстановка метаданных, общих для всех потоков (чтобы не писать одинаковые значения во множестве файлов) — реализовано параметром джарника api.data.default, метаданные оттуда добавляются к пользовательским метаданным при отсутствии по ключам. Возможный пример — адрес системы мониторинга, куда все потоки на стенде должны отправлять статистики/метрики своей работы (если эта фича реализована в шаблонах потоков). Этот адрес отличается между стендами, но бизнес-пользователи скорее всего не хотят его знать и писать в своих метаданных, так что логично добавить автоматически в Jenkins при сборке дистрибутива потока.
Заметка по длительности импорта: для обновления потока в NiFi нужно знать его UUID (идентификатор в методах REST API). Но джарник знает лишь имя в Registry (registryFlowName), так что рекурсивно обходит все process groups в NiFi в поисках той, которая связана с этим потоком в Registry. При количестве групп верхнего уровня около 300 время обхода составляет порядка нескольких минут. Это можно оптимизировать параметром джарника parent.group.id — искать группы не от корня, а от указанной. Если потоки в NiFi организованы в виде дерева, и если мы заранее знаем, в какую ветку должен попасть наш поток, можно её указать в этом параметре.
Особенности и ограничения:
процесс обновления версии потока и переменных в NiFi асинхронный — отправили запрос, периодически проверяем статус. Чтобы не зависнуть навечно, джарник имеет параметры — интервал и количество итераций проверки
если версионированная process group ссылается на controller services, объявленные в родительской не-версионированной группе, они не сохраняются в Registry при коммите, соответственно не экспортируются в файлы и не импортируются в целевой стенд, пропадают после цикла экспорта-импорта; джарник это не контролирует, надо вручную создавать такие компоненты в целевом стенде, или вообще так не делать, все компоненты потока (версионированной группы) создавать в нём самом
при экспорте с single instance (например, из NiFi на localhost разработчика, если нет выделенного Dev стенда) всем процессорам ставится "executionNode": "ALL". Если целевой стенд кластерный, и если определённым процессорам нужен запуск на одной ноде, например primary, это надо вручную прописать в файле шаблона нужным процессорам — "executionNode": "PRIMARY"
