Come interrogare Synapse Analytics da Azure Functions

Questo articolo riassume come eseguire query da Azure Functions utilizzando Azure Synapse Analytics.
Procedura
Trasformazione e copia dei dati con Data Factory
Per prima cosa, prepariamo i dati da analizzare. In questo esempio analizziamo log salvati nello storage. Dopo aver trasformato i dati in un formato più adatto all’analisi, li posizioniamo in Azure Data Lake Storage Gen2.
Qui utilizziamo un servizio chiamato Data Factory. Con Data Factory eseguiamo la copia dei dati. Poiché durante la copia è possibile scegliere il formato, li convertiamo in formato Parquet.
Di seguito un esempio Bicep. Questo esempio utilizza wildcard ed espressioni nella configurazione dei percorsi, ma modificate queste parti in base ai requisiti. Spesso è più chiaro creare il tutto dall’UI in Data Factory Studio. È anche possibile esportare i pipeline creati in Studio come file 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.principalIdLe risorse che compaiono qui sono le seguenti.
| Risorsa | Descrizione |
|---|---|
| Microsoft.DataFactory/factories | L’istanza di Data Factory |
| Microsoft.DataFactory/factories/pipelines | Definizione del pipeline. Il pipeline esegue copie dati, ecc. |
| Microsoft.DataFactory/factories/linkedservices | Linked service che collegano lo storage di input e output |
| Microsoft.DataFactory/factories/datasets | Dataset di input e output |
| Microsoft.DataFactory/factories/triggers | Configurazione del trigger. Non necessario se non serve l’esecuzione automatica. |
Creare Synapse
Successivamente, distribuiamo Synapse Analytics. Poiché per impostazione predefinita è disponibile un pool SQL serverless integrato, creiamo lì un database.
Creare un database
Creare un database nel pool SQL serverless integrato.
1CREATE DATABASE [myDB];2GO3 4USE [myDB];5GOAggiungere una sorgente dati esterna
Creare una sorgente dati esterna e collegarla al data lake.
In questa fase, creare WorkspaceIdentity e configurare l’accesso a questa sorgente dati esterna utilizzando i permessi di 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);13GOCreare una tabella esterna
Creare una tabella esterna e impostare DATA_SOURCE sulla sorgente dati esterna creata in precedenza.
Per LOCATION, specificare il percorso del blob. È possibile utilizzare wildcard.
1-- Creare lo schema2CREATE SCHEMA ext AUTHORIZATION dbo;3GO4 5CREATE SCHEMA func AUTHORIZATION dbo;6GO7 8-- Creare il formato file9CREATE EXTERNAL FILE FORMAT [ParquetFormat]10WITH (FORMAT_TYPE = PARQUET);11GO12 13-- Creare la tabella esterna14CREATE EXTERNAL TABLE ext.myTable15(16 -- Definizione delle colonne17 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);28GOCreare una view
Creare una view e limitare le proprietà (colonne) a cui accederanno le Functions.
1CREATE VIEW func.viewOfMyTable2AS3SELECT4 [time],5 [propertyA],6 [propertyB],7 ...8FROM ext.myTable9WHERE <scrivi qui una condizione se necessario>;10GOConcedere permessi al service principal
Questa è una configurazione per aggiungere permessi al database dal pipeline in CI/CD. Se non prevedete di farlo, potete saltare questa sezione.
Usando il nome dello SPN utilizzato dal pipeline, creare un utente esterno e concedere i permessi.
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];8GOConcedere il ruolo “Directory Readers” di Entra ID alla managed identity del workspace Synapse
Se volete concedere permessi dinamicamente a Functions, potete specificare il nome della Function: il workspace cercherà automaticamente l’objectId della managed identity corrispondente e assegnerà i permessi. Per farlo, sembra che il workspace stesso debba avere il ruolo “Directory Readers”.
Aggiungete il ruolo alla identity del workspace da Entra ID > Ruoli e amministratori > Directory Readers > Aggiungi assegnazioni.
Potete verificare l’objectId della managed identity del workspace in Synapse Studio, nel menu di amministrazione Credentials.
Concedere permessi a Functions dal pipeline
Quando distribuite una nuova Function, concedetele l’accesso al database. Per eseguirlo su Azure Pipelines, ho seguito questi passaggi.
- Preparare uno script SQL
- Sostituire dinamicamente il nome della Function nello script durante l’esecuzione del pipeline
- Eseguire il SQL usando
SqlAzureDacpacDeployment@1
Di seguito spiego i passaggi in ordine.
Preparare il SQL
Preparare un SQL come il seguente. La parte FunctionAppMiName verrà poi sostituita con il nome della Function target.
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 spiegazione dei permessi:
db_datareader è necessario per leggere i dati.
Ho aggiunto anche il permesso di riferimento su WorkspaceIdentity, perché senza non riuscivo ad accedere alla sorgente dati esterna.
Infine, concedo l’accesso alla view e, per impedire query dirette sulle tabelle esterne, applico un DENY sullo schema.
Inserire il nome della Function
Ho riscritto il file SQL usando un task PowerShell@2 come segue, ma qualsiasi metodo va bene purché possiate sostituire il nome della Function nel file.
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 # Leggere il template15 $content = Get-Content $templatePath -Raw16 17 # Sostituire (inserire valori usando variabili della pipeline)18 $content = $content -replace 'FunctionAppMiName', '$(deploy_resources.functionName)'19 20 # Creare la directory di output se non esiste21 $dir = Split-Path $outputPath22 if (-not (Test-Path $dir)) {23 New-Item -Path $dir -ItemType Directory -Force | Out-Null24 }25 26 # Scrivere l'SQL27 $content | Set-Content $outputPath -Encoding UTF828 29 Write-Host "Generated SQL file:"30 Get-Content $outputPathEseguire il SQL
Eseguite il SQL riscritto. Ad esempio con un task come il seguente.
1- task: SqlAzureDacpacDeployment@12 displayName: 'Grant db_datareader to Function MI (Synapse serverless)'3 inputs:4 # Connessione ad Azure (nome della service connection)5 azureSubscription: '${{parameters.azureServiceConnection}}'6 7 # Synapse serverless SQL endpoint8 ServerName: '${{parameters.synapseWorkspaceName}}.sql.azuresynapse.net'9 DatabaseName: 'myDB'10 11 # Autenticazione (usando lo SPN della service connection)12 SqlUserName: '' # Può essere lasciato vuoto13 SqlPassword: '' # Può essere lasciato vuoto14 AuthenticationType: 'servicePrincipal'15 16 # Modalità di esecuzione: script SQL (non DACPAC)17 deployType: 'SqlTask'18 SqlFile: '$(System.DefaultWorkingDirectory)/drop/sql/grant-function-mi.sql' # Percorso al file .sql generato sopra19 20 # Opzioni (se necessario)21 IpDetectionMethod: 'AutoDetect'Nel mio caso ho usato SqlAzureDacpacDeployment@1, ma questo metodo non consente di passare il nome della Function come parametro. Di conseguenza, il flusso consiste nel riscrivere in anticipo il SQL per includere il nome della Function e poi eseguire il task.
Il punto importante è concedere i permessi alla Function come indicato nel file SQL: non è necessario seguire esattamente questa procedura.
Query da Functions
Da Functions, inviate una query alla view come segue.
In .NET utilizzo il pacchetto 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";Riepilogo
Per interrogare da Functions, dovete concedere i permessi alla Function. Inoltre, se volete aggiungere i permessi da un pipeline CI/CD, serve anche una configurazione dei permessi per lo SPN, e questo può essere un po’ complicato. Dato che ci sono due livelli (permessi della managed identity e permessi di accesso al database), fate attenzione a non confonderli.
In questo articolo abbiamo creato una sorgente dati esterna e una view. Sarebbe anche possibile accedere ai dati direttamente da Functions usando OPENROWSET, ma non ho scelto questo approccio perché richiederebbe di concedere permessi più ampi alla Function.




