Deploy a .NET web app using GitHub Actions
Tip
This content is an excerpt from the eBook, DevOps for ASP.NET Core Developers, available on .NET Docs or as a free downloadable PDF that can be read offline.
Warning
Please complete the Build tutorial before starting this lab.
In this article, you'll:
- Learn about Environments in GitHub Actions.
- Create two environments and specify environment protection rules.
- Create environment secrets for managing environment-specific configuration.
- Extend the workflow YAML file to add deployment steps.
- Add a manual dispatch trigger.
Environments
Now that you've published an artifact that's potentially deployable, you'll add deployment jobs to the workflow. There's nothing special about a deployment job, other than the fact that it references an environment. Environments are logical constructs that allow you to specify environment protection rules, such as approvals, on any group of resources that you're targeting.
In this walkthrough, you'll be deploying to two environments: PRE-PROD and PROD. In a typical development lifecycle, you'll want to deploy the latest code to a soft environment (typically DEV) that is expected to be a bit unstable. You'll use PRE-PROD as this soft environment. The "higher" environments (like UAT and PROD) are harder environments that are expected to be more stable. To enforce this, you can build protection rules into higher environments. You'll configure an approval protection rule on the PROD environment: whenever a deployment job targets an environment with an approval rule, it will pause until approval is granted before executing.
GitHub environments are logical. They represent the physical (or virtual) resources that you're deploying to. In this case, the PRE-PROD is just a deployment slot on the Azure Web App. PROD is the production slot. The PRE-PROD deployment job will deploy the published .NET app to the staging slot. The PROD deployment job will swap the slots.
Once you have these steps in place, you'll update the workflow to handle environment-specific configuration using environment secrets.
Note
For more information, see GitHub Actions - Environments.
Azure authentication
To perform actions such as deploying code to an Azure resource, you need the correct permissions. For deployment to Azure Web Apps, you can use a publishing profile. If you want to deploy to a staging slot, then you'll need the publishing profile for the slot too. Instead, you can use a service principal (SPN) and assign permission to this service principal. You can then authenticate using credentials for the SPN before using any commands that the SPN has permissions to perform.
Once you have an SPN, you'll create a repository secret to securely store the credentials. You can then refer to the secret whenever you need to authenticate. The secret is encrypted and once it has been saved, can never be viewed or edited (only deleted or re-created).
Create an SPN
In your terminal or Cloud Shell, run the following command to create a service principal with contributor permissions to the web app you created earlier:
az ad sp create-for-rbac --name "{sp-name}" --sdk-auth --role contributor \ --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.Web/sites/{webappname}The command should output JSON that has credentials embedded:
{ "clientId": "<GUID>", "clientSecret": "<GUID>", "subscriptionId": "<GUID>", "tenantId": "<GUID>", ... }Make sure to record the
clientId,clientSecret,subscription, andtenantId. You can also leave the terminal open for copy/paste later.
Create a repository secret
Now you're going to create an encrypted secret to store the credentials. You'll create this secret at the repository level.
Navigate to GitHub and select your repository Settings tab. Then select Secrets. Select New repository secret:

Figure 1: Create a secret.
Copy and paste the JSON from the
az ad sp create-for-rbaccommand into the body of the secret. You can create this JSON by hand too if you have the relevant fields for your SPN. The secret should be namedAZURE_CREDENTIALS. Select Add secret to save the new secret:
Figure 2: Add Azure credentials.
You'll consume this secret in a workflow in later steps. To access it, use the variable notation
${{}}. In this case,${{ AZURE_CREDENTIAL }}will be populated with the JSON you saved.
Add environments
Environments are used as a logical boundary. You can add approvals to environments to ensure quality. You can also track deployments to environments and specify environment-specific values (secrets) for configuration.
For this example, you're going to split the actual Azure environment into two logical environments called PRE-PROD and PROD. When you deploy the web app, you'll deploy to the staging slot of the Azure web app, represented by the PRE-PROD environment. When you're ready to deploy to PROD, you'll just perform a slot swap.
In this case, the only difference between the environments is the slot that you're deploying to. In real life, there would typically be different web apps (and separate web app plans), separate resource groups, and even separate subscriptions. Typically, there's an SPN per environment. You may want to override the AZURE_CREDENTIAL value that you saved as a repository secret by creating it as an environment secret.
Note
Precedence works from Environment to repository. If a targeted environment has a secret called MY_SECRET, then that value is used. If not, the repository value of MY_SECRET (if any) is used.
Select Settings and then Environments in your repository. Select New Environment:

