Одна из вещей, которые мне больше всего нравятся в Azure Static Web Apps, также известной как SWA, заключается в том, что она генерирует для вас файл рабочего процесса GitHub Actions, гарантируя, что у вас есть CI/CD конвейер, который будет развертывать код по мере внесения изменений, делая повторяющиеся развертывания запланированными. Если вы не используете GitHub Actions, нет проблем, вы можете использовать Azure Pipelines, GitLab, Bitbucket или недавно выпущенную команду cli deploy и добиться того же повторяющегося рабочего процесса, а не возвращаться к копированию файлов на удаленный сервер.
В этой статье я буду использовать GitHub Actions, поскольку именно их я использую для своего блога (на котором основана эта статья), но шаблоны будут такими же и для других платформ сборки.
Чтобы освежить в памяти или для тех, кто не знаком с SWA, вот задание, которое будет сгенерировано, чтобы собрать и развернуть ваше приложение в Azure:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for GitHub integrations (i.e. PR comments)
action: "upload"
###### Repository/Build Configurations ######
app_location: "src" # App source code path relative to repository root
api_location: "api" # Api source code path relative to repository root - optional
output_location: "public" # Built app content directory, relative to app_location - optional
###### End of Repository/Build Configurations ######
Действительно важной частью этого процесса является задание Build And Deploy, поскольку оно отвечает за две задачи: создание фронт-энда (и Functions API, если он существует), а затем загрузку его в Azure.
Хотя этот рабочий процесс охватывает многие случаи использования, вы можете захотеть выйти за его пределы. Возможно, вы хотите запускать тесты как часть конвейера, или добавить процесс утверждения для развертывания, или что-то еще, что означает, что объединение этапа сборки с этапом развертывания может усложнить задачу.
Выход за рамки стандартного
Давайте рассмотрим, как выйти за рамки стандартного, и проиллюстрируем сложный конвейер GitHub Actions, который в конечном итоге развертывается на SWA:
Это изображение рабочего процесса для моего блога, который состоит из семи заданий, выполняемых с помощью почти 40 шагов. Некоторые из этих заданий выполняются параллельно, некоторые — последовательно, но в целом именно так я развертываю свой блог.
Итак, почему все так сложно? Ну, мой сайт создан для трех разных платформ: Hugo для самого блога, .NET для поиска на основе Blazor и TypeScript для API (о котором я скоро расскажу отдельно). Из-за этого стандартное действие SWA не работает; оно не знает, что строить!
Из-за этого у меня есть три основных параллельных задания, build_hugo
, build_api
и build_search_ui
, и каждое из них будет генерировать артефакты для развертывания. В этом посте я опишу гораздо более простой процесс, но вы можете посмотреть мой полный (и, возможно, слишком сложный…) рабочий процесс в build-and-deploy.yml
.
Сначала сборка, потом развертывание
Первое, что мы хотим сделать, это отделить этап сборки от остальной части конвейера. Фактические шаги, которые вы будете выполнять в GitHub Actions, будут зависеть от того, что вы собираете, давайте возьмем приложение JavaScript:
job:
build:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
environment: build
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm ci
- run: npm run build
Важная часть здесь — знать, куда будет отправлен вывод, и в данном случае мы просто предполагаем, что он находится в папке build
этого гипотетического приложения.
Но как мы получим их позже? Превратив их в артефакт работы, используя действие actions/upload-artifact
:
- name: Publish website output
uses: actions/upload-artifact@v3
with:
name: website
path: ${{ github.workspace }}/build
Это упакует выходное содержимое нашего шага сборки и загрузит его в рабочий процесс, который мы сможем использовать позже.
Артефакты с большим количеством файлов
Если вы работаете с результатом, который будет содержать много файлов, например, папку node_modules
(потому что это приложение без пакета), вы можете захотеть упаковать их в архив, а затем загрузить этот архив (как я делаю с моим API):
- run: npm ci
- run: npm run build
- run: mkdir ${{ github.workspace }}/${{ env.OUTPUT_FOLDER }}
- run: tar -cvf ${{ github.workspace }}/${{ env.OUTPUT_FOLDER }}/api.tar .
- name: Publish API output
uses: actions/upload-artifact@v1
with:
name: api
path: ${{ github.workspace }}/${{ env.OUTPUT_FOLDER }}/api.tar
Это происходит потому, что при загрузке он будет загружать файл за файлом, а когда файлов много, это может занять ооооочень много времени (что делает сборку медленнее), но если мы создадим архив, он будет загружать только один файл, что требует гораздо меньше IO.
Развертывание из артефактов
Теперь, когда мы отделили нашу фазу сборки от SWA Action, как нам ее использовать?
Начните с определения нового задания в нашем рабочем процессе, deploy
, и добавьте к нему раздел needs
, говорящий о том, что ему необходимо, чтобы задание build
было выполнено первым, иначе это задание будет выполняться параллельно, и мы не сможем развернуть, пока не выполним сборку!
job:
build:
# snip
deploy:
runs-on: ubuntu-latest
environment: production
needs: [build]
steps:
# todo
В отличие от задания build
, нам не понадобится actions/checkout
, потому что нам не нужен исходный код для нашего приложения, мы будем использовать предварительно собранный артефакт, который мы получим из actions/download-artifact
:
job:
build:
# snip
deploy:
runs-on: ubuntu-latest
environment: production
needs: [build]
steps:
- name: Download website
uses: actions/download-artifact@v1
with:
name: website
path: ${{ github.workspace }}
Укажите место, куда вы хотите загрузить артефакт. В данном случае мы поместим его в корень агента, так как мы знаем, что это новый агент для этой работы, нет других файлов, о которых нам нужно беспокоиться.
Далее мы подключим действие azure/static-web-apps-deploy
, чтобы мы могли развернуть в Azure:
job:
build:
# snip
deploy:
runs-on: ubuntu-latest
environment: production
needs: [build]
steps:
- name: Download website
uses: actions/download-artifact@v1
with:
name: website
path: ${{ github.workspace }}
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for GitHub integrations (i.e. PR comments)
action: "upload"
###### Repository/Build Configurations ######
app_location: "" # App source code path relative to repository root
api_location: "api" # Api source code path relative to repository root - optional
skip_app_build: true
###### End of Repository/Build Configurations ######
Здесь нужны два секрета: GITHUB_TOKEN
, который предоставляется GitHub, и AZURE_STATIC_WEB_APPS_API_TOKEN
, который является маркером развертывания, который генерируется при первом подключении репозитория к SWA, может быть получен через портал или через Azure CLI (и который я сливал в своих логах, что послужило поводом для этой записи в блоге).
Другие параметры, которые нам нужно изменить для действия, это то, что мы установим app_location
в место относительно ${{ github.workspace }}
(которое в нашем случае пустое) и затем установим skip_app_build
в true
, так как мы уже создали, все, что нам нужно сделать, это развернуть.
Резюме
Итак, у нас есть завершенный многоэтапный рабочий процесс, который выглядит следующим образом для сборки и развертывания SWA (я исключил триггеры для простоты):
job:
build:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
environment: build
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm ci
- run: npm run build
- name: Publish website output
uses: actions/upload-artifact@v3
with:
name: website
path: ${{ github.workspace }}/build
deploy:
runs-on: ubuntu-latest
environment: production
needs: [build]
steps:
- name: Download website
uses: actions/download-artifact@v1
with:
name: website
path: ${{ github.workspace }}
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for GitHub integrations (i.e. PR comments)
action: "upload"
###### Repository/Build Configurations ######
app_location: "" # App source code path relative to repository root
api_location: "api" # Api source code path relative to repository root - optional
skip_app_build: true
###### End of Repository/Build Configurations ######
Мы видели, как мы можем использовать артефакты для перемещения результатов от одного задания к другому, что позволяет четко определить фазы сборки и развертывания в нашем рабочем процессе.
С помощью этой настройки мы можем ввести в рабочий процесс любые дополнительные шаги, такие как запуск тестов, развертывание SWA с помощью Bicep или запуск параллельных заданий для ускорения выполнения рабочего процесса.
У меня в блоге используется более сложная форма этого процесса, которую вы можете увидеть в моем рабочем процессе в build-and-deploy.yml
.
Бонус — разделение управления PR
SWA автоматически генерирует среду предварительного просмотра из PR, и часть этого требует второго задания для очистки, когда PR закрывается:
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
action: "close"
Это задание включается в создаваемый файл рабочего процесса, и из-за этого у нас есть некоторые if
проверки на заданиях, поскольку рабочий процесс запускается на PR, но нам нужно выборочно запускать задания в зависимости от того, какое событие вызвало PR.
Но мы также можем разделить это, чтобы у нас был рабочий процесс «закрыть PR», который не зависит от нашего задания «сборка и развертывание», и мы можем сделать это, изменив триггеры для рабочего процесса.
Давайте начнем с рабочего процесса сборки и развертывания:
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
Этот рабочий процесс все еще будет выполняться на PR, но он будет выполняться только в том случае, если PR открыт
, синхронизирован
(на него выкладываются файлы) или открыт
. Это означает, что мы можем удалить проверку if
из нашего задания build
.
Затем создайте другой файл рабочего процесса и переместите в него задание close_pull_request_job
:
name: Close PR
on:
pull_request:
types: [closed]
branches:
- main
jobs:
close_pull_request_job:
runs-on: ubuntu-latest
name: Close Pull Request Job
environment: production
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
action: "close"
Этот процесс срабатывает только при событии closed
для PR, и выполняет один шаг для уничтожения среды предварительного просмотра.
Конечно, это означает наличие дополнительного файла рабочего процесса (и некоторый потенциал для дублирования кода), но я предпочитаю более чистый вид и то, что понятно, какие рабочие процессы когда будут выполняться.