Автоматизация разработки и деплоя потоков Apache NiFi

от автора

Я Игорь Юрченко, 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.

Термины:

  1. поток — версионированная (сохранённая в Registry) process group в NiFi

  2. шаблон — файл описания потока в формате JSON, совместимый с NiFi REST API (содержит процессоры и controller services, их свойства, связи, вложенные process groups и т.д.). Шаблон кастомизируется метаданными и превращается в поток при деплое (1 шаблон * N метаданных = N потоков). Не путать с template в NiFi, ниже описана разница

  3. метаданные — файл параметров потока в формате JSON. Параметры могут быть бизнесовые (фильтры загружаемых сущностей, даты, Avro-схемы и т.д. — статические значения, одинаковые между стендами) и технические (HTTP URL, JDBC connection strings, Kafka brokers — отличаются между стендами, дополнительно кастомизируются через Ansible). Также метаданные делятся на variables (которые в NiFi можно заполнять в отдельном окне, а ссылки на них писать в свойства компонентов выражениями Expression Language — ${variable}, они разрешаются самим NiFi при работе потока), и так называемые api-data (разрешаются не в NiFi, а при деплое — значения заранее подставляются в свойства процессоров, не поддерживающих Expression Language)

В статье не описаны установка и настройка NiFi + Registry (в т.ч. настройки аутентификации пользователей). В интернете можно найти материалы, а тут предполагаем, что это уже сделано.

Общее описание подхода

Процесс разработки и деплоя:

  1. Создаем/меняем process group в Dev NiFi (или на localhost); группа может быть на любом уровне вложенности

  2. Коммитим изменения в Dev Registry (при необходимости создаем bucket, https://nifi.apache.org/docs/nifi-registry-docs/html/user-guide.html#create-a-bucket) — ручное действие

  3. Экспортируем шаблон из Registry в локальный файл, вместе с набором переменных (метаданными) этого потока в отдельном файле для удобства изменения — автоматизировано. Теоретически это можно делать и вручную (https://nifi.apache.org/docs/nifi-registry-docs/html/user-guide.html#export-a-flow-version), но Registry экспортирует процессоры в случайном порядке, автоматизация позволяет отсортировать процессоры в файле, а это минимизирует Git diff

  4. Вручную правим шаблон — подставляем элементы 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)

  5. Вручную правим файл метаданных (в технических variables подставляем Ansible-переменные вида {{ var }} — HTTP URL-ы, строки подключения к БД и т.д.; добавляем массив api-data с необходимыми элементами api — то, что в шаблоне описано в виде [[ api-data ]])

  6. Коммитим шаблон и метаданные в Git — ручное действие. Шаблон и метаданные могут храниться в разных репозиториях и ветках, любой Git flow на ваш вкус, можно merge, revert, diff, blame

  7. Импортируем файл шаблона в Prod Registry, при этом создаются bucket и flow — автоматизировано (тут же элементы метаданных api-data подставляются в файл шаблона — текстовая замена по конструкциям вида [[ api-data ]])

  8. Импортируем flow из Prod Registry в Prod NiFi (в т.ч. если потока еще нет в NiFi, он создается) — автоматизировано

  9. Обновляем значения переменных потока из файла метаданных — автоматизировано

Пункты до «коммитим в 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 default19.05.2026 14:39:02.142 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets19.05.2026 14:39:03.384 [main] INFO ApiRegistry - using bucket 2cf0ac20-3ccd-41ed-97c1-97baecf45dfc19.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-97baecf45dfc19.05.2026 14:39:03.384 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows19.05.2026 14:39:03.699 [main] INFO ApiRegistry - using flow 20be5260-9249-4626-99d8-e95b7286836e19.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/latest19.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-0000451bf61119.05.2026 14:39:20.650 [main] INFO ApiNiFiGeneric - get variable registry from process group c9143fc0-c492-3fa7-95ae-81c7de3245af19.05.2026 14:39:20.650 [main] DEBUG HttpClient - GET https://.../nifi-api/process-groups/c9143fc0-c492-3fa7-95ae-81c7de3245af/variable-registry19.05.2026 14:39:20.854 [main] INFO getMetadataFromNiFi$ - get nested process groups for c9143fc0-c492-3fa7-95ae-81c7de3245af19.05.2026 14:39:20.855 [main] DEBUG HttpClient - GET https://.../nifi-api/flow/process-groups/c9143fc0-c492-3fa7-95ae-81c7de3245af19.05.2026 14:39:20.990 [main] INFO writeTemplateAndMetadata$ - write template http-2-db-1.24.json19.05.2026 14:39:21.003 [main] INFO writeMetadata$ - write metadata http-2-db-1.24-var.json19.05.2026 14:39:21.062 [main] INFO exportTemplateAndMetadataFromRegistry$ - version 1 exported

Исправим файл шаблона:

  1. Для доступа к REST API на Dev-стенде в потоке используется имя пользователя «rest_api_test_user», но на других стендах оно может быть другим — это параметр. Свойство Username процессора GetHTTP не поддерживает Expression Language (задано в NiFi явно, не через variable), так что в файле шаблона заменим его на конструкцию вида [[ restApiUser ]] (строка 685 — «Username» : «[[ restApiUser ]]»). Ключ restApiUser может быть любым, задаётся автором шаблона (по-хорошему должен адекватно описывать суть параметра). Две квадратные скобки — специальный синтаксис, по которому выполняется текстовая замена этих параметров при деплое

  2. Секретные свойства компонентов не экспортируются из 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 ]]»

