|
| 1 | +# Example |
| 2 | +# Azure DevOps CI/CD Pipeline Template for Azure Functions |
| 3 | +# This template provides a complete CI/CD workflow for building and deploying Azure Function Apps |
| 4 | +# It includes both build and deployment stages with support for deployment slots and multi-environment deployments |
| 5 | + |
| 6 | +parameters: |
| 7 | + # Build Configuration (Optional) |
| 8 | + # Specifies whether to build in Debug or Release mode |
| 9 | + buildConfiguration: 'Release' |
| 10 | + |
| 11 | + # Branch Name (Required) |
| 12 | + # The Git branch to build from (e.g., 'main', 'develop', 'feature/xyz') |
| 13 | + branchName: '' |
| 14 | + |
| 15 | + # Artifact Name (Optional) |
| 16 | + # Name for the resulting build artifact |
| 17 | + artifactName: 'Setup' |
| 18 | + |
| 19 | + # NuGet Feed (Required) |
| 20 | + # Your Azure DevOps NuGet feed name (e.g., 'MyOrganization/MyFeed') |
| 21 | + nugetFeed: '' |
| 22 | + |
| 23 | + # .NET SDK Version (Optional) |
| 24 | + # The version of .NET SDK to use for building |
| 25 | + dotnetVersion: '6.0.x' |
| 26 | + |
| 27 | + # Azure Subscription (Required for deployment) |
| 28 | + # The name of your Azure DevOps service connection to Azure |
| 29 | + azureSubscription: '' |
| 30 | + |
| 31 | + # Function App Name (Required for deployment) |
| 32 | + # The name of your Azure Function App resource |
| 33 | + functionAppName: '' |
| 34 | + |
| 35 | + # Resource Group Name (Required for deployment) |
| 36 | + # The name of the Azure resource group containing your Function App |
| 37 | + resourceGroupName: '' |
| 38 | + |
| 39 | + # Deploy to Slot (Optional) |
| 40 | + # Whether to use deployment slots for zero-downtime deployments |
| 41 | + deployToSlot: false |
| 42 | + |
| 43 | + # Slot Name (Optional, required if deployToSlot is true) |
| 44 | + # Name of the deployment slot if using slots |
| 45 | + slotName: 'staging' |
| 46 | + |
| 47 | + # Environment (Optional) |
| 48 | + # Target environment name (dev/test/prod) |
| 49 | + environment: 'dev' |
| 50 | + |
| 51 | + # Swap After Deployment (Optional) |
| 52 | + # Whether to swap slots after successful deployment |
| 53 | + swapAfterDeployment: true |
| 54 | + |
| 55 | + # App Settings (Optional) |
| 56 | + # Array of application settings to configure (key-value pairs) |
| 57 | + appSettings: [] |
| 58 | + |
| 59 | + # Deployment Method (Optional) |
| 60 | + # The method used to deploy the Function App |
| 61 | + # Valid options: |
| 62 | + # - 'auto': Lets Azure decide the best deployment method based on the app type and configuration. |
| 63 | + # - 'zipDeploy': Basic ZIP deployment that extracts files to the wwwroot folder. |
| 64 | + # - 'zipDeployWithRestartAppSetting': Sets WEBSITE_RUN_FROM_PACKAGE=1 and deploys as a ZIP package (run-from-package). |
| 65 | + # - 'runFromPackage': Sets WEBSITE_RUN_FROM_PACKAGE to a URL of your package. |
| 66 | + # - 'webDeploy': Uses MSDeploy (Web Deploy) protocol for deployment. |
| 67 | + # For writable deployments prefer 'zipDeploy' or 'webDeploy'. |
| 68 | + deploymentMethod: 'zipDeploy' |
| 69 | + |
| 70 | + # Writable Deployment |
| 71 | + # true => deploy files to wwwroot (writable approach). Pre-deploy step will remove WEBSITE_RUN_FROM_PACKAGE. |
| 72 | + # false => keep mounted/package behavior (run-from-package). Use 'zipDeployWithRestartAppSetting' or 'runFromPackage'. |
| 73 | + writableDeployment: true |
| 74 | + |
| 75 | +stages: |
| 76 | + ################################################# |
| 77 | + # BUILD STAGE |
| 78 | + # Compiles the code and creates deployment packages |
| 79 | + ################################################# |
| 80 | + - stage: Build |
| 81 | + displayName: Build ${{ parameters.artifactName }} for ${{ parameters.branchName }} |
| 82 | + jobs: |
| 83 | + - job: Build |
| 84 | + displayName: Build Function App |
| 85 | + steps: |
| 86 | + # Step 1: Checkout the correct branch |
| 87 | + # This step fetches the source code from the repository |
| 88 | + - checkout: self |
| 89 | + clean: true # Remove any previous files |
| 90 | + persistCredentials: true # Keep git credentials for potential later git operations |
| 91 | + fetchDepth: 0 # Fetch full history |
| 92 | + displayName: Checkout ${{ parameters.branchName }} |
| 93 | + |
| 94 | + # Step 2: Ensure branch synchronization |
| 95 | + # This ensures we're working with the latest code from the specified branch |
| 96 | + # It fetches all remote branches and resets the local branch to match the remote |
| 97 | + - script: | |
| 98 | + git fetch --all |
| 99 | + git clean -fd |
| 100 | + git reset --hard origin/${{ parameters.branchName }} |
| 101 | + displayName: Synchronize with ${{ parameters.branchName }} |
| 102 | +
|
| 103 | + # Step 3: Install .NET Core SDK |
| 104 | + # Ensures the correct .NET SDK version is available for building |
| 105 | + - task: UseDotNet@2 |
| 106 | + inputs: |
| 107 | + packageType: 'sdk' |
| 108 | + version: '${{ parameters.dotnetVersion }}' |
| 109 | + displayName: Install .NET SDK Version ${{ parameters.dotnetVersion }} |
| 110 | + |
| 111 | + # Step 4: Restore NuGet Packages |
| 112 | + - task: NuGetCommand@2 |
| 113 | + inputs: |
| 114 | + command: 'restore' |
| 115 | + restoreSolution: '**/*.sln' |
| 116 | + feedsToUse: 'select' |
| 117 | + vstsFeed: '${{ parameters.nugetFeed }}' |
| 118 | + displayName: Restore NuGet Packages |
| 119 | + |
| 120 | + # Step 5: Build the Solution |
| 121 | + - task: DotNetCoreCLI@2 |
| 122 | + displayName: Publish Function App |
| 123 | + inputs: |
| 124 | + command: 'publish' |
| 125 | + projects: '**/*.csproj' |
| 126 | + arguments: '--configuration ${{ parameters.buildConfiguration }} --output $(Build.ArtifactStagingDirectory)/publish' |
| 127 | + publishWebProjects: false # Ensures we publish all projects, not just web projects |
| 128 | + zipAfterPublish: false # Don't zip during publish, we'll do that in next step for more control |
| 129 | + |
| 130 | + # Step 6: Archive Published Artifacts |
| 131 | + - task: ArchiveFiles@2 |
| 132 | + inputs: |
| 133 | + rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/publish' |
| 134 | + includeRootFolder: false # Don't include the publish folder itself in the archive |
| 135 | + archiveType: 'zip' # Create a ZIP archive |
| 136 | + archiveFile: '$(Build.ArtifactStagingDirectory)/archives/${{ parameters.artifactName }}-${{ parameters.environment }}-${{ parameters.branchName }}.zip' |
| 137 | + replaceExistingArchive: true # Overwrite any existing archive with the same name |
| 138 | + displayName: Archive Function App Package |
| 139 | + |
| 140 | + # Step 7: Publish Archived Build Artifacts |
| 141 | + - task: PublishBuildArtifacts@1 |
| 142 | + inputs: |
| 143 | + PathtoPublish: '$(Build.ArtifactStagingDirectory)/archives' |
| 144 | + ArtifactName: 'archives' # Name of the artifact feed in Azure DevOps |
| 145 | + publishLocation: 'Container' # Publish to the pipeline's artifact storage |
| 146 | + displayName: Publish Function App Artifacts |
| 147 | + |
| 148 | + ################################################# |
| 149 | + # DEPLOYMENT STAGE |
| 150 | + # Deploys the built artifacts to Azure Function App |
| 151 | + # This stage will only run if the required deployment parameters are provided |
| 152 | + ################################################# |
| 153 | + - stage: Deploy |
| 154 | + displayName: Deploy to ${{ parameters.environment }} Environment |
| 155 | + dependsOn: Build # Only run after Build stage completes successfully |
| 156 | + condition: and(succeeded(), ne('${{ parameters.azureSubscription }}', ''), ne('${{ parameters.functionAppName }}', ''), ne('${{ parameters.resourceGroupName }}', '')) |
| 157 | + jobs: |
| 158 | + - deployment: DeployFunctionApp |
| 159 | + displayName: Deploy Function App |
| 160 | + # Create an environment in Azure DevOps for tracking deployments |
| 161 | + environment: ${{ parameters.environment }}-${{ parameters.functionAppName }} |
| 162 | + strategy: |
| 163 | + # Define a standard deployment strategy |
| 164 | + runOnce: |
| 165 | + deploy: |
| 166 | + steps: |
| 167 | + # Step 1: Download the artifacts from the build stage |
| 168 | + # This retrieves the ZIP package created in the Build stage |
| 169 | + # The deployment job automatically downloads artifacts to $(Pipeline.Workspace) |
| 170 | + - download: current |
| 171 | + artifact: archives |
| 172 | + displayName: Download Build Artifacts |
| 173 | + |
| 174 | + # --- PRE-DEPLOY: Prepare app for writable deployment --- |
| 175 | + # If writableDeployment is true we remove the WEBSITE_RUN_FROM_PACKAGE app setting (recommended) |
| 176 | + # This converts the app from mounted-package (run-from-package) to writable wwwroot. |
| 177 | + ${{ if eq(parameters.writableDeployment, true) }}: |
| 178 | + - task: AzureCLI@2 |
| 179 | + displayName: 'Prepare app for writable deployment: unset WEBSITE_RUN_FROM_PACKAGE' |
| 180 | + inputs: |
| 181 | + azureSubscription: '${{ parameters.azureSubscription }}' |
| 182 | + scriptType: 'bash' |
| 183 | + scriptLocation: 'inlineScript' |
| 184 | + inlineScript: | |
| 185 | + set -e |
| 186 | + APP_NAME='${{ parameters.functionAppName }}' |
| 187 | + RG='${{ parameters.resourceGroupName }}' |
| 188 | + # If using slot, include slot argument |
| 189 | + SLOT_ARG='' |
| 190 | + if [ '${{ parameters.deployToSlot }}' = 'true' ]; then |
| 191 | + # Note: azure CLI slot argument uses --slot <name> (no quotes) |
| 192 | + SLOT_ARG="--slot ${{ parameters.slotName }}" |
| 193 | + fi |
| 194 | +
|
| 195 | + # Option A (default): delete the app setting key (recommended to fully remove run-from-package) |
| 196 | + echo "Deleting WEBSITE_RUN_FROM_PACKAGE app setting (if present)..." |
| 197 | + az webapp config appsettings delete --name "$APP_NAME" --resource-group "$RG" --setting-names WEBSITE_RUN_FROM_PACKAGE $SLOT_ARG || true |
| 198 | +
|
| 199 | + # Optional: display current setting (for diagnostics) |
| 200 | + echo "Current WEBSITE_RUN_FROM_PACKAGE value (if any):" |
| 201 | + az webapp config appsettings list --name "$APP_NAME" --resource-group "$RG" $SLOT_ARG --query "[?name=='WEBSITE_RUN_FROM_PACKAGE']" -o table || true |
| 202 | +
|
| 203 | + echo "App prepared for writable deployment." |
| 204 | +
|
| 205 | + # --- DEPLOY PACKAGE --- |
| 206 | + # This deploys the ZIP package to the Azure Function App using the specified deployment method. |
| 207 | + - task: AzureFunctionApp@1 |
| 208 | + inputs: |
| 209 | + azureSubscription: '${{ parameters.azureSubscription }}' # Azure service connection |
| 210 | + appType: 'functionApp' # Specifies this is a Function App |
| 211 | + appName: '${{ parameters.functionAppName }}' # Name of the Function App in Azure |
| 212 | + # Configure slot deployment if enabled |
| 213 | + ${{ if eq(parameters.deployToSlot, true) }}: |
| 214 | + deployToSlotOrASE: true # Deploy to a slot |
| 215 | + resourceGroupName: '${{ parameters.resourceGroupName }}' # Resource group containing the Function App |
| 216 | + slotName: '${{ parameters.slotName }}' # Name of the slot to deploy to |
| 217 | + ${{ if ne(parameters.deployToSlot, true) }}: |
| 218 | + deployToSlotOrASE: false # Deploy directly to production |
| 219 | + package: '$(Pipeline.Workspace)/archives/${{ parameters.artifactName }}-${{ parameters.environment }}-${{ parameters.branchName }}.zip' # Path to the ZIP package |
| 220 | + deploymentMethod: '${{ parameters.deploymentMethod }}' # Use the specified deployment method |
| 221 | + displayName: Deploy to Azure Function App ${{ parameters.functionAppName }} using ${{ parameters.deploymentMethod }} |
| 222 | + |
| 223 | + # Step 3: Configure Application Settings |
| 224 | + # This sets up environment variables and app settings for the Function App |
| 225 | + # Only runs if appSettings parameter is provided |
| 226 | + - task: AzureAppServiceSettings@1 |
| 227 | + condition: and(succeeded(), gt(length('${{ parameters.appSettings }}'), 0)) |
| 228 | + inputs: |
| 229 | + azureSubscription: '${{ parameters.azureSubscription }}' |
| 230 | + appName: '${{ parameters.functionAppName }}' |
| 231 | + resourceGroupName: '${{ parameters.resourceGroupName }}' |
| 232 | + ${{ if eq(parameters.deployToSlot, true) }}: |
| 233 | + slotName: '${{ parameters.slotName }}' |
| 234 | + appSettings: '${{ parameters.appSettings }}' |
| 235 | + displayName: Configure Application Settings |
| 236 | + |
| 237 | + # Step 4: Swap Slots (if enabled) |
| 238 | + # This swaps the staging slot with production for zero-downtime deployments |
| 239 | + # Only runs if deployToSlot and swapAfterDeployment are both true |
| 240 | + - task: AzureAppServiceManage@0 |
| 241 | + condition: and(succeeded(), eq(parameters.deployToSlot, true), eq(parameters.swapAfterDeployment, true)) |
| 242 | + inputs: |
| 243 | + azureSubscription: '${{ parameters.azureSubscription }}' |
| 244 | + Action: 'Swap Slots' |
| 245 | + WebAppName: '${{ parameters.functionAppName }}' |
| 246 | + ResourceGroupName: '${{ parameters.resourceGroupName }}' |
| 247 | + SourceSlot: '${{ parameters.slotName }}' |
| 248 | + displayName: Swap Deployment Slots |
| 249 | + |
| 250 | + # --- POST-DEPLOY: Restart the app if using writableDeployment (good to clear state) --- |
| 251 | + ${{ if eq(parameters.writableDeployment, true) }}: |
| 252 | + - task: AzureCLI@2 |
| 253 | + displayName: 'Restart Function App (writable deployment)' |
| 254 | + inputs: |
| 255 | + azureSubscription: '${{ parameters.azureSubscription }}' |
| 256 | + scriptType: 'bash' |
| 257 | + scriptLocation: 'inlineScript' |
| 258 | + inlineScript: | |
| 259 | + APP_NAME='${{ parameters.functionAppName }}' |
| 260 | + RG='${{ parameters.resourceGroupName }}' |
| 261 | + SLOT_ARG='' |
| 262 | + if [ '${{ parameters.deployToSlot }}' = 'true' ]; then |
| 263 | + SLOT_ARG="--slot ${{ parameters.slotName }}" |
| 264 | + fi |
| 265 | +
|
| 266 | + echo "Restarting $APP_NAME..." |
| 267 | + az webapp restart --name "$APP_NAME" --resource-group "$RG" $SLOT_ARG || true |
| 268 | + echo "Restart completed." |
| 269 | +
|
| 270 | + # Step 5: Verify Deployment |
| 271 | + # This checks if the deployment was successful |
| 272 | + - script: | |
| 273 | + echo "Deployment to ${{ parameters.functionAppName }} completed successfully" |
| 274 | + echo "Environment: ${{ parameters.environment }}" |
| 275 | + echo "Deployment Method: ${{ parameters.deploymentMethod }}" |
| 276 | + if [ '${{ parameters.deployToSlot }}' = 'true' ]; then |
| 277 | + echo "Deployed to slot: ${{ parameters.slotName }}" |
| 278 | + fi |
| 279 | + echo "Function App URL: https://${{ parameters.functionAppName }}${{ if eq(parameters.deployToSlot, true) }}-${{ parameters.slotName }}${{ end }}.azurewebsites.net" |
| 280 | + displayName: Verify Deployment |
| 281 | +
|
| 282 | + ################################################# |
| 283 | + # OPTIONAL: SMOKE TEST STAGE |
| 284 | + # Runs basic tests against the deployed Function App |
| 285 | + # This stage will only run if the deployment stage succeeds |
| 286 | + ################################################# |
| 287 | + - stage: SmokeTest |
| 288 | + displayName: Run Smoke Tests |
| 289 | + dependsOn: Deploy |
| 290 | + condition: and(succeeded(), ne('${{ parameters.azureSubscription }}', ''), ne('${{ parameters.functionAppName }}', '')) |
| 291 | + jobs: |
| 292 | + - job: TestFunctionApp |
| 293 | + displayName: Test Function App Endpoints |
| 294 | + steps: |
| 295 | + # Run basic health check tests against the deployed Function App |
| 296 | + # This ensures the deployment is functioning correctly |
| 297 | + - task: PowerShell@2 |
| 298 | + inputs: |
| 299 | + targetType: 'inline' |
| 300 | + script: | |
| 301 | + # Define the base URL for the Function App (slot-aware) |
| 302 | + $baseUrl = "https://${{ parameters.functionAppName }}.azurewebsites.net" |
| 303 | + if ('${{ parameters.deployToSlot }}' -eq 'true') { |
| 304 | + $baseUrl = "https://${{ parameters.functionAppName }}-${{ parameters.slotName }}.azurewebsites.net" |
| 305 | + } |
| 306 | + |
| 307 | + Write-Host "Running health check against $baseUrl" |
| 308 | + |
| 309 | + try { |
| 310 | + # You can replace this with actual endpoint tests specific to your Function App |
| 311 | + $response = Invoke-WebRequest -Uri $baseUrl -Method Get -TimeoutSec 30 -ErrorAction Stop |
| 312 | + Write-Host "Health check status code: $($response.StatusCode)" |
| 313 | + |
| 314 | + if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) { |
| 315 | + Write-Host "##vso[task.complete result=Succeeded;]Health check passed" |
| 316 | + } else { |
| 317 | + Write-Warning "Health check returned unexpected status code: $($response.StatusCode)" |
| 318 | + } |
| 319 | + } catch { |
| 320 | + Write-Warning "Health check failed: $_" |
| 321 | + } |
| 322 | + failOnStderr: false |
| 323 | + pwsh: true |
| 324 | + displayName: Run Function App Health Check |
| 325 | + continueOnError: true # Continue even if health check fails |
0 commit comments