Azure_Function_SA-JSON
Azure Function SA, JSON
Step-by-step:
Focusing on DefaultAzureCredential for Azure Storage access and calling another function.
Assumptions:
You have Azure CLI installed and configured (
az login).You have .NET 9 SDK installed.
You have an Azure Subscription where you can create resources.
The "target" Azure Function (the one being called) already exists or you know its URL and function key. If it doesn't exist, you'll need to create a simple HTTP trigger function for testing.
Phase 1: Project Setup and Code Implementation
Create New Azure Function Project (Isolated Worker, .NET 9):
dotnet new func --isolated-worker --target-framework net9.0 -o StorageToJsonRelayFunction cd StorageToJsonRelayFunctionfunc init StorageJsonRelayFunc --worker-runtime dotnet-isolated --language C#Add Necessary NuGet Packages:
dotnet add package Azure.Identity # For DefaultAzureCredential dotnet add package Azure.Storage.Blobs # For Azure Blob Storage dotnet add package Microsoft.Extensions.Http # For IHttpClientFactory dotnet add package Microsoft.Extensions.Azure # For simplified Azure SDK client registration (optional but good) # Microsoft.Extensions.Configuration.Json, Options.ConfigurationExtensions, DependencyInjection are usually includedDefine Model Classes:
Models/DataModel.cs(Represents the structure ofdata.json):// Models/DataModel.cs namespace StorageToJsonRelayFunction.Models; public class DataModel { public string? Id { get; set; } public string? OriginalMessage { get; set; } public int SomeValue { get; set; } public DateTime Timestamp { get; set; } }Models/ForwardingPayload.cs(Data to be sent to the target function):// Models/ForwardingPayload.cs namespace StorageToJsonRelayFunction.Models; public class ForwardingPayload { public string? SourceId { get; set; } public string? ProcessedMessage { get; set; } public int Value { get; set; } }
Define Configuration Settings Classes:
Settings/StorageSettings.cs:// Settings/StorageSettings.cs namespace StorageToJsonRelayFunction.Settings; public class StorageSettings { public const string SectionName = "Storage"; public string AccountName { get; set; } = string.Empty; public string ContainerName { get; set; } = string.Empty; public string FileName { get; set; } = "data.json"; // Default file name }Settings/ForwardingSettings.cs:// Settings/ForwardingSettings.cs namespace StorageToJsonRelayFunction.Settings; public class ForwardingSettings { public const string SectionName = "Forwarding"; public string TargetFunctionUrl { get; set; } = string.Empty; public string? TargetFunctionKey { get; set; } // Nullable, as some functions might use other auth }
Create Services:
Services/IStorageService.cs:// Services/IStorageService.cs using StorageToJsonRelayFunction.Models; using System.Threading.Tasks; namespace StorageToJsonRelayFunction.Services; public interface IStorageService { Task<DataModel?> GetJsonDataAsync(); }Services/StorageService.cs:// Services/StorageService.cs using Azure.Identity; using Azure.Storage.Blobs; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StorageToJsonRelayFunction.Models; using StorageToJsonRelayFunction.Settings; using System; using System.IO; using System.Text.Json; using System.Threading.Tasks; namespace StorageToJsonRelayFunction.Services; public class StorageService : IStorageService { private readonly StorageSettings _settings; private readonly ILogger<StorageService> _logger; private readonly BlobServiceClient _blobServiceClient; public StorageService(IOptions<StorageSettings> settings, ILogger<StorageService> logger) { _settings = settings.Value; _logger = logger; if (string.IsNullOrWhiteSpace(_settings.AccountName)) { throw new ArgumentException("Storage AccountName must be configured.", nameof(settings)); } // Use DefaultAzureCredential for authentication // It will try various auth methods: Managed Identity, Azure CLI, VS, etc. var blobServiceUri = new Uri($"httpsकर्मियों://{_settings.AccountName}.blob.core.windows.net"); _blobServiceClient = new BlobServiceClient(blobServiceUri, new DefaultAzureCredential()); } public async Task<DataModel?> GetJsonDataAsync() { try { _logger.LogInformation("Attempting to read {FileName} from container {ContainerName} in storage account {AccountName}.", _settings.FileName, _settings.ContainerName, _settings.AccountName); BlobContainerClient containerClient = _blobServiceClient.GetBlobContainerClient(_settings.ContainerName); BlobClient blobClient = containerClient.GetBlobClient(_settings.FileName); if (!await blobClient.ExistsAsync()) { _logger.LogWarning("Blob {FileName} does not exist in container {ContainerName}.", _settings.FileName, _settings.ContainerName); return null; } var response = await blobClient.DownloadStreamingAsync(); using var streamReader = new StreamReader(response.Value.Content); string content = await streamReader.ReadToEndAsync(); var dataModel = JsonSerializer.Deserialize<DataModel>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); _logger.LogInformation("Successfully read and deserialized {FileName}.", _settings.FileName); return dataModel; } catch (Exception ex) { _logger.LogError(ex, "Error reading or deserializing JSON data from blob storage."); return null; } } }Services/IFunctionOrchestratorService.cs:// Services/IFunctionOrchestratorService.cs using System.Threading.Tasks; namespace StorageToJsonRelayFunction.Services; public interface IFunctionOrchestratorService { Task<bool> ProcessAndForwardDataAsync(); }Services/FunctionOrchestratorService.cs:// Services/FunctionOrchestratorService.cs using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StorageToJsonRelayFunction.Models; using StorageToJsonRelayFunction.Settings; using System; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; // Requires System.Net.Http.Json NuGet package (often transitive) using System.Text.Json; using System.Threading.Tasks; namespace StorageToJsonRelayFunction.Services; public class FunctionOrchestratorService : IFunctionOrchestratorService { private readonly IStorageService _storageService; private readonly ForwardingSettings _forwardingSettings; private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger<FunctionOrchestratorService> _logger; public FunctionOrchestratorService( IStorageService storageService, IOptions<ForwardingSettings> forwardingSettings, IHttpClientFactory httpClientFactory, ILogger<FunctionOrchestratorService> logger) { _storageService = storageService; _forwardingSettings = forwardingSettings.Value; _httpClientFactory = httpClientFactory; _logger = logger; if (string.IsNullOrWhiteSpace(_forwardingSettings.TargetFunctionUrl)) { throw new ArgumentException("TargetFunctionUrl must be configured.", nameof(forwardingSettings)); } } public async Task<bool> ProcessAndForwardDataAsync() { _logger.LogInformation("Starting data processing and forwarding..."); var data = await _storageService.GetJsonDataAsync(); if (data == null) { _logger.LogWarning("No data retrieved from storage. Aborting forwarding."); return false; } var payload = new ForwardingPayload { SourceId = data.Id, ProcessedMessage = $"Modified: {data.OriginalMessage} - Processed at {DateTime.UtcNow:O}", Value = data.SomeValue }; _logger.LogInformation("Data processed. Forwarding payload to {TargetFunctionUrl}", _forwardingSettings.TargetFunctionUrl); try { var httpClient = _httpClientFactory.CreateClient("ForwardingClient"); var request = new HttpRequestMessage(HttpMethod.Post, _forwardingSettings.TargetFunctionUrl); if (!string.IsNullOrWhiteSpace(_forwardingSettings.TargetFunctionKey)) { request.Headers.Add("x-functions-key", _forwardingSettings.TargetFunctionKey); _logger.LogInformation("Added x-functions-key header."); } else { _logger.LogInformation("No TargetFunctionKey provided. Assuming target function uses other auth (e.g., AAD, anonymous)."); } request.Content = JsonContent.Create(payload); var response = await httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { _logger.LogInformation("Successfully forwarded data. Target function responded with {StatusCode}.", response.StatusCode); return true; } else { var errorContent = await response.Content.ReadAsStringAsync(); _logger.LogError("Failed to forward data. Target function responded with {StatusCode}. Content: {ErrorContent}", response.StatusCode, errorContent); return false; } } catch (Exception ex) { _logger.LogError(ex, "Error occurred while forwarding data to target function."); return false; } } }
Create the Azure Function Trigger:
ProcessStorageDataAndForwardFunction.cs(HTTP Trigger for easy testing, could be TimerTrigger):// ProcessStorageDataAndForwardFunction.cs using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; using StorageToJsonRelayFunction.Services; using System.Net; using System.Threading.Tasks; namespace StorageToJsonRelayFunction; public class ProcessStorageDataAndForwardFunction { private readonly ILogger<ProcessStorageDataAndForwardFunction> _logger; private readonly IFunctionOrchestratorService _orchestratorService; public ProcessStorageDataAndForwardFunction( ILogger<ProcessStorageDataAndForwardFunction> logger, IFunctionOrchestratorService orchestratorService) { _logger = logger; _orchestratorService = orchestratorService; } [Function("ProcessAndRelayData")] public async Task<HttpResponseData> Run( [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequestData req) { _logger.LogInformation("ProcessAndRelayData HTTP trigger function invoked."); bool success = await _orchestratorService.ProcessAndForwardDataAsync(); var response = req.CreateResponse(success ? HttpStatusCode.OK : HttpStatusCode.InternalServerError); response.Headers.Add("Content-Type", "application/json; charset=utf-8"); if (success) { await response.WriteStringAsync("{\"status\": \"Data processed and forwarded successfully.\"}"); } else { await response.WriteStringAsync("{\"status\": \"Failed to process or forward data. Check logs.\"}"); } return response; } }
Update
Program.csfor Configuration and DI:// Program.cs using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; // For explicit logging config using StorageToJsonRelayFunction.Services; using StorageToJsonRelayFunction.Settings; using System.IO; // Required for Path.GetDirectoryName var host = new HostBuilder() .ConfigureAppConfiguration((hostingContext, config) => { var env = hostingContext.HostingEnvironment; var appAssemblyPath = System.Reflection.Assembly.GetExecutingAssembly().Location; config.SetBasePath(Path.GetDirectoryName(appAssemblyPath)) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); if (env.IsDevelopment()) { config.AddUserSecrets<Program>(optional: true); } // Environment variables (from local.settings.json or Azure App Settings) are added by default // by ConfigureFunctionsWorkerDefaults or can be added explicitly: config.AddEnvironmentVariables(); }) .ConfigureFunctionsWorkerDefaults() // Or .ConfigureFunctionsWebApplication() if using ASP.NET Core integration .ConfigureServices((hostContext, services) => { IConfiguration configuration = hostContext.Configuration; // Register strongly-typed settings services.Configure<StorageSettings>(configuration.GetSection(StorageSettings.SectionName)); services.Configure<ForwardingSettings>(configuration.GetSection(ForwardingSettings.SectionName)); // Register HttpClientFactory services.AddHttpClient("ForwardingClient", client => { // You can configure default headers or base address here if needed // client.Timeout = TimeSpan.FromSeconds(30); // Example }); // Register custom services services.AddSingleton<IStorageService, StorageService>(); services.AddSingleton<IFunctionOrchestratorService, FunctionOrchestratorService>(); // Example of more granular logging control if needed services.AddLogging(loggingBuilder => { loggingBuilder.AddConfiguration(configuration.GetSection("Logging")); loggingBuilder.AddConsole(); // Add other providers like ApplicationInsights if needed }); }) .Build(); host.Run();Configuration Files:
appsettings.json(defaults, commit this):{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.Hosting.Lifetime": "Information", "StorageToJsonRelayFunction": "Debug" // More verbose for our app } }, "Storage": { "AccountName": "YOUR_DEFAULT_STORAGE_ACCOUNT_NAME_HERE_IF_ANY", // e.g. some shared dev one "ContainerName": "input-data", "FileName": "data.json" }, "Forwarding": { "TargetFunctionUrl": "https://YOUR_DEFAULT_TARGET_FUNCTION_URL.azurewebsites.net/api/ReceiveDataTrigger" // TargetFunctionKey should NOT be here. Use User Secrets or local.settings.json for local, Key Vault for Azure. } }Ensure "Copy to Output Directory" is set to "Copy if newer" or "Copy always" for
appsettings.jsonand anyappsettings.Development.jsonin their file properties or.csproj:<ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> <None Update="appsettings.Development.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <DependentUpon>appsettings.json</DependentUpon> </None> <!-- Add other appsettings like appsettings.Production.json if needed --> </ItemGroup>appsettings.Development.json(local dev overrides, commit this if no secrets):{ "Storage": { "AccountName": "stcdsoptmzdev" // Example for dev environment }, "Forwarding": { "TargetFunctionUrl": "http://localhost:7071/api/MyTargetFunction" // If testing target locally } }local.settings.json(local Azure Functions host settings, DO NOT COMMIT if it contains secrets):{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", // For Azurite "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "AZURE_FUNCTIONS_ENVIRONMENT": "Development", // Critical for loading appsettings.Development.json // These override appsettings.json and appsettings.Development.json "Storage:AccountName": "stcdsoptmzdev", // Your specific dev storage account "Storage:ContainerName": "rawdata", // Your specific container "Storage:FileName": "data.json", "Forwarding:TargetFunctionUrl": "https://your-actual-dev-target-func.azurewebsites.net/api/YourTargetHttpTrigger", // For local dev, you can put the key here or use User Secrets. // User Secrets are generally safer if this file might be accidentally committed. "Forwarding:TargetFunctionKey": "YOUR_DEV_TARGET_FUNCTION_KEY_HERE" // If DefaultAzureCredential needs explicit setup for local dev (e.g. Service Principal): // "AZURE_CLIENT_ID": "your-sp-client-id", // "AZURE_TENANT_ID": "your-tenant-id", // "AZURE_CLIENT_SECRET": "your-sp-client-secret" } }Important: Add
local.settings.jsonto your.gitignore.User Secrets (Optional but Recommended for local
TargetFunctionKey): Right-click project -> "Manage User Secrets" or CLI:dotnet user-secrets init dotnet user-secrets set "Forwarding:TargetFunctionKey" "YOUR_LOCAL_DEV_TARGET_FUNCTION_KEY"If you use User Secrets for the key, you can remove it from
local.settings.json.
Sample
data.jsonfor Storage: Create a file nameddata.jsonwith content like this and upload it to your Azure Blob Storage container:{ "id": "doc123", "originalMessage": "Hello from Blob Storage!", "someValue": 42, "timestamp": "2023-10-27T10:30:00Z" }
Phase 2: Local Testing
Set up Azurite (Storage Emulator):
Install and run Azurite (e.g., via VS Code extension or Docker).
Ensure
local.settings.jsonhas"AzureWebJobsStorage": "UseDevelopmentStorage=true",.Create a container (e.g., "rawdata") in Azurite and upload your
data.json.Update
local.settings.json->"Storage:AccountName"to be your Azurite account name (usuallydevstoreaccount1whenUseDevelopmentStorage=trueis fully parsed, butDefaultAzureCredentialwon't useUseDevelopmentStorage=truefor your custom blob access. Instead, for local testing with Azurite andDefaultAzureCredential, you might need to target127.0.0.1:10000/devstoreaccount1or ensure your Azure CLI login has access to a real dev storage account).Easier for
DefaultAzureCredentiallocally:Log in with Azure CLI:
az loginEnsure your logged-in user has "Storage Blob Data Reader" role on the Azure Storage Account specified in
local.settings.json(stcdsoptmzdev).Upload
data.jsonto therawdatacontainer instcdsoptmzdevon Azure.
Run the Function Locally:
func start # or F5 in Visual Studio / VS CodeOpen the URL provided for
ProcessAndRelayData(e.g.,http://localhost:7071/api/ProcessAndRelayData) in a browser or Postman. Check the console logs for output.
Phase 3: Deployment with Azure CLI
This script creates necessary resources and deploys the function.
deploy-function.sh(Bash script):#!/bin/bash # --- Configuration --- RESOURCE_GROUP="rg-cds-optmz-dev" LOCATION="eastus" # Or your preferred region STORAGE_ACCOUNT_NAME="stcdsoptmzdev$(openssl rand -hex 4)" # Make unique FUNCTION_APP_NAME="func-storagerelay-cds-dev-$(openssl rand -hex 4)" # Make unique APP_INSIGHTS_NAME="appi-${FUNCTION_APP_NAME}" # Target Function - assumed to exist or you'll replace these # For a real deployment, TARGET_FUNCTION_KEY would come from Key Vault TARGET_FUNCTION_URL_VALUE="https://YOUR_EXISTING_TARGET_FUNCTION.azurewebsites.net/api/YourTargetHttpTriggerName" TARGET_FUNCTION_KEY_VALUE="YOUR_TARGET_FUNCTION_KEY_FOR_DEV_ENV" # Replace or manage via Key Vault later # Blob Storage details (where data.json is) DATA_STORAGE_ACCOUNT_NAME="stcdsoptmzdev" # The account where data.json resides DATA_STORAGE_CONTAINER_NAME="rawdata" DATA_STORAGE_FILE_NAME="data.json" # --- Script --- echo "Starting Azure Function deployment..." # 1. Create Resource Group (if not exists) echo "Creating/Updating resource group: $RESOURCE_GROUP" az group create --name "$RESOURCE_GROUP" --location "$LOCATION" -o none # 2. Create Storage Account for Function App (if not exists) # This is for the Function App's own operational storage, NOT necessarily where data.json is. echo "Creating storage account for Function App: $STORAGE_ACCOUNT_NAME" az storage account create \ --name "$STORAGE_ACCOUNT_NAME" \ --resource-group "$RESOURCE_GROUP" \ --location "$LOCATION" \ --sku Standard_LRS \ --kind StorageV2 \ -o none # 3. Create Application Insights (optional but recommended) echo "Creating Application Insights: $APP_INSIGHTS_NAME" az monitor app-insights component create \ --app "$APP_INSIGHTS_NAME" \ --resource-group "$RESOURCE_GROUP" \ --location "$LOCATION" \ --kind web \ -o none APP_INSIGHTS_KEY=$(az monitor app-insights component show --app "$APP_INSIGHTS_NAME" -g "$RESOURCE_GROUP" --query "instrumentationKey" -o tsv) # 4. Create Function App echo "Creating Function App: $FUNCTION_APP_NAME" az functionapp create \ --name "$FUNCTION_APP_NAME" \ --storage-account "$STORAGE_ACCOUNT_NAME" \ --resource-group "$RESOURCE_GROUP" \ --consumption-plan-location "$LOCATION" \ --runtime dotnet-isolated \ --runtime-version 9.0 \ --functions-version 4 \ --os-type Windows \ --assign-identity "[system]" \ --app-insights "$APP_INSIGHTS_NAME" \ --app-insights-key "$APP_INSIGHTS_KEY" \ -o none echo "Function App created. Waiting a bit for identity to propagate..." sleep 30 # Give Azure time to provision the managed identity # 5. Grant Managed Identity access to the DATA Storage Account # Get the Principal ID of the Function App's System-Assigned Managed Identity FUNC_PRINCIPAL_ID=$(az functionapp identity show --name "$FUNCTION_APP_NAME" --resource-group "$RESOURCE_GROUP" --query "principalId" -o tsv) if [ -z "$FUNC_PRINCIPAL_ID" ] || [ "$FUNC_PRINCIPAL_ID" == "null" ]; then echo "ERROR: Could not retrieve Principal ID for Function App. Exiting." exit 1 fi echo "Function App Principal ID: $FUNC_PRINCIPAL_ID" # Get the Resource ID of the DATA Storage Account DATA_STORAGE_ACCOUNT_ID=$(az storage account show --name "$DATA_STORAGE_ACCOUNT_NAME" --resource-group "$RESOURCE_GROUP" --query "id" -o tsv) if [ -z "$DATA_STORAGE_ACCOUNT_ID" ]; then echo "ERROR: Could not retrieve Resource ID for Data Storage Account '$DATA_STORAGE_ACCOUNT_NAME'. Ensure it exists in '$RESOURCE_GROUP'." exit 1 fi echo "Data Storage Account ID: $DATA_STORAGE_ACCOUNT_ID" echo "Assigning 'Storage Blob Data Reader' role to Function App's Managed Identity on Data Storage Account..." az role assignment create \ --assignee "$FUNC_PRINCIPAL_ID" \ --role "Storage Blob Data Reader" \ --scope "$DATA_STORAGE_ACCOUNT_ID" \ -o none echo "Role assignment complete." # 6. Configure Function App Settings # For production, TargetFunctionKey should be a Key Vault reference echo "Configuring App Settings for $FUNCTION_APP_NAME..." az functionapp config appsettings set --name "$FUNCTION_APP_NAME" --resource-group "$RESOURCE_GROUP" \ --settings \ "AZURE_FUNCTIONS_ENVIRONMENT=Development" \ "FUNCTIONS_WORKER_RUNTIME=dotnet-isolated" \ "Storage__AccountName=$DATA_STORAGE_ACCOUNT_NAME" \ "Storage__ContainerName=$DATA_STORAGE_CONTAINER_NAME" \ "Storage__FileName=$DATA_STORAGE_FILE_NAME" \ "Forwarding__TargetFunctionUrl=$TARGET_FUNCTION_URL_VALUE" \ "Forwarding__TargetFunctionKey=$TARGET_FUNCTION_KEY_VALUE" \ "APPINSIGHTS_INSTRUMENTATIONKEY=$APP_INSIGHTS_KEY" \ -o none echo "App Settings configured." # 7. Deploy the Function App code # Ensure you are in the project root directory (StorageToJsonRelayFunction) echo "Building the project..." dotnet publish -c Release -o ./publish_output echo "Deploying Function App code from ./publish_output..." cd ./publish_output || exit zip -r ../deploy.zip . cd .. az functionapp deployment source config-zip \ --name "$FUNCTION_APP_NAME" \ --resource-group "$RESOURCE_GROUP" \ --src "./deploy.zip" \ -o none echo "Deployment submitted." echo "Function App URL: https://$FUNCTION_APP_NAME.azurewebsites.net" echo "To invoke: https://$FUNCTION_APP_NAME.azurewebsites.net/api/ProcessAndRelayData (may require function key based on HttpTrigger auth level)" # Clean up build artifacts rm -rf ./publish_output rm ./deploy.zip echo "Script finished."Before running:
Make sure
DATA_STORAGE_ACCOUNT_NAME(stcdsoptmzdev) andRESOURCE_GROUP(rg-cds-optmz-dev) exist and you have permissions.The script creates a new storage account for the function app itself. The
DATA_STORAGE_ACCOUNT_NAMEis where yourdata.jsonlives.Replace
TARGET_FUNCTION_URL_VALUEandTARGET_FUNCTION_KEY_VALUEwith actual values for your dev environment.Run
chmod +x deploy-function.shand then./deploy-function.sh.
Phase 4: Azure DevOps YAML Pipeline Update
The pipeline will now need to handle:
The specific settings for this function.
Setting up Key Vault references for secrets like
TargetFunctionKey.
Updated azure-pipelines.yml:
trigger:
- main
pool:
vmImage: 'windows-latest' # Or 'ubuntu-latest'
variables:
- name: buildConfiguration
value: 'Release'
- name: dotnetVersion
value: '9.0.x'
- name: functionAppNameBase # Base name, environment will be appended
value: 'func-storagerelay-cds'
- name: dataStorageAccountName # The storage account where data.json is located
value: 'stcdsoptmzdev' # This should be consistent for the dev env or configured per env
- name: dataStorageContainerName
value: 'rawdata'
- name: dataStorageFileName
value: 'data.json'
stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- task: UseDotNet@2
displayName: 'Use .NET SDK $(dotnetVersion)'
inputs:
packageType: 'sdk'
version: '$(dotnetVersion)'
- task: DotNetCoreCLI@2
displayName: 'Restore, Build, Publish Function App'
inputs:
command: 'publish'
publishWebProjects: false
projects: '**/StorageToJsonRelayFunction.csproj' # Path to your csproj
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/App --runtime win-x64 --self-contained false'
zipAfterPublish: true
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifact: App'
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/App'
ArtifactName: 'App'
publishLocation: 'Container'
- stage: DeployDev
displayName: 'Deploy to Development'
dependsOn: Build
condition: succeeded()
variables:
- group: StorageRelayFunction-Dev-Vars # Variable group for DEV
# This group should contain:
# DevTargetFunctionUrl: (your dev target function URL)
# DevTargetFunctionKeySecretUri: (Key Vault Secret URI for the target function key)
# DevFunctionAppName: (e.g., func-storagerelay-cds-dev)
jobs:
- deployment: DeployFunctionAppDev
environment: 'StorageRelayFunction-Development' # Azure DevOps Environment
strategy:
runOnce:
deploy:
steps:
- task: AzureFunctionApp@2
displayName: 'Deploy Azure Function App to Dev'
inputs:
azureSubscription: 'Your-Azure-Dev-Subscription-Service-Connection'
appType: 'functionApp'
appName: '$(DevFunctionAppName)' # From Variable Group or define directly
package: '$(Pipeline.Workspace)/App/**/*.zip'
deploymentMethod: 'auto'
appSettings: >-
-AZURE_FUNCTIONS_ENVIRONMENT "Development"
-Storage__AccountName "$(dataStorageAccountName)"
-Storage__ContainerName "$(dataStorageContainerName)"
-Storage__FileName "$(dataStorageFileName)"
-Forwarding__TargetFunctionUrl "$(DevTargetFunctionUrl)"
-Forwarding__TargetFunctionKey "@Microsoft.KeyVault(SecretUri=$(DevTargetFunctionKeySecretUri))"
# Ensure Function App has Managed Identity enabled and role assignment for dataStorageAccountName
# --- Similar stages for Test, UAT, Prod ---
# Example for Prod (ensure approvals are set on the 'StorageRelayFunction-Production' ADO Environment)
- stage: DeployProd
displayName: 'Deploy to Production'
dependsOn: DeployDev # Or DeployUAT if you have it
condition: succeeded() # And potentially manual approval on the environment
variables:
- group: StorageRelayFunction-Prod-Vars # Variable group for PROD
# This group should contain:
# ProdTargetFunctionUrl: (your prod target function URL)
# ProdTargetFunctionKeySecretUri: (Key Vault Secret URI for the prod target function key)
# ProdFunctionAppName: (e.g., func-storagerelay-cds-prod)
jobs:
- deployment: DeployFunctionAppProd
environment: 'StorageRelayFunction-Production'
strategy:
runOnce:
deploy:
steps:
- task: AzureFunctionApp@2
displayName: 'Deploy Azure Function App to Prod'
inputs:
azureSubscription: 'Your-Azure-Prod-Subscription-Service-Connection'
appType: 'functionApp'
appName: '$(ProdFunctionAppName)'
package: '$(Pipeline.Workspace)/App/**/*.zip'
deploymentMethod: 'runFromPackage' # Recommended for prod
appSettings: >-
-AZURE_FUNCTIONS_ENVIRONMENT "Production"
-Storage__AccountName "$(dataStorageAccountName)" # Or make this env-specific too
-Storage__ContainerName "$(dataStorageContainerName)"
-Storage__FileName "$(dataStorageFileName)"
-Forwarding__TargetFunctionUrl "$(ProdTargetFunctionUrl)"
-Forwarding__TargetFunctionKey "@Microsoft.KeyVault(SecretUri=$(ProdTargetFunctionKeySecretUri))"
Azure DevOps Variable Groups Setup:
For each environment (e.g., StorageRelayFunction-Dev-Vars):
Create the Variable Group in Azure DevOps (Pipelines -> Library).
Link to Azure Key Vault:
Toggle "Link secrets from an Azure key vault as variables".
Select Azure subscription and the Key Vault for that environment (e.g.,
kv-cds-optmz-dev).Add secrets from Key Vault. For example, if your Key Vault secret for the dev target function key is named
TargetFuncDevKey, you'd add it. The pipeline variable might then be namedTargetFuncDevKeyor you could give it an alias in the variable group.In the YAML, you then reference it like
$(DevTargetFunctionKeySecretUri). This variable in the group should hold the full Secret URI from Key Vault:https://your-kv-name.vault.azure.net/secrets/YourSecretName/YourSecretVersionGuid. Or, better, if your Key Vault is linked and you add the secretMySecretNameFromKVto the variable group, you can directly use@Microsoft.KeyVault(VaultName=yourKVNameInVG;SecretName=MySecretNameFromKV)if the AzureFunctionApp task supports this simplified syntax for linked KVs. If not, store the full Secret URI as a variable in the VG. The most robust way is to have a variable in the VG likeDevTargetFunctionKeySecretUriwhose value is the actual Secret URI from Key Vault.
Add Non-Secret Variables:
DevTargetFunctionUrl:https://dev-target-func.azurewebsites.net/api/MyTriggerDevFunctionAppName:func-storagerelay-cds-devIf
dataStorageAccountNamechanges per environment, add it here too.
Important for Pipeline Deployment & Managed Identity:
The Function App deployed by the pipeline must have a System-Assigned or User-Assigned Managed Identity. This is usually configured when the Function App is first provisioned (e.g., via ARM/Bicep template or initial
az functionapp create --assign-identity).This Managed Identity must be granted the "Storage Blob Data Reader" role on the
DATA_STORAGE_ACCOUNT_NAME(stcdsoptmzdevin this example). This role assignment needs to be done once per environment. It can be done manually, viaaz cli(as in thedeploy-function.shscript), or as part of your Infrastructure as Code (IaC) setup.
This detailed setup provides a full cycle from local development to CI/CD deployment using best practices for configuration and Azure resource interaction. Remember to replace placeholders with your actual resource names, URLs, and service connection names.
When the func is not loaded
The command dotnet new func relies on the Azure Functions Core Tools templates being installed and recognized by the dotnet new command system.
The error message "No templates or sub commands found matching: 'func'" clearly indicates that the .NET CLI cannot find the Azure Functions project templates. This usually means the Azure Functions Core Tools (which provide these templates) are either not installed, or their templates haven't been properly registered with dotnet new.
Step 1: Install/Update Azure Functions Core Tools
The Azure Functions Core Tools are required for local Azure Functions development and provide the necessary project templates. The recommended way to install them often depends on your OS and preferences, but using npm is a common cross-platform method.
Option A: Using npm (Node Package Manager - Recommended for cross-platform)
Install Node.js and npm: If you don't have Node.js and npm installed, download and install them from https://nodejs.org/. LTS version is usually fine.
Install or Update Azure Functions Core Tools: Open your terminal or command prompt and run:
npm install -g azure-functions-core-tools@4 --unsafe-perm true@4specifies version 4.x, which is the current major version for Azure Functions V4 runtime (compatible with .NET 6, 7, 8, and upcoming .NET 9).--unsafe-perm trueis sometimes needed on Linux/macOS for global installs to handle permissions correctly.
Option B: Using Chocolatey (Windows)
Install Chocolatey: If you don't have it, follow instructions at https://chocolatey.org/install.
Install Azure Functions Core Tools:
choco install azure-functions-core-tools
Option C: Using Homebrew (macOS)
Install Homebrew: If you don't have it, follow instructions at https://brew.sh/.
Install Azure Functions Core Tools:
brew tap azure/functions brew install azure-functions-core-tools@4
Option D: Platform-Specific Installers (Windows MSI)
You can also download the MSI installer directly from the Azure Functions Core Tools GitHub releases page. Look for Azure.Functions.Cli.*.x64.msi.
Step 2: Verify Core Tools Installation and dotnet new Templates
Close and Reopen your Terminal/Command Prompt: This is crucial to ensure that any PATH changes made by the installer take effect.
Verify
funccommand:func --versionYou should see an output like
4.x.xxxx.Check
dotnet newlist for function templates:dotnet new list functionOr, more broadly:
dotnet new listYou should now see Azure Functions templates listed, such as:
Azure Functions(func)Http Trigger(HttpTrigger)Timer Trigger(TimerTrigger)etc.
If they are still missing after installing/reinstalling Core Tools and restarting the terminal, you might need to manually install the templates:
dotnet new install Microsoft.Azure.WebJobs.ItemTemplates dotnet new install Microsoft.Azure.WebJobs.ProjectTemplatesThen check
dotnet new listagain.
Step 3: Re-run the Project Creation Command
Once you've confirmed the Azure Functions Core Tools are installed and the templates are available, the original command should work:
dotnet new func --isolated-worker --target-framework net9.0 -o StorageToJsonRelayFunction
cd StorageToJsonRelayFunctionReasoning for the Original Error & Fix:
The dotnet new command system is extensible. Different SDKs and tools (like Azure Functions Core Tools) can provide their own templates. When you run dotnet new <template-short-name>, the CLI looks for installed templates matching that short name. func is the short name for the main Azure Functions project template. If the Core Tools (which supply this template) aren't installed or their template manifest isn't registered, dotnet new won't find it.
By installing the Azure Functions Core Tools, you're not just getting the func CLI for running functions locally, but you're also installing the necessary project and item templates that integrate with dotnet new.
Thank you for catching that! It's a common prerequisite that's easy to overlook when jumping straight into the dotnet new command.
Last updated