Figure 3: Create an environment.
Enter
PRE-PRODand select Configure environment:
Figure 4: Name the environment.
Since deploying to a staging slot doesn't affect the web app, you can safely deploy to the slot without requiring an approval first. A reviewer could be added if desired. For this example, leave the Environment protection rules empty.
Note
If you target an environment in a workflow and it does not exist, an "empty" environment is created automatically. The environment would look exactly the same as the
PRE-PRODenvironment - it would exist, but would not have any protection rules enabled.Select Environments again and again select New Environment. Now enter
PRODas the name and select Configure environment.Check the Required reviewers rule and add yourself as a reviewer. Don't forget to select Save protection rules:

Figure 5: Add protection rules.
Deploy to staging
You can now add additional jobs to the workflow to deploy to the environments! You'll start by adding a deployment to the PRE-PROD environment, which in this case is the web app staging slot.
Navigate to the .github/workflows/dotnet.yml file and select the pencil icon to edit the file.
You're going to use the web app name a few times in this workflow, and will need the name of the resource group too. You'll define the app and resource group names as variables. With the variables, you can maintain the values in one place in the workflow file.
Add this snippet below the
onblock and above thejobsblock:env: app-name: "<name of your web app>" rg-name: "<name of your resource group>" jobs: # <-- this is the existing jobs lineWarning
You'll need to replace
<name of your web app>with the actual name of your web app, and<name of your resource group>with the actual name of your resource group.Add a new job below the
buildjob as follows:if-no-files-found: error # <-- last line of build job: insert below this line deploy_staging: needs: build runs-on: ubuntu-latest environment: name: PRE-PROD url: ${{ steps.deploywebapp.outputs.webapp-url }} steps: - name: Download a Build Artifact uses: actions/download-artifact@v2.0.8 with: name: website path: website - name: Login via Azure CLI uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Deploy web app id: deploywebapp uses: azure/webapps-deploy@v2 with: app-name: ${{ env.app-name }} slot-name: staging package: website - name: az cli logout run: az logoutThe preceding workflow defines several steps:
- You're creating a new job called
deploy_staging. - You specify a dependency using
needs. This job needs thebuildjob to complete successfully before it starts. - This job also runs on the latest Ubuntu hosted agent, as specified with the
runs-onattribute. - You specify that this job is targeting the
PRE-PRODenvironment using theenvironmentobject. You also specify theurlproperty. This URL will be displayed in the workflow diagram, giving users an easy way to navigate to the environment. The value of this property is set as theoutputof thestepwithiddeploywebapp, which is defined below. - You're executing a
download-artifactstep to download the artifact (compiled web app) from thebuildjob. - You then
loginto Azure using theAZURE_CREDENTIALSsecret you saved earlier. Note the${{ }}notation for dereferencing variables. - You then perform a
webapp-deploy, specifying theapp-name,slot-name, and path to the downloaded artifact (package). This action also defines anoutputparameter that you use to set theurlof the environment above. - Finally, you execute a
logoutto log out of the Azure context.
- You're creating a new job called
Commit the file.
When the run completes, you should see two successful jobs. The URL for the
PRE-PRODstage has been set and selecting it will navigate you to your web app staging slot:
Figure 6: Deployment to PRE-PROD is successful.
Notice how the staging slot's direct URL contains
-staging:
Figure 7: The staging slot running.
You can also now see deployments. Navigate to
https://{your repository url}/deploymentsto view your deployments:
Figure 8: View deployments.
Deploy to production
Now that you've deployed successfully to PRE-PROD, you'll want to deploy to PROD. Deployment to PROD will be slightly different since you don't need to copy the website again - you just need to swap the staging slot with the production slot. You'll do this using an Azure CLI (az) command.
Navigate to the .github/workflows/dotnet.yml file and select the pencil icon to edit the file.
Add a new job below the
deploy_stagingjob as follows:run: az logout # <-- last line of previous job: insert below this line deploy_prod: needs: deploy_staging runs-on: ubuntu-latest environment: name: PROD url: ${{ steps.slot_swap.outputs.url }} steps: - name: Login via Azure CLI uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Swap staging slot into production id: slot_swap run: | az webapp deployment slot swap -g ${{ env.rg-name }} -n ${{ env.app-name }} -s staging url=$(az webapp show -g ${{ env.rg-name }} -n ${{ env.app-name }} --query "defaultHostName" -o tsv) echo "::set-output name=url::http://$url" - name: az cli logout run: az logoutThe deployment to the
PRODenvironment workflow specifies several steps:- Once again, you specify a new job
deploy_prodthatneedsdeploy_stagingto complete before starting. - You're targeting the
PRODenvironment this time. Also, theurlvalue is different from before. - For the steps, you don't need to download the artifact since you're just going to perform a slot swap. You start by executing a
loginto the Azure context. - The
Swap staging slot into productionstep is a multi-lineruncommand (note the use of the pipe symbol|). You also specify anidfor this step so that you can refer to it (you refer to it in theurlproperty of theenvironment). The first line executes the slot swap using the variables you defined above in the workflow. The second line uses anaz webapp showcommand to extract the URL of the target web app. This final line uses::set-outputin an echo to create an output variable for this task, setting the value to the web app URL.
Note
The URL must start with
http://orhttps://or it won't render.- Once again, you specify a new job
Commit the file.
Let the workflow run for a couple minutes until it has deployed to
PRE-PROD. At this point, the workflow will pause and wait for the required approval since you're targeting thePRODenvironment, which requires an approval as defined earlier:
Figure 9: Waiting for an approval.
Select Review deployments, select the PROD checkbox, optionally add a comment, and then select Approve and deploy to start the
PRODjob.
Figure 10: Approve the PROD deployment.
The deployment should only take a few seconds. Once it has completed, the URL for the
PRODenvironment will update.
Figure 11: PROD deployment completed.
Selecting the
PRODURL will navigate you to thePRODsite.
Figure 12: The PROD site.
Add a manual queue option
You now have an end-to-end build and deploy workflow, including approvals. One more change you can make is to add a manual trigger to the workflow so that the workflow can be triggered from within the Actions tab of the repository.
Navigate to the .github/workflows/dotnet.yml file and select the pencil icon to edit the file.
Add a new trigger between
onandpushon lines 3 and 4:on: workflow_dispatch: # <-- this is the new line push:The
workflow_dispatchtrigger displays aRun workflowbutton in the Actions tab of the repository—but only if the trigger is defined in the default branch. However, once this trigger is defined in the workflow, you can select the branch for the run.Commit the file.
To see the Run workflow button, select the Actions tab. Select the
.NETworkflow in the list of workflows. At the top of the list of runs, you'll see the Run workflow button. If you select it, you can choose the branch to run the workflow against and queue it:
Figure 13: Manual dispatch.
Handle environment configuration
Your workflow is deploying the same binary to each environment. This concept is important to ensure that the binaries you test in one environment are the same that you deploy to the next. However, environments typically have different settings like database connection strings. You want to ensure that the DEV app is using DEV settings and the PROD app is using PROD settings.
For this simple app, there's no database connection string. However, there's an example configuration setting that you can modify for each environment. If you open the simple-feed-reader/SimpleFeedReader/appsettings.json file, you'll see that the configuration includes a setting for the Header text on the Index page:
"UI": {
"Index": {
"Header": "Simple News Reader"
}
},
To show how environment configuration can be handled, you're going to add a secret to each environment and then substitute that value into the settings as you deploy.
Add environment secrets
On your repository, select Settings > Environments > PRE-PROD.
Select Add secret and add a secret called
index_headerwith the valuePRE PROD News Reader. Select Add secret.
Figure 14: Add an environment secret.
Repeat these steps to add a secret called
index_headerwith the valuePROD News Readerfor thePRODenvironment.If you select Settings > Secrets in the repository, you'll see the changes. They should look something like this:

