Перейти до основного вмісту

Створення та розгортання конфігурації

Конфігурація Kandra розгортається як один застосунок ASP.NET Core: проєкт Acme.WebApi хостить API і обслуговує Blazor WebAssembly-клієнт з того самого джерела (origin). У цій документації немає жодного Kandra-специфічного інструментарію для розгортання й жодного рецепта хостингу для конкретної платформи (Azure, Docker, IIS…) — далі йде опис того, що насправді видає dotnet publish, як він поводиться, коли запущений у Production, і перелік речей, які типові налаштування не зроблять безпечно за вас.

Прочитайте перелік перед тим, як відкривати доступ до інстансу

Типові налаштування підлаштовані під розробку. Щойно опублікований інстанс, запущений у Production, усе одно створює наперед відомі облікові записи, обслуговує публічну документацію API та приймає cross-origin запити з будь-якого сайту. Дивіться Перед тим, як відкривати доступ.

Створення нової конфігурації "з нуля"​

Це описано крок за кроком у Як почати — коротка версія:

dotnet new install Kandra.ConfigTemplate --add-source https://nugets.kandra.tech/index.json
dotnet new kandra-config -n Acme --IncludeSqlite

або, з установленим інтерактивним майстром (dotnet tool install --global Kandra.ConfigCreateTool --add-source https://nugets.kandra.tech/index.json):

kandra-new-config

Kandra.* надається з публічного фіда Kandra, https://nugets.kandra.tech/index.json, який анонімний: токен не потрібен ні на вашій машині, ні на build-сервері. Скафолд постачає nuget.config, що мапить Kandra.* на цей фід, а все інше — на nuget.org (packageSourceMapping, який не дає пакету з такою самою назвою з іншого джерела виграти відновлення). Тримайте цей файл у репозиторії, адже dotnet restore на build-сервері читає його теж; повний файл і пояснення — у Як почати.

Публікація​

dotnet publish src/Acme.WebApi/Acme.WebApi.csproj -c Release -o ./publish

Acme.WebApi посилається на проєкт Blazor-клієнта, тож одна публікація видає обидві половини. Вихід публікації еталонної конфігурації (KandraWms.WebApi; ваша матиме таку саму форму) склав приблизно 190 МБ у Debug-публікації й містив:

У виводіЩо це таке
Acme.WebApi.exe, Acme.WebApi.dll, *.runtimeconfig.json, *.deps.jsonСерверний хост
Kandra.*.dll, Acme.*.dll, збірки третіх сторінРушій, ваша конфігурація, залежності
wwwroot/_framework/Blazor WebAssembly-клієнт (кілька сотень файлів, кожен також попередньо стиснутий як .br і .gz)
wwwroot/css, _content/Статичні ресурси, включно з ресурсами бібліотеки UI
appsettings.json (та appsettings.Development.json)Конфігурація — дивіться нижче
db/Де лежатиме файл SQLite-бази даних за замовчуванням (порожня папка у виводі; дивіться Папка даних)
web.configПрисутній для хостингу в IIS
мовні папки (uk, de, …)Супутні ресурси із залежностей

Release-публікація уникає лише-для-debug частин (BlazorDebugProxy, файли .pdb), які захопила вище показана Debug-публікація. Папка db/ порожня, доки застосунок не запуститься вперше; папки сховища файлів, ключів і резервних копій створюються під час виконання всередині папки даних.

Запуск​

cd publish
ASPNETCORE_ENVIRONMENT=Production ASPNETCORE_URLS=http://localhost:5299 ./Acme.WebApi.exe

Під час старту застосунок застосовує будь-які незастосовані міграції й засіває дані (MigrateAndSeedAsync), тож перший запуск створює базу даних. Еталонний хост, запущений у Production, відповів на вказаній вище URL. API (/api/v1/…), інтерактивна документація API та вхід у систему підтверджено робочими; клієнт обслуговується тим самим хостом (UseBlazorFrameworkFiles плюс запасний варіант на index.html).

ASPNETCORE_ENVIRONMENT=Production обирає перевизначення з appsettings.Production.json, якщо ви його надаєте, і вмикає обробник винятків та HSTS; він не вимикає жоден із пунктів переліку нижче.

Ендпоінти стану​

Хост надає два анонімні ендпоінти-проби для healthcheck контейнерів, реверс-проксі та проб Kubernetes/Azure:

ЕндпоінтВідповідає 200, коли
/health/liveпроцес працює (інші перевірки не виконуються)
/health/readyбаза даних доступна і не має невиконаних міграцій

Обидва повертають лише слово Healthy або Unhealthy (503) — жодних подробиць; причина збою потрапляє в журнал застосунку. Вони доступні і через звичайний HTTP: /health/* виключено з перенаправлення на HTTPS, бо проби не вміють його виконувати.

У chiseled/distroless-образах немає curl, тож командний Docker HEALTHCHECK там недоступний; націльте HTTP-пробу оркестратора (livenessProbe/readinessProbe у Kubernetes, health probe в Azure) на ці шляхи.

Конфігурація​

Хост використовує стандартну конфігурацію ASP.NET Core, тож будь-яке налаштування в appsettings.json можна перевизначити змінною середовища, використовуючи __ для вкладеності — звичний спосіб тримати секрети поза опублікованими файлами:

НалаштуванняЗмінна середовищаЗначення
ProviderProviderЯкий провайдер бази даних працює: Sqlite, SqlServer чи PostgreSql. Інші ключі провайдера у файлі (_Provider, __Provider) — це просто закоментовані альтернативи.
ConnectionStrings:<Provider>ConnectionStrings__PostgreSqlРядок підключення для обраного провайдера
Kandra:DataPathKandra__DataPathЄдина папка, у якій лежить увесь локальний стан — дивіться Папка даних
JwtOptions:SecretJwtOptions__SecretКлюч підпису для токенів входу. Не задавайте його — і кожна інсталяція згенерує власний (дивіться Ключ підпису JWT)
JwtOptions:Issuer / AudienceJwtOptions__Issuer / JwtOptions__AudienceПеревіряються при кожному запиті
JwtOptions:Expires / RefreshExpiresТривалість життя токенів (за замовчуванням 1 день / 14 днів)
Kandra:Database:*Kandra__Database__BackupsToKeep …Резервні копії SQLite та відновлення — дивіться Резервні копії перед міграціями
BlobStorage:*BlobStorage__LocalDiskRootPath …Сховище вкладень — дивіться Файлове сховище та вкладення
FeatureManagement:*Прапорці функцій (Feature flags)
ReportResultCache:*TTL і розмір кешу результатів звітів
Licensing:LicenseFilePath / LicenseValueLicensing__LicenseFilePathЗвідки зчитується підписана ліцензія

Перед тим, як відкривати доступ​

Пункти 2, 3, 4 і 6 перевірено, запустивши опублікований еталонний хост у Production на тестовій базі даних. Пункти 5 і 7 зчитано з постачених конфігурації та коду конвеєра.

  1. Знайте, звідки береться ключ підпису. appsettings.json постачається без JwtOptions:Secret: під час першого запуску кожна інсталяція генерує власний ключ у папку keys/ всередині папки даних, тож дві інсталяції однієї збірки ніколи не ділять один ключ. Будь-хто, хто має ключ, може випускати дійсні токени для будь-якого користувача цієї інсталяції, тому тримайте папку даних закритою та задайте ваші реальні Issuer та Audience. Кілька вузлів або потрібен ключ під вашим контролем? Задайте JwtOptions__Secret самі — дивіться Ключ підпису JWT.

  2. Змініть або вилучіть наперед створені облікові записи. Під час старту сідер створює будь-який з цих записів, якого бракує, admin — із фіксованим паролем, що є частиною платформи, решту — з випадковими:

    КористувачПарольПримітки
    adminAdmin!1Адміністратор
    mcp-agent(випадковий)Обліковий запис нелюдського викликача; автентифікується API-ключем, а не паролем
    jobs-scheduler(випадковий)Назавжди заблокований для інтерактивного входу, тож увійти під ним неможливо — існує, щоб атрибутувати заплановані задачі

    Відомий пароль має лише admin; mcp-agent і jobs-scheduler отримують випадковий, якого ніхто не бачить. Звичайна інсталяція не має зразкових користувачів: вони з'являються лише в демоінсталяції, зі списку демокористувачів вашої конфігурації (див. Імпорт даних і демодані). База даних, створена до цієї зміни, зберігає наявні облікові записи user0 … user7, тож перевірте їх на оновленому інстансі. Негайно змініть пароль admin після першого старту. Оскільки сідер створює користувача, лише якщо його бракує, зміна пароля лишається чинною — але видалення наперед створеного користувача призводить лише до того, що його створять знову при наступному старті (admin — з паролем за замовчуванням). Замість видалення блокуйте чи вимикайте непотрібні вам облікові записи й ніколи не покладайтесь на їхню відсутність.

  3. Вирішіть, чи має документація API бути публічною. Swagger UI та swagger.json увімкнені за замовчуванням у кожному середовищі, включно з Production, і відповідають без автентифікації. Вимкніть їх через Kandra__Swagger__Enabled=false або обмежте на реверс-проксі.

  4. Затягніть CORS, якщо на іншому origin розміщено щось іще. За замовчуванням жоден міжджерельний виклик не дозволений. Клієнт обслуговується з того самого origin, що й API, тож CORS йому не потрібен. Щоб дозволити інший origin, перелічіть його в Kandra:Cors:AllowedOrigins (змінна середовища Kandra__Cors__AllowedOrigins__0, __1, …); credentials отримують лише перелічені origin, а * ігнорується.

  5. Правильно термінуйте TLS. Конвеєр викликає UseHttpsRedirection() і, поза розробкою, UseHsts(), але застосунок не конфігурує сертифікат. Використовуйте реверс-проксі або сконфігуруйте endpoint Kestrel, і переконайтесь, що застосунок бачить оригінальну схему та порт HTTPS за проксі, інакше редиректу нема куди йти.

  6. Знайте, які endpoint анонімні. GET /api/v1/About (ідентифікатор інсталяції, версія, статус ліцензії) потребує входу користувача. Endpoint входу та текст EULA лишаються анонімними.

  7. Оберіть базу даних, яку ви справді будете використовувати. SQLite — щоденний провайдер для розробки, і він працює для одного невеликого інстансу. Міграції SQL Server і PostgreSQL згенеровані, але в самій роботі Kandra рідко перевіряються на живому сервері, тож перш ніж покладатися на такий провайдер, пройдіть увесь шлях на ньому — міграції, засівання, ваші дані. Дивіться Міграції бази даних.

Папка даних​

Увесь стан, який працюючий інстанс тримає на власному диску, лежить в одній папці, тож для повторного розгортання, оновлення чи перезапуску контейнера є рівно одна річ, яку треба зберегти. Задайте її через Kandra:DataPath (в appsettings.json, змінною середовища Kandra__DataPath або в командному рядку). Якщо не задано, це папка самого застосунку (content root): dotnet run використовує папку проєкту, а опублікований застосунок — папку публікації, незалежно від того, з якої папки його запущено. Хост, який знає краще, може задати власне значення за замовчуванням через Kandra:HostDataPath (так робить десктопний хост — папка даних користувача). Відносне значення розміщується в папці застосунку.

У папці данихЩо це
db/База SQLite — відносний Data Source у рядку підключення (за замовчуванням ./db/<name>.db) розміщується тут, а не в поточній папці процесу. Абсолютний Data Source використовується як є. Не застосовується до SQL Server і PostgreSQL.
blobs/Завантажені вкладення (BlobStorage:LocalDiskRootPath, за замовчуванням blobs; відносне значення розміщується тут; інсталяція, що вже має App_Data/blobs і не має blobs/, далі користується нею). Втрата цієї папки залишає без пари кожне посилання на файл у базі даних.
keys/Згенерований ключ підпису JWT.
backups/Резервні копії SQLite, зроблені перед міграціями, і запис про скидання (нижче).

Визначена папка записується в лог під час старту (Kandra data path: …). Для контейнера змонтуйте один том і спрямуйте налаштування на нього: Kandra__DataPath=/data.

Код хоста

Program.cs хоста має створювати білдер через KandraWebApplication.CreateBuilder(args) замість WebApplication.CreateBuilder(args). ASP.NET Core бере папку застосунку з робочої папки процесу, тож опублікований застосунок, запущений з іншої папки (ярлик, менеджер служб), інакше працює без свого appsettings.json. Варіант Kandra переходить у власну папку застосунку, якщо в робочій папці немає appsettings.json, а в папці застосунку він є.

Також бережіть ліцензію, якщо використовуєте LicenseFilePath. Вона навмисно тримається поза базою даних, тож відновлення резервної копії на іншій машині не переносить разом із нею продуктивну ліцензію. З SQL Server чи PostgreSQL сама база лежить на сервері, і резервні копії робляться там.

Ключ підпису JWT​

Якщо JwtOptions:Secret не задано, перший запуск генерує випадковий ключ і зберігає його в keys/jwt-signing.key у папці даних (читається лише користувачем застосунку, де це дозволяє операційна система). Той самий ключ використовується при кожному наступному запуску, тож користувачі, що увійшли, переживають перезапуск. Видаліть файл, щоб анулювати всі випущені токени.

Задане значення завжди має пріоритет: задайте JwtOptions__Secret довгим випадковим рядком, коли кілька вузлів мають приймати токени одне одного (інакше кожен згенерує свій ключ), або коли секрети керуються централізовано. Ніколи не комітьте його.

Резервні копії перед міграціями​

При кожному запуску з провайдером SQLite, якщо база вже існує і має незастосовані міграції, застосунок спершу записує узгоджену копію в backups/kandra-<мітка часу>-before-<міграція>.db (через онлайн-резервне копіювання SQLite, тож зміни, що ще лежать у журналі WAL, включені) і логує шлях. Він зберігає п'ять найновіших (Kandra:Database:BackupsToKeep; 0 вимикає копії). Нова база, або база без незастосованих міграцій, копії не отримує.

Відновлення: зупиніть застосунок, скопіюйте резервну копію поверх файлу бази (і видаліть поруч kandra.db-wal / kandra.db-shm), запустіть попередню версію застосунку. Інші провайдери: власні засоби резервного копіювання сервера.

Після релізу історія міграцій — це контракт​

Щойно в інсталяцій з'являються справжні бази, історія міграцій, яку ви постачаєте, і є тим, що їх оновлює. Не склеюйте (squash) міграції після публічного релізу — база, що записала старі міграції, не може продовжити з переписаною історією. Під час старту така SQLite-база виявляється (вона містить міграції, яких збірка не знає), а застосунок відмовляється стартувати й пояснює чому, замість того, щоб зламатись посеред міграції. Нічого не змінюється.

Якщо ви все ж склеїли — зазвичай на тестовому чи демо-інстансі, дані якого не потрібні, — скиньте SQLite-базу, задавши одне з цих налаштувань там, де працює застосунок. Це звичайні налаштування, тож вони працюють і для контейнера, до якого вже не дістатися (Azure Application settings, compose-файл, -e):

НалаштуванняДія
Kandra__Database__ResetToken=<будь-яке раніше не використане значення>Одноразово. При наступному запуску базу зберігають у backups/, видаляють і створюють заново; значення запам'ятовується в backups/reset-tokens.txt, тож залишене налаштування при подальших перезапусках нічого не робить. Нове значення — нове скидання. Працює незалежно від того, сумісна база чи ні.
Kandra__Database__IncompatibleHistory=ResetПостійна політика. Щоразу, коли база містить міграції, яких збірка не знає, її зберігають у backups/, видаляють і створюють заново. Приберіть налаштування після відновлення, якщо не хочете цього при кожному squash.

У будь-якому разі стара база лишається в backups/ (зберігаються найновіші копії; скопіюйте її звідти, якщо вона потрібна надовго), а папки файлів і ключів не чіпаються. Обидва налаштування діють лише для SQLite і ніколи не спрацьовують для SQL Server чи PostgreSQL.

Ліцензування​

Без налаштованої ліцензії інстанс працює як Free: усе працює, у межах вбудованих у компіляцію лімітів використання (проєктний документ задає 5 користувачів, 1000 документів на місяць, 200 рядків на документ). Надайте підписану ліцензію через Licensing__LicenseFilePath (шлях до файлу, що містить рядок ліцензії) або Licensing__LicenseValue (сам рядок, для середовищ, де файл незручний); шлях до файлу має пріоритет, якщо задано обидва. GET /api/v1/About показує поточний статус ("status":"free" у перевіреному запуску). Адміністратор також може керувати ліцензією зі сторінки Admin → License.

Запуск більш ніж одного інстансу​

Запускайте один. Кілька частин стану прив'язані до процесу:

  • планувальник задач тримає свій розклад у пам'яті й запускав би кожну задачу на кожному інстансі (Планувальник задач);
  • кеш результатів звітів — це кеш у пам'яті;
  • файлове сховище за замовчуванням використовує локальний диск, який інші інстанси не бачать;
  • ключ підпису JWT генерується для кожної інсталяції, якщо ви не задасте JwtOptions__Secret — вузли мають ділити один.

Платформа поки що не має історії багатовузлової підтримки. Масштабуйтесь вертикально, перш ніж масштабуватись горизонтально.

Що тут навмисно не задокументовано​

Навмисно: покрокові інструкції для Azure/AWS/IIS, конвеєри CI/CD та конфігурація реверс-проксі. Ніщо в Kandra не специфічне для жодного з них — це стандартний застосунок ASP.NET Core — і вигадувати рецепт, який ніхто не прогнав, було б гірше, ніж чесно про це сказати. Коли такий рецепт з'явиться і буде прогнаний від початку до кінця, йому місце на цій сторінці.

Дивіться також​