От исходного кода к контейнеру

.NET + Angular - от исходного кода к контейнеру

Как обычно, сначала определим цель: чего нужно достичь?

  • Реализовать, развернуть и запустить SPA SSR и API в контейнере в Azure.

Чтобы достичь цели, нужно пройти несколько шагов:

  • Реализовать приложение.
  • Собрать контейнер.
  • Создать Azure Container Registry.
  • Отправить приложение в GitHub.
  • Настроить GitHub workflow для сборки и тестов.
  • Настроить и запустить приложение.

Реализация:

Здесь есть две основные возможности:

  • Использовать шаблон, где .NET API и Angular уже настроены вместе и предоставляются как единое решение.
  • Создать два разных приложения.

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

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

Детали реализации самого приложения выходят за рамки этой статьи и могут быть рассмотрены в будущих публикациях. Здесь мы сосредоточимся на настройке окружения и организации процесса развертывания.

Сборка контейнера

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

Для этого можно начать со стандартного образа .NET SDK и расширить его, установив Node.js.

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

# Install Node.js
RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl gnupg \
    && mkdir -p /etc/apt/keyrings \
    && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
    && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
    && apt-get update \
    && apt-get install -y --no-install-recommends nodejs \
    && rm -rf /var/lib/apt/lists/*

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

WORKDIR /src

#
# Copy projects
#
COPY api/ ./api/
COPY ui/ ./ui/

После этого соберем каждое приложение отдельно.

# ---------- Angular ----------
WORKDIR /src/ui
RUN npm ci --legacy-peer-deps
RUN npm run build
RUN ls -la /src/ui/dist
RUN find /src/ui/dist -maxdepth 3 -type f | head -20

# ---------- .NET ----------
WORKDIR /src/api
RUN dotnet restore "app/app.csproj"
RUN dotnet build "app/app.csproj" -c Release -o /app/build --no-restore

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

Наконец, скопируем результат сборки Angular в папку wwwroot приложения ASP.NET, чтобы обе части были упакованы в единый артефакт развертывания.

# Copy Angular to wwwroot
RUN mkdir -p /app/publish/wwwroot
RUN cp -R /src/ui/dist/*/browser/* /app/publish/wwwroot/ \
    && cp /app/publish/wwwroot/index.csr.html /app/publish/wwwroot/index.html

Финальный этап - создание runtime-образа.

FROM base AS final
WORKDIR /app
EXPOSE 80
EXPOSE 443
ENV ASPNETCORE_ENVIRONMENT=Production
COPY --from=publish /app/publish .

ENTRYPOINT ["dotnet", "app.dll"]

Создание образа и отправка в Azure Container Registry

Сборка Docker-образа локально и отправка его в Azure Container Registry (ACR) после каждого изменения - вполне рабочий подход во время экспериментов. Но со временем это быстро превращается в повторяющуюся ручную задачу. Если что-то можно автоматизировать, это стоит сделать.

GitHub Actions workflow выполняет следующие шаги:

  • Checkout API-репозитория в директорию api.
  • Checkout UI-репозитория в директорию ui.
  • Сборка .NET API.
  • Запуск всех backend-тестов и генерация отчетов code coverage.
  • Сборка Angular-приложения.
  • Запуск всех frontend-тестов и генерация отчетов code coverage.
  • Аутентификация в Azure.
  • Аутентификация в Azure Container Registry.
  • Сборка Docker-образа.
  • Отправка образа в Azure Container Registry.

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

Настройка и запуск приложения

Для этого проекта в качестве платформы хостинга был выбран Azure Container Apps по нескольким причинам:

  • Он специально предназначен для запуска контейнеризированных приложений, включая масштабирование и настройку окружения.
  • Он поддерживает ревизии приложения, что делает развертывания безопаснее.
  • Он хорошо интегрируется с Azure Container Registry, Log Analytics, Azure Storage, Azure Key Vault и другими сервисами Azure.
  • Метрики приложения и Grafana dashboards доступны из коробки.

Начальная конфигурация состоит из нескольких шагов:

  1. Ingress. Включить ingress, настроить целевой порт, выбрать HTTP как тип ingress и разрешить внешний трафик, если не требуется другая сетевая конфигурация.
  2. Containers.
    1. Properties. Настроить Docker-образ, который будет использоваться.
    2. Environment variables. Определить необходимые значения конфигурации. При желании они могут заполняться напрямую из Azure secrets.
    3. Health probes. Настроить Startup, Readiness и Liveness probes, указав endpoint, порт и протокол. Это критически важный шаг: если probes завершаются ошибкой, Azure Container Apps считает контейнер unhealthy и автоматически перезапускает или отключает его.
    4. Volume mounts. Подключить Azure Storage volumes, если приложению требуется постоянное файловое хранилище.

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

Однако Azure Container Apps не определяет автоматически, что в Azure Container Registry был отправлен более новый образ с тем же тегом, например latest.

Простой workaround - создать переменную окружения, например FORCE_REDEPLOY, и использовать в качестве значения текущую дату или timestamp. Каждый раз, когда это значение меняется и конфигурация сохраняется, Azure создает новую ревизию, загружает последний Docker-образ и запускает новый контейнер без дополнительных изменений конфигурации.

Итоги

Описанных выше шагов оказалось достаточно, чтобы собрать приложение из нескольких репозиториев, упаковать его в один Docker-образ, опубликовать в Azure Container Registry, развернуть в Azure Container Apps и в итоге получить стабильный, повторяемый и полностью автоматизированный deployment pipeline.

Хотя каждый отдельный шаг относительно прост, их объединение в полноценный CI/CD-процесс значительно упрощает будущую разработку. После начальной настройки каждый commit может автоматически создавать протестированный Docker-образ, готовый к развертыванию с минимальными усилиями.