Cómo consultar Synapse Analytics desde Azure Functions

Este artículo resume cómo ejecutar consultas desde Azure Functions utilizando Azure Synapse Analytics.
Pasos
Transformación y copia de datos con Data Factory
Primero, prepare los datos que desea analizar. En este ejemplo, analizamos registros almacenados en Storage. Después de transformar los datos a un formato más adecuado para el análisis, colóquelos en Azure Data Lake Storage Gen2.
Aquí usamos un servicio llamado Data Factory. Con Data Factory se realiza la copia de datos. Como durante la copia puede elegir el formato, lo convertiremos a Parquet.
A continuación se muestra un ejemplo de Bicep. En este ejemplo se usan comodines y expresiones en la configuración de rutas, pero ajuste esas partes según sus requisitos. Normalmente es más fácil crearlo desde la UI en Data Factory Studio. También puede exportar los pipelines creados en Studio como archivos Bicep.
1param factoryName string2param tags object3@secure()4param subscriptionId string5param sourceResourceGroupName string6param inputStorageAccountName string7param outputStorageAccountName string8 9param triggerStartTime string = utcNow('yyyy-MM-ddTHH:mm:ssZ')10 11resource factory 'Microsoft.DataFactory/factories@2018-06-01' = {12 identity: {13 type: 'SystemAssigned'14 }15 location: resourceGroup().location16 name: factoryName17 properties: {18 publicNetworkAccess: 'Enabled'19 }20 tags: tags21}22 23resource pipelines 'Microsoft.DataFactory/factories/pipelines@2018-06-01' = {24 parent: factory25 name: 'clicklog-pipeline'26 dependsOn: [27 inputDataSet28 outputDataSet29 ]30 properties: {31 activities: [32 {33 dependsOn: [34 {35 activity: 'LookupBlob'36 dependencyConditions: [37 'Succeeded'38 ]39 }40 ]41 policy: {42 timeout: '0.12:00:00'43 retry: 044 retryIntervalInSeconds: 3045 secureOutput: false46 secureInput: false47 }48 name: 'CopyAccessLog'49 userProperties: [50 {51 name: 'Source'52 value: 'container-name/<your path>/'53 }54 {55 name: 'Destination'56 value: 'destination-container-name//'57 }58 ]59 type: 'Copy'60 inputs: [61 {62 referenceName: 'SourceDataset'63 type: 'DatasetReference'64 parameters: {}65 }66 ]67 outputs: [68 {69 referenceName: 'DestinationDataset'70 type: 'DatasetReference'71 parameters: {}72 }73 ]74 typeProperties: {75 source: {76 type: 'JsonSource'77 storeSettings: {78 type: 'AzureBlobStorageReadSettings'79 recursive: true80 wildcardFolderPath: {81 value: 'your-path/@{concat(\'y=\', formatDateTime(addHours(pipeline().TriggerTime, -2), \'yyyy\'))}/@{concat(\'m=\', formatDateTime(addHours(pipeline().TriggerTime, -2), \'MM\'))}/@{concat(\'d=\', formatDateTime(addHours(pipeline().TriggerTime, -2), \'dd\'))}'82 type: 'Expression'83 }84 wildcardFileName: '*'85 enablePartitionDiscovery: false86 }87 formatSettings: {88 type: 'JsonReadSettings'89 }90 }91 sink: {92 type: 'ParquetSink'93 storeSettings: {94 type: 'AzureBlobStorageWriteSettings'95 copyBehavior: 'FlattenHierarchy'96 }97 formatSettings: {98 type: 'ParquetWriteSettings'99 }100 }101 enableStaging: false102 validateDataConsistency: false103 translator: {104 type: 'TabularTranslator'105 mappings: [ // Define mappings here106 {107 source: {108 path: '$[\'time\']'109 }110 sink: {111 name: 'time'112 type: 'DateTime'113 }114 }115 // ...116 ]117 }118 }119 }120 ]121 policy: {122 elapsedTimeMetric: {}123 }124 }125}126 127resource inputBlob 'Microsoft.DataFactory/factories/linkedservices@2018-06-01' = {128 parent: factory129 name: 'AzureBlobStorageInput'130 properties: {131 annotations: []132 type: 'AzureBlobStorage'133 typeProperties: {134 serviceEndpoint: 'https://${inputStorageAccountName}.blob.${environment().suffixes.storage}/'135 accountKind: 'StorageV2'136 }137 // For remaining properties, see LinkedService objects138 }139}140 141resource outputBlob 'Microsoft.DataFactory/factories/linkedservices@2018-06-01' = {142 parent: factory143 name: 'AzureBlobStorageOutput'144 properties: {145 annotations: []146 type: 'AzureBlobStorage'147 typeProperties: {148 serviceEndpoint: 'https://${outputStorageAccountName}.blob.${environment().suffixes.storage}/'149 accountKind: 'StorageV2'150 }151 // For remaining properties, see LinkedService objects152 }153}154 155resource outputDataSet 'Microsoft.DataFactory/factories/datasets@2018-06-01' = {156 parent: factory157 name: 'DestinationDataset'158 dependsOn: [159 outputBlob160 ]161 properties: {162 annotations: []163 description: 'string'164 folder: {165 name: 'string'166 }167 linkedServiceName: {168 referenceName: 'AzureBlobStorageOutput'169 type: 'LinkedServiceReference'170 }171 typeProperties: {172 location: {173 type: 'AzureBlobStorageLocation'174 folderPath: {175 value: '@{concat(\'y=\', formatDateTime(addHours(pipeline().TriggerTime, -2), \'yyyy\'))}/@{concat(\'m=\', formatDateTime(addHours(pipeline().TriggerTime, -2), \'MM\'))}/@{concat(\'d=\', formatDateTime(addHours(pipeline().TriggerTime, -2), \'dd\'))}'176 type: 'Expression'177 }178 container: 'container-name'179 }180 compressionCodec: 'snappy'181 }182 type: 'Parquet'183 // For remaining properties, see Dataset objects184 }185}186 187resource inputDataSet 'Microsoft.DataFactory/factories/datasets@2018-06-01' = {188 parent: factory189 name: 'SourceDataset'190 dependsOn: [191 inputBlob192 ]193 properties: {194 annotations: []195 description: 'string'196 folder: {197 name: 'string'198 }199 linkedServiceName: {200 referenceName: 'AzureBlobStorageInput'201 type: 'LinkedServiceReference'202 }203 typeProperties: {204 location: {205 type: 'AzureBlobStorageLocation'206 folderPath: 'your-path'207 container: 'container-name'208 }209 }210 type: 'Json'211 schema: {212 type: 'object'213 properties: {214 time: {215 type: 'string'216 }217 ...218 }219 }220 // For remaining properties, see Dataset objects221 }222}223 224resource DailyTrigger 'Microsoft.DataFactory/factories/triggers@2018-06-01' = {225 parent: factory226 name: 'DailyTrigger'227 dependsOn: [228 pipelines229 ]230 properties: {231 annotations: []232 // runtimeState: 'Started'233 pipelines: [234 {235 pipelineReference: {236 referenceName: 'your-pipeline-name'237 type: 'PipelineReference'238 }239 parameters: {}240 }241 ]242 type: 'ScheduleTrigger'243 typeProperties: {244 recurrence: {245 frequency: 'Day'246 interval: 1247 startTime: triggerStartTime248 timeZone: 'UTC'249 schedule: {250 minutes: [251 0252 ]253 hours: [254 0255 ]256 }257 }258 }259 // For remaining properties, see Trigger objects260 }261}262 263output managedIdentityPrincipalId string = factory.identity.principalIdLos recursos que aparecen aquí son los siguientes.
| Recurso | Descripción |
|---|---|
| Microsoft.DataFactory/factories | La instancia de Data Factory |
| Microsoft.DataFactory/factories/pipelines | Definición del pipeline. El pipeline realiza copias de datos, etc. |
| Microsoft.DataFactory/factories/linkedservices | Linked services que vinculan el almacenamiento de entrada y salida |
| Microsoft.DataFactory/factories/datasets | Datasets de entrada y salida |
| Microsoft.DataFactory/factories/triggers | Configuración del trigger. No es necesario si no desea ejecución automática. |
Crear Synapse
A continuación, implemente Synapse Analytics. Como existe un pool SQL serverless integrado por defecto, crearemos una base de datos allí.
Crear una base de datos
Cree una base de datos en el pool SQL serverless integrado.
1CREATE DATABASE [myDB];2GO3 4USE [myDB];5GOAgregar una fuente de datos externa
Cree una fuente de datos externa y conéctela al data lake.
En este momento, cree WorkspaceIdentity y configure el acceso a esta fuente de datos externa utilizando los permisos de WorkspaceIdentity.
1CREATE MASTER KEY2ENCRYPTION BY PASSWORD = 'StrongPassword_ChangeThis!';3 4CREATE DATABASE SCOPED CREDENTIAL WorkspaceIdentity5WITH IDENTITY = 'Managed Identity';6GO7 8CREATE EXTERNAL DATA SOURCE MyDataLake9WITH (10 LOCATION = 'https://<your storage name>.blob.core.windows.net/<container name>',11 CREDENTIAL = WorkspaceIdentity12);13GOCrear una tabla externa
Cree una tabla externa y establezca DATA_SOURCE en la fuente de datos externa creada anteriormente.
En LOCATION especifique la ruta del blob. Se pueden usar comodines.
1-- Crear esquema2CREATE SCHEMA ext AUTHORIZATION dbo;3GO4 5CREATE SCHEMA func AUTHORIZATION dbo;6GO7 8-- Crear formato de archivo9CREATE EXTERNAL FILE FORMAT [ParquetFormat]10WITH (FORMAT_TYPE = PARQUET);11GO12 13-- Crear tabla externa14CREATE EXTERNAL TABLE ext.myTable15(16 -- Definición de columnas17 time datetime2 NULL,18 propertyA nvarchar(512) NULL,19 propertyB nvarchar(512) NULL,20 ...21)22WITH23(24 LOCATION = 'y=*/**',25 DATA_SOURCE = [MyDataLake],26 FILE_FORMAT = [ParquetFormat]27);28GOCrear una vista
Cree una vista y reduzca las propiedades a las que accederán las Functions.
1CREATE VIEW func.viewOfMyTable2AS3SELECT4 [time],5 [propertyA],6 [propertyB],7 ...8FROM ext.myTable9WHERE <escriba aquí una condición si es necesario>;10GOConceder permisos al service principal
Esta es una configuración para agregar permisos a la base de datos desde un pipeline en CI/CD. Si no planea hacerlo, puede omitir esta sección.
Usando el nombre del SPN utilizado en el pipeline, cree un usuario externo y conceda permisos.
1CREATE USER [pipeline-sp] FROM EXTERNAL PROVIDER;2GO3 4ALTER ROLE db_accessadmin ADD MEMBER [pipeline-sp];5GO6 7ALTER ROLE db_owner ADD MEMBER [pipeline-sp];8GOConceder el rol “Directory Readers” (Entra ID) a la identidad administrada del workspace de Synapse
Si desea conceder permisos dinámicamente a Functions, puede especificar el nombre de la Function y el workspace buscará automáticamente el objectId de la identidad administrada correspondiente y concederá permisos. Para hacerlo, parece que el propio workspace necesita tener el rol “Directory Readers”.
Agregue el rol a la identidad del workspace desde Entra ID > Roles y administradores > Directory Readers > Agregar asignaciones.
Puede confirmar el objectId de la identidad administrada del workspace en Synapse Studio, en el menú de administración Credentials.
Conceder permisos a Functions desde el pipeline
Cuando implemente una nueva Function, concédale acceso a la base de datos. Para ejecutarlo en Azure Pipelines, utilicé los siguientes pasos.
- Preparar un script SQL
- Reemplazar dinámicamente el nombre de la Function dentro del script durante la ejecución del pipeline
- Ejecutar el SQL usando
SqlAzureDacpacDeployment@1
A continuación lo explico paso a paso.
Preparar el SQL
Prepare un SQL como el siguiente. La parte FunctionAppMiName se reemplazará más adelante por el nombre de la Function objetivo.
1USE [myDB];2 3IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N'FunctionAppMiName')4BEGIN5 PRINT 'Creating user [FunctionAppMiName] FROM EXTERNAL PROVIDER...';6 CREATE USER [FunctionAppMiName] FROM EXTERNAL PROVIDER;7END8ELSE9BEGIN10 PRINT 'User [FunctionAppMiName] already exists.';11END;12 13IF NOT EXISTS (14 SELECT 115 FROM sys.database_role_members rm16 JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id17 JOIN sys.database_principals m ON rm.member_principal_id = m.principal_id18 WHERE r.name = N'db_datareader'19 AND m.name = N'FunctionAppMiName'20)21BEGIN22 PRINT 'Adding [FunctionAppMiName] to db_datareader...';23 ALTER ROLE db_datareader ADD MEMBER [FunctionAppMiName];24 GRANT REFERENCES ON DATABASE SCOPED CREDENTIAL::WorkspaceIdentity TO [FunctionAppMiName];25 GRANT SELECT ON OBJECT::func.viewOfMyTable TO [FunctionAppMiName];26 DENY SELECT ON SCHEMA::ext TO [FunctionAppMiName];27END28ELSE29BEGIN30 PRINT 'Already in db_datareader.';31END;Breve explicación de los permisos:
db_datareader es necesario para leer datos.
También agregué el permiso de referencia sobre WorkspaceIdentity, porque sin él no podía accederse a la fuente de datos externa.
Además, concedo acceso a la vista y quiero prohibir consultas directas a las tablas externas, por lo que hago DENY sobre el esquema.
Insertar el nombre de la Function
Reescribí el archivo SQL usando una tarea PowerShell@2 como se muestra a continuación, pero cualquier método es válido mientras pueda reemplazar el nombre de la Function en el archivo.
1- task: PowerShell@22 displayName: 'Generate grant-function-mi.sql from template'3 inputs:4 targetType: 'inline'5 pwsh: true6 script: |7 $templatePath = "$(System.DefaultWorkingDirectory)/drop/sql/grant-function-mi.sql.template"8 $outputPath = "$(System.DefaultWorkingDirectory)/drop/sql/grant-function-mi.sql"9 10 if (-not (Test-Path $templatePath)) {11 throw "Template not found: $templatePath"12 }13 14 # Leer plantilla15 $content = Get-Content $templatePath -Raw16 17 # Reemplazar (rellenar usando variables del pipeline)18 $content = $content -replace 'FunctionAppMiName', '$(deploy_resources.functionName)'19 20 # Crear el directorio de salida si no existe21 $dir = Split-Path $outputPath22 if (-not (Test-Path $dir)) {23 New-Item -Path $dir -ItemType Directory -Force | Out-Null24 }25 26 # Escribir SQL27 $content | Set-Content $outputPath -Encoding UTF828 29 Write-Host "Generated SQL file:"30 Get-Content $outputPathEjecutar el SQL
Ejecute el SQL reescrito. Por ejemplo, con una tarea como la siguiente.
1- task: SqlAzureDacpacDeployment@12 displayName: 'Grant db_datareader to Function MI (Synapse serverless)'3 inputs:4 # Conexión a Azure (nombre de la conexión de servicio)5 azureSubscription: '${{parameters.azureServiceConnection}}'6 7 # Synapse serverless SQL endpoint8 ServerName: '${{parameters.synapseWorkspaceName}}.sql.azuresynapse.net'9 DatabaseName: 'myDB'10 11 # Autenticación (usando el SPN de la conexión de servicio)12 SqlUserName: '' # Puede quedar vacío13 SqlPassword: '' # Puede quedar vacío14 AuthenticationType: 'servicePrincipal'15 16 # Modo de ejecución: script SQL (no DACPAC)17 deployType: 'SqlTask'18 SqlFile: '$(System.DefaultWorkingDirectory)/drop/sql/grant-function-mi.sql' # Ruta al archivo .sql generado arriba19 20 # Opciones (según sea necesario)21 IpDetectionMethod: 'AutoDetect'En mi caso utilicé SqlAzureDacpacDeployment@1, pero este método no permite pasar el nombre de la Function como parámetro. Por ello, el flujo consiste en reescribir el SQL previamente para insertar el nombre de la Function y luego ejecutar la tarea.
Lo importante es conceder permisos a la Function como se muestra en el archivo SQL, por lo que no necesariamente tiene que seguir exactamente este procedimiento.
Consulta desde Functions
Desde Functions, envíe una consulta a la vista como la siguiente.
En .NET uso el paquete Microsoft.Data.SqlClient.
1var queryString = @"2SELECT TOP (@maxItemCount)3 [time],4 [requestUri]5 -- ...6FROM func.viewOfMyTable7WHERE8 [time] >= @startDate9 AND [time] <= @endDate10ORDER BY11 [time] DESC;12";Resumen
Para consultar desde Functions, debe conceder permisos a la Function. Además, si desea agregar permisos desde un pipeline de CI/CD, también necesita configurar permisos para el SPN, lo que puede ser un poco complejo. Como hay dos capas (permisos de identidad administrada y permisos de acceso a la base de datos), tenga cuidado de no confundirlas.
En este artículo creamos una fuente de datos externa y una vista. También sería posible acceder a los datos directamente desde Functions usando OPENROWSET, pero no elegí ese enfoque porque requeriría conceder permisos más amplios a la Function.