Исправим файл метаданных:

  1. все параметры шаблона (name из [[ name ]]) добавим в массив api-data на верхнем уровне метаданных, для каждого укажем value — реальное значение, которое при деплое подставится в шаблон вместо [[ name ]]

  2. для самого процесса деплоя нужны общие параметры, независимые от шаблона — имя потока, с которым он будет импортирован в 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.json31.05.2026 12:43:59.925 [main] INFO deployTemplateAndMetadata2RegistryAndNiFi$ - read metadata from file ./http-2-db-1.24-var.json31.05.2026 12:44:00.138 [main] INFO deployTemplateAndMetadata2RegistryAndNiFi$ - parse template from file ./http-2-db-1.24.json31.05.2026 12:44:00.188 [main] INFO ApiRegistry - get all flow versions from Registry31.05.2026 12:44:00.189 [main] INFO ApiRegistry - get bucket ID for bucket name default31.05.2026 12:44:00.189 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets31.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-97baecf45dfc31.05.2026 12:44:00.698 [main] INFO ApiRegistry - get flow ID for flow name http-2-db and bucketId 2cf0ac20-3ccd-41ed-97c1-97baecf45dfc31.05.2026 12:44:00.698 [main] DEBUG HttpClient - GET https://.../nifi-registry-api/buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows31.05.2026 12:44:01.097 [main] INFO ApiRegistry - create flow http-2-db31.05.2026 12:44:01.105 [main] DEBUG HttpClient - POST https://.../nifi-registry-api/buckets/2cf0ac20-3ccd-41ed-97c1-97baecf45dfc/flows31.05.2026 12:44:01.451 [main] INFO ApiRegistry - using flow d46dcc26-9ed3-4b9c-9f73-1f36738be47c31.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/versions31.05.2026 12:44:01.579 [main] INFO importFlow2Registry$ - update flow http-2-db in Registry31.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/versions31.05.2026 12:44:01.969 [main] INFO importFlow2Registry$ - ./http-2-db-1.24.json imported to Registry31.05.2026 12:44:01.970 [main] INFO importGroupFromRegistry2NiFi$ - search/create recursively tmp>>prod>>http-2-db31.05.2026 12:44:01.971 [main] DEBUG HttpClient - GET https://.../nifi-api/flow/process-groups/root31.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-0000451bf61131.05.2026 12:44:02.896 [Thread-21] INFO vci.getVersionControlInfo$ - get version control info for http-2-db recursively from root31.05.2026 12:50:15.502 [main] DEBUG HttpClient - GET https://.../nifi-api/controller/registry-clients31.05.2026 12:50:15.826 [main] INFO importGroupFromRegistry2NiFi$ - registry b463120f-018c-1000-0000-00006c563b2731.05.2026 12:50:15.830 [main] DEBUG HttpClient - POST https://.../nifi-api/process-groups/3f508567-019e-1000-ffff-ffffc155c0cd/process-groups31.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 не позволяет удалять очереди, в которых есть файлы (и соответственно процессоры, соединённые с этими очередями). Это также нужно разруливать вручную — запустить процессоры и прокачать все данные в потоке перед деплоем, если при обновлении должны удалиться процессоры или измениться их связи.

Заключение

Дополнительные возможности джарника:

  1. режим импорта только метаданных. Цель — быстрое обновление параметров потока, без пересоздания процессоров и связей между ними. Для этого не требуется файл шаблона, только файл метаданных и имя потока (параметр джарника parent.group.id)

  2. подстановка метаданных, общих для всех потоков (чтобы не писать одинаковые значения во множестве файлов) — реализовано параметром джарника api.data.default, метаданные оттуда добавляются к пользовательским метаданным при отсутствии по ключам. Возможный пример — адрес системы мониторинга, куда все потоки на стенде должны отправлять статистики/метрики своей работы (если эта фича реализована в шаблонах потоков). Этот адрес отличается между стендами, но бизнес-пользователи скорее всего не хотят его знать и писать в своих метаданных, так что логично добавить автоматически в Jenkins при сборке дистрибутива потока.

Заметка по длительности импорта: для обновления потока в NiFi нужно знать его UUID (идентификатор в методах REST API). Но джарник знает лишь имя в Registry (registryFlowName), так что рекурсивно обходит все process groups в NiFi в поисках той, которая связана с этим потоком в Registry. При количестве групп верхнего уровня около 300 время обхода составляет порядка нескольких минут. Это можно оптимизировать параметром джарника parent.group.id — искать группы не от корня, а от указанной. Если потоки в NiFi организованы в виде дерева, и если мы заранее знаем, в какую ветку должен попасть наш поток, можно её указать в этом параметре.

Особенности и ограничения:

  1. процесс обновления версии потока и переменных в NiFi асинхронный — отправили запрос, периодически проверяем статус. Чтобы не зависнуть навечно, джарник имеет параметры — интервал и количество итераций проверки

  2. если версионированная process group ссылается на controller services, объявленные в родительской не-версионированной группе, они не сохраняются в Registry при коммите, соответственно не экспортируются в файлы и не импортируются в целевой стенд, пропадают после цикла экспорта-импорта; джарник это не контролирует, надо вручную создавать такие компоненты в целевом стенде, или вообще так не делать, все компоненты потока (версионированной группы) создавать в нём самом

  3. при экспорте с single instance (например, из NiFi на localhost разработчика, если нет выделенного Dev стенда) всем процессорам ставится «executionNode»: «ALL». Если целевой стенд кластерный, и если определённым процессорам нужен запуск на одной ноде, например primary, это надо вручную прописать в файле шаблона нужным процессорам — «executionNode»: «PRIMARY»

ссылка на оригинал статьи https://habr.com/ru/articles/1052274/