Данная статья является краткой инструкцией по развертыванию ASP.NET Core приложения в Kubernetes с написанием Dockerfile для формирования образа (Docker image) и минимального манифеста для создания деплоймента и объекта, предоставляющего доступ к нему, – в статье будет использован ingress в исполнении nginx.

Используемые инструменты:

  • Microsoft Visual Studio Community 2022 17.4.2

  • Postman v9.14.14

  • Docker Desktop 4.15.0

  • Kubernetes v1.25.2

Описание приложения

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

  • ‘api/’ – возвращает ответ ‘online’, что используется для проверки работоспособности приложения;

  • ‘api/authors’ – возвращает список авторов статьи;

  • ‘api/info’ – возвращает информацию об окружении: переменные, NetBIOS имя сервера, имя пользователя, под которым работает приложение, имя хоста и IP адреса всех интерфейсов хоста.

Исходный код можно найти тут.

Построение образа

Dockerfile к приложению ASP.NET Core можно составлять как вручную, так и автоматически, воспользовавшись встроенным в VS генератором. Для этого при создании проекта ASP.NET Core во вкладке с дополнительным конфигурированием нужно поставить галочку в пункте «Enable Docker» и выбрать используемую докером OS (см. рисунок).

Окно дополнительной конфигурации
Окно дополнительной конфигурации

Очень удобно перекладывать генерацию Dockerfile на VS. Но не стоит всецело полагаться на неё, и лучше понимать, какие именно инструкции генерируются, их аргументы и общую структуру полученного Dockerfile. Это может пригодиться, например, при переходе на более свежую версию .NET – учитывая скорость выхода последних версий (5.0, 6.0, 7.0), такая ситуация вполне реальная.

Сгенерированный Dockerfile для приложения выглядит следующим образом:

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["HabrWebApi/HabrWebApi.csproj", "HabrWebApi/"]
RUN dotnet restore "HabrWebApi/HabrWebApi.csproj"
COPY . .
WORKDIR /src/HabrWebApi
RUN dotnet build "HabrWebApi.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "HabrWebApi.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "HabrWebApi.dll"]

Для только что познакомившихся с Docker пользователей такая структура с множеством инструкций ‘FROM’ может ввести в ступор. Данный файл является приближенным к «боевым» Dockerfile’ам и построен с помощью технологии многоэтапной сборки (см. тут).

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

На первой в нашем Dockerfile стадии «base» подтягивается образ ASP.NET из Microsoft Container Registry (MCR) и открывается порт 80. Этот образ содержит все необходимое для запуска приложения ASP.NET Core. Тег образа указывает на версию .NET.

Далее, вторая стадия «build» основывается на образе sdk, который содержит необходимые инструменты для построения приложения. Именно на этой стадии происходит компиляция приложения, восстановления его зависимостей, генерация необходимых исполняемый файлов.

Следующая стадия «publish» расширяет предыдущую стадию «build», добавляя команду «dotnet publish». В результате получаются файлы приложения, готовые к выполнению в среде ASP.NET.

Финальная стадия «final» основывается на первой стадии и использует результаты стадии «publish», т.е. совмещает среду развертывания ASP.NET и файлы нашего приложения, которые будут исполняться в ней. Финальная строка Dockerfile будет выполнена при старте контейнера, созданного по нашему образу, - она запускает наше приложение.

После создания Dockerfile можно приступать к созданию образа нашего будущего контейнера. Для этого необходимо перейти в папку с файлом решения приложения (.sln), открыть терминал в этот папке и выполнить команду: «docker build -f [путь к Dockerfile] --force-rm --tag [тег] .». Данная команда собирает образ контейнера по указанному Dockerfile, при этом назначает образу указанный тег, удаляет все образы контейнеров, созданные на промежуточных стадиях (опция ‘--force-rm’), из указанного контекста (текущая директория). Построение образа должно быть выполнено без ошибок. Готовый образ можно взять тут.

На данном этапе необходимо проверить работоспособность контейнера, создаваемого по нашему образу. Для этого выполним команду: «docker run -d -P --name [имя контейнера] [тег образа или его id]». Результат выполнения команды должен быть такой:

Как видно, контейнер был успешно создан и запущен. Теперь можно перейти к проверке работоспособности запущенного в нем приложения. Для этого в Postman отправим запросы к каждому методу нашего приложения. Порт, который был привязан к контейнеру можно узнать с помощью команды «docker container ls»:

Отправим запросы к нашему приложению:

  • /api

  • /api/authors

  • /api/info

Развертывание приложения в Kubernetes

Развертывание приложения в Kubernetes начинается с написания манифеста, содержащего описание деплоймента (Deployment). Ключевым плюсом деплоймента является встроенная поддержка нужного количества экземпляров приложения (подов). Также деплоймент отслеживает все изменения в своей конфигурации, например, изменение образа контейнеров в подах. Это дает возможность «путешествовать» по истории изменения деплоймента, и в случае применения фатальных изменений легко вернуться к последней стабильной версии.

