Ditch the Secrets: Passwordless Azure Deployments with GitHub Actions

I have been a long time Bitbucket/CircleCI user now and have been running our CI/CD pipelines with CircleCI without any issues. I was starting a new project and wanted to give Github Actions a try!

If you're migrating from Bitbucket to GitHub, check out my previous article here.

This guide walks through setting up a complete CI/CD pipeline using GitHub Actions for Node.js + TypeScript Azure Functions. We'll configure automatic deployments to staging from the main branch and add manual approval gates for production deployments.

What We're Building

Our pipeline will handle:

  • Automated builds and linting on pull requests
  • Automatic deployment to staging when code is merged to main
  • Manual approval workflow for production deployments
  • Secure Azure deployments without storing credentials

Prerequisites

Before getting started, make sure you have the following tools installed and configured:

Required Tools

Required Access

  • Azure subscription with permissions to:
    • Create service principals
    • Manage Microsoft Entra ID (Azure AD) applications
    • Create and manage Azure Functions
  • GitHub repository with admin access to configure secrets and environments
  • GitHub organization (if using organization-level variables)

Azure Resources

  • An existing Azure Function App or App Service with staging and production slots
  • A resource group containing your Azure resources

Note: This setup works for both Azure Functions and App Service web apps.

Step 1: Create Azure Service Principal with Federated Credentials

First, we'll need to create a service principal that GitHub Actions can use to authenticate with Azure. I chose to use federated identity instead of storing secrets as it's more secure.

How Federated Identity Works: Instead of storing Azure credentials as GitHub secrets, federated identity allows GitHub Actions to exchange its own OIDC token for an Azure access token. Here's the flow:

  1. GitHub Actions generates an OIDC token containing the repository and environment information.
  2. This token is sent to Microsoft Entra ID along with your federated credential configuration.
  3. Entra ID validates the token and returns an Azure access token.
  4. GitHub Actions uses this Azure token to deploy to your resources.

The beauty of this approach is that only GitHub's own token is used - no Azure passwords or secrets are stored anywhere! Additionally, the Azure access token that's generated never leaves the secure GitHub Actions environment and is automatically disposed of when the workflow completes.

Create the Base Service Principal

Start by creating a service principal with the appropriate permissions for your subscription:

az ad sp create-for-rbac \
  --name "GitHub-Actions-SP" \
  --role "Website Contributor" \
  --scopes "/subscriptions/YOUR_SUBSCRIPTION_ID"

Replace YOUR_SUBSCRIPTION_ID with your actual Azure subscription ID.

Bulk Configure Federated Credentials

If you're managing multiple repositories (like we are), you can use this script to configure federated credentials for all of them at once:

#!/usr/bin/env bash

# Configuration
organizationName='my-awesome-org'  # Replace with your GitHub organization name
servicePrincipalId='4fa0327f-22a3-4a44-a028-fdb808d4cea1'  # Get this from the previous command output

# Map of GitHub repository names to Azure resource names
declare -A githubAzureHashMap=(
    ["web-api"]="webapp-api-prod"
    ["user-service"]="user-management-svc"
    ["payment-gateway"]="payment-gateway"
    ["notification-service"]="notifications-hub"
    ["auth-functions"]="auth-functions"
    ["data-processor"]="data-processor"
    ["webhook-handler"]="webhook-handler"
    ["email-service"]="email-service"
    ["analytics-engine"]="analytics-engine"
    ["file-uploader"]="file-storage-api"
)

echo "Creating federated credentials for GitHub Actions..."
echo "Organization: $organizationName"
echo "Service Principal ID: $servicePrincipalId"
echo ""

for githubRepoName in "${!githubAzureHashMap[@]}"; do
    azureResourceName="${githubAzureHashMap[$githubRepoName]}"

    echo "Processing repository: $githubRepoName"
    echo "Azure resource: $azureResourceName"

    # Create federated credential for staging environment
    echo "  Creating staging credential..."
    az ad app federated-credential create \
        --id "$servicePrincipalId" \
        --parameters "{
            \"name\": \"MyAwesomeOrg-${azureResourceName}-staging\",
            \"issuer\": \"https://token.actions.githubusercontent.com\",
            \"subject\": \"repo:${organizationName}/${githubRepoName}:environment:staging\",
            \"audiences\": [\"api://AzureADTokenExchange\"],
            \"description\": \"Federated credential for $githubRepoName staging environment\"
        }"

    # Create federated credential for production environment
    echo "  Creating production credential..."
    az ad app federated-credential create \
        --id "$servicePrincipalId" \
        --parameters "{
            \"name\": \"MyAwesomeOrg-${azureResourceName}-production\",
            \"issuer\": \"https://token.actions.githubusercontent.com\",
            \"subject\": \"repo:${organizationName}/${githubRepoName}:environment:production\",
            \"audiences\": [\"api://AzureADTokenExchange\"],
            \"description\": \"Federated credential for $githubRepoName production environment\"
        }"

    echo "  ✅ Completed $githubRepoName"
    echo ""
done

echo "All federated credentials created successfully!"

Save this script as setup-federated-credentials.sh and run it after updating the variables with your specific values.

Step 2: Configure GitHub Repository Settings

Now we need to configure GitHub with the Azure tenant and subscription details, along with other required variables.

Repository/Organization Variables

Navigate to your repository or organization settings and add these variables:

Required Azure Variables:

  • AZURE_CLIENT_ID - The Application (client) ID from your service principal
  • AZURE_TENANT_ID - Your Microsoft Entra ID tenant ID
  • AZURE_SUBSCRIPTION_ID - Your Azure subscription ID

