Share:

Развертывание ASP.NET Core приложений на Ubuntu Linux

YPermitinв.NET

2024-03-12

#.NET

#ASP.NET Core

#*.nix

#Linux

#Ubuntu

#публикация

#развертывание

#Apache

Последовательность действий для развертывания ASP.NET Core сервиса в Ubuntu Linux. От создания и адаптации приложения, до его развертывания и запуска. Рассматриваются вопросы регистрации репозиториев Microsoft, установка Apache и .NET, реверс-прокси и некоторая диагностика.

Содержание

Мы начинаем

Минули те времена, когда платформа .NET (в те времена .NET Framework) фактически была без поддержки кроссплатформенности, и вся ее жизнь вертелась вокруг экосистемы Microsoft Windows. Начиная с 2016 года, компания Microsoft выпустила .NET Core, новую ветку развития экосистемы .NET с поддержкой кроссплатформенной разработки и развертывания приложений различных видов. На начало 2024 года .NET Core уже эволюционировал в .NET 9 с огромным количеством улучшений, расширений функционала и прочим.

С тех пор мы получили более обширные возможности в части создания и построения приложений. В том числе, теперь мы можем создавать веб-службы на базе ASP.NET Core, фактически API с поддержкой архитектуры REST, полностью на кроссплатформенных рельсах.

Но сегодня мы не будем делать обзор на все возможности платформы .NET, а лишь рассмотрим шаги по развертыванию готовых приложений ASP.NET Core Web API в *.nix-системах на примере Ubuntu Linux. В официальной документации есть исчерпывающая информация по этой теме, однако, мы попытаемся представить более наглядный, сквозной пример, начиная от установки веб-сервера, настройки репозиториев пакетов, установки окружения .NET и добавления демона для работы сервиса.

Для более подробного изучения материала Вы также можете обратиться к следующим ссылкам:

Итак, вступление законечно. Поехали!

Сервис готов к запуску

В рамках публикации мы создадим минималистичный API для развертывания, но никто не мешает Вам использовать свой какой-либо проект в качестве подопытного при публикации.

В случае, если Вам нужно создать какое-то минимальное API для развертывания, то можно воспользоваться отличным руководством Руководство. Создание минимального API с помощью ASP.NET Core, либо воспользоваться простой командой в консоли:

dotnet new webapi -n SimpleApiToDeploy
Создание проекта API

SimpleApiToDeploy - имя проекта проекта API, которое мы будем использовать в дальнейшем. Переходим непосредственно в каталог проекта и проверяем содержимое.

cd SimpleApiToDeploy
ls -lt

Мы должны увидеть несколько файлов / каталогов. Что-то вроде этого.

Содержимое простейшего проекта

Вся логика работы этого минималистичного проекта API содержится в файле Program.cs:

var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Фактически API имеет единственный метод GET по адресу /weatherforecast, который возвращает случайную информацию о погоде в 5 городах из списка. Нам этот проект подходит для демонстрации.

Соберем проект и запустим его.

dotnet restore
dotnet run

Сервис будет запущен на случайном порту, который будет указан в логах в самом начале при старте приложения. В нашем случае это порт 5297.

Проверочный запуск минимального API