Figure 15: View secrets.
Update the workflow to handle configuration
Navigate to the .github/workflows/dotnet.yml file and select the pencil icon to edit the file.
Add the following step before the
az cli logoutstep in thedeploy_stagingjob:- name: Update config uses: Azure/appservice-settings@v1 with: app-name: ${{ env.app-name }} slot-name: staging app-settings-json: | [ { "name": "UI:Index:Header", "value": "${{ secrets.INDEX_HEADER }}", "slotSetting": true } ] - name: az cli logout # <-- this exists alreadyAdd almost the same code to the
deploy_prodjob above itsaz cli logoutstep. The only difference is that you don't specify aslot-name, since you're targeting the production slot:- name: Update config uses: Azure/appservice-settings@v1 with: app-name: ${{ env.app-name }} app-settings-json: | [ { "name": "UI:Index:Header", "value": "${{ secrets.INDEX_HEADER }}", "slotSetting": true } ] - name: az cli logout # <-- this exists alreadyCommit the file.
Let the workflow run and approve the deployment to
PRODonce the approval is reached.You should see the following headers on the index page for both sites:

Figure 16: Settings changed in the environments.
Final workflow file
The final workflow file should look like this:
name: .NET
on:
workflow_dispatch:
inputs:
reason:
description: 'The reason for running the workflow'
required: true
default: 'Manual build from GitHub UI'
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
app-name: "cd-simplefeedreader"
rg-name: "cd-dotnetactions"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Print manual run reason'
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
echo 'Reason: ${{ github.event.inputs.reason }}'
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 2.1.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
- name: Publish
run: dotnet publish SimpleFeedReader/SimpleFeedReader.csproj -c Release -o website
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.2
with:
name: website
path: SimpleFeedReader/website/**
if-no-files-found: error
deploy_staging:
needs: build
runs-on: ubuntu-latest
environment:
name: STAGING
url: ${{ steps.deploywebapp.outputs.webapp-url }}
steps:
- name: Download a Build Artifact
uses: actions/download-artifact@v2.0.8
with:
name: website
path: website
- name: Login via Azure CLI
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy web app
id: deploywebapp
uses: azure/webapps-deploy@v2
with:
app-name: ${{ env.app-name }}
slot-name: staging
package: website
- name: Update config
uses: Azure/appservice-settings@v1
with:
app-name: ${{ env.app-name }}
slot-name: staging
app-settings-json: |
[
{
"name": "UI:Index:Header",
"value": "${{ secrets.INDEX_HEADER }}",
"slotSetting": true
}
]
- name: az cli logout
run: az logout
deploy_prod:
needs: deploy_staging
runs-on: ubuntu-latest
environment:
name: PROD
url: ${{ steps.slot_swap.outputs.url }}
steps:
- name: Login via Azure CLI
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Swap staging slot into production
id: slot_swap
run: |
az webapp deployment slot swap -g ${{ env.rg-name }} -n ${{ env.app-name }} -s staging
url=$(az webapp show -g ${{ env.rg-name }} -n ${{ env.app-name }} --query "defaultHostName" -o tsv)
echo "::set-output name=url::http://$url"
- name: Update config
uses: Azure/appservice-settings@v1
with:
app-name: ${{ env.app-name }}
app-settings-json: |
[
{
"name": "UI:Index:Header",
"value": "${{ secrets.INDEX_HEADER }}",
"slotSetting": true
}
]
- name: az cli logout
run: az logout
Povratne informacije
Pošalјite i prikažite povratne informacije za