你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。

教程:使用装载的 Azure 文件共享进行持久文本分析

在本教程中,你将部署一个 Python Azure Functions 应用,该应用使用 Durable Functions 来协调并行文本文件分析。 函数应用会装载 Azure 文件存储共享,并行分析多个文本文件(扇出),聚合结果(扇入),然后将其返回给调用方。 此方法演示了存储挂载点的一个关键优势:能够跨多个函数实例共享文件访问,而无需为每个请求增加网络开销。

在本教程中,你将了解:

  • 使用 Azure Developer CLI 通过装载的 Azure 文件存储共享在 Flex 消耗计划中部署 Durable Functions 应用
  • 触发编排以并行处理示例文本文件
  • 验证聚合分析结果

注释

本文的代码示例在 Azure Functions Flex Consumption with Azure 文件存储 OS Mount Samples GitHub 存储库中提供。

先决条件

本教程中的 CLI 示例使用 Bash 语法,并在 Azure Cloud Shell (Bash) 和 Linux/macOS 终端中进行了测试。

初始化示例项目

可以在 Azure Functions Flex Consumption with Azure 文件存储 OS Mount Samples GitHub 存储库中找到本教程的示例代码。 该 durable-text-analysis 文件夹包含函数应用代码、预配所需 Azure 资源的 Bicep 模板和上传示例文本文件的部署后脚本。

  1. 打开终端,转到要克隆存储库的目录。

  2. 克隆存储库:

    git clone https://github.com/Azure-Samples/Azure-Functions-Flex-Consumption-with-Azure-Files-OS-Mount-Samples.git
    
  3. 转到项目文件夹:

    cd Azure-Functions-Flex-Consumption-with-Azure-Files-OS-Mount-Samples/durable-text-analysis
    
  4. azd初始化环境。 出现提示时,输入环境名称,例如 durable-text:

    azd init
    

查看代码

使此示例工作的三个关键部分是创建装载的基础结构、上传示例文件的脚本以及协调分析的函数代码。

在运行时,编排遵循以下流程:

  1. 向/api/start-analysis发送的 HTTP POST 将启动Durable Functions编排。
  2. 业务流程协调程序调用 list_text_files 活动来查找装载中的所有 .txt 文件。
  3. 业务流程协调程序将扇出,从而并行为每个文件调用 analyse_text_file。
  4. 完成所有并行任务后,业务流程协调程序会调用 aggregate_results 将每文件指标合并到单个摘要中。
  5. 聚合结果作为编排输出返回。

该 mounts.bicep 模块在函数应用上配置 Azure 文件共享 SMB 挂载。 该值 mountPath 确定文件在运行时出现的本地路径。 将存储帐户访问密钥作为参数传递,平台通过 密钥保管库 引用在运行时解析它:

@description('Function app name')
param functionAppName string

@description('Storage account name')
param storageAccountName string

@description('Storage account access key or app setting reference for Azure Files SMB mount')
param accessKey string

@description('Array of mount configurations')
param mounts array

// Function app reference
resource functionApp 'Microsoft.Web/sites@2023-12-01' existing = {
  name: functionAppName
}

// Azure Files OS mount configuration
// Deploys azureStorageAccounts site config with all mounts in one shot
resource mountConfig 'Microsoft.Web/sites/config@2023-12-01' = {
  parent: functionApp
  name: 'azurestorageaccounts'
  properties: reduce(mounts, {}, (cur, mount) => union(cur, {
    '${mount.name}': {
      type: 'AzureFiles'
      shareName: mount.shareName
      mountPath: mount.mountPath
      accountName: storageAccountName
      accessKey: accessKey
    }
  }))
}

output mountPaths array = [for mount in mounts: mount.mountPath]

