itservice@fajweb.com

Phone : +447557537411

Deploy Azure SQL Database with GitHub Actions and Bicep

Categories: ,

Deploying Azure SQL Database by hand is useful for learning, but repeatable environments need infrastructure as code and an automated delivery pipeline. This guide shows how to deploy Azure SQL Database with Bicep and GitHub Actions, using Microsoft Entra workload identity federation instead of a long-lived Azure client secret.

Security note: All IDs, names, IP addresses and user principal names shown here are examples or redacted screenshots. Substitute values from your own Azure tenant and GitHub repository. Never commit passwords, access keys or unredacted credentials.

What This Deployment Creates

  • An Azure SQL logical server and user database
  • A resource group for the selected environment
  • A Log Analytics workspace for operational diagnostics
  • A storage account for auditing, when enabled by the Bicep template
  • A GitHub Actions workflow authenticated to Azure through OpenID Connect (OIDC)

Why use OIDC?

OIDC lets GitHub request a short-lived Azure token for an explicitly trusted repository, branch or environment. The workflow does not need a reusable Azure client secret. GitHub still stores the application client ID, tenant ID and subscription ID, but those values identify the deployment context rather than acting as passwords.

Prerequisites

  1. An Azure subscription and permission to create resource groups, role assignments and app registrations.
  2. Azure CLI, Git and optionally GitHub CLI installed locally.
  3. A GitHub repository containing your Bicep modules and workflow file.
  4. A naming convention for development, test and production resources.
  5. A safe method for generating and storing the SQL administrator and optional Entra user passwords.

Step 1: Create the bootstrap resource group and Key Vault

Use a separate bootstrap resource group for secrets needed before the main Bicep deployment. The following example enables Azure RBAC on the vault and prompts for passwords without placing them in shell history.

$Location          = 'swedencentral'
$BootstrapRg       = 'rg-azuredb-bootstrap'
$KeyVaultName      = 'kv-azuredb-<unique-suffix>'
$SqlAdminPassword  = Read-Host 'SQL administrator password' -AsSecureString
$EntraUserPassword = Read-Host 'Entra user password' -AsSecureString

az group create `
  --name $BootstrapRg `
  --location $Location `
  --output none

az keyvault create `
  --name $KeyVaultName `
  --resource-group $BootstrapRg `
  --location $Location `
  --enable-rbac-authorization true `
  --output none

$SqlPlain   = [System.Net.NetworkCredential]::new('', $SqlAdminPassword).Password
$EntraPlain = [System.Net.NetworkCredential]::new('', $EntraUserPassword).Password

az keyvault secret set --vault-name $KeyVaultName --name 'sql-admin-password' --value $SqlPlain --output none
az keyvault secret set --vault-name $KeyVaultName --name 'entra-user-password' --value $EntraPlain --output none

Remove-Variable SqlPlain, EntraPlain

az keyvault secret list `
  --vault-name $KeyVaultName `
  --query "[].{Secret:name,Enabled:attributes.enabled}" `
  --output table
PowerShell terminal confirming the Key Vault and secret names were created
The bootstrap Key Vault contains secret names only; secret values are never displayed.

Store the vault name as a GitHub repository or environment variable because the vault name is configuration, not a secret:

$Repo = '<OWNER>/<REPOSITORY>'
gh variable set AZURE_KEY_VAULT_NAME --repo $Repo --body $KeyVaultName
GitHub repository settings screen for an Actions configuration value
GitHub Actions settings for the Key Vault name.

Step 2: Select the correct tenant and subscription

$TenantId       = '<TENANT-ID>'
$SubscriptionId = '<SUBSCRIPTION-ID>'

az login --tenant $TenantId
az account set --subscription $SubscriptionId
az account show `
  --query '{subscription:id,tenant:tenantId,signedInAs:user.name}' `
  --output table
Azure CLI output verifying the selected subscription, tenant and signed-in account
Confirm the target subscription and tenant before creating any identity or resource.

Step 3: Register the required Azure resource providers

$Providers = @(
  'Microsoft.Sql',
  'Microsoft.Insights',
  'Microsoft.OperationalInsights',
  'Microsoft.Storage'
)

foreach ($Provider in $Providers) {
  az provider register --namespace $Provider
}

az provider list `
  --query "[?namespace=='Microsoft.Sql' || namespace=='Microsoft.Insights' || namespace=='Microsoft.OperationalInsights' || namespace=='Microsoft.Storage'].{Provider:namespace,State:registrationState}" `
  --output table
Azure CLI output showing the required resource providers as registered
Wait until every required provider reports a Registered state.

Step 4: Create the deployment application and service principal

$GitHubAppName = 'sp-github-azuredb'

$App = az ad app create `
  --display-name $GitHubAppName `
  --output json | ConvertFrom-Json

az ad sp create --id $App.appId --output none

$ApplicationClientId       = $App.appId
$ApplicationObjectId       = $App.id
$ServicePrincipalObjectId  = az ad sp show `
  --id $ApplicationClientId `
  --query id `
  --output tsv