Deployment Variables:

  • RESOURCE_GROUP - The name of your Azure resource group
  • NODE_VERSION - Node.js version for your project (e.g., "18.x")

Approval Settings:

  • APPROVERS - Comma-separated list of GitHub usernames who can approve production deployments

Important: Use GitHub usernames for approvers, not email addresses. I learned this the hard way after debugging API errors for way too long!

Environment Configuration

Create two environments in your repository settings:

  1. staging - No protection rules needed
  2. production - Add the approvers from your APPROVERS variable as required reviewers

Deployment Strategy

The branching strategy I've set up follows this pattern:

  • Feature branches → Create PR → Run tests/linting
  • Main branch → Auto-deploy to staging
  • Production branch → Rebase production onto main → Push to production branch (triggers workflow) → Manual approval → Deploy to production → Sync back to staging

The final step syncs production back to staging to ensure both environments stay in sync after any hotfixes or manual changes.

Step 3: Create the GitHub Actions Workflow

Create a new file at .github/workflows/action.yml (or whatever name makes sense for your project):

name: CI/CD Pipeline for Azure Functions

on:
  push:
    branches:
      - main
      - production
  pull_request:
    branches:
      - '**'

jobs:
  # Checkout the repository code
  checkout_code:
    uses: harshithkashyap/github-actions-workflows/.github/workflows/checkout.yml@main
    with:
      project_name: ${{ github.event.repository.name }}

  # Install dependencies and build the project
  bundle_dependencies:
    needs: checkout_code
    uses: harshithkashyap/github-actions-workflows/.github/workflows/bundle_dependencies.yml@main
    with:
      project_name: ${{ github.event.repository.name }}
      node_version: ${{ vars.NODE_VERSION }}

  # Run linting on all branches except production
  lint:
    needs: bundle_dependencies
    if: github.ref_name != 'production'
    uses: harshithkashyap/github-actions-workflows/.github/workflows/lint.yml@main
    with:
      project_name: ${{ github.event.repository.name }}
      node_version: ${{ vars.NODE_VERSION }}

  # Deploy to staging when code is pushed to main
  deploy_to_staging:
    needs: [lint]
    if: github.ref_name == 'main'
    uses: harshithkashyap/github-actions-workflows/.github/workflows/deploy_to_staging.yml@main
    with:
      project_name: ${{ github.event.repository.name }}
      node_version: ${{ vars.NODE_VERSION }}
    secrets: inherit

  # Manual approval gate for production deployments
  hold:
    needs: [bundle_dependencies]
    if: github.ref_name == 'production'
    uses: harshithkashyap/github-actions-workflows/.github/workflows/hold.yml@main
    secrets: inherit

  # Deploy to production after approval
  deploy_to_production:
    needs: [hold]
    if: github.ref_name == 'production'
    uses: harshithkashyap/github-actions-workflows/.github/workflows/deploy_to_production.yml@main
    with:
      project_name: ${{ github.event.repository.name }}
      node_version: ${{ vars.NODE_VERSION }}
    secrets: inherit

  # Sync production changes back to staging
  redeploy_to_staging:
    needs: [deploy_to_production]
    if: github.ref_name == 'production'
    uses: harshithkashyap/github-actions-workflows/.github/workflows/deploy_to_staging.yml@main
    with:
      project_name: ${{ github.event.repository.name }}
      node_version: ${{ vars.NODE_VERSION }}
    secrets: inherit

Understanding the Workflow

Let's break down what happens in this pipeline:

Job Dependencies

The workflow uses needs to ensure jobs run in the correct order:

  1. Code checkout happens first
  2. Dependencies are installed after checkout
  3. Linting runs after dependencies are ready
  4. Deployments only happen after successful builds/tests

Reusable Workflows

Each job references a reusable workflow from my GitHub Actions collection. I found this approach helps to:

  • Reduces code duplication across repositories
  • Centralizes maintenance of common CI/CD patterns
  • Makes it easy to update all projects at once

The secrets: inherit directive passes all repository secrets to the reusable workflows, allowing them to authenticate with Azure.

Troubleshooting Common Issues

Authentication Problems

If you see authentication failures:

  1. Verify your service principal has the correct permissions.
  2. Check that federated credentials match your repository and environment names exactly.
  3. Ensure AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID are correctly set.

Approval Workflow Issues

Remember to use GitHub usernames (not email addresses) in your APPROVERS variable. The approval system won't work with email addresses.

Branch Name Mismatches

Make sure your condition in the workflow (github.ref_name == 'main') matches your actual default branch name. Some repositories use master instead of main.

Next Steps

This setup gives you a solid foundation for Azure Functions CI/CD with GitHub Actions, including a blue-green deployment strategy using Azure's native deployment slots. You can extend it by:

  • Adding more robust automated testing stages against the staging slot before the swap
  • Adding monitoring and alerting integration
  • Creating environment-specific configuration management

Reusable Workflows Deep Dive

For me, the real power of this setup comes from the reusable workflows. Each workflow is designed to be generic enough to work across different projects while still being configurable through parameters.

Some CI/CD systems, like CircleCI, use YAML anchors (&) to reduce duplication within a single workflow file. GitHub Actions takes a different, more powerful approach with reusable workflows. Instead of being limited to a single file, you can create truly modular and centralized workflow components that can be shared and versioned across all repositories in your organization. This means you won't miss out on capabilities like anchors; in fact, you gain a more scalable and maintainable way to manage your CI/CD pipelines.

0
Subscribe to my newsletter

Read articles from Harshith Kashyap directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harshith Kashyap
Harshith Kashyap