由于 Azure 文件存储 SMB 装载尚不支持托管标识身份验证,因此需要存储帐户密钥。 最佳做法是将此密钥存储在 Azure 密钥保管库 中,并在应用设置中使用 密钥保管库 引用 。 装载配置使用 @AppSettingRef()引用应用设置,因此密钥永远不会显示在 Bicep 模板中。 该 keyvault.bicep 模块创建保管库、存储密钥并授予 RBAC 角色:

@description('Key Vault name')
param name string

@description('Location')
param location string

@description('Tags')
param tags object = {}

@description('Storage account name')
param storageAccountName string

@description('Principal ID of the function app identity (receives Key Vault Secrets User role)')
param functionAppPrincipalId string

@description('Principal ID of the deploying user (receives Key Vault Secrets Officer role)')
param deployerPrincipalId string = ''

// Storage account reference
resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' existing = {
  name: storageAccountName
}

// Key Vault with RBAC authorization
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: name
  location: location
  tags: tags
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: tenant().tenantId
    enableRbacAuthorization: true
    enabledForTemplateDeployment: true
    enableSoftDelete: true
    softDeleteRetentionInDays: 7
  }
}

// Store storage account key as a secret (Azure Files mounts require shared key)
resource storageKeySecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
  parent: keyVault
  name: 'storageAccountKey'
  properties: {
    value: storage.listKeys().keys[0].value
    contentType: 'Storage account access key for Azure Files SMB mount'
  }
}

// Built-in Key Vault RBAC role IDs
var roles = {
  KeyVaultSecretsOfficer: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7')
  KeyVaultSecretsUser: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
}

// Grant the function app identity read access to secrets
resource functionAppSecretsUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(keyVault.id, functionAppPrincipalId, roles.KeyVaultSecretsUser)
  scope: keyVault
  properties: {
    roleDefinitionId: roles.KeyVaultSecretsUser
    principalId: functionAppPrincipalId
    principalType: 'ServicePrincipal'
  }
}

// Grant the deployer manage access to secrets
resource deployerSecretsOfficer 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(deployerPrincipalId)) {
  name: guid(keyVault.id, deployerPrincipalId, roles.KeyVaultSecretsOfficer)
  scope: keyVault
  properties: {
    roleDefinitionId: roles.KeyVaultSecretsOfficer
    principalId: deployerPrincipalId
    principalType: 'User'
  }
}

output name string = keyVault.name
output uri string = keyVault.properties.vaultUri
output storageKeySecretUri string = storageKeySecret.properties.secretUri

main.bicep 文件调用装载和密钥保管库模块:


// Key Vault for secure storage of Azure Files access key
module keyVault './app/keyvault.bicep' = {
  name: 'keyVault'
  scope: rg
  params: {
    name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}'
    location: location
    tags: tags
    storageAccountName: storage.outputs.name
    functionAppPrincipalId: processorIdentity.outputs.principalId
    deployerPrincipalId: principalId
  }
}

// Azure Files mount configuration (access key resolved via Key Vault reference)
module azureFilesMount './app/mounts.bicep' = {
  name: 'azureFilesMount'
  scope: rg
  params: {
    functionAppName: functionApp.outputs.name
    storageAccountName: storage.outputs.name
    accessKey: '@AppSettingRef(MOUNT_SECRET_REFERENCE)'
    mounts: [
      {
        name: 'data'
        shareName: 'data'
        mountPath: '/mounts/data/'
      }
    ]
  }
  dependsOn: [
    functionAppRoleAssignments
  ]

使用 Azure 开发人员 CLI 进行部署

此示例是 Azure 开发人员 CLI (azd) 模板。 单个 azd up 命令预配基础结构、部署函数代码并将示例文本文件上传到 Azure 文件共享。

  1. 登录到Azure。 部署后脚本使用 Azure CLI 命令,因此需要使用这两种工具进行身份验证:

    azd auth login
    az login
    
  2. 配置和部署所有内容:

    azd up
    

    出现提示时,选择要使用的 Azure 订阅和位置。 命令如下:

