Home > Blockchain >  Azure Function App infra redeployment makes existing functions in app fail because of missing requir
Azure Function App infra redeployment makes existing functions in app fail because of missing requir

Time:11-02

I'm facing quite a big problem. I have a function app that I deploy by Azure Bicep in the following fashion:

param environmentType string
param location string
param storageAccountSku string
param vnetIntegrationSubnetId string
param kvName string

/*
This module contains the IaC for deploying the Premium function app
*/

/// Just a single minimum instance to start with and max scaling of 3 for dev, 5 for prd ///
var minimumElasticSize = 1
var maximumElasticSize = ((environmentType == 'prd') ? 5 : 3)
var name = 'nlp'
var functionAppName = 'function-app-${name}-${environmentType}'

/// Storage account for service ///
resource functionAppStorage 'Microsoft.Storage/storageAccounts@2019-06-01' = {
  name: 'st4functionapp${name}${environmentType}'
  location: location
  kind: 'StorageV2'
  sku: {
    name: storageAccountSku
  }
  properties: {
    allowBlobPublicAccess: false
    accessTier: 'Hot'
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
  }
}

/// Premium app plan for the service ///
resource servicePlanfunctionApp 'Microsoft.Web/serverfarms@2021-03-01' = {
  name: 'plan-${name}-function-app-${environmentType}'
  location: location
  kind: 'linux'
  sku: {
    name: 'EP1'
    tier: 'ElasticPremium'
    family: 'EP'
  }
  properties: {
    reserved: true
    targetWorkerCount: minimumElasticSize
    maximumElasticWorkerCount: maximumElasticSize
    elasticScaleEnabled: true
    isSpot: false
    zoneRedundant: ((environmentType == 'prd') ? true : false)
  }
}

// Create log analytics workspace
resource logAnalyticsWorkspacefunctionApp 'Microsoft.OperationalInsights/workspaces@2021-06-01' = {
  name: '${name}-functionapp-loganalytics-workspace-${environmentType}'
  location: location
  properties: {
    sku: {
      name: 'PerGB2018' // Standard
    }
  }
}

/// Log analytics workspace insights ///
resource applicationInsightsfunctionApp 'Microsoft.Insights/components@2020-02-02' = {
  name: 'application-insights-${name}-function-${environmentType}'
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    Flow_Type: 'Bluefield'
    publicNetworkAccessForIngestion: 'Enabled'
    publicNetworkAccessForQuery: 'Enabled'
    Request_Source: 'rest'
    RetentionInDays: 30
    WorkspaceResourceId: logAnalyticsWorkspacefunctionApp.id
  }
}

// App service containing the workflow runtime ///
resource sitefunctionApp 'Microsoft.Web/sites@2021-03-01' = {
  name: functionAppName
  location: location
  kind: 'functionapp,linux'
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    clientAffinityEnabled: false
    httpsOnly: true
    serverFarmId: servicePlanfunctionApp.id
    siteConfig: {
      linuxFxVersion: 'python|3.9'
      minTlsVersion: '1.2'
      pythonVersion: '3.9'
      use32BitWorkerProcess: true
      appSettings: [
        {
          name: 'FUNCTIONS_EXTENSION_VERSION'
          value: '~4'
        }
        {
          name: 'FUNCTIONS_WORKER_RUNTIME'
          value: 'python'
        }
        {
          name: 'AzureWebJobsStorage'
          value: 'DefaultEndpointsProtocol=https;AccountName=${functionAppStorage.name};AccountKey=${listKeys(functionAppStorage.id, '2019-06-01').keys[0].value};EndpointSuffix=core.windows.net'
        }
        {
          name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
          value: 'DefaultEndpointsProtocol=https;AccountName=${functionAppStorage.name};AccountKey=${listKeys(functionAppStorage.id, '2019-06-01').keys[0].value};EndpointSuffix=core.windows.net'
        }
        {
          name: 'WEBSITE_CONTENTSHARE'
          value: 'app-${toLower(name)}-functionservice-${toLower(environmentType)}a6e9'
        }
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: applicationInsightsfunctionApp.properties.InstrumentationKey
        }
        {
          name: 'ApplicationInsightsAgent_EXTENSION_VERSION'
          value: '~2'
        }
        {
          name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
          value: applicationInsightsfunctionApp.properties.ConnectionString
        }
        {
          name: 'ENV'
          value: toUpper(environmentType)
        }
      ]
    }
  }

  /// VNET integration so flows can access storage and queue accounts ///
  resource vnetIntegration 'networkConfig@2022-03-01' = {
    name: 'virtualNetwork'
    properties: {
      subnetResourceId: vnetIntegrationSubnetId
      swiftSupported: true
    }
  }
}

/// Outputs for creating access policies ///
output functionAppName string = sitefunctionApp.name
output functionAppManagedIdentityId string = sitefunctionApp.identity.principalId

Output is used for giving permissions to blob/queue and some keyvault stuff. This code is a single module called in a main.bicep module and deployed via an Azure Devops pipeline.