[PSCustomObject]@{
  ApplicationClientId      = $ApplicationClientId
  ApplicationObjectId      = $ApplicationObjectId
  ServicePrincipalObjectId = $ServicePrincipalObjectId
} | Format-List
PowerShell output showing redacted application and service principal identifiers
Record the client ID and object IDs securely.

Step 5: Configure the GitHub OIDC federated credential

Create .github/oidc/federated-credential.main.json. The subject must exactly match the repository owner, repository name and branch used by the workflow.

{
  "name": "github-main",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:<OWNER>/<REPOSITORY>:ref:refs/heads/main",
  "description": "GitHub Actions main branch",
  "audiences": ["api://AzureADTokenExchange"]
}
Visual Studio Code displaying the GitHub OIDC federated credential JSON file
Federated credential definition for the main branch.
az ad app federated-credential create `
  --id $ApplicationClientId `
  --parameters '.\.github\oidc\federated-credential.main.json' `
  --output none

az ad app federated-credential list `
  --id $ApplicationClientId `
  --output table
Azure CLI output confirming the GitHub federated credential issuer, name and subject
Verify the issuer, audience and subject before running the workflow.

Step 6: Assign least-privilege Azure RBAC

The original deployment used Contributor at subscription scope. That works, but it is broader than many pipelines need. Prefer the narrowest practical scope, such as a dedicated resource group. Use subscription scope only when the workflow must create the resource group or deploy across several resource groups.

$DeploymentResourceGroup = 'rg-sql-public-dev'
$ResourceGroupScope = "/subscriptions/$SubscriptionId/resourceGroups/$DeploymentResourceGroup"

# Create the target resource group first when using resource-group scope.
az group create `
  --name $DeploymentResourceGroup `
  --location 'swedencentral' `
  --output none

