CI/CD: Azure Pipelines
Настраиваем конвейер, который сам собирает код после каждого коммита, прогоняет тесты и выкатывает приложение в App Service — без единого клика мышью.
CI/CD — непрерывная интеграция (каждый коммит автоматически собирается и тестируется) и непрерывная доставка (прошедшая проверки сборка автоматически едет в облако).
Зачем это нужно на практике
Ручной деплой выглядит безобидно: собрал проект, заархивировал, зашёл в портал, загрузил. Пять минут. Проблема в том, что эти пять минут повторяются двадцать раз в неделю, и рано или поздно кто-то соберёт не ту ветку, забудет прогнать тесты или зальёт билд с локальной машины, где лежит недокоммиченный «временный фикс».
Пайплайн убирает человека из петли. Он всегда собирает ровно то, что лежит в репозитории, всегда одинаково, в чистом окружении. Плюс появляется то, чего при ручном деплое нет в принципе: прослеживаемость. Про любую версию в проде можно сказать, из какого коммита она собрана, кто её одобрил и когда уехала.
В Azure DevOps весь пайплайн — это YAML-файл azure-pipelines.yml в самом репозитории. Значит, пайплайн тоже код: живёт рядом с приложением, меняется через pull request, откатывается вместе с ним.
Пайплайн сборки и деплоя целиком
Разберём рабочий пример: .NET-приложение, которое собирается и уезжает в Azure App Service.
trigger:
branches:
include:
- main
variables:
buildConfiguration: 'Release'
webAppName: 'shop-prod-web'
stages:
- stage: Build
displayName: 'Сборка и тесты'
jobs:
- job: build
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
version: '8.0.x'
- script: dotnet restore
displayName: 'Восстановление пакетов'
- script: dotnet build --configuration $(buildConfiguration) --no-restore
displayName: 'Сборка'
- script: dotnet test --configuration $(buildConfiguration) --logger trx
displayName: 'Тесты'
- script: dotnet publish -c $(buildConfiguration) -o $(Build.ArtifactStagingDirectory)
displayName: 'Публикация'
- publish: $(Build.ArtifactStagingDirectory)
artifact: drop
- stage: Deploy
displayName: 'Деплой в App Service'
dependsOn: Build
condition: succeeded()
jobs:
- deployment: deployWeb
environment: 'production'
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
azureSubscription: 'sc-azure-prod'
appType: 'webAppLinux'
appName: $(webAppName)
package: '$(Pipeline.Workspace)/drop/*.zip'
deployToSlotOrASE: true
slotName: 'staging'
Построчный разбор
trigger — когда запускать. Здесь: на каждый push в main. Если убрать этот блок, Azure DevOps по умолчанию запускает пайплайн на все ветки — и вы удивитесь счёту за минуты агента.
variables — обычные переменные, подставляются синтаксисом $(имя).
stages — крупные фазы. Разделение на Build и Deploy — не украшательство: между стадиями можно вставить ручное подтверждение, а одну и ту же сборку переиспользовать для нескольких сред. Собрали один раз — задеплоили в test, потом ровно тот же артефакт в prod. Пересобирать под каждую среду — верный способ выкатить в прод не то, что тестировали.
pool: vmImage: 'ubuntu-latest' — на какой машине выполнять. Это агент, о нём ниже.
steps — шаги. script — просто команда в шелле агента. task — готовое действие из каталога (установить .NET, задеплоить в App Service, залогиниться в реестр). Записи вида UseDotNet@2 читаются как «таск UseDotNet версии 2».
publish кладёт результат сборки в артефакт — хранилище, переживающее агента. В стадии Deploy job типа deployment скачивает артефакт автоматически, поэтому путь и выглядит как $(Pipeline.Workspace)/drop/.
environment: 'production' — не просто ярлык. Это объект в Azure DevOps: к нему привязывается история деплоев и правила — например, approval check, из-за которого стадия встанет и будет ждать, пока тимлид нажмёт «Approve».
slotName: 'staging' — деплой не сразу в прод, а в слот: копию приложения с отдельным URL. Прогреваем, проверяем, а потом делаем swap — Azure меняет слоты местами мгновенно, без даунтайма, и с возможностью так же мгновенно откатиться.
Агенты: где вообще всё это выполняется
Пайплайн выполняется не «в облаке вообще», а на конкретной виртуалке — агенте.
| Тип | Плюсы | Минусы и цена |
Microsoft-hosted (vmImage: ubuntu-latest) | чистая машина на каждый запуск, ничего не настраивать, предустановлены .NET, Node, Docker, Python | приватным проектам бесплатно ограниченное число минут (порядка 1800 в месяц на одном параллельном job); дальше параллельные job'ы покупаются помесячно. Публичным open-source проектам — бесплатно и щедро |
| Self-hosted | свои зависимости, доступ во внутреннюю сеть, кэш между запусками, нет лимита минут | вы сами обновляете и чините машину, а виртуалка тикает по счётчику 24/7, даже когда пайплайн не запускался |
Ключевое свойство hosted-агента: он одноразовый. Каждый запуск получает свежую машину, а после — умирает. Ничего не «остаётся с прошлого раза»: ни кэш npm, ни установленные глобально пакеты, ни файлы. Отсюда и главный источник недоумения новичков — «локально работает, в пайплайне нет»: локально у вас накопилось окружение, а у агента его нет.
Секреты в пайплайне
Правило номер один: никаких паролей в YAML. Он лежит в git и виден всем, у кого есть доступ к репозиторию.
Правильных способов два. Первый — service connection: объект в настройках проекта, через который пайплайн ходит в Azure (в примере это azureSubscription: 'sc-azure-prod'). Никаких ключей в коде; современный вариант — workload identity federation, где долгоживущего секрета нет вообще.
Второй — variable group, связанная с Azure Key Vault: секреты лежат в хранилище, пайплайн подтягивает их по имени.
variables:
- group: shop-prod-secrets # связана с Key Vault
steps:
- script: |
./migrate.sh
displayName: 'Миграции БД'
env:
DB_PASSWORD: $(dbPassword) # секрет пробрасываем ЯВНО через env
Обратите внимание на блок env. Обычные переменные Azure Pipelines сам кладёт в окружение процесса, а секретные — нет: это защита от того, чтобы секрет случайно утёк в дочерний процесс или в лог. Пробрасывать их нужно руками, ровно туда, где они нужны. Это не баг, а фича, но именно на ней спотыкается каждый второй.
Как это работает
При push в main Azure DevOps ловит событие, читает azure-pipelines.yml из того самого коммита и ставит запуск в очередь. Свободный агент забирает задание, клонирует репозиторий в чистую рабочую папку и выполняет шаги по порядку. Каждый шаг — отдельный процесс; ненулевой код возврата валит job (поэтому dotnet test с падающим тестом останавливает конвейер сам, без дополнительных настроек).
Результат сборки уезжает в хранилище артефактов, агент уничтожается. Стадия Deploy ждёт успеха Build (dependsOn + condition), при необходимости упирается в approval-чек и только потом берёт новый агент, скачивает артефакт и через service connection дёргает API Azure — по сути тот же ARM, что и в прошлом уроке.
Важная деталь про логи: Azure Pipelines маскирует значения секретных переменных в выводе, заменяя их на ***. Но маскирование посимвольное и наивное — если вы закодируете секрет в base64 или разрежете его пополам, он утечёт в лог как есть. Не печатайте секреты. Никогда.
Частые ошибки
- Забыли
triggerили указали слишком широкий. Пайплайн начинает запускаться на каждую ветку и каждый коммит, бесплатные минуты сгорают за неделю, а после — вы платите. Ограничивайте ветки и пути (paths: exclude: ['docs/*']). - Self-hosted агент на постоянно включённой VM. Пайплайн работает 10 минут в день, виртуалка — 24 часа. Это чистые деньги в никуда: либо scale set с автоотключением, либо hosted-агент.
- Пересборка под каждую среду. В test уехал один артефакт, в prod — собранный заново. Формально «тот же коммит», фактически другой билд. Собирайте один раз, деплойте много.
- Секрет в
echo«для отладки». Логи пайплайна видит вся команда, и они хранятся месяцами. Утёкший ключ придётся ротировать. - Прямой деплой в прод без слота. Приложение стартует несколько секунд и всё это время отдаёт ошибки. Слот + swap решают проблему бесплатно (на тарифах Standard и выше слоты уже включены в цену).
- Плавающая версия инструмента.
version: '8.x'илиubuntu-latestозначают, что однажды образ обновится и «ничего не менявший» пайплайн покраснеет. Для критичных вещей фиксируйте версии явно.
Итоги
- Пайплайн — это YAML в репозитории: живёт рядом с кодом, ревьюится и откатывается вместе с ним.
- Стадии
BuildиDeployразделяйте: собрали артефакт один раз — задеплойте его во все среды. - Hosted-агент одноразовый и чистый: всё, что нужно сборке, должно быть в репозитории или в шагах.
- Секреты — только через service connection и variable group с Key Vault; секретные переменные пробрасывайте явно в
env. environmentдаёт историю деплоев и ручные approval'ы, слот со swap — деплой без даунтайма и мгновенный откат.- Следите за минутами агентов и не оставляйте self-hosted VM включённой круглосуточно — это самая тихая статья расходов.