Как установить файл конфигурации в .NET Core Console app для нескольких сред разработки при запуске Docker-контейнера
На ранних стадиях разработки проектов возникают проблемы при развёртывании сервисов в нескольких средах разработки. А именно сложности касаются установки различных версий файла конфигурации appsettings.json для нескольких сред разработки. Правка файла конфигурации вручную приводит к ошибкам и потере времени из-за человеческого фактора, поэтому необходимо автоматизировать процесс развёртывания сервисов. Как решить эту проблему без команды DevOps и в очень короткие сроки я опишу в пошаговой инструкции.
Для примера я рассмотрю сервис обработки сообщений из Kafka. Он представляет собой консольное приложение, которое подписывается на топики, и при появлении сообщения в каждом из них выполняет определённый алгоритм обработки очередного сообщения.
Стоит учесть следующие начальные условия:
- Сервис представляет собой Console Application и в отличии от ASP.NET Core Web Application для консольного приложения не существует решения из коробки.
- Запуск приложения происходит из docker-контейнера.
Итак, как использовать мульти конфигурацию в консольном приложении. Для начала необходимо создать файлы конфигураций для окружений Dev и Testing:

В консольных приложениях нет вложенности по умолчанию. Поэтому следует открыть файл проекта .csproj и добавить:
<ItemGroup> <Content Include="appsettings.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Content Include="appsettings.Dev.json;appsettings.Testing.json;"> <DependentUpon>appsettings.json</DependentUpon> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup>
В файле appsettings.json появились вложенные файлы с названием среды разработки:

В файлы appsettings.{environment}.json надо добавить те части конфига, которые меняются относительно среды. Например, для изменения названия топиков для Kafka в контуре нагрузочного тестирования надо добавить в файл appsettings.Testing.json следующие изменения:
{
"Kafka": {
"EventMainTopicTitle": "Test_EventMain",
"EventDelayTopicTitle": "Test_EventDelay",
"EventRejectTopicTitle": "Test_EventReject"
}
}
Осталось только выбрать нужный файл appsettings.json во время старта сервиса. Для этого необходимо внести изменения в класс Program:
/// <summary>
/// Конфигурация сервиса
/// </summary>
private static IServiceProvider ConfigureServices()
{
const string environmentVariableName = "ASPNETCORE_ENVIRONMENT";
var environmentName =
Environment.GetEnvironmentVariable(environmentVariableName);
var services = new ServiceCollection();
_configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetParent(AppContext.BaseDirectory).FullName)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environmentName}.json")
.AddEnvironmentVariables()
.Build();
services.AddSingleton(_configuration);
services.AddSingleton<KafkaHandler>();
return services.BuildServiceProvider();
}
Теперь всё готово к запуску сервиса в docker-контейнере.
Для начала надо указать переменные окружения для контейнера. Есть несколько способов сделать это:
- командная строка
- текстовый файл
- docker compose
Я остановилась на указании переменных в командной строке. Вот пример скрипта для создания образа и запуска контейнера:
# Build image # docker build . -t consoleapp # Run container on Dev # docker run -d <i>--env ASPNETCORE_ENVIRONMENT=Dev</i> --name app consoleapp
Я рассмотрела именно этот способ, как наиболее подходящий на ранних стадиях разработки, потому что его можно реализовать за короткое время, а время является критическим фактором.
GitHub с проектом — по ссылке.
Спасибо за внимание и приятного кодинга!
4 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівКоментар порушує правила спільноти і видалений модераторами.
При изменении любого параметра в одном из файлов конфигурации нужно пересобирать докер-имидж? Да, и это называется «хардкод». Такое используют разве что на самом раннем этапе разработки, или слабыми командами.
Как рекомендуется с точки зрения DevOps:
— приложение конфигурируется на работу с appsettings.json из кастомной директории, к примеру /app
— используется дополнительный volume, точка монтирования которого в контейнере как раз /app
— файл appsettings.json подготавливается перед стартом контейнера
— запускается контейнер, монтируется volume в /app, стартует приложение, читается файл /app/appsettings.json
Этот метод является несколько сложнее описанного в статье, но он есть product-ready approach и если ваш проект собирается дойти до прод-системы, лучше внедрить и начать использовать Configuration Management сразу же, а не заниматься пересборкой имиджей для прод-системы.
Плюсы:
— формируем конфигурацию как отдельный layer в системе, а не как часть приложения
— способ работает как для одиночных докер-контейнеров на локалхосте, так и для K8s
— при правильном подходе приложение может периодично выполнять poll метаданных конфиг-файла(ов) и перечитывать конфиг, позволяя динамически реконфигурировать приложение без редеплоя или перезапуска контейнера
— файл /app/appsettings.json может формироваться с помощью sidecar-контейнер из Hashicorp Vault/Consul, что есть следующим шагом Configuration Management-а
Минусы:
— для запуска контейнера нужен или батник или пайплайн
— нужно импрувить приложение
Для «продукт реди апроач» по хорошему нужно маунтить сикрет стор с кредами в keyvault а уже в приложении сразу тянуть конфигурцию из него. Или как минимум просто хранить конфигурацию в сикрет сторе если не хочется подключать key vault.
Как более advanced решение для веб приложений:
Пишем условный сикрет провайдер, деплоим на супер секьюрную тачку под 7 замками.
Он Post запросом, зная ip, передает конфигурацию на задеплоенное приложение. Получается как KeyVault только наоборот push модель. В итоге все секреты будут только в памяти приложения. Но это дорого писать, хотя может что-то есть готовое.
Ну а описанный в статье способ это конечно смех:)
Да, я забыл еще про интеграцию приложения с Vault/Consul напрямую, чтобы без sidecar-костылей ...
А почему бы не использовать стандартный шаблон (GenericHost) и RunConsoleAsync() ?
будет всё тоже самое, только всё из коробки.