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
- Azure CLI (version 2.30.0 or later) - Installation Guide
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:
- GitHub Actions generates an OIDC token containing the repository and environment information.
- This token is sent to Microsoft Entra ID along with your federated credential configuration.
- Entra ID validates the token and returns an Azure access token.
- 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 principalAZURE_TENANT_ID
- Your Microsoft Entra ID tenant IDAZURE_SUBSCRIPTION_ID
- Your Azure subscription ID
Deployment Variables:
RESOURCE_GROUP
- The name of your Azure resource groupNODE_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:
- staging - No protection rules needed
- 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
ontomain
→ Push toproduction
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:
- Code checkout happens first
- Dependencies are installed after checkout
- Linting runs after dependencies are ready
- 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:
- Verify your service principal has the correct permissions.
- Check that federated credentials match your repository and environment names exactly.
- Ensure
AZURE_CLIENT_ID
,AZURE_TENANT_ID
, andAZURE_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.
Subscribe to my newsletter
Read articles from Harshith Kashyap directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
