Azure Functions Deployment — CI/CD and Hosting Plans
Deploy Azure Functions in .NET: Consumption vs Premium vs Dedicated hosting plans, GitHub Actions CI/CD pipeline, deployment slots, environment configuration, and production-ready packaging.
Hosting Plan Comparison
Consumption Plan:
→ Pay per execution (first 1M free/month)
→ Auto-scale to 0 — no cost when idle
→ Cold start: 1-10 seconds on first invocation after idle
→ Max timeout: 10 minutes (configurable to 60 with host.json)
→ Not suitable for: Timer triggers with strict timing, WebSocket connections
Premium Plan:
→ Pre-warmed instances — no cold start
→ Longer max timeout (unlimited with Dedicated plan)
→ VNet integration — access to private resources (SQL, Service Bus)
→ More expensive: pay per instance hour, not per execution
→ Use when: cold start is unacceptable, VNet integration required
Dedicated (App Service) Plan:
→ Run on your existing App Service plan
→ No cold start (always warm)
→ Cost included in App Service plan
→ Use when: you already pay for App Service and want to co-host functions
Clinical recommendation:
→ HTTP webhooks on Consumption (low volume, cold start acceptable for callbacks)
→ Timer triggers on Premium (no missed runs, no cold start delays)
→ Service Bus consumers on Consumption (auto-scale to demand, cost-effective)GitHub Actions Deployment
# .github/workflows/deploy-functions.yml
name: Deploy Azure Functions
on:
push:
branches: [main]
paths:
- 'src/Functions/**'
- '.github/workflows/deploy-functions.yml'
env:
DOTNET_VERSION: '8.x'
FUNCTION_APP_NAME: 'clinical-functions'
AZURE_RESOURCE_GROUP: 'clinical-rg'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Build and publish Functions
run: |
dotnet publish \
src/Functions/ClinicalFunctions/ClinicalFunctions.csproj \
--configuration Release \
--output ./functions-publish
- name: Login to Azure
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to staging slot
uses: azure/functions-action@v1
with:
app-name: ${{ env.FUNCTION_APP_NAME }}
slot-name: staging
package: './functions-publish'
respect-pom-xml: false
- name: Run smoke tests against staging
run: |
curl -f "https://${{ env.FUNCTION_APP_NAME }}-staging.azurewebsites.net/api/health" \
-H "x-functions-key: ${{ secrets.FUNCTIONS_STAGING_KEY }}"
- name: Swap staging to production
uses: azure/cli@v2
with:
inlineScript: |
az functionapp deployment slot swap \
--resource-group ${{ env.AZURE_RESOURCE_GROUP }} \
--name ${{ env.FUNCTION_APP_NAME }} \
--slot staging \
--target-slot productionhost.json Configuration
{
"version": "2.0",
"functionTimeout": "00:10:00",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"maxTelemetryItemsPerSecond": 20
}
}
},
"extensions": {
"serviceBus": {
"prefetchCount": 5,
"messageHandlerOptions": {
"maxConcurrentCalls": 4,
"autoComplete": false
}
}
},
"concurrency": {
"dynamicConcurrencyEnabled": true,
"snapshotPersistenceEnabled": true
}
}local.settings.json for Development
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"ServiceBus": "Endpoint=sb://clinical-servicebus.servicebus.windows.net/;...",
"ConnectionStrings__Clinical": "Server=localhost;Database=ClinicalDev;...",
"KeyVaultUri": "https://clinical-kv.vault.azure.net/"
}
}IMPORTANT:
→ local.settings.json is NOT deployed to Azure (it's listed in .gitignore)
→ All settings in local.settings.json["Values"] become environment variables
→ In Azure, set these via Function App → Configuration → Application Settings
→ For secrets: use Key Vault references in Application Settings
Value: @Microsoft.KeyVault(SecretUri=https://clinical-kv.vault.azure.net/secrets/ServiceBus/...)Multi-Environment Configuration
// In Program.cs — environment-aware configuration
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureAppConfiguration((context, config) =>
{
config.AddEnvironmentVariables();
// Load Key Vault in non-development environments
var keyVaultUri = Environment.GetEnvironmentVariable("KeyVaultUri");
if (!string.IsNullOrEmpty(keyVaultUri))
{
config.AddAzureKeyVault(
new Uri(keyVaultUri),
new DefaultAzureCredential());
}
// Local development: use Azurite for storage
if (context.HostingEnvironment.IsDevelopment())
{
Environment.SetEnvironmentVariable(
"AzureWebJobsStorage", "UseDevelopmentStorage=true");
}
})
// ...
.Build();Deployment Validation
// Health check endpoint for smoke tests after deployment
[Function("Health")]
public HttpResponseData Run(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "health")]
HttpRequestData req)
{
var response = req.CreateResponse(HttpStatusCode.OK);
response.WriteAsJsonAsync(new
{
Status = "Healthy",
Version = Assembly.GetExecutingAssembly()
.GetName().Version?.ToString(),
Timestamp = DateTime.UtcNow
});
return response;
}Production issue I've seen: A team deployed Azure Functions on a Consumption plan and had Timer Triggers running nightly prescription expiry jobs. The function app had not run for 14 hours (no HTTP traffic), so it was fully deallocated. The Timer Trigger at 2:00 AM failed to start for 7 minutes (cold start on Consumption plan with a large dependency graph). By the time it started, the timer had been delayed past the expected window. The expired prescription cleanup ran 7 minutes late — acceptable for most scenarios, but it missed 3 prescriptions that had a hard 48-hour cutoff based on the timer's expected firing time. Moving the Timer Trigger to a Premium plan with pre-warmed instances resolved cold start delays.
Key Takeaway
Choose the hosting plan based on workload: Consumption for bursty workloads with cold start tolerance, Premium for always-on or VNet-connected functions, Dedicated for co-hosting with existing App Service plans. Use deployment slots (staging → production swap) for zero-downtime releases. Never commit
local.settings.json— it's for local development only. In Azure, set Application Settings directly or use Key Vault references. Always include a health check function endpoint for post-deployment smoke tests.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.