az role assignment create `
  --assignee-object-id $ServicePrincipalObjectId `
  --assignee-principal-type ServicePrincipal `
  --role Contributor `
  --scope $ResourceGroupScope `
  --output none

az role assignment list `
  --assignee-object-id $ServicePrincipalObjectId `
  --scope $ResourceGroupScope `
  --include-inherited `
  --output table
Azure CLI output confirming the deployment service principal role assignment
Verify the principal, role and scope.
Optional: Microsoft Graph permissions

Do not grant Microsoft Graph application permissions for a basic Azure SQL infrastructure deployment. They are required only if the workflow itself creates or modifies Entra users or groups. If that functionality is genuinely required, grant only the specific permissions needed, obtain tenant-admin consent and document the additional risk.

Verification output for optional Microsoft Graph application permissions
Optional Graph permissions used only by identity-management automation.

Step 7: Configure GitHub Actions secrets and variables

Create the following GitHub Actions values. Use environment secrets for production deployments when you want approval gates and stronger separation.

NameTypePurpose
AZURE_CLIENT_IDSecretMicrosoft Entra application client ID
AZURE_TENANT_IDSecretMicrosoft Entra tenant ID
AZURE_SUBSCRIPTION_IDSecretTarget Azure subscription
AZURE_KEY_VAULT_NAMEVariableBootstrap Key Vault name
MY_PUBLIC_IPSecret or variableTemporary firewall rule source IP, when required
SQL_ADMIN_PASSWORDSecretSQL administrator password when passed through the workflow
$Repo = '<OWNER>/<REPOSITORY>'

$AZURE_CLIENT_ID       = $ApplicationClientId
$AZURE_TENANT_ID       = az account show --query tenantId --output tsv
$AZURE_SUBSCRIPTION_ID = az account show --query id --output tsv
$MY_PUBLIC_IP          = (Invoke-RestMethod -Uri 'https://api.ipify.org?format=text').Trim()

gh secret set AZURE_CLIENT_ID       --repo $Repo --body $AZURE_CLIENT_ID
gh secret set AZURE_TENANT_ID       --repo $Repo --body $AZURE_TENANT_ID
gh secret set AZURE_SUBSCRIPTION_ID --repo $Repo --body $AZURE_SUBSCRIPTION_ID
gh secret set MY_PUBLIC_IP          --repo $Repo --body $MY_PUBLIC_IP
gh variable set AZURE_KEY_VAULT_NAME --repo $Repo --body $KeyVaultName
PowerShell output showing redacted Azure deployment identifiers and public IP
Verify that no required value is blank before sending it to GitHub.
GitHub CLI output listing repository secret names and update times
GitHub lists secret names and update times but never displays stored values.

Step 8: Authenticate and deploy from GitHub Actions

The workflow job needs id-token: write so GitHub can issue the OIDC token, plus contents: read to check out the repository.

name: Deploy Azure SQL with Bicep

on:
  workflow_dispatch:
    inputs:
      environment:
        description: Deployment environment
        required: true
        default: dev
        type: choice
        options: [dev, test, prod]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Sign in to Azure with OIDC
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Validate Bicep deployment
        run: |
          az deployment group validate \
            --resource-group "rg-sql-public-${{ inputs.environment }}" \
            --template-file ./infra/main.bicep \
            --parameters environment="${{ inputs.environment }}" \
                         sqlAdminPassword="${{ secrets.SQL_ADMIN_PASSWORD }}" \
                         allowedIpAddress="${{ secrets.MY_PUBLIC_IP }}"

      - name: Preview Azure changes
        run: |
          az deployment group what-if \
            --resource-group "rg-sql-public-${{ inputs.environment }}" \
            --template-file ./infra/main.bicep \
            --parameters environment="${{ inputs.environment }}" \
                         sqlAdminPassword="${{ secrets.SQL_ADMIN_PASSWORD }}" \
                         allowedIpAddress="${{ secrets.MY_PUBLIC_IP }}"

      - name: Deploy Bicep template
        run: |
          az deployment group create \
            --name "azure-sql-${{ github.run_number }}" \
            --resource-group "rg-sql-public-${{ inputs.environment }}" \
            --template-file ./infra/main.bicep \
            --parameters environment="${{ inputs.environment }}" \
                         sqlAdminPassword="${{ secrets.SQL_ADMIN_PASSWORD }}" \
                         allowedIpAddress="${{ secrets.MY_PUBLIC_IP }}"

Run the workflow from GitHub or with GitHub CLI:

gh workflow run deploy-unified.yml `
  --repo '<OWNER>/<REPOSITORY>' `
  --ref main `
  --field database_type=azuresql `
  --field environment=dev
GitHub CLI confirming an Azure SQL deployment workflow dispatch event
The workflow dispatch request was accepted.
gh run list `
  --repo '<OWNER>/<REPOSITORY>' `
  --workflow deploy-unified.yml `
  --limit 3

gh run watch <RUN-ID> `
  --repo '<OWNER>/<REPOSITORY>' `
  --exit-status
GitHub CLI showing a successful Azure SQL infrastructure deployment job
Every deployment step completed successfully.

Step 9: Verify the deployed Azure resources

az resource list `
  --resource-group 'rg-sql-public-dev' `
  --query "[].{Name:name,Type:type,Location:location}" `
  --output table
Azure CLI output listing the SQL server, database, Log Analytics workspace and audit storage account
The expected Azure SQL and monitoring resources are present.
  • SQL logical server
  • User database
  • Log Analytics workspace
  • Audit storage account

Step 10: Test database connectivity

Connect with SQL Server Management Studio or Azure Data Studio using the server’s fully qualified domain name. If the deployment created an Entra user with a temporary password, complete the required password update before testing the database connection.

Microsoft Entra password update and SQL Server Management Studio connection screens with account details redacted
Complete the first sign-in password update, then connect with Microsoft Entra authentication.
SQL Server Management Studio Object Explorer showing the deployed Azure SQL database
The deployed user database is visible in Object Explorer.

Common Problems

ProblemWhat to check
OIDC login failsThe federated credential subject exactly matches the repository, branch or GitHub environment.
Workflow cannot deployThe service principal has an appropriate role at the actual deployment scope.
ProviderNotRegisteredAll required Azure resource providers have reached the Registered state.
SQL connection is blockedThe firewall rule contains the runner/client public IP and the server name is correct.
Audit logs do not arriveThe auditing and diagnostic-settings resources both target valid destinations.

Summary

You now have a repeatable Azure SQL deployment pipeline built with Bicep and GitHub Actions. OIDC removes the need for a long-lived Azure client secret, Bicep keeps the infrastructure definition version-controlled, and workflow validation plus what-if makes changes easier to review before deployment.

References and Next Steps

Leave a Reply

Your email address will not be published. Required fields are marked *