Минимальный деплоймент содержит количество реплик (подов/экземпляров приложения) и конфигурацию подов: описание контейнеров (образ, порты, переменные окружения, используемые хранилища (volumes)). В нашем приложении используется один контейнер на под с портом 80 со стандартной для ASP.NET переменной окружения «ASPNETCORE_ENVIRONMENT». Приложение не использует хранилища. Минимальный манифест деплоймента выглядит следующим образом:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: habrwebapi-deployment # Имя деплоймента
spec:
  replicas: 2 # Количество реплик
  selector:
    matchLabels: # Определение меток и их значений, по которым будет определяться принадлежность пода деплойменту
      app: habrwebapi-pod
  template: # Описание конфигурации подов
    metadata:
      labels:
        app: habrwebapi-pod # Специальная метка для подов, по которой определяется их принадлежность данному деплойменту
    spec:
      containers: # Описание контейнеров, создаваемых в поде
      - name: habrwepapi-container # Имя контейнера внутри пода
        image: hrenaki/habrwebapi-image:1.0 # Название образа контейнера с указанием репозитория
        ports: # Описание портов контейнера
        - name: pod-port
          containerPort: 80
        env: # Перечисление переменных окружения и их значений
        - name: "ASPNETCORE_ENVIRONMENT"
          value: "Kubernetes"

Проверить состояние созданного деплоймента можно вызвав команду «kubectl get all». Результат выполнения:

Как видно, был создан деплоймент, replicaset и два пода со статусом «Running». Это означает, что наше приложение развернуто и готово к работе. Осталось создать объект, который будет предоставлять доступ к нашему приложению. В данной статье, следуя best practice, для этой цели будет использован ingress.

Для осуществления работы ingress необходимо создать сервис, который будет направлять трафик, приходящий из ingress, на поды деплоймента. Создадим манифест с сервисом:

apiVersion: v1
kind: Service
metadata:
  name: habrwebapi-service
spec:
  selector:
    app: habrwebapi-pod
  ports:
  - targetPort: pod-port
    port: 80

Теперь можно переходить к написанию манифеста ingress. Ingress управляет маршрутизацией трафика, приходящего извне, к сервисам, находящимся внутри кластера Kubernetes. В нашем кластере всего один сервис, поэтому направим весь трафик, который приходит по пути «/» на него. В результате манифест для ingress выглядит следующим образом:

apiVersion: networking.k8s.io/v1
kind: Ingress 
metadata:
  name: webapp-ingress
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name:  habrwebapi-service
            port:
              number: 80

Перед тем, как создавать ingress объекты, необходимо в кластер установить nginx контроллер, выполнив команду:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.5.1/deploy/static/provider/cloud/deploy.yaml

После создания всех объектов, ingress доступен по адресу localhost. Сделаем запрос к нашему приложению, используя адрес «http://localhost/api/info»:

Итоги

В статье был изложен путь быстрого развертывания примитивного ASP.NET Core приложения. Рассмотрены минимальные версии образа и манифестов k8s. Вышеперечисленный материал может служить основой для развертывания более серьезных приложений.

Комментарии (13)


  1. codecity
    07.01.2023 11:56
    +2

    Планируете ли продолжать? Для минимальной полезности нужно масштабирование (а иначе зачем кубер) и желательно HTTPS (куда же без него).


    1. shachneff
      07.01.2023 14:32
      +3

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


      1. codecity
        07.01.2023 16:04
        +1

        Хранилище - уже выходит за рамки Kuber.


      1. alexs0ff
        07.01.2023 17:45

        Кубер это обычно микросервисы, а там у каждого своя база.


        1. shachneff
          07.01.2023 18:23
          +1

          У каждого инстанса микросервиса? Или у каждого микросервиса?


          1. alexs0ff
            07.01.2023 20:31
            -1

            а причем тут это? Я имел ввиду, что база данных в микросервисной архитектуре, далеко не узкое место.


            1. mayorovp
              08.01.2023 09:46

              Притом, что базу данных разные инстансы микросервисов обычно все же разделяют — а потому она легко становится тем самым узким местом.


              1. alexs0ff
                08.01.2023 11:31
                -2

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

                И в вашей синтенции ошибка - "разные инстансы микросервисов", а нужно писать "разные инстансы микросервиса". База данных должна принадлежать, только одному микросервису, иначе это грубое нарушение проектирование такой архитектуры.


                1. mayorovp
                  08.01.2023 11:50

                  Ну да, поправка принимается. Я, конечно же, говорил про разные инстансы микросервиса.


                  А вот с тем, что узкое место каждой части не становится узким местом системы, я не согласен. Микросервис делается не просто так, а для решения некоторых задач системы. Если система может справиться с задачей без него — на кой он вообще нужен? Поэтому, когда он не справляется со своей задачей — с этой задачей не справляется и система в целом.


          1. Eujene
            09.01.2023 14:57

            У каждого микросервиса.

            Наверное, можно и для каждого инстанса. Но это же создаёт проблему синхронизации данных между множеством инстанцов. Как-то сложно


      1. s207883
        07.01.2023 18:31
        +1

        Базу тоже можно размазать репликацией по нескольким серверам. Тогда производительность повышается.


      1. stitrace
        08.01.2023 07:23

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


  1. dabrahabra
    08.01.2023 01:22

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