Reputation: 3413
I've created a simple Azure Function and I'd like to use it as an exercise of good DevOps practices. I prepared an azure-pipelines.yml
, which does the following:
I heard a lot about Infrastructure as a Code, and I really wanted to try it out, that's why point 4 is there.
Here's my azure-pipelines.yml
:
trigger:
- master
variables:
azureServiceConnection: service-connection
appName: az-func-123456123
resourceGroup: rg-1223456123
location: North Europe
buildConfiguration: Release
pool:
vmImage: 'ubuntu-latest'
stages:
- stage: CI
jobs:
- job: Azure_Function
displayName: 'Azure Functions'
steps:
- checkout: self
- task: DotNetCoreCLI@2
displayName: Restore
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: Build
inputs:
command: build
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: Test
inputs:
command: test
projects: '**/*Tests/*.csproj'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: Zip Artifact
inputs:
command: publish
publishWebProjects: false
arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: True
workingDirectory: src/az-function-with-deployment
- publish: $(Build.ArtifactStagingDirectory)
artifact: AzureFunction
- stage: Deployment
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
jobs:
- deployment: Deploy
environment: test-env
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: AzureResourceGroupDeployment@2
displayName: Deploy Azure resources
inputs:
deploymentScope: 'Resource Group'
ConnectedServiceName: '$(azureServiceConnection)'
action: 'Create Or Update Resource Group'
resourceGroupName: $(resourceGroup)
location: $(location)
templateLocation: 'Linked artifact'
csmFile: 'templates/function-app-deployment.json'
deploymentMode: 'Incremental'
- task: AzureFunctionApp@1
displayName: Deploy Azure Function
inputs:
azureSubscription: $(azureServiceConnection)
resourceGroupName: $(resourceGroup)
appType: functionAppLinux
appName: $(appName)
package: $(Pipeline.Workspace)/AzureFunction/*.zip
Here's my templates/function-app-deployment.json
that the AzureResourceGroupDeployment@2
task uses:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"appName": {
"type": "string",
"defaultValue": "az-func-123456123",
"metadata": {
"description": "The name of the function app that you wish to create."
}
},
"storageAccountType": {
"type": "string",
"defaultValue": "Standard_LRS",
"allowedValues": [
"Standard_LRS",
"Standard_GRS",
"Standard_RAGRS"
],
"metadata": {
"description": "Storage Account type"
}
},
"location": {
"type": "string",
"defaultValue": "[resourceGroup().location]",
"metadata": {
"description": "Location for all resources."
}
},
"appInsightsLocation": {
"type": "string",
"defaultValue": "[resourceGroup().location]",
"metadata": {
"description": "Location for Application Insights"
}
},
"runtime": {
"type": "string",
"defaultValue": "dotnet",
"allowedValues": [
"node",
"dotnet",
"java"
],
"metadata": {
"description": "The language worker runtime to load in the function app."
}
}
},
"variables": {
"functionAppName": "[parameters('appName')]",
"hostingPlanName": "[parameters('appName')]",
"applicationInsightsName": "[parameters('appName')]",
"storageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]",
"functionWorkerRuntime": "[parameters('runtime')]"
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-06-01",
"name": "[variables('storageAccountName')]",
"location": "[parameters('location')]",
"sku": {
"name": "[parameters('storageAccountType')]"
},
"kind": "Storage"
},
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2020-06-01",
"name": "[variables('hostingPlanName')]",
"location": "[parameters('location')]",
"kind": "linux",
"sku": {
"tier": "Dynamic",
"name": "Y1"
},
"properties": {
"name": "[variables('hostingPlanName')]",
"reserved": true
}
},
{
"type": "Microsoft.Web/sites",
"apiVersion": "2020-06-01",
"name": "[variables('functionAppName')]",
"location": "[parameters('location')]",
"kind": "functionapp",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
],
"properties": {
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]",
"siteConfig": {
"appSettings": [
{
"name": "AzureWebJobsStorage",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
},
{
"name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';EndpointSuffix=', environment().suffixes.storage, ';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
},
{
"name": "WEBSITE_CONTENTSHARE",
"value": "[toLower(variables('functionAppName'))]"
},
{
"name": "FUNCTIONS_EXTENSION_VERSION",
"value": "~2"
},
{
"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
"value": "[reference(resourceId('microsoft.insights/components', variables('applicationInsightsName')), '2020-02-02-preview').InstrumentationKey]"
},
{
"name": "FUNCTIONS_WORKER_RUNTIME",
"value": "[variables('functionWorkerRuntime')]"
}
],
"linuxFxVersion": "dotnet|3.1"
}
}
},
{
"type": "microsoft.insights/components",
"apiVersion": "2020-02-02-preview",
"name": "[variables('applicationInsightsName')]",
"location": "[parameters('appInsightsLocation')]",
"tags": {
"[concat('hidden-link:', resourceId('Microsoft.Web/sites', variables('applicationInsightsName')))]": "Resource"
},
"properties": {
"ApplicationId": "[variables('applicationInsightsName')]",
"Request_Source": "IbizaWebAppExtensionCreate"
}
}
]
}
The deployment works and I get my resources created. However, I noticed a few issues:
In general, I'll be happy to see any comments about both the YAML and JSON that I posted. I am sure there's a lot of place for improvement.
Upvotes: 2
Views: 1781
Reputation: 463
There is also another issue you should consider.
Infrastructure deployment is not always fully synchronous. Even if you wait five minutes the infrastructure deployment might be running something in the background and ends up deleting the functions you deployed after your infrastructure task or pipeline is completed.
If you don't have good monitoring issue like that is easy to miss.
I heard a lot about Infrastructure as a Code, and I really wanted to try it out, that's why point 4 is there.
That is the correct way to do things but multiple Azure resources sadly are not designed for that.
Function app team really should use some time re-inventing app config section and split it into two parts:
Upvotes: 0
Reputation: 40603
The issue that you run ARM deployment each time you want to deploy function. So it provides you AppSettings only those declared in template. Please take a look here - Don't delete AppSettings not declared in a template.
What you can do to approach this?!
Move ARM template deployment to separate pipeline - the one dedicated just to update infrastructure for you pipeline. There is no need to redeploy infrastructure each time you want to deploy code. You can use path filter to be sure that your pipeline will run only when proper changes was done. But, this doesn't solve issue with removing app settings. It makes that it happens less often.
To address issue with deleted settings you need to do one of the thing:
Upvotes: 2