Запуск приложений на .NET в качестве службы на Linux-системе с systemd

—

от автора

image

В этом материале будет рассмотрено, как запускать приложение .NET Core / .NET 5 в качестве сервиса под Linux. Мы воспользуемся Systemd, чтобы интегрировать наше приложение с операционной системой, научимся запускать и останавливать наш сервис, а также получать от него логи.

Чтобы организовать атаку на цепочку поставок при помощи .NET, мне потребовалось настроить DNS-сервер, который перехватывал бы те хост-имена, которые ко мне направляются. Давайте возьмём этот кейс для примера.

❯ Создание .NET-приложения, которое будет работать как сервис

.NET-сервису потребуется использовать модель хостинга Microsoft.Extensions.Hosting, применяемую в Microsoft. Так мы обеспечим работоспособность любого приложения ASP.NET Core, а также проектов, выполняемых с применением шаблона Worker Service (dotnet new worker). Здесь будем работать с Rider.

image

Далее потребуется установить пакет NuGet: Microsoft.Extensions.Hosting.Systemd. Этот пакет предоставляет хостинговую инфраструктуру .NET для сервисов Systemd. Иными словами, в нём содержится инфраструктура, необходимая для работы с демоном Systemd под Linux.

Осталось сделать ещё одну вещь: зарегистрировать в нашем приложении хостинговые расширения Systemd. В Program.cs понадобится добавить .UseSystemd() к сборщику хоста:

public class Program { // ...   public static IHostBuilder CreateHostBuilder(string[] args) =>     Host.CreateDefaultBuilder(args)         .UseSystemd() // add this         .ConfigureServices((hostContext, services) =>         {             services.AddHostedService<Worker>();         }); }

Когда всё это настроено, наше приложение может работать как сервис Systemd под Linux! Ну, почти… Осталось сделать ещё две вещи:

  • Написать приложение
  • Зарегистрировать его для работы с Systemd

Начнём с первого пункта и реализуем DNS-сервер, который будем использовать в качестве сервиса.

❯ Реализуем сервер .NET DNS в качестве сервиса

DNS-сервер, рассмотренный здесь, пишется на .NET с применением отличного DNS-пакета от Мирзы Капетановича, а далее при помощи Systemd размещается на Linux как сервис.

Давайте соберём простой DNS-сервис, возвращающий актуальное время как запись в формате TXT.

Примечание: можете пропустить этот раздел, если просто хотите изучить, как регистрировать приложения .NET в качестве сервисов Systemd.

Сперва установим пакет DNS. В нём содержится почти готовый DNS-сервер, так что нам понадобится всего лишь записать его код и реализовать серверную логику.

Класс Worker, созданный на основе шаблона worker service (сервис-исполнитель) – хорошая отправная точка. В нём можно применить инъекцию конструктора, чтобы получить доступ ко всем сервисам, имеющимся в вашем приложении. Мы воспользуемся интерфейсом ILogger (уже имеющимся в шаблоне) и IConfiguration, чтобы получить доступ к appsettings.json/переменным окружения/аргументам командной строки. Здесь мы наследуем BackgroundService, в котором есть метод ExecuteAsync, именно этот метод и отвечает за выполнение нашего сервиса. Вот так выглядит костяк Worker:

public class Worker : BackgroundService { private readonly ILogger<Worker> _logger; private readonly IConfiguration _configuration;   public Worker(ILogger<Worker> logger, IConfiguration configuration) {     _logger = logger;     _configuration = configuration; }   protected override async Task ExecuteAsync(CancellationToken stoppingToken) {     // ... здесь идёт вся логика ... } }

Теперь давайте напишем сам DNS-сервер. В методе ExecuteAsync добавим следующий код:

protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var server = new DnsServer(new SampleRequestResolver()); server.Listening += (_, _) => _logger.LogInformation("DNS server is listening..."); server.Requested += (_, e) => _logger.LogInformation("Request: {Domain}", e.Request.Questions.First().Name); server.Errored += (_, e) => {     _logger.LogError(e.Exception, "An error occurred");     if (e.Exception is ResponseException responseError)     {         _logger.LogError("Response: {Response}", responseError.Response);     } };   await Task.WhenAny(new[] {     server.Listen(         port: int.TryParse(_configuration["Port"], out var port) ? port : 53531,         ip: IPAddress.Any),       Task.Delay(-1, stoppingToken) }); }

А теперь объясню развёрнуто:

