Demo of Azure Functions with C# showcasing identity-based authentication to Functions storage account to avoid access keys. Also uses private endpoints to keep the storage account private.
All Azure infrastructure is rolled out via Bicep as infrastructure as code approach.
For local execution and debugging, see official Microsoft documentation.
If not present, the local.settings.json file content for reference:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
}
}The main goal is to deploy Azure Function App and it's needed Azure Storage Account for correct functionaing.
To avoid public access to the Azure Storage Account, we use Private Endpoints and a Virtual Network (VNET) to define private network connectivty. Additionally, we need Private DNS Zones as supporting services.
To integrate Azure Function App to a VNET, we need a supporting hosting option. Here we use App Service Plan, minimum B1 size. It's cheap (approx. $13/month for Linux) and gives also predictable pricing by good control over scaling behavior.
In the following diagram the main dependencies are illustrated:
graph TD;
A[Function App] --> B[Storage Account];
A --> C[Application Insights];
C --> D[Log Analytics Workspace];
B --> E[Private Endpoint];
E --> F[Private Dns Zone];
A --> G[VNET];
G --> F;
A --> H[App Service Plan];
Additionally, the Log Analytics Workspace has active daily ingestion cap to avoid cost overruns for sudden high access and alerting configured, so we aware, that we exceed over defined cap.
For the actual deployment, we use Bicep as infrastructure-as-code technology. Reasons for that:
- It is native to Azure
- Portal directly supports Bicep exports
- Good tooling support - the Bicep plugin in VS Code is outstanding
The source code for it is stored in the iac subfolder and we use a parameter file (main.bicepparam) as well. With other parameter-files, we could roll out the main code for other environments with different sizing etc.
To avoid typing every Azure resource on our own, we use Azure Verified Modules (AVM). They are available for Bicep and Terraform and allow a more consistent interface to all resource definitions and are maintained by Microsoft.
Two GitHub Actions pipeline deploy the complete solution: one the Bicep to provision the Azure infrastructure, the other builds and deploys the C# Azure Functions project into the provisioned Functions App.
To authentication from the GitHub pipeline to Azure, we use User Defined Managed Identities and federated credentials to avoid the management of the secret of a typical service principal.
The helper script devops-setup.sh creates with Azure CLI the needed infrastructure:
- an Azure resource group
devops, which holds the user defined managed identity - an Azure resource group, which will be used by our Bicep deployment pipeline
- configures federated credentials for GitHub (allow main branch and pull requests)
- configures RBAC permissions
With that done, we can enter the needed GitHub repository secrets and variables:
- Secrets
AZURE_TENANT_ID: The Azure tenant id we targetAZURE_SUBSCRIPTION_ID: The Azure subscription id we targetAZURE_CLIENT_ID: The client id of the created user defined managed identity
- Variables
IAC_TARGET_RESOURCE_GROUP: The resource group name into which the Bicep template should be deployed into.FUNCTION_APP_NAME: The name of theAzure Function Appinstance in which we want to deploy into.
With the parameterization it's clear, that before the function can be deployed, the Bicep template must be run, that the final name is clear. I prefered the separation to have faster iteration cycles in my C# function project, then not always the Bicep template runs as well.