    • 创建资源组、存储帐户、密钥保管库、采用 Durable Functions 配置的 Flex 消耗函数应用、Application Insights 实例和托管标识。
    • 部署 Python 函数代码
    • 将示例文本文件上传到 Azure 文件共享
    • 运行运行状况检查

    注释

    由于 Azure 文件存储 SMB 装载尚不支持托管标识身份验证,因此需要存储帐户密钥。 最佳做法是,部署将此密钥存储在 Azure 密钥保管库 中,并使用 密钥保管库 引用 ,以便密钥永远不会在应用设置中公开。 此方法提供对密钥轮换的集中式机密管理、审核和支持。

    部署需要几分钟时间。 完成后,会看到已创建资源的摘要。

  3. 将资源名称另存为剩余步骤的 shell 变量:

    RESOURCE_GROUP=$(azd env get-value AZURE_RESOURCE_GROUP)
    FUNCTION_APP_NAME=$(azd env get-value AZURE_FUNCTION_APP_NAME)
    FUNCTION_APP_URL=$(azd env get-value AZURE_FUNCTION_APP_URL)
    

触发业务流程协调程序

  1. 获取函数主机密钥:

    HOST_KEY=$(az functionapp keys list \
      --resource-group $RESOURCE_GROUP \
      --name $FUNCTION_APP_NAME \
      --query "functionKeys.default" \
      -o tsv)
    
  2. 启动编排。 可以选择通过 {"mount_path": "/mounts/data/"} 传递 JSON 正文来覆盖默认装载路径:

    curl -s -X POST "${FUNCTION_APP_URL}/api/start-analysis?code=${HOST_KEY}" | jq .
    

    响应包括实例 ID 和管理 URI,可用于检查状态、发送事件或终止业务流程:

    {
      "id": "abc123def456",
      "statusQueryGetUri": "https://<your-app>.azurewebsites.net/...",
      "sendEventPostUri": "https://...",
      "terminatePostUri": "https://..."
    }
    

验证结果

  1. 使用触发器响应中的 statusQueryGetUri 检查编排状态。

    STATUS_URL="<statusQueryGetUri-from-trigger-response>"
    
    curl -s "${STATUS_URL}" | jq .
    

    在业务流程正在运行时,runtimeStatus 为 Running。 完成后,响应如下所示:

    {
      "instanceId": "abc123def456",
      "runtimeStatus": "Completed",
      "output": {
        "total_files": 3,
        "total_words": 45,
        "total_lines": 12,
        "total_chars": 303,
        "overall_avg_word_length": 4.82,
        "overall_top_characters": [["e", 42], ["t", 38], ["a", 35]],
        "per_file": [
          {
            "file_path": "/mounts/data/sample1.txt",
            "word_count": 15,
            "line_count": 4,
            "char_count": 98,
            "avg_word_length": 5.1,
            "top_characters": [["e", 14], ["t", 12]]
          },
          {
            "file_path": "/mounts/data/sample2.txt",
            "word_count": 18,
            "line_count": 5,
            "char_count": 120,
            "avg_word_length": 4.6,
            "top_characters": [["a", 16], ["e", 15]]
          },
          {
            "file_path": "/mounts/data/sample3.txt",
            "word_count": 12,
            "line_count": 3,
            "char_count": 85,
            "avg_word_length": 4.9,
            "top_characters": [["e", 13], ["t", 11]]
          }
        ]
      }
    }
    

    该应用还公开了一个便捷接口,该 /api/status/{instance_id} 接口返回简化状态响应。

Tip

函数应用程序通过存储挂载并行访问所有三个文件。 应用不需要每个请求的任何网络调用。 该函数使用标准文件 I/O 直接从挂载的共享中读取它们。 这种方法演示了将存储装载与 Durable Functions 相结合将获得的强大功能。

清理资源

若要避免持续收费,请删除本教程创建的所有资源:

azd down --purge

Warning

此命令删除资源组及其中的所有资源,包括函数应用、存储帐户和 Application Insights 实例。