  • Мы настраиваем DnsServer, который будет разрешать записи при помощи SampleRequestResolver (его нам ещё потребуется собрать).
  • Мы подписываемся на некоторые события, предоставляемые DnsServer, так что сможем просматривать логи входящих запросов и сразу замечать, не пошло ли что-нибудь не так.
  • Наконец, начинаем слушать входящие запросы.

Для ExecuteAsync мы передаём CancellationToken, который задействуется, когда Systemd прикажет нашему сервису завершиться. Поскольку сама библиотека DNS не поддерживает CancellationToken, я воспользуюсь здесь Task.WhenAny, чтобы запустить DNS-сервер. Так мы сможем завершить наш сервис, как только будет затребован его сброс.

Сам сервер будет слушать порт, указанный в конфигурации. Когда этот порт прямо не указан, по умолчанию задаётся 53531.

И ещё: тот самый класс SampleRequestResolver. Он должен реализовывать интерфейс IRequestResolver и собирать ответы на входящие DNS-запросы. Поскольку эта тема выходит за рамки данного поста, просто покажу быструю реализацию, которая возвращает текущие значения даты и времени для любой запрошенной TXT-записи. Для всех прочих запросов возвращается ошибка:

public class SampleRequestResolver : IRequestResolver { public Task<IResponse> Resolve(IRequest request, CancellationToken cancellationToken = new CancellationToken()) {     IResponse response = Response.FromRequest(request);       foreach (var question in response.Questions)     {         if (question.Type == RecordType.TXT)         {             response.AnswerRecords.Add(new TextResourceRecord(                 question.Name, CharacterString.FromString(DateTime.UtcNow.ToString("O"))));         }         else         {             response.ResponseCode = ResponseCode.Refused;         }     }       return Task.FromResult(response); } }

Попробуйте выполнить сервис на локальной машине и убедитесь – он работает! Его удобно протестировать при помощи таких инструментов как nslookup и dig.

image

Посмотрите README.md проекта DNS – там есть и другие примеры. Будьте осторожны, чтобы случайно не собрать открытый DNS-преобразователь, если исследуете, как обрабатывать вашу собственную DNS.

❯ Пишем конфигурацию сервисного блока

Давайте развернём наш сервис на Linux! Systemd использует файлы конфигурации сервисного блока, в которых определяется, что делает сервис, должен ли он перезапускаться, и всё подобное.

Вам потребуется создать файл .service на той машине с Linux, на которой вы собираетесь зарегистрировать и запускать сервис. В простейшей форме данный сервисный файл выглядит так:

[Unit] Description=DNS Server   [Service] Type=notify ExecStart=/usr/sbin/DnsServer --port=53   [Install] WantedBy=multi-user.target

В разделе [Unit] содержится самая общая информация о нашем приложении. Systemd может выполнять не только сервисы, и [Unit] – это общий раздел для всех типов приложений, которые можно так запускать. Я добавил только описание (Description), но здесь доступны и многие другие опции.

В разделе [Service] определяются детали, касающиеся нашего приложения. Для приложений .NET тип Type будет notify, так, что мы сможем уведомлять Systemd, когда хост начинает или завершает работу. За всё это отвечает пакет Microsoft.Extensions.Hosting.Systemd. В ExecStart определяется путь к двоичному файлу, используемому для запуска нашего сервиса. В показанном далее примере я воспользуюсь самодостаточным .NET-приложением и через аргумент командной строки сообщу ему, какой порт слушать.

Наконец, в разделе [Install] определяется, в каких операционных системах можно будет запускать наш сервис. В данном случае выставлено значение multi-user.target, при котором мы можем запускать сервис всякий раз, когда находимся в многопользовательской среде (то есть, почти всегда). Здесь также можно было бы задать graphical.target, и тогда для запуска этого сервиса у вас должна быть загружена графическая среда.

❯ Создаём самодостаточное .NET-приложение

Сам наш сервис должен быть доступен на машине с Linux, выбранной в качестве платформы, и на ней мы определим, где Systemd может найти его (это делается при помощи ExecStart): /usr/sbin/DnsServer.

Последнее, что нам остаётся сделать – развернуть .NET на целевой машине. Поэтому я решил собрать самодостаточное приложение, представляющее собой единый исполняемый файл – в нём содержится и среда выполнения .NET, и все необходимые зависимости. Для сборки приложения воспользуюсь следующей командой:

dotnet publish -c Release -r linux-x64 --self-contained=true -p:PublishSingleFile=true -p:GenerateRuntimeConfigurationFiles=true -o artifacts

Так создаётся исполняемый файл DnsServer размером примерно 62 МБ (внём содержится всё, что нам нужно от среды выполнения .NET). Скопируйте его в /usr/sbin/DnsServer на машине с Linux и убедитесь, что он поддаётся выполнению (sudo chmod 0755 /usr/sbin/DnsServer).

❯ Установка и запуск сервиса под Linux

Созданный нами файл .service (я назвал его dnsserver.service) должен находиться в каталоге /etc/systemd/system/ на той машине Linux, на которой мы собираемся разворачивать сервис.

Далее выполните следующую команду, чтобы Systemd загрузил этот новый конфигурационный файл:

sudo systemctl daemon-reload

На данном этапе мы уже должны быть в состоянии посмотреть статус нашего DNS-сервера, работающего как сервис:

sudo systemctl status dnsserver.service

image

Упомяну и несколько других команд, которые могут вам пригодиться:

  • Для запуска и остановки сервиса:

sudo systemctl start dnsserver.service sudo systemctl stop dnsserver.service sudo systemctl restart dnsserver.service

  • Чтобы убедиться, что сервис запускается одновременно с той машиной, на которой стоит Linux:

sudo systemctl enable dnsserver.service

Поздравляю, теперь у нас есть приложение .NET, работающее как сервис на Linux!

❯ Что происходит в моём сервисе?

Итак, сервис работает, но ведь ещё и интересно узнать, что он делает. Можно просмотреть самые свежие записи логов, которые выдаёт наше приложение. Для этого воспользуемся следующей командой:

sudo systemctl status dnsserver.service

Итак, мы видим, что сервис работает, можем узнать детали о процессе с заданным id, а также изучить последние записи логов.

image

Если логи вас очень интересуют, то при помощи journalctl можно вывести все логи для блока (-u) – это и есть наш DNS-сервер:

sudo journalctl -u dnsserver.service

В данном случае круто, что благодаря пакету Microsoft.Extensions.Hosting.Systemd мы также получаем здесь цветовые коды, а также данные об уровне серьёзности по каждой записи в логах:

image

❯ Заключение

В этом посте было рассмотрено, как использовать приложение .NET в качестве сервиса Systemd под Linux. Для этого мы воспользовались пакетом Microsoft.Extensions.Hosting.Systemd. Правда, мы только минимально познакомились с темой. Есть ещё множество опций, которые можно сообщить Systemd, ещё можно фильтровать уровни логов при помощи journalctl, и делать многое другое. Рекомендую посмотреть статью Нильса Свимберге, если хотите подробнее изучить эту тему.


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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *