Skip to main content

Deploy to App Service with GitHub Actions

In this lab, you set up continuous deployment from a GitHub repository to Azure App Service using GitHub Actions. Every push to your main branch builds your app and deploys it, with no manual steps.

You'll lead with the recommended keyless path: OpenID Connect (OIDC) federated credentials. Your workflow signs in to Azure with a short-lived token that GitHub issues at run time, so there are no long-lived secrets or publish profiles to store or rotate. The lab also shows the publish-profile and service-principal-secret alternatives so you can recognize them, but OIDC is the pattern to reach for.

You'll set this up three ways so you can pick the workflow that fits you:

  • Azure Developer CLI (azd) - azd pipeline config wires up the federated credential, role assignment, and workflow for you.
  • Azure CLI (az) - explicit commands that create the identity, federated credential, and role assignment.
  • Azure portal - the visual Deployment Center experience.

You can follow the lab in your language of choice: .NET, Node.js, Python, Java, or PHP. Each path calls out the differences between Linux and Windows App Service plans.

App Service Labs complements Microsoft Learn

This lab is a hands-on, end-to-end walkthrough. For reference depth on any concept, follow the "Learn more" links to the official Microsoft Learn articles.

Estimated time: 25-35 minutes

What you'll build

A GitHub Actions workflow that builds your app and deploys it to an App Service web app on every push to main. The workflow authenticates to Azure with an OIDC federated credential on a user-assigned managed identity, so no secrets are stored in GitHub. When it finishes, the app returns HTTP 200 at its https://<app-name>.azurewebsites.net hostname.

Objectives

By the end of this lab you will be able to:

  • Explain how GitHub Actions authenticates to Azure with OIDC federated credentials instead of stored secrets.
  • Create a user-assigned managed identity, add a GitHub federated credential, and grant it least-privilege access to your web app.
  • Author a .github/workflows pipeline that builds and deploys .NET, Node.js, Python, Java, or PHP on Linux or Windows.
  • Verify a deployment end to end and confirm the app returns HTTP 200.
  • Recognize the publish-profile and service-principal-secret alternatives and why OIDC is preferred.

Prerequisites

Before you begin, you will need an Azure subscription with Owner permissions and a GitHub account.

In addition, you will need the following tools installed on your local machine:

Setup Azure CLI

Start by logging into Azure by run the following command and follow the prompts:

az login --use-device-code
tip

You can log into a different tenant by passing in the --tenant flag to specify your tenant domain or tenant ID.

Choose a region and low-cost tier

This lab uses the East US region and the B1 (Basic) pricing tier, a low-cost option that's ideal for learning. You can change the region to one near you.

Permissions for OIDC

Creating a user-assigned managed identity and a federated credential needs Contributor on your resource group - the same permission you use to create the app - so the recommended path in this lab does not require creating a Microsoft Entra app registration. The app-registration alternative later in the lab is marked [Manual / admin] because it needs directory permissions many corporate tenants restrict.

How continuous deployment works

When you push to your repository, GitHub Actions runs your workflow on a hosted runner. The workflow builds your app, then signs in to Azure and deploys. With OIDC, the sign-in step exchanges a GitHub-issued token for a short-lived Azure access token - no secret is stored anywhere.

The trust is established by a federated credential: you tell Azure to trust tokens from GitHub's issuer for a specific repository and branch (the token's subject). GitHub never holds an Azure secret; it proves its identity per run.

Choose your authentication method

azure/login supports three ways to authenticate. Lead with OIDC.

MethodWhat GitHub storesRotationRecommendation
OIDC federated credential (this lab)Client, tenant, and subscription IDs only (not secrets)Nothing to rotateRecommended - keyless and least-privilege
Service principal secretA client secret in AZURE_CREDENTIALSExpires; must rotateUse only where OIDC isn't possible
Publish profileThe app's publish profile XMLRegenerate if leakedSimplest, but a broad long-lived credential
Why keyless

A leaked publish profile or client secret is a standing risk until you notice and rotate it. An OIDC federated credential has nothing to leak: the token GitHub presents is valid for minutes and only for the repository and branch you trust. See Use GitHub Actions to connect to Azure.

Set up the app and identity

Set your path once, then follow the matching steps and code samples below.

Choose your path
Set these once - every matching step and code sample below follows your choice.
Tooling
Language
OS

The Azure Developer CLI can provision your app and configure the GitHub Actions pipeline, including the OIDC federated credential and role assignment, in one command.

1. Initialize and provision

From your app's folder, initialize an azd project (or start from a template), then provision the app:

azd auth login
azd init
azd provision

azd init detects your language and scaffolds azure.yaml plus an infra/ folder. azd provision creates the App Service plan and web app.

2. Configure the pipeline with OIDC

azd pipeline config --provider github

This command:

  • Creates (or reuses) a managed identity or app registration.
  • Adds a federated credential so GitHub Actions can sign in without a secret.
  • Grants the identity access to your resources.
  • Commits a ready-to-run workflow under .github/workflows/.

When prompted, choose federated credential (OIDC) rather than a client secret.

azd writes the workflow for you

azd pipeline config generates a workflow that runs azd deploy. If you'd rather use the language-specific azure/webapps-deploy workflow shown in the next section, keep the identity and secrets azd created and replace the workflow file.

3. Push to trigger a deploy

git add .
git commit -m "Configure GitHub Actions deployment"
git push

Open the Actions tab of your repository to watch the run.

Add the GitHub Actions workflow

Create .github/workflows/deploy.yml in your repository. The workflow has two jobs: build produces a deployable artifact, and deploy signs in with OIDC and publishes it. Pin action versions and use azure/login@v2.

Two permissions are required for OIDC

The permissions block is mandatory: id-token: write lets the job request the OIDC token, and contents: read lets it check out your code. Without id-token: write, azure/login can't use federated credentials.

Set AZURE_WEBAPP_NAME to your app's name. The AZURE_* values come from the repository secrets you created.

The build job publishes a self-contained folder; the deploy job pushes it. On Windows plans, the same workflow works - only the app's runtime differs.

name: Deploy to App Service

on:
push:
branches: [main]
workflow_dispatch:

permissions:
id-token: write
contents: read

env:
AZURE_WEBAPP_NAME: your-app-name
DOTNET_VERSION: '8.0.x'

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Publish
run: dotnet publish -c Release -o ${{ github.workspace }}/publish
- uses: actions/upload-artifact@v4
with:
name: app
path: ${{ github.workspace }}/publish

deploy:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v4
with:
name: app
path: ./publish
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
package: ./publish

Commit and push the workflow to trigger a deployment:

git add .github/workflows/deploy.yml
git commit -m "Add GitHub Actions deployment workflow"
git push

The Azure Login step signs in with the OIDC token - notice there's no secret in the log, only the federated exchange.

Verify your deployment

When the workflow completes, open the app's URL in a browser, or test it from the command line and confirm you get an HTTP 200 response:

curl -I https://<your-app-name>.azurewebsites.net

Expected output (validated during authoring):

HTTP/1.1 200 OK
Content-Type: text/html

You can confirm no secrets are stored: in your repository, go to Settings > Secrets and variables > Actions. You should see only AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID - no publish profile and no client secret.

First request can be slow

The first request after a deployment may take a few seconds while the app starts. If you see a "starting" or 503 page, wait a moment and refresh.

Alternatives to OIDC

OIDC is the recommended path, but you may encounter these older approaches. Both store a long-lived credential in GitHub, so treat them as fallbacks.

Service principal with a client secret

If you can't use OIDC, create a service principal secret and store the whole JSON blob as one secret named AZURE_CREDENTIALS.

[Manual / admin] Creating a service principal needs permission to create a Microsoft Entra app registration, which many corporate tenants restrict. Ask an administrator if the command fails.

az ad sp create-for-rbac \
--name gha-deploy-sp \
--role "Website Contributor" \
--scopes /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.Web/sites/<app-name> \
--json-auth

Store the output as the AZURE_CREDENTIALS secret, then sign in with creds instead of the three IDs:

- name: Azure Login
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

The client secret expires and must be rotated. Prefer adding a federated credential to the same app registration to go keyless.

Publish profile

The simplest option is the app's publish profile, a single file that contains deployment credentials. Download it and store it as a secret named AZURE_WEBAPP_PUBLISH_PROFILE:

az webapp deployment list-publishing-profiles \
--resource-group <rg> --name <app-name> --xml

Then deploy without a separate login step:

- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: your-app-name
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: .
Publish profiles are broad, long-lived credentials

A publish profile grants full deployment access and doesn't expire on its own. If it leaks, regenerate it immediately with az webapp deployment list-publishing-profiles. Disable basic authentication and use OIDC where you can.

Clean up resources

To avoid ongoing charges, delete the resources when you're done. Deleting the resource group removes the web app, App Service plan, and managed identity. Removing the GitHub secrets and workflow file is optional.

azd down --purge --force

Summary

In this lab, you set up continuous deployment from GitHub to Azure App Service and confirmed the app returns HTTP 200. You learned how to:

  • Authenticate GitHub Actions to Azure with OIDC federated credentials instead of stored secrets.
  • Create a user-assigned managed identity, add a GitHub federated credential, and grant least-privilege access with the Website Contributor role.
  • Author a build-and-deploy workflow for .NET, Node.js, Python, Java, or PHP, on Linux or Windows.
  • Set up the pipeline three ways - with azd, the Azure CLI, and the portal's Deployment Center.
  • Recognize the publish-profile and service-principal-secret alternatives and why OIDC is preferred.

Troubleshooting

azure/login fails with AADSTS70021: No matching federated identity record found. The token's subject doesn't match your federated credential. Confirm the credential's subject matches the trigger - for a main push it must be repo:<owner>/<repo>:ref:refs/heads/main. Check with az identity federated-credential list --identity-name <name> -g <rg> -o table.

azure/login fails with Unable to get ACTIONS_ID_TOKEN_REQUEST_URL. The job is missing permissions: id-token: write. Add the permissions block at the workflow or job level.

Deploy step returns 403 or AuthorizationFailed. The identity lacks a role on the app. Assign Website Contributor (or Contributor) scoped to the web app, as shown in step 4. Role assignments can take a minute to propagate.

The app shows a default page or 503 after a successful deploy. The package layout may not match the runtime's expectations, or the app is still starting. Wait and refresh. For Linux Node or Python, confirm your start command and that dependencies were installed (set SCM_DO_BUILD_DURING_DEPLOYMENT=true if you deploy source).

Workflow doesn't trigger on push. Confirm the file is at .github/workflows/deploy.yml on the branch in on: push: branches, and that Actions is enabled under the repository's Settings > Actions.

Learn more