При обращении к сервису по следующему URL (http://localhost:5297/weatherforecast) мы получим ожидаемый результат (ну или почти ожидаемый, ведь он формируется случайными образом).

[
{
"date": "2024-03-10",
"temperatureC": 37,
"summary": "Hot",
"temperatureF": 98
},
{
"date": "2024-03-11",
"temperatureC": 25,
"summary": "Sweltering",
"temperatureF": 76
},
{
"date": "2024-03-12",
"temperatureC": 54,
"summary": "Warm",
"temperatureF": 129
},
{
"date": "2024-03-13",
"temperatureC": 2,
"summary": "Hot",
"temperatureF": 35
},
{
"date": "2024-03-14",
"temperatureC": -11,
"summary": "Balmy",
"temperatureF": 13
}
]

Итого, у нас есть готовый API-сервис для запуска на Linux! На самом деле подойдет и любой другой сервис на современных версиях ASP.NET Core, поэтому дальнейшие инструкции по развертыванию будут в некотором смысле универсальными. Напоследок отметим, что весь пример будет выполняться в следующем окружении:

  • .NET 8
  • Веб-сервер Apache
  • Ubuntu Server 22.04
Однако инструкция актуальна и для более ранних версий .NET, веб-сервера Apache и Ubuntu Server.

Подготовка проекта

У нас есть документация и некоторый проект для публикации. Но на самом деле проект еще не готов! Да, он запустится в окружении разработчика, но если мы его опубликуем в таком виде как есть, то заставить его работать не получится. Сервис просто не будет обрабатывать наши запросы при использовании веб-сервера Apache (или Nginx) в качестве реверс-прокси.

Все дело в том, что при запуске API-сервиса на базе ASP.NET Core в *.nix-системах (да в целом и в других тоже), платформа .NET использует встроенный высокопроизводительный веб-сервер Kestrel, который может обособленно работать. Но обычно Kestrel не используется явно для публикации сервиса. Вместо этого используется конфигурация реверс-прокси, когда запросы извне сначала поступают на внешний веб-сервер (Apache, Nginx), а уже далее переадресуются непосредственно на веб-сервер Kestrel и сам сервис для обработки запроса.

Причины, по которым чаще всего используется схема с реверс-прокси:

  • Без реверс-прокси, совместное использование одного и того же IP-адреса и порта несколькими процессами не поддерживается.
  • Реверс-прокси дает возможность ограничивать общедоступную область приложений.
  • Обеспечивает дополнительный уровень конфигурации и защиты.
  • Значительно упрощает настройку балансировщика нагрузки.
  • И многое другое.

Более подробную информацию об использовании Kestrel в конфигурации реверс-прокси можно посмотреть в официальной документации.

Мы не будем исключением и также будем использовать схему реверс-прокси при развертывании. В нашем случае в качестве веб-сервера будет Apache. Но прежде чем перейти к настройкам веб-сервера нам необходимо адаптировать наше решение, чтобы оно могло корректно обрабатывать запросы, присланные через ревеверс-прокси.

Модифицируем файл Program.cs, добавив поддержку обработки пересылаемых заголовков через реверс-прокси. Новые строки выделены через !!!.

using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
// !!! Конфигурируем обработку пересылаемых заголовков запросов
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
// !!! Конфигурируем обработку пересылаемых заголовков запросов
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// !!! Добавляем в конвеер обработки HTTP-запроса компонент работы с пересылаемыми заголовками
app.UseForwardedHeaders();
// !!! Добавляем в конвеер обработки HTTP-запроса компонент работы с пересылаемыми заголовками
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Фактически, сначала мы добавили конфигурацию обработки пересылаемых заголовков.

// ...
// !!! Конфигурируем обработку пересылаемых заголовков запросов
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
// !!! Конфигурируем обработку пересылаемых заголовков запросов
// ...

А после добавили использование компонента с указанными выше настройками в конвеер обработки HTTP-запросов.

// ...
// !!! Добавляем в конвеер обработки HTTP-запроса компонент работы с пересылаемыми заголовками
app.UseForwardedHeaders();
// !!! Добавляем в конвеер обработки HTTP-запроса компонент работы с пересылаемыми заголовками
// ...

Не забываем добавлять все необходимые пространства имен, но это Вам уже подскажет IDE. Более подробную информацию о настройке реверс-прокси Вы можете узнать в официальной документации, в том числе о работе с конфигурацией IIS, HTTP.sys, Kestrel + IIS, Nginx и др.

После проделанных изменений проект готов к сборке и публикации. Продолжим мы с установки и настройки веб-сервера.

Установка веб-сервера

Как уже говорилось ранее, для реверс-прокси будем использовать Apache. Подробную информацию Вы можете посмотреть в официальной документации.

Подключимся к нашему серверу и установим все необходимые пакеты. В нашем примере это Ubuntu Server 22.04, но в целом подход будет одинаковый практически для всех *.nix-дистрибутивов.

Ниже команда для установки как самого веб-сервера Apache, так и модуля mod_proxy для него с другими необходимыми компонентами.

sudo apt install apache2
sudo a2enmod rewrite
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod headers
sudo a2enmod ssl
sudo systemctl restart apache2

На этом базовая установка Apache для публикации нашего приложения завершена. Позже мы еще вернемся к настройкам публикации, но это после настройки .NET-окружения.

Подготовка среды .NET

Для работы нашего приложения нужно, чтобы на сервере были установлены компоненты платформы .NET. Выше мы уже упоминали, что приложение использует .NET 8. Эту версию и установим в два шага. На первом - настроим репозитории Microsoft, чтобы устанавливать пакеты последних версий из официального репозитория. На втором - установим уже непосредственно пакеты нужных версий.

Настройка репозитория Microsoft

Многие дистрибутивы уже имеют в репозиториях пакеты платформы .NET, но они могут быть устаревших версий и с задержкой получать обновления. Поэтому мне более предпочтительно использовать официальный репозиторий Microsoft, т.к. кроме .NET из него получаю и другие пакеты (PowerShell, например).

Добавить официальный репозиторий пакетов Microsoft можно по следующей инструкции. Для нашей ситуации на Ubuntu 22.04 регистрация нового источника пакетов будет выглядеть следующим образом.

# Определяем версию Ubuntu Server
declare repo_version=$(if command -v lsb_release &> /dev/null; then lsb_release -r -s; else grep -oP '(?<=^VERSION_ID=).+' /etc/os-release | tr -d '"'; fi)
# Загружаем ключ и пакет для регистрации репозитория Microsoft
wget https://packages.microsoft.com/config/ubuntu/$repo_version/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
# Устанавливаем
sudo dpkg -i packages-microsoft-prod.deb
# Удаляем загруженный пакет, т.к. он уже больше не понадобится
rm packages-microsoft-prod.deb
# Обновляем список доступных пакетов с учетом нового источника
sudo apt update

Но радоваться рано, т.к. пакеты в репозитории Microsoft и в репозитории дистрибутива могут быть одни и те же, то часто можно встретиться с конфликтами при установке и обновлении пакетов. Чтобы избежать подобных проблем, необходимо настроить приоритеты источников пакетов. Выполним для этого следующие команды, создающие файлы настроек приоритетов для пакетов dotnet* и aspnet*.

sudo sh -c "cat > /etc/apt/preferences.d/dotnet <<'EOF'
Package: dotnet*
Pin: origin packages.microsoft.com
Pin-Priority: 1001
EOF"
sudo sh -c "cat > /etc/apt/preferences.d/aspnet <<'EOF'
Package: aspnet*
Pin: origin packages.microsoft.com
Pin-Priority: 1001
EOF"

На этом регистрация репозитория пакетов Microsoft завершена, можно переходить к установке .NET 8.

Установка пакетов

Для работы нашего приложения достаточно установить пакет aspnetcore-runtime-8.0, в котором содержится .NET Runtime 8и часть платформы ASP.NET Core для работы веб-приложений.

Не будем ходить вокруг да около, установим нужный пакет.

sudo apt install aspnetcore-runtime-8.0

После завершения процесса скачивания и установки пакетов, мы можем проверить корректность установки. Выполним команду получения списка установленных версий .NET.

dotnet --list-runtimes

В результате мы получим список установленных пакетов, который будет выглядеть примерно так.

Microsoft.AspNetCore.App 8.0.2 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 8.0.2 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

Теперь все готово для публикации непосредственно приложения и дальнейшей настройки веб-сервера.

Подробнее про установку .NET на различные дистрибутивы *.nix и в разных вариантах можно узнать в официальной документации.

Публикация приложения

Публикация нашего сервиса будет в три этапа. Сначала мы разместим файлы приложения. После этого настроим демона, чтобы приложение работала в фоне и им было удобно управлять. И на последнем этапе настроим Apache для работы в качестве реверс-прокси, о чем уже говорилось ранее. Все подготовительные работы на данный момент уже выполнены, поэтому проблем быть не должно.

.NET-приложение

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

dotnet publish -c Release

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

./SimpleApiToDeploy/bin/Release/net8.0/publish

Нам нужно упаковать эти файлы и отправить на сервер для публикации. Сделаем это через SFTP, но Вы можете сделать любым другим удобным способом.

# Помещаем каталог с файлами публикации в архив
zip -r publish.zip publish
# Подключаемся к серверу по SFTP (в нашем случае это srv-dev-02, он же 192.168.88.41)
sftp srv-dev-02.yy.corp
# Отправляем архив на сервер для развертывания
# sftp>
put publish.zip
# Выходим из сессии SFTP
exit

При этом передача архива с файлами публикации выглядит так.

Передача файлов по SFTP

Теперь архив с файлами приложения на конечном сервере, можно продолжать настройку. Создадим каталог api по пути /var/www/api и дадим на каталог права для пользователя www-data.

# Создаем каталог для файлов приложения сервиса
sudo mkdir /var/www/api
# Установим unzip для распаковки архива, если не был установлен ранее
sudo apt-get install unzip
# Распаковываем архив с файлами приложения
unzip publish.zip
# Переносим файлы приложения в каталог приложения
sudo cp /home/ypermitin/publish/* /var/www/api
# Настраиваем права доступа для возможности веб-сервера работать с ними
sudo chown -R www-data:www-data /var/www/api

На данном этапе мы можем вручную запустить приложение для проверки. Выполняем команду.

dotnet SimpleApiToDeploy.dll
Ручной запуск сервиса на сервере

Конечно, сервис после запуска недоступен извне, но он уже запускается и даже работает. Для теста можно отправить запрос непосредственно с сервера, из другой SSH-сессии.

curl http://localhost:5000/weatherforecast

В ответ мы также получим данные метода /weatherforecast (пример JSON смотрите выше), значит, все работает штатно. Ура!

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

Настройка демона *.nix

Для запуска нашего сервиса в виде демона используем systemd, который позволит управлять процессом сервиса (запуск, остановка и перезапуск и др.).

Первым делом создадим файл конфигурации демона.

sudo nano /etc/systemd/system/api.service

Содержимое файла настроек следующее.

[Unit]
# Описание
Description=SimpleApiToDeploy
[Service]
# Каталог с файлами приложения
WorkingDirectory=/var/www/api/
# Команда запуска процесса
ExecStart=/usr/bin/dotnet /var/www/api/SimpleApiToDeploy.dll
# Перезапускать всегда
Restart=always
# При аварийном завершении процесса перезапускаем его через 10 секунд
RestartSec=10
KillSignal=SIGINT
# Идентификатор
SyslogIdentifier=api
# Пользователь для запуска
User=www-data
# Имя окружения для приложения
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target

В файле конфигурации даны комментарии на каждый элемент.

Стандартно Kestrel запускается и слушает порт 5000 и только с localhost (то есть непосредственно с самого сервера). Это поведение можно изменить, поменяв порт или открыв прослушивание вне сервера. Изменить настройку можно двумя способами.

  • В файл appsettings.json нужно добавить секцию с настройками точек подключения для Kestrel.

    "Kestrel": {
    "Endpoints": {
    "Http": {
    "Url": "http://localhost:5000"
    }
    }
    }

    Это настройка по умолчанию. Если необходимо изменить порт, например, на 5555 и при этом разрешить подключение извне, то настройка будет выглядеть следующим образом.

    "Kestrel": {
    "Endpoints": {
    "Http": {
    "Url": "http://*:5555"
    }
    }
    }

  • Альтернативный способ - это прописать настройки в параметрах запуска приложения. Например, если само приложение мы запускаем такой командой.

    dotnet SimpleApiToDeploy.dll

    То добавив соответствующие параметры запуска мы получим тот же результат, что и добавление конфигурации в appsettings.json.

    dotnet SimpleApiToDeploy.dll --urls "http://*:5555"

По правде говоря, существует гораздо больше способов настройки Kestrel с различными опциями, но рассматривать тут это мы не будем. Вместо этого Вы можете обратиться к официальной документации.

Тем более что в нашем примере нет никакой необходимости изменять настройки точек подключения Kestrel - мы оставим параметры по умолчанию. Приложение будет прослушивать порт 5000 и только внутри сервера. Веб-сервер Apache, выступающий реверс-прокси, будет переадресовать весь трафик по этим настройкам подключения.

Итак, приложение мы опубликовали, добавили файл настроек демона systemd. Осталось включить его и запустить, после чего проверить состояние.

# Включаем
sudo systemctl enable api.service
# Запускаем
sudo systemctl start api.service
# Проверяем
sudo systemctl status api.service
Проверка работы демона systemd

Теперь управление состоянием сервиса проще простого, systemd сделает всю работу за нас. Остался последний вопрос - это открыть доступ к сервису извне, чтобы он мог принимать запросы не только непосредственно с сервера, но и с других машин в сети. То есть остается настроить реверс-прокси.

Реверс-прокси

Настроить реверс-прокси не так уж и сложно. Может потребоваться время для изучения документации веб-сервера, который Вы используете, но в целом процесс и логика настройки везде одинаковая. В официальной документации этот процесс достаточно подробно расписан.

Мы же сейчас пройдемся по основным шагам настройки. Выше мы уже установили сам веб-сервер Apache и все необходимые модули для поддержки режима работы как реверс-прокси. Осталось добавить настройки сайта.

Создадим файл настроек сайта.

sudo nano /etc/apache2/sites-available/api.conf

Содержимое файла настроек будет следующее.

<VirtualHost *:80>
ProxyPreserveHost On
ProxyPass / http://localhost:5000/
ProxyPassReverse / http://localhost:5000/
ServerName api
ServerAlias *.api
ErrorLog ${APACHE_LOG_DIR}api-error.log
CustomLog ${APACHE_LOG_DIR}api-access.log common
</VirtualHost>

Суть настройки в том, что запросы, поступающие на сервер и порт 80 (стандартный порт для HTTP) будут переадресованы на наш сервис, который прослушивает порт 5000. Также в файле настроек определены файлы логов для сайта и имена, которые в нашем примере не играют никакой роли.

После сделанных настроек включаем новый сайт.

sudo a2ensite api
sudo systemctl reload apache2

Т.к. у нас простейший пример, и мы не привязали DNS-имя к сайту, то для работы нужно отключить сайт по умолчанию для Apache, чтобы только наша добавленная конфигурация прослушивала 80-й порт.

sudo a2dissite 000-default.conf
service apache2 reload

Теперь сервис доступен извне. На скриншоте ниже мы обращаемся напрямую через IP-адрес и порт 80 (по умолчанию, поэтому явно не задается).

Сервис доступен из вне

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

Фактически, мы уже прошли весь путь по подготовке и настройке сервиса, реверс-прокси для него и публикации на сервере. Поздравляю!

Проверка работы

На самом деле саму работу мы уже проверили, перейдя по ссылке метода /weatherforecast выше на скриншоте. Но мы можем проверить работу и более технологичным способом :). Сделаем несколько таких же запросов, но через Postman, ведь именно его чаще всего используют для отладки работы различных API.

Оно живое! Оно работает!

Это еще не конец

На этом наш путь по подготовке и публикации API-сервиса на базе ASP.NET Core WebAPI закончен. Мы настроили Ubuntu Server 22.04 для установки пакетов Microsoft и установили .NET 8 для запуска приложения, установили веб-сервер Apache и настроили реверс прокси.

На самом деле на рабочем окружении редко приходится все это проделывать вручную, т.к. приложение либо публикуется в контейнере (где все подобное уже настроено через yaml-файл Docker Compose), либо публикация автоматизирована через, например, Jenkins. Но иногда и ручной процесс применяется, плюс он важен для понимания происходящего.

Ниже оставлю полезные ссылки по теме для более детального изучения.

Удачи в делах и успехов!

Полезные ссылки

Y

YPermitin

.NET, TSQL, DevOps, 1C:Enterprise

Developer, just developer.

Поделиться

Другие статьи

Расширение для SQL Server. Быстро и просто. SQLCLR снова в деле
Расширение для SQL Server. Быстро и просто. SQLCLR снова в деле
Решение проблем с модулями VMware в Ubuntu 22.04
Решение проблем с модулями VMware в Ubuntu 22.04
Берем процессы под контроль в .NET
Берем процессы под контроль в .NET

Все статьи от автора: YPermitin

Copyright © 2024 Убежище инженера