I have a second repository in which I have some functions and which I also deploy via Azure Pipelines. This one contains three .yaml files for deploying, 2 templates (CI and CD) and 1 main pipeline called azure-pipelines.yml pulling it all together:

functions-ci.yml:

parameters:
- name: environment
  type: string

jobs:
- job:
  displayName: 'Publish the function as .zip'
  steps:
  - task: UsePythonVersion@0
    inputs:
      versionSpec: '$(pythonVersion)'
    displayName: 'Use Python $(pythonVersion)'

  - task: CopyFiles@2
    displayName: 'Create project folder'
    inputs:
      SourceFolder: '$(System.DefaultWorkingDirectory)'
      Contents: |
          **
      TargetFolder: '$(Build.ArtifactStagingDirectory)'

  - task: Bash@3
    displayName: 'Install requirements for running function'
    inputs:
      targetType: 'inline'
      script: |
        python3 -m pip install --upgrade pip
        pip install setup
        pip install --target="./.python_packages/lib/site-packages" -r ./requirements.txt
      workingDirectory: '$(Build.ArtifactStagingDirectory)'

  - task: ArchiveFiles@2
    displayName: 'Create project zip'
    inputs:
      rootFolderOrFile: '$(Build.ArtifactStagingDirectory)'
      includeRootFolder: false
      archiveType: 'zip'
      archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip'
      replaceExistingArchive: true

  - task: PublishPipelineArtifact@1
    displayName: 'Publish project zip artifact'
    inputs:
      targetPath: '$(Build.ArtifactStagingDirectory)'
      artifactName: 'functions$(environment)'
      publishLocation: 'pipeline'

functions-cd.yml:

parameters:
- name: environment
  type: string
- name: azureServiceConnection
  type: string

jobs:
- job: worfklowsDeploy
  displayName: 'Deploy the functions'
  steps:
  # Download created artifacts, containing the zipped function codes
  - task: DownloadPipelineArtifact@2
    inputs:
      buildType: 'current'
      artifactName: 'functions$(environment)'
      targetPath: '$(Build.ArtifactStagingDirectory)'

  # Zip deploy the functions code
  - task: AzureFunctionApp@1
    inputs:
      azureSubscription: $(azureServiceConnection)
      appType: functionAppLinux
      appName: function-app-nlp-$(environment)
      package: $(Build.ArtifactStagingDirectory)/**/*.zip
      deploymentMethod: 'zipDeploy'

They are pulled together in azure-pipelines.yml:

trigger:
  branches:
    include:
      - develop
      - main

pool:
  name: "Hosted Ubuntu 1804"

variables:
  ${{ if notIn(variables['Build.SourceBranchName'], 'main') }}:
    environment: dev
    azureServiceConnection: SC-NLPDT
  ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
    environment: prd
    azureServiceConnection: SC-NLPPRD
    pythonVersion: '3.9'

stages:
  # Builds the functions as .zip
  - stage: functions_ci
    displayName: 'Functions CI'
    jobs:
    - template: ./templates/functions-ci.yml
      parameters:
        environment: $(environment)
  
  # Deploys .zip workflows
  - stage: functions_cd
    displayName: 'Functions CD'
    jobs:
    - template: ./templates/functions-cd.yml
      parameters:
        environment: $(environment)
        azureServiceConnection: $(azureServiceConnection)

So this successfully deploys my function app the first time around when I have also deployed the infra code. The imports are done well, the right function app is deployed, and the code runs when I trigger it.

But, when I go and redeploy the infra (bicep) code, all of a sudden I the newest version of the functions is gone and is replaced by a previous version. Also, running this previous version doesn't work anymore since all my requirements that were installed in the pipeline (CI part) via pip install --target="./.python_packages/lib/site-packages" -r ./requirements.txt suddenly cannot be found anymore, giving import errors (i.e. Result: Failure Exception: ModuleNotFoundError: No module named 'azure.identity'). Mind you, this version did work previously just fine.

This is a big problem for me since I need to be able to update some infra stuff (like adding an APP_SETTING) without this breaking the current deployment of functions.

I had thought about just redeploying the function automatically after an infra update, but then I still miss the previous invocations which I need to be able to see.

Am I missing something in the above code because I cannot figure out what would be going wrong here that causes my functions to change on infra deployment...

CodePudding user response:

Looking at the documentation:

To enable your function app to run from a package, add a WEBSITE_RUN_FROM_PACKAGE setting to your function app settings.

1 Indicates that the function app runs from a local package file deployed in the d:\home\data\SitePackages (Windows) or /home/data/SitePackages (Linux) folder of your function app.

In your case, when you deploy your function app code using AzureFunctionApp@1 and zipDeploy, this automatically add this appsetting into your function app. When redeploying your infrastructure, this setting is removed and the function app host does not know where to find the code.

If you add this app setting in your bicep file this should work:

{
  name: 'WEBSITE_RUN_FROM_PACKAGE'
  value: '1'
}
  • Related