├── .gitattributes ├── .gitignore ├── BlobEventGridFunctionApp.sln ├── BlobEventGridFunctionApp ├── BlobEventGridFunctionApp.csproj ├── ProcessBlobEvents.cs ├── Properties │ └── PublishProfiles │ │ └── EventGridBlobEvents - Zip Deploy.pubxml ├── host.json └── local.settings.json ├── Images ├── ApplicationInsights.png ├── Architecture.png ├── AzureEventGridViewer.png ├── AzureStorageExplorer.png ├── EventGrid.png └── ServiceBusExplorer.png ├── README.md └── Scripts ├── create-event-grid-subscription-for-azure-function.sh ├── create-event-grid-subscription-for-local-function.sh └── create-event-grid-subscription-for-web-app.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /BlobEventGridFunctionApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.168 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlobEventGridFunctionApp", "BlobEventGridFunctionApp\BlobEventGridFunctionApp.csproj", "{95A128BC-E819-4492-94F7-84BA4E5140CA}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{0D93B30B-6272-4A1C-9532-E35C5716EBED}" 9 | ProjectSection(SolutionItems) = preProject 10 | Scripts\create-event-grid-subscription-for-azure-function.sh = Scripts\create-event-grid-subscription-for-azure-function.sh 11 | Scripts\create-event-grid-subscription-for-local-function.sh = Scripts\create-event-grid-subscription-for-local-function.sh 12 | Scripts\create-event-grid-subscription-for-web-app.sh = Scripts\create-event-grid-subscription-for-web-app.sh 13 | EndProjectSection 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Images", "Images", "{D4C18C87-7DAD-48A8-9A82-72B40983EF44}" 16 | ProjectSection(SolutionItems) = preProject 17 | Images\ApplicationInsights.png = Images\ApplicationInsights.png 18 | Images\Architecture.png = Images\Architecture.png 19 | Images\AzureEventGridViewer.png = Images\AzureEventGridViewer.png 20 | Images\AzureStorageExplorer.png = Images\AzureStorageExplorer.png 21 | Images\EventGrid.png = Images\EventGrid.png 22 | Images\ServiceBusExplorer.png = Images\ServiceBusExplorer.png 23 | EndProjectSection 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Readme", "Readme", "{12C31E30-96D4-4D37-82A7-840FB09E2ECD}" 26 | ProjectSection(SolutionItems) = preProject 27 | README.md = README.md 28 | EndProjectSection 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Release|Any CPU = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 36 | {95A128BC-E819-4492-94F7-84BA4E5140CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {95A128BC-E819-4492-94F7-84BA4E5140CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {95A128BC-E819-4492-94F7-84BA4E5140CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {95A128BC-E819-4492-94F7-84BA4E5140CA}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(ExtensibilityGlobals) = postSolution 45 | SolutionGuid = {74843D70-51B8-4564-9E99-8F20516ECEA1} 46 | EndGlobalSection 47 | EndGlobal 48 | -------------------------------------------------------------------------------- /BlobEventGridFunctionApp/BlobEventGridFunctionApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.1 4 | v2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | PreserveNewest 18 | Never 19 | 20 | 21 | -------------------------------------------------------------------------------- /BlobEventGridFunctionApp/ProcessBlobEvents.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/blob-event-grid-function-app/83f4380c30b24685e9e567cad3edee89594741b7/BlobEventGridFunctionApp/ProcessBlobEvents.cs -------------------------------------------------------------------------------- /BlobEventGridFunctionApp/Properties/PublishProfiles/EventGridBlobEvents - Zip Deploy.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | ZipDeploy 8 | AzureWebSite 9 | Release 10 | Any CPU 11 | http://eventgridblobevents.azurewebsites.net 12 | False 13 | /subscriptions/1a45a694-ae23-4650-9774-89a571c462f6/resourceGroups/EventGridBlobEventsResourceGroup/providers/Microsoft.Web/sites/EventGridBlobEvents 14 | $EventGridBlobEvents 15 | <_SavePWD>True 16 | https://eventgridblobevents.scm.azurewebsites.net/ 17 | 18 | -------------------------------------------------------------------------------- /BlobEventGridFunctionApp/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /BlobEventGridFunctionApp/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 6 | "APPINSIGHTS_INSTRUMENTATIONKEY": "", 7 | "ServiceBusConnectionString": "", 8 | "QueueName": "" 9 | } 10 | } -------------------------------------------------------------------------------- /Images/ApplicationInsights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/blob-event-grid-function-app/83f4380c30b24685e9e567cad3edee89594741b7/Images/ApplicationInsights.png -------------------------------------------------------------------------------- /Images/Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/blob-event-grid-function-app/83f4380c30b24685e9e567cad3edee89594741b7/Images/Architecture.png -------------------------------------------------------------------------------- /Images/AzureEventGridViewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/blob-event-grid-function-app/83f4380c30b24685e9e567cad3edee89594741b7/Images/AzureEventGridViewer.png -------------------------------------------------------------------------------- /Images/AzureStorageExplorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/blob-event-grid-function-app/83f4380c30b24685e9e567cad3edee89594741b7/Images/AzureStorageExplorer.png -------------------------------------------------------------------------------- /Images/EventGrid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/blob-event-grid-function-app/83f4380c30b24685e9e567cad3edee89594741b7/Images/EventGrid.png -------------------------------------------------------------------------------- /Images/ServiceBusExplorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paolosalvatori/blob-event-grid-function-app/83f4380c30b24685e9e567cad3edee89594741b7/Images/ServiceBusExplorer.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | services: azure-functions, app-service, azure-monitor, event-grid, service-bus-messaging, storage 3 | platforms: dotnet-core, aspnet-core 4 | author: paolosalvatori 5 | --- 6 | 7 | ## Consume Blob Event Grid events with an Azure Function 8 | This sample demonstrates how to create a serverless application to receive and process events via an Azure Event Grid Subscription any time a blob is created, updated or deleted in a given container inside an [Azure storage account](https://docs.microsoft.com/en-us/azure/storage/common/storage-account-overview). 9 | 10 | ## What is Azure Event Grid? 11 | [Azure Event Grid](https://docs.microsoft.com/en-us/azure/event-grid/overview) is a fully-managed intelligent event routing service that allows for uniform event consumption using a publish-subscribe model. Azure Event Grid allows you to easily build event-processging applications using a microservice and serverless architecture. The Azure Event Grid support a rich set of publishers and consumers. A publisher is the service or resource that originates the event. For example, an Azure blob storage account is a publisher, and a blob upload or deletion is an event. Some Azure services have built-in support for publishing events to Event Grid. 12 | 13 | ![Azure Event Grid](Images/EventGrid.png) 14 | 15 | ## Publishers or Event Sources 16 | For full details on the capabilities of each event publisher as well as related articles, see [event sources](https://docs.microsoft.com/en-us/azure/event-grid/event-sources). Currently, the following Azure services support sending events to Event Grid: 17 | 18 | - Azure Subscriptions (management operations) 19 | - Container Registry 20 | - Custom Topics 21 | - Event Hubs 22 | - IoT Hub 23 | - Media Services 24 | - Resource Groups (management operations) 25 | - Service Bus 26 | - Storage Blob 27 | - Storage General-purpose v2 (GPv2) 28 | 29 | ## Event Handlers or Consumers 30 | For full details on the capabilities of each event consumer as well as related articles, see [event handlers](https://docs.microsoft.com/en-us/azure/event-grid/event-handlers). Currently, the following Azure services support handling events from Event Grid: 31 | 32 | - Azure Automation 33 | - Azure Functions 34 | - Event Hubs 35 | - Hybrid Connections 36 | - Logic Apps 37 | - Microsoft Flow 38 | - Queue Storage 39 | - WebHooks 40 | 41 | ## Architecture 42 | The following picture shows the architecture design of the application. 43 | 44 | ![Architecture](Images/Architecture.png) 45 | 46 | ## Message Flow 47 | 1. Use the [Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/) to upload one more files as blobs to a container in an Azure storage account. 48 | 2. The Azure Storage Account Topic sends an event to 3 Azure Event Grid Subscriptions: 49 | - Azure Function Subscription: this subscription is configured to send events to the webhook endpoint exposed by an Azure Function that makes use of [Event Grid trigger for Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-event-grid) 50 | - Local Debugging Subscription: this subscription is configured to send events to a local function on your via a public HTTP/S endpoint exposed by [ngrok](https://ngrok.com/) on your development machine. For more information, see [Azure Function Event Grid Trigger Local Debugging](https://docs.microsoft.com/en-us/azure/azure-functions/functions-debug-event-grid-trigger-local). 51 | - Azure Event Grid Viewer: this subscription is configured to send events to a pre-built Azure Web App that displays the event messages. For more information on how to use this web app to see the events generated by an Azure Event Grid Topic, see [Azure Event Grid Viewer](https://github.com/Azure-Samples/azure-event-grid-viewer). For more information on how to send storage events to a web endpoint with Azure CLI, see the [Quickstart: Route storage events to web endpoint with Azure CLI](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-event-quickstart). 52 | 3. An Azure Function uses the [Event Grid trigger for Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-event-grid) to receive events via an Event Grid Subscription and send events to a queue in a Service Bus namespace. You can use the [Service Bus Explorer](https://github.com/paolosalvatori/ServiceBusExplorer) to receive or peek messages from the queue. The Azure Function is instrumented to collect and send metrics and logs to Application Insights. 53 | 54 | ## Blob Trigger vs Event Grid Trigger 55 | The [Blob storage trigger](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-blob#trigger) starts a function when a new or updated blob is detected. The blob contents are provided as input to the function. The [Event Grid](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-event-grid) trigger has built-in support for blob events and can also be used to start a function when a new or updated blob is detected. When using the Blob storage trigger, you can directly access the content of the new or updated blob. 56 | Use Event Grid instead of the Blob storage trigger for the following scenarios: 57 | * Blob storage accounts: Blob storage accounts are supported for blob input and output bindings 58 | but not for blob triggers. Blob storage triggers require a general-purpose storage account. 59 | * High scale: high scale can be loosely defined as containers that have more than 100,000 blobs 60 | in them or storage accounts that have more than 100 blob updates per second. 61 | * Minimizing latency: if your function app is on the Consumption plan, there can be up to a 10-minute delay 62 | in processing new blobs if a function app has gone idle. To avoid this latency, you can switch to an App 63 | Service plan with Always On enabled. You can also use an Event Grid trigger with your Blob storage account. 64 | * Blob delete events: you cannot handle blob delete events with the Blob Storage trigger 65 | 66 | 67 | ## Configuration 68 | Make sure to specify the following data in the local.settings.json: 69 | 70 | - [AzureWebJobStorage](https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings#azurewebjobsstorage): the Azure Functions runtime uses this storage account connection string for all functions except for HTTP triggered functions. 71 | - [APPINSIGHTS_INSTRUMENTATIONKEY](https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings#appinsightsinstrumentationkey): The Application Insights instrumentation key if you're using Application Insights. For more information, see [Monitor Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-monitoring). 72 | - ServiceBusConnectionString: the connection string of the Service Bus namespace containing the queue where the Azure Function sends messages to. 73 | - QueueName: the names of the queue. 74 | 75 | ```elixir 76 | { 77 | "IsEncrypted": false, 78 | "Values": { 79 | "AzureWebJobsStorage": "", 80 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 81 | "APPINSIGHTS_INSTRUMENTATIONKEY": "", 82 | "ServiceBusConnectionString": "", 83 | "QueueName": "" 84 | } 85 | } 86 | ``` 87 | ## Azure Function Code 88 | The following table contains the code of the Azure Function. 89 | 90 | ```csharp 91 | #region Using Directives 92 | using System; 93 | using System.Threading.Tasks; 94 | using Microsoft.Azure.EventGrid.Models; 95 | using Microsoft.Azure.WebJobs; 96 | using Microsoft.Azure.WebJobs.ServiceBus; 97 | using Microsoft.ApplicationInsights; 98 | using Microsoft.ApplicationInsights.Extensibility; 99 | using Microsoft.Extensions.Logging; 100 | using Microsoft.Azure.ServiceBus; 101 | using System.Text; 102 | using Microsoft.Azure.WebJobs.Extensions.EventGrid; 103 | using Newtonsoft.Json.Linq; 104 | using System.Collections.Generic; 105 | #endregion 106 | 107 | namespace BlobEventGridFunctionApp 108 | { 109 | public static class ProcessBlobEvents 110 | { 111 | #region Private Constants 112 | private const string BlobCreatedEvent = "Microsoft.Storage.BlobCreated"; 113 | private const string BlobDeletedEvent = "Microsoft.Storage.BlobDeleted"; 114 | #endregion 115 | 116 | #region Private Static Fields 117 | private static readonly string key = TelemetryConfiguration.Active.InstrumentationKey = Environment.GetEnvironmentVariable("APPINSIGHTS_INSTRUMENTATIONKEY", EnvironmentVariableTarget.Process); 118 | private static readonly TelemetryClient telemetry = new TelemetryClient() { InstrumentationKey = key }; 119 | #endregion 120 | 121 | #region Azure Functions 122 | [FunctionName("ProcessBlobEvents")] 123 | public static async Task Run([EventGridTrigger]EventGridEvent eventGridEvent, 124 | [ServiceBus("%QueueName%", Connection = "ServiceBusConnectionString", EntityType = EntityType.Queue)] IAsyncCollector asyncCollector, 125 | ExecutionContext context, 126 | ILogger log) 127 | { 128 | try 129 | { 130 | if (eventGridEvent == null && string.IsNullOrWhiteSpace(eventGridEvent.EventType)) 131 | { 132 | throw new ArgumentNullException("Null or Invalid Event Grid Event"); 133 | } 134 | 135 | log.LogInformation($@"New Event Grid Event: 136 | - Id=[{eventGridEvent.Id}] 137 | - EventType=[{eventGridEvent.EventType}] 138 | - EventTime=[{eventGridEvent.EventTime}] 139 | - Subject=[{eventGridEvent.Subject}] 140 | - Topic=[{eventGridEvent.Topic}]"); 141 | 142 | if (eventGridEvent.Data is JObject jObject) 143 | { 144 | // Create message 145 | var message = new Message(Encoding.UTF8.GetBytes(jObject.ToString())) 146 | { 147 | MessageId = eventGridEvent.Id 148 | }; 149 | 150 | switch (eventGridEvent.EventType) 151 | { 152 | case BlobCreatedEvent: 153 | { 154 | var blobCreatedEvent = jObject.ToObject(); 155 | var storageDiagnostics = JObject.Parse(blobCreatedEvent.StorageDiagnostics.ToString()).ToString(Newtonsoft.Json.Formatting.None); 156 | 157 | log.LogInformation($@"Received {BlobCreatedEvent} Event: 158 | - Api=[{blobCreatedEvent.Api}] 159 | - BlobType=[{blobCreatedEvent.BlobType}] 160 | - ClientRequestId=[{blobCreatedEvent.ClientRequestId}] 161 | - ContentLength=[{blobCreatedEvent.ContentLength}] 162 | - ContentType=[{blobCreatedEvent.ContentType}] 163 | - ETag=[{blobCreatedEvent.ETag}] 164 | - RequestId=[{blobCreatedEvent.RequestId}] 165 | - Sequencer=[{blobCreatedEvent.Sequencer}] 166 | - StorageDiagnostics=[{storageDiagnostics}] 167 | - Url=[{blobCreatedEvent.Url}] 168 | "); 169 | 170 | // Set message label 171 | message.Label = "BlobCreatedEvent"; 172 | 173 | // Add custom properties 174 | message.UserProperties.Add("id", eventGridEvent.Id); 175 | message.UserProperties.Add("topic", eventGridEvent.Topic); 176 | message.UserProperties.Add("eventType", eventGridEvent.EventType); 177 | message.UserProperties.Add("eventTime", eventGridEvent.EventTime); 178 | message.UserProperties.Add("subject", eventGridEvent.Subject); 179 | message.UserProperties.Add("api", blobCreatedEvent.Api); 180 | message.UserProperties.Add("blobType", blobCreatedEvent.BlobType); 181 | message.UserProperties.Add("clientRequestId", blobCreatedEvent.ClientRequestId); 182 | message.UserProperties.Add("contentLength", blobCreatedEvent.ContentLength); 183 | message.UserProperties.Add("contentType", blobCreatedEvent.ContentType); 184 | message.UserProperties.Add("eTag", blobCreatedEvent.ETag); 185 | message.UserProperties.Add("requestId", blobCreatedEvent.RequestId); 186 | message.UserProperties.Add("sequencer", blobCreatedEvent.Sequencer); 187 | message.UserProperties.Add("storageDiagnostics", storageDiagnostics); 188 | message.UserProperties.Add("url", blobCreatedEvent.Url); 189 | 190 | // Add message to AsyncCollector 191 | await asyncCollector.AddAsync(message); 192 | 193 | // Telemetry 194 | telemetry.Context.Operation.Id = context.InvocationId.ToString(); 195 | telemetry.Context.Operation.Name = "BlobCreatedEvent"; 196 | telemetry.TrackEvent($"[{blobCreatedEvent.Url}] blob created"); 197 | var properties = new Dictionary 198 | { 199 | { "BlobType", blobCreatedEvent.BlobType }, 200 | { "ContentType ", blobCreatedEvent.ContentType } 201 | }; 202 | telemetry.TrackMetric("ProcessBlobEvents Created", 1, properties); 203 | } 204 | break; 205 | 206 | case BlobDeletedEvent: 207 | { 208 | var blobDeletedEvent = jObject.ToObject(); 209 | var storageDiagnostics = JObject.Parse(blobDeletedEvent.StorageDiagnostics.ToString()).ToString(Newtonsoft.Json.Formatting.None); 210 | 211 | log.LogInformation($@"Received {BlobDeletedEvent} Event: 212 | - Api=[{blobDeletedEvent.Api}] 213 | - BlobType=[{blobDeletedEvent.BlobType}] 214 | - ClientRequestId=[{blobDeletedEvent.ClientRequestId}] 215 | - ContentType=[{blobDeletedEvent.ContentType}] 216 | - RequestId=[{blobDeletedEvent.RequestId}] 217 | - Sequencer=[{blobDeletedEvent.Sequencer}] 218 | - StorageDiagnostics=[{storageDiagnostics}] 219 | - Url=[{blobDeletedEvent.Url}] 220 | "); 221 | 222 | // Set message label 223 | message.Label = "BlobDeletedEvent"; 224 | 225 | // Add custom properties 226 | message.UserProperties.Add("id", eventGridEvent.Id); 227 | message.UserProperties.Add("topic", eventGridEvent.Topic); 228 | message.UserProperties.Add("eventType", eventGridEvent.EventType); 229 | message.UserProperties.Add("eventTime", eventGridEvent.EventTime); 230 | message.UserProperties.Add("subject", eventGridEvent.Subject); 231 | message.UserProperties.Add("api", blobDeletedEvent.Api); 232 | message.UserProperties.Add("blobType", blobDeletedEvent.BlobType); 233 | message.UserProperties.Add("clientRequestId", blobDeletedEvent.ClientRequestId); 234 | message.UserProperties.Add("contentType", blobDeletedEvent.ContentType); 235 | message.UserProperties.Add("requestId", blobDeletedEvent.RequestId); 236 | message.UserProperties.Add("sequencer", blobDeletedEvent.Sequencer); 237 | message.UserProperties.Add("storageDiagnostics", storageDiagnostics); 238 | message.UserProperties.Add("url", blobDeletedEvent.Url); 239 | 240 | // Add message to AsyncCollector 241 | await asyncCollector.AddAsync(message); 242 | 243 | // Telemetry 244 | telemetry.Context.Operation.Id = context.InvocationId.ToString(); 245 | telemetry.Context.Operation.Name = "BlobDeletedEvent"; 246 | telemetry.TrackEvent($"[{blobDeletedEvent.Url}] blob deleted"); 247 | var properties = new Dictionary 248 | { 249 | { "BlobType", blobDeletedEvent.BlobType }, 250 | { "ContentType ", blobDeletedEvent.ContentType } 251 | }; 252 | telemetry.TrackMetric("ProcessBlobEvents Deleted", 1, properties); 253 | } 254 | break; 255 | } 256 | } 257 | } 258 | catch (Exception ex) 259 | { 260 | log.LogError(ex, ex.Message); 261 | throw; 262 | } 263 | } 264 | #endregion 265 | } 266 | } 267 | ``` 268 | 269 | ## Scripts 270 | The solution includes 3 bash scripts can be used to create the 3 Azure Event Grid Subscriptions used to send events any time a blob is created, updated or deleted in the files container in the source storage account, respectively to: 271 | 272 | - Azure Function running on Azure 273 | - Azure Funtion running in you development machine 274 | - Event Grid Event Viewer web app on Azure 275 | 276 | Make sure to properly assign a value to variables before running the script. Also make sure to run scripts using an Azure AD account that is the owner or a contributor of the Azure subscription where you intend to deploy the solution. 277 | 278 | **create-event-grid-subscription-for-azure-function.sh** 279 | ```bash 280 | #!/bin/bash 281 | 282 | # variables 283 | location="WestEurope" 284 | storageAccountName="babofiles" 285 | storageAccountResourceGroup="BaboFilesResourceGroup" 286 | functionAppName="EventGridBlobEvents" 287 | functionAppResourceGroup="EventGridBlobEventsResourceGroup" 288 | functionName="ProcessBlobEvents" 289 | subscriptionName='BaboFilesAzureFunctionSubscriber' 290 | deadLetterContainerName="deadletter" 291 | filesContainerName="files" 292 | subjectBeginsWith="/blobServices/default/containers/"$filesContainerName 293 | 294 | # functions 295 | function getEventGridExtensionKey 296 | { 297 | # get Kudu username 298 | echo "Retrieving username from ["$functionAppName"] Azure Function publishing profile..." 299 | username=$(az functionapp deployment list-publishing-profiles --name $1 --resource-group $2 --query '[?publishMethod==`MSDeploy`].userName' --output tsv) 300 | 301 | if [ -n $username ]; then 302 | echo "["$username"] username successfully retrieved" 303 | else 304 | echo "No username could be retrieved" 305 | return 306 | fi 307 | 308 | # get Kudu password 309 | echo "Retrieving password from ["$functionAppName"] Azure Function publishing profile..." 310 | password=$(az functionapp deployment list-publishing-profiles --name $1 --resource-group $2 --query '[?publishMethod==`MSDeploy`].userPWD' --output tsv) 311 | 312 | if [ -n $password ]; then 313 | echo "["$password"] password successfully retrieved" 314 | else 315 | echo "No password could be retrieved" 316 | return 317 | fi 318 | 319 | # get jwt 320 | echo "Retrieving JWT token from Azure Function \ Kudu Management API..." 321 | jwt=$(sed -e 's/^"//' -e 's/"$//' <<< $(curl https://$functionAppName.scm.azurewebsites.net/api/functions/admin/token --user $username":"$password --silent)) 322 | 323 | if [ -n $jwt ]; then 324 | echo "JWT token successfully retrieved" 325 | else 326 | echo "No JWT token could be retrieved" 327 | return 328 | fi 329 | 330 | # get eventgrid_extension key 331 | echo "Retrieving [eventgrid_extension] key..." 332 | eventGridExtensionKey=$(sed -e 's/^"//' -e 's/"$//' <<< $(curl -H 'Accept: application/json' -H "Authorization: Bearer ${jwt}" https://$functionAppName.azurewebsites.net/admin/host/systemkeys/eventgrid_extension --silent | jq .value)) 333 | 334 | if [ -n $eventGridExtensionKey ]; then 335 | echo "[eventgrid_extension] key successfully retrieved" 336 | else 337 | echo "No [eventgrid_extension] key could be retrieved" 338 | return 339 | fi 340 | } 341 | 342 | # check if the storage account exists 343 | echo "Checking if ["$storageAccountName"] storage account actually exists..." 344 | 345 | set +e 346 | ( 347 | az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup &> /dev/null 348 | ) 349 | 350 | if [ $? != 0 ]; then 351 | echo "No ["$storageAccountName"] storage account actually exists" 352 | set -e 353 | ( 354 | # create the storage account 355 | az storage account create \ 356 | --name $storageAccountName \ 357 | --resource-group $storageAccountResourceGroup \ 358 | --location $location \ 359 | --sku Standard_LRS \ 360 | --kind BlobStorage \ 361 | --access-tier Hot 1> /dev/null 362 | ) 363 | echo "["$storageAccountName"] storage account successfully created" 364 | else 365 | echo "["$storageAccountName"] storage account already exists" 366 | fi 367 | 368 | # get storage account connection string 369 | echo "Retrieving the connection string for ["$storageAccountName"] storage account..." 370 | connectionString=$(az storage account show-connection-string --name $storageAccountName --resource-group $storageAccountResourceGroup --query connectionString --output tsv) 371 | 372 | if [ -n $connectionString ]; then 373 | echo "The connection string for ["$storageAccountName"] storage account is ["$connectionString"]" 374 | else 375 | echo "Failed to retrieve the connection string for ["$storageAccountName"] storage account" 376 | return 377 | fi 378 | 379 | # checking if deadletter container exists 380 | echo "Checking if ["$deadLetterContainerName"] container already exists..." 381 | set +e 382 | ( 383 | az storage container show --name $deadLetterContainerName --connection-string $connectionString &> /dev/null 384 | ) 385 | 386 | if [ $? != 0 ]; then 387 | echo "No ["$deadLetterContainerName"] container actually exists in ["$storageAccountName"] storage account" 388 | set -e 389 | ( 390 | # create deadletter container 391 | az storage container create \ 392 | --name $deadLetterContainerName \ 393 | --public-access off \ 394 | --connection-string $connectionString 1> /dev/null 395 | ) 396 | echo "["$deadLetterContainerName"] container successfully created in ["$storageAccountName"] storage account" 397 | else 398 | echo "A container called ["$deadLetterContainerName"] already exists in ["$storageAccountName"] storage account" 399 | fi 400 | 401 | # checking if files container exists 402 | echo "Checking if ["$filesContainerName"] container already exists..." 403 | set +e 404 | ( 405 | az storage container show --name $filesContainerName --connection-string $connectionString &> /dev/null 406 | ) 407 | 408 | if [ $? != 0 ]; then 409 | echo "No ["$filesContainerName"] container actually exists in ["$storageAccountName"] storage account" 410 | set -e 411 | ( 412 | # create files container 413 | az storage container create \ 414 | --name $filesContainerName \ 415 | --public-access off \ 416 | --connection-string $connectionString 1> /dev/null 417 | ) 418 | echo "["$filesContainerName"] container successfully created in ["$storageAccountName"] storage account" 419 | else 420 | echo "A container called ["$filesContainerName"] already exists in ["$storageAccountName"] storage account" 421 | fi 422 | 423 | # retrieve resource id for the storage account 424 | echo "Retrieving the resource id for ["$storageAccountName"] storage account..." 425 | storageAccountId=$(az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup --query id --output tsv 2> /dev/null) 426 | 427 | if [ -n $storageAccountId ]; then 428 | echo "Resource id for ["$storageAccountName"] storage account successfully retrieved: ["$storageAccountId"]" 429 | else 430 | echo "Failed to retrieve resource id for ["$storageAccountName"] storage account" 431 | return 432 | fi 433 | 434 | # retrieve eventgrid_extensionkey 435 | getEventGridExtensionKey $functionAppName $functionAppResourceGroup 436 | 437 | if [ -z $eventGridExtensionKey ]; then 438 | echo "Failed to retrieve eventgrid_extensionkey" 439 | return 440 | fi 441 | 442 | # creating the endpoint URL for the Azure Function 443 | endpointUrl="https://$functionAppName.azurewebsites.net/runtime/webhooks/eventgrid?functionName=$functionName&code=$eventGridExtensionKey" 444 | 445 | echo "The endpoint for the ["$functionName"] function in the ["$functionAppName"] function app is ["$endpointUrl"]" 446 | 447 | echo "Checking if Azure CLI eventgrid extension is installed..." 448 | set +e 449 | ( 450 | az extension show --name eventgrid --query name --output tsv &> /dev/null 451 | ) 452 | 453 | if [ $? != 0 ]; then 454 | echo "The Azure CLI eventgrid extension was not found. Installing the extension..." 455 | az extension add --name eventgrid 456 | else 457 | echo "Azure CLI eventgrid extension successfully found. Updating the extension to the latest version..." 458 | az extension update --name eventgrid 459 | fi 460 | 461 | # checking if the subscription already exists 462 | echo "Checking if ["$subscriptionName"] Event Grid subscription already exists for ["$storageAccountName"] storage account..." 463 | set +e 464 | ( 465 | az eventgrid event-subscription show --name $subscriptionName --source-resource-id $storageAccountId &> /dev/null 466 | ) 467 | 468 | if [ $? != 0 ]; then 469 | echo "No ["$subscriptionName"] Event Grid subscription actually exists for ["$storageAccountName"] storage account" 470 | echo "Creating a subscription for the ["$endpointUrl"] endpoint of the ["$functionName"] Azure Function..." 471 | 472 | set +e 473 | ( 474 | az eventgrid event-subscription create \ 475 | --source-resource-id $storageAccountId \ 476 | --name $subscriptionName \ 477 | --endpoint-type webhook \ 478 | --endpoint $endpointUrl \ 479 | --subject-begins-with $subjectBeginsWith \ 480 | --deadletter-endpoint $storageAccountId/blobServices/default/containers/$deadLetterContainerName 1> /dev/null 481 | ) 482 | 483 | if [ $? == 0 ]; then 484 | echo "["$subscriptionName"] Event Grid subscription successfully created" 485 | fi 486 | else 487 | echo "An Event Grid subscription called ["$subscriptionName"] already exists for ["$storageAccountName"] storage account" 488 | fi 489 | ``` 490 | 491 | **create-event-grid-subscription-for-local-function.sh** 492 | ```bash 493 | #!/bin/bash 494 | 495 | # variables 496 | location="WestEurope" 497 | storageAccountName="babofiles" 498 | storageAccountResourceGroup="BaboFilesResourceGroup" 499 | subscriptionName='BaboFilesLocalDebugging' 500 | ngrockSubdomain="db1abac5" 501 | functionName="ProcessBlobEvents" 502 | endpointUrl="https://"$ngrockSubdomain".ngrok.io/runtime/webhooks/EventGrid?functionName="$functionName 503 | deadLetterContainerName="deadletter" 504 | filesContainerName="files" 505 | subjectBeginsWith="/blobServices/default/containers/"$filesContainerName 506 | 507 | # check if the storage account exists 508 | echo "Checking if ["$storageAccountName"] storage account actually exists..." 509 | 510 | set +e 511 | ( 512 | az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup &> /dev/null 513 | ) 514 | 515 | if [ $? != 0 ]; then 516 | echo "No ["$storageAccountName"] storage account actually exists" 517 | set -e 518 | ( 519 | # create the storage account 520 | az storage account create \ 521 | --name $storageAccountName \ 522 | --resource-group $storageAccountResourceGroup \ 523 | --location $location \ 524 | --sku Standard_LRS \ 525 | --kind BlobStorage \ 526 | --access-tier Hot 1> /dev/null 527 | ) 528 | echo "["$storageAccountName"] storage account successfully created" 529 | else 530 | echo "["$storageAccountName"] storage account already exists" 531 | fi 532 | 533 | # get storage account connection string 534 | echo "Retrieving the connection string for ["$storageAccountName"] storage account..." 535 | connectionString=$(az storage account show-connection-string --name $storageAccountName --resource-group $storageAccountResourceGroup --query connectionString --output tsv) 536 | 537 | if [ -n $connectionString ]; then 538 | echo "The connection string for ["$storageAccountName"] storage account is ["$connectionString"]" 539 | else 540 | echo "Failed to retrieve the connection string for ["$storageAccountName"] storage account" 541 | return 542 | fi 543 | 544 | # checking if deadletter container exists 545 | echo "Checking if ["$deadLetterContainerName"] container already exists..." 546 | set +e 547 | ( 548 | az storage container show --name $deadLetterContainerName --connection-string $connectionString &> /dev/null 549 | ) 550 | 551 | if [ $? != 0 ]; then 552 | echo "No ["$deadLetterContainerName"] container actually exists in ["$storageAccountName"] storage account" 553 | set -e 554 | ( 555 | # create deadletter container 556 | az storage container create \ 557 | --name $deadLetterContainerName \ 558 | --public-access off \ 559 | --connection-string $connectionString 1> /dev/null 560 | ) 561 | echo "["$deadLetterContainerName"] container successfully created in ["$storageAccountName"] storage account" 562 | else 563 | echo "A container called ["$deadLetterContainerName"] already exists in ["$storageAccountName"] storage account" 564 | fi 565 | 566 | # checking if files container exists 567 | echo "Checking if ["$filesContainerName"] container already exists..." 568 | set +e 569 | ( 570 | az storage container show --name $filesContainerName --connection-string $connectionString &> /dev/null 571 | ) 572 | 573 | if [ $? != 0 ]; then 574 | echo "No ["$filesContainerName"] container actually exists in ["$storageAccountName"] storage account" 575 | set -e 576 | ( 577 | # create files container 578 | az storage container create \ 579 | --name $filesContainerName \ 580 | --public-access off \ 581 | --connection-string $connectionString 1> /dev/null 582 | ) 583 | echo "["$filesContainerName"] container successfully created in ["$storageAccountName"] storage account" 584 | else 585 | echo "A container called ["$filesContainerName"] already exists in ["$storageAccountName"] storage account" 586 | fi 587 | 588 | # retrieve resource id for the storage account 589 | echo "Retrieving the resource id for ["$storageAccountName"] storage account..." 590 | storageAccountId=$(az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup --query id --output tsv 2> /dev/null) 591 | 592 | if [ -n $storageAccountId ]; then 593 | echo "Resource id for ["$storageAccountName"] storage account successfully retrieved: ["$storageAccountId"]" 594 | else 595 | echo "Failed to retrieve resource id for ["$storageAccountName"] storage account" 596 | return 597 | fi 598 | 599 | echo "Checking if Azure CLI eventgrid extension is installed..." 600 | set +e 601 | ( 602 | az extension show --name eventgrid --query name --output tsv &> /dev/null 603 | ) 604 | 605 | if [ $? != 0 ]; then 606 | echo "The Azure CLI eventgrid extension was not found. Installing the extension..." 607 | az extension add --name eventgrid 608 | else 609 | echo "Azure CLI eventgrid extension successfully found. Updating the extension to the latest version..." 610 | az extension update --name eventgrid 611 | fi 612 | 613 | # checking if the subscription already exists 614 | echo "Checking if ["$subscriptionName"] Event Grid subscription already exists for ["$storageAccountName"] storage account..." 615 | set +e 616 | ( 617 | az eventgrid event-subscription show --name $subscriptionName --source-resource-id $storageAccountId &> /dev/null 618 | ) 619 | 620 | if [ $? != 0 ]; then 621 | echo "No ["$subscriptionName"] Event Grid subscription actually exists for ["$storageAccountName"] storage account" 622 | echo "Creating a subscription for the ["$endpointUrl"] ngrock local endpoint..." 623 | 624 | set +e 625 | ( 626 | az eventgrid event-subscription create \ 627 | --source-resource-id $storageAccountId \ 628 | --name $subscriptionName \ 629 | --endpoint-type webhook \ 630 | --endpoint $endpointUrl \ 631 | --subject-begins-with $subjectBeginsWith \ 632 | --deadletter-endpoint $storageAccountId/blobServices/default/containers/$deadLetterContainerName 1> /dev/null 633 | ) 634 | 635 | if [ $? == 0 ]; then 636 | echo "["$subscriptionName"] Event Grid subscription successfully created" 637 | fi 638 | else 639 | echo "An Event Grid subscription called ["$subscriptionName"] already exists for ["$storageAccountName"] storage account" 640 | fi 641 | ``` 642 | 643 | **create-event-grid-subscription-for-web-app.sh** 644 | ```bash 645 | #!/bin/bash 646 | 647 | # variables 648 | location="WestEurope" 649 | storageAccountName="babofiles" 650 | storageAccountResourceGroup="BaboFilesResourceGroup" 651 | subscriptionName='BaboFilesWebApp' 652 | webAppSubdomain="baboeventgridviewer" 653 | functionName="ProcessBlobEvents" 654 | endpointUrl="https://"$webAppSubdomain".azurewebsites.net/api/updates" 655 | deadLetterContainerName="deadletter" 656 | filesContainerName="files" 657 | 658 | # check if the storage account exists 659 | echo "Checking if ["$storageAccountName"] storage account actually exists..." 660 | 661 | set +e 662 | ( 663 | az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup &> /dev/null 664 | ) 665 | 666 | if [ $? != 0 ]; then 667 | echo "No ["$storageAccountName"] storage account actually exists" 668 | set -e 669 | ( 670 | # create the storage account 671 | az storage account create \ 672 | --name $storageAccountName \ 673 | --resource-group $storageAccountResourceGroup \ 674 | --location $location \ 675 | --sku Standard_LRS \ 676 | --kind BlobStorage \ 677 | --access-tier Hot 1> /dev/null 678 | ) 679 | echo "["$storageAccountName"] storage account successfully created" 680 | else 681 | echo "["$storageAccountName"] storage account already exists" 682 | fi 683 | 684 | # get storage account connection string 685 | echo "Retrieving the connection string for ["$storageAccountName"] storage account..." 686 | connectionString=$(az storage account show-connection-string --name $storageAccountName --resource-group $storageAccountResourceGroup --query connectionString --output tsv) 687 | 688 | if [ -n $connectionString ]; then 689 | echo "The connection string for ["$storageAccountName"] storage account is ["$connectionString"]" 690 | else 691 | echo "Failed to retrieve the connection string for ["$storageAccountName"] storage account" 692 | return 693 | fi 694 | 695 | # checking if deadletter container exists 696 | echo "Checking if ["$deadLetterContainerName"] container already exists..." 697 | set +e 698 | ( 699 | az storage container show --name $deadLetterContainerName --connection-string $connectionString &> /dev/null 700 | ) 701 | 702 | if [ $? != 0 ]; then 703 | echo "No ["$deadLetterContainerName"] container actually exists in ["$storageAccountName"] storage account" 704 | set -e 705 | ( 706 | # create deadletter container 707 | az storage container create \ 708 | --name $deadLetterContainerName \ 709 | --public-access off \ 710 | --connection-string $connectionString 1> /dev/null 711 | ) 712 | echo "["$deadLetterContainerName"] container successfully created in ["$storageAccountName"] storage account" 713 | else 714 | echo "A container called ["$deadLetterContainerName"] already exists in ["$storageAccountName"] storage account" 715 | fi 716 | 717 | # checking if files container exists 718 | echo "Checking if ["$filesContainerName"] container already exists..." 719 | set +e 720 | ( 721 | az storage container show --name $filesContainerName --connection-string $connectionString &> /dev/null 722 | ) 723 | 724 | if [ $? != 0 ]; then 725 | echo "No ["$filesContainerName"] container actually exists in ["$storageAccountName"] storage account" 726 | set -e 727 | ( 728 | # create files container 729 | az storage container create \ 730 | --name $filesContainerName \ 731 | --public-access off \ 732 | --connection-string $connectionString 1> /dev/null 733 | ) 734 | echo "["$filesContainerName"] container successfully created in ["$storageAccountName"] storage account" 735 | else 736 | echo "A container called ["$filesContainerName"] already exists in ["$storageAccountName"] storage account" 737 | fi 738 | 739 | # retrieve resource id for the storage account 740 | echo "Retrieving the resource id for ["$storageAccountName"] storage account..." 741 | storageAccountId=$(az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup --query id --output tsv 2> /dev/null) 742 | 743 | if [ -n $storageAccountId ]; then 744 | echo "Resource id for ["$storageAccountName"] storage account successfully retrieved: ["$storageAccountId"]" 745 | else 746 | echo "Failed to retrieve resource id for ["$storageAccountName"] storage account" 747 | return 748 | fi 749 | 750 | echo "Checking if Azure CLI eventgrid extension is installed..." 751 | set +e 752 | ( 753 | az extension show --name eventgrid --query name --output tsv &> /dev/null 754 | ) 755 | 756 | if [ $? != 0 ]; then 757 | echo "The Azure CLI eventgrid extension was not found. Installing the extension..." 758 | az extension add --name eventgrid 759 | else 760 | echo "Azure CLI eventgrid extension successfully found. Updating the extension to the latest version..." 761 | az extension update --name eventgrid 762 | fi 763 | 764 | # checking if the subscription already exists 765 | echo "Checking if ["$subscriptionName"] Event Grid subscription already exists for ["$storageAccountName"] storage account..." 766 | set +e 767 | ( 768 | az eventgrid event-subscription show --name $subscriptionName --source-resource-id $storageAccountId &> /dev/null 769 | ) 770 | 771 | if [ $? != 0 ]; then 772 | echo "No ["$subscriptionName"] Event Grid subscription actually exists for ["$storageAccountName"] storage account" 773 | echo "Creating the subscription for the ["$endpointUrl"] endpoint of the Azure Event Grid Viewer web app..." 774 | set +e 775 | ( 776 | az eventgrid event-subscription create \ 777 | --source-resource-id $storageAccountId \ 778 | --name $subscriptionName \ 779 | --endpoint-type webhook \ 780 | --endpoint $endpointUrl \ 781 | --deadletter-endpoint $storageAccountId/blobServices/default/containers/$deadLetterContainerName 1> /dev/null 782 | ) 783 | 784 | if [ $? == 0 ]; then 785 | echo "["$subscriptionName"] Event Grid subscription successfully created" 786 | fi 787 | else 788 | echo "An Event Grid subscription called ["$subscriptionName"] already exists for ["$storageAccountName"] storage account" 789 | fi 790 | ``` 791 | 792 | ## Run the sample 793 | You can use the [Azure Storage Explorer](https://azure.microsoft.com/en-us/features/storage-explorer/) to upload files to the container in the storage account monitored by the Event Grid Storage Topic, as shown by the following picture. 794 | 795 | ![Azure Storage Explorer](Images/AzureStorageExplorer.png) 796 | 797 | You can use the [Service Bus Explorer](https://github.com/paolosalvatori/ServiceBusExplorer) to see the messages sent by the Azure Function to the queue in the target Service Bus namespace. 798 | 799 | ![Service Bus Explorer](Images/ServiceBusExplorer.png) 800 | 801 | Finally, you can use [Kusto Query Language](https://kusto.azurewebsites.net/docs/) to create a queries in Application Insights to analyze the metrics and logs generated by the Azure Function. 802 | 803 | ![Application Insights](Images/ApplicationInsights.png) 804 | 805 | Here are a couple of sample queries: 806 | 807 | **customMetrics** 808 | ``` 809 | let min_t = toscalar(customMetrics| where name in ('ProcessBlobEvents Created', 'ProcessBlobEvents Deleted') | summarize min(timestamp)); 810 | let max_t = toscalar(customMetrics| where name in ('ProcessBlobEvents Created', 'ProcessBlobEvents Deleted') | summarize max(timestamp)); 811 | customMetrics 812 | | where name in ('ProcessBlobEvents Created', 'ProcessBlobEvents Deleted') 813 | | extend contentType = tostring(customDimensions.ContentType) 814 | | make-series eventCount=count() default=0 on timestamp in range(min_t, max_t, 1s) by contentType 815 | | extend movingAverage=series_fir(eventCount, repeat(1, 5), true, true), 816 | series_fit_2lines(eventCount), 817 | series_fit_line(eventCount) 818 | | render timechart 819 | ``` 820 | 821 | **customEvents** 822 | ``` 823 | customEvents 824 | | project timestamp, message=name, operation=operation_Name 825 | ``` 826 | -------------------------------------------------------------------------------- /Scripts/create-event-grid-subscription-for-azure-function.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # variables 4 | location="WestEurope" 5 | storageAccountName="babofiles" 6 | storageAccountResourceGroup="BaboFilesResourceGroup" 7 | functionAppName="EventGridBlobEvents" 8 | functionAppResourceGroup="EventGridBlobEventsResourceGroup" 9 | functionName="ProcessBlobEvents" 10 | subscriptionName='BaboFilesAzureFunctionSubscriber' 11 | deadLetterContainerName="deadletter" 12 | filesContainerName="files" 13 | subjectBeginsWith="/blobServices/default/containers/"$filesContainerName 14 | 15 | # functions 16 | function getEventGridExtensionKey 17 | { 18 | # get Kudu username 19 | echo "Retrieving username from ["$functionAppName"] Azure Function publishing profile..." 20 | username=$(az functionapp deployment list-publishing-profiles --name $1 --resource-group $2 --query '[?publishMethod==`MSDeploy`].userName' --output tsv) 21 | 22 | if [ -n $username ]; then 23 | echo "["$username"] username successfully retrieved" 24 | else 25 | echo "No username could be retrieved" 26 | return 27 | fi 28 | 29 | # get Kudu password 30 | echo "Retrieving password from ["$functionAppName"] Azure Function publishing profile..." 31 | password=$(az functionapp deployment list-publishing-profiles --name $1 --resource-group $2 --query '[?publishMethod==`MSDeploy`].userPWD' --output tsv) 32 | 33 | if [ -n $password ]; then 34 | echo "["$password"] password successfully retrieved" 35 | else 36 | echo "No password could be retrieved" 37 | return 38 | fi 39 | 40 | # get jwt 41 | echo "Retrieving JWT token from Azure Function \ Kudu Management API..." 42 | jwt=$(sed -e 's/^"//' -e 's/"$//' <<< $(curl https://$functionAppName.scm.azurewebsites.net/api/functions/admin/token --user $username":"$password --silent)) 43 | 44 | if [ -n $jwt ]; then 45 | echo "JWT token successfully retrieved" 46 | else 47 | echo "No JWT token could be retrieved" 48 | return 49 | fi 50 | 51 | # get eventgrid_extension key 52 | echo "Retrieving [eventgrid_extension] key..." 53 | eventGridExtensionKey=$(sed -e 's/^"//' -e 's/"$//' <<< $(curl -H 'Accept: application/json' -H "Authorization: Bearer ${jwt}" https://$functionAppName.azurewebsites.net/admin/host/systemkeys/eventgrid_extension --silent | jq .value)) 54 | 55 | if [ -n $eventGridExtensionKey ]; then 56 | echo "[eventgrid_extension] key successfully retrieved" 57 | else 58 | echo "No [eventgrid_extension] key could be retrieved" 59 | return 60 | fi 61 | } 62 | 63 | # check if the storage account exists 64 | echo "Checking if ["$storageAccountName"] storage account actually exists..." 65 | 66 | set +e 67 | ( 68 | az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup &> /dev/null 69 | ) 70 | 71 | if [ $? != 0 ]; then 72 | echo "No ["$storageAccountName"] storage account actually exists" 73 | set -e 74 | ( 75 | # create the storage account 76 | az storage account create \ 77 | --name $storageAccountName \ 78 | --resource-group $storageAccountResourceGroup \ 79 | --location $location \ 80 | --sku Standard_LRS \ 81 | --kind BlobStorage \ 82 | --access-tier Hot 1> /dev/null 83 | ) 84 | echo "["$storageAccountName"] storage account successfully created" 85 | else 86 | echo "["$storageAccountName"] storage account already exists" 87 | fi 88 | 89 | # get storage account connection string 90 | echo "Retrieving the connection string for ["$storageAccountName"] storage account..." 91 | connectionString=$(az storage account show-connection-string --name $storageAccountName --resource-group $storageAccountResourceGroup --query connectionString --output tsv) 92 | 93 | if [ -n $connectionString ]; then 94 | echo "The connection string for ["$storageAccountName"] storage account is ["$connectionString"]" 95 | else 96 | echo "Failed to retrieve the connection string for ["$storageAccountName"] storage account" 97 | return 98 | fi 99 | 100 | # checking if deadletter container exists 101 | echo "Checking if ["$deadLetterContainerName"] container already exists..." 102 | set +e 103 | ( 104 | az storage container show --name $deadLetterContainerName --connection-string $connectionString &> /dev/null 105 | ) 106 | 107 | if [ $? != 0 ]; then 108 | echo "No ["$deadLetterContainerName"] container actually exists in ["$storageAccountName"] storage account" 109 | set -e 110 | ( 111 | # create deadletter container 112 | az storage container create \ 113 | --name $deadLetterContainerName \ 114 | --public-access off \ 115 | --connection-string $connectionString 1> /dev/null 116 | ) 117 | echo "["$deadLetterContainerName"] container successfully created in ["$storageAccountName"] storage account" 118 | else 119 | echo "A container called ["$deadLetterContainerName"] already exists in ["$storageAccountName"] storage account" 120 | fi 121 | 122 | # checking if files container exists 123 | echo "Checking if ["$filesContainerName"] container already exists..." 124 | set +e 125 | ( 126 | az storage container show --name $filesContainerName --connection-string $connectionString &> /dev/null 127 | ) 128 | 129 | if [ $? != 0 ]; then 130 | echo "No ["$filesContainerName"] container actually exists in ["$storageAccountName"] storage account" 131 | set -e 132 | ( 133 | # create files container 134 | az storage container create \ 135 | --name $filesContainerName \ 136 | --public-access off \ 137 | --connection-string $connectionString 1> /dev/null 138 | ) 139 | echo "["$filesContainerName"] container successfully created in ["$storageAccountName"] storage account" 140 | else 141 | echo "A container called ["$filesContainerName"] already exists in ["$storageAccountName"] storage account" 142 | fi 143 | 144 | # retrieve resource id for the storage account 145 | echo "Retrieving the resource id for ["$storageAccountName"] storage account..." 146 | storageAccountId=$(az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup --query id --output tsv 2> /dev/null) 147 | 148 | if [ -n $storageAccountId ]; then 149 | echo "Resource id for ["$storageAccountName"] storage account successfully retrieved: ["$storageAccountId"]" 150 | else 151 | echo "Failed to retrieve resource id for ["$storageAccountName"] storage account" 152 | return 153 | fi 154 | 155 | # retrieve eventgrid_extensionkey 156 | getEventGridExtensionKey $functionAppName $functionAppResourceGroup 157 | 158 | if [ -z $eventGridExtensionKey ]; then 159 | echo "Failed to retrieve eventgrid_extensionkey" 160 | return 161 | fi 162 | 163 | # creating the endpoint URL for the Azure Function 164 | endpointUrl="https://$functionAppName.azurewebsites.net/runtime/webhooks/eventgrid?functionName=$functionName&code=$eventGridExtensionKey" 165 | 166 | echo "The endpoint for the ["$functionName"] function in the ["$functionAppName"] function app is ["$endpointUrl"]" 167 | 168 | echo "Checking if Azure CLI eventgrid extension is installed..." 169 | set +e 170 | ( 171 | az extension show --name eventgrid --query name --output tsv &> /dev/null 172 | ) 173 | 174 | if [ $? != 0 ]; then 175 | echo "The Azure CLI eventgrid extension was not found. Installing the extension..." 176 | az extension add --name eventgrid 177 | else 178 | echo "Azure CLI eventgrid extension successfully found. Updating the extension to the latest version..." 179 | az extension update --name eventgrid 180 | fi 181 | 182 | # checking if the subscription already exists 183 | echo "Checking if ["$subscriptionName"] Event Grid subscription already exists for ["$storageAccountName"] storage account..." 184 | set +e 185 | ( 186 | az eventgrid event-subscription show --name $subscriptionName --source-resource-id $storageAccountId &> /dev/null 187 | ) 188 | 189 | if [ $? != 0 ]; then 190 | echo "No ["$subscriptionName"] Event Grid subscription actually exists for ["$storageAccountName"] storage account" 191 | echo "Creating a subscription for the ["$endpointUrl"] endpoint of the ["$functionName"] Azure Function..." 192 | 193 | set +e 194 | ( 195 | az eventgrid event-subscription create \ 196 | --source-resource-id $storageAccountId \ 197 | --name $subscriptionName \ 198 | --endpoint-type webhook \ 199 | --endpoint $endpointUrl \ 200 | --subject-begins-with $subjectBeginsWith \ 201 | --deadletter-endpoint $storageAccountId/blobServices/default/containers/$deadLetterContainerName 1> /dev/null 202 | ) 203 | 204 | if [ $? == 0 ]; then 205 | echo "["$subscriptionName"] Event Grid subscription successfully created" 206 | fi 207 | else 208 | echo "An Event Grid subscription called ["$subscriptionName"] already exists for ["$storageAccountName"] storage account" 209 | fi 210 | -------------------------------------------------------------------------------- /Scripts/create-event-grid-subscription-for-local-function.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # variables 4 | location="WestEurope" 5 | storageAccountName="babofiles" 6 | storageAccountResourceGroup="BaboFilesResourceGroup" 7 | subscriptionName='BaboFilesLocalDebugging' 8 | ngrockSubdomain="db1abac5" 9 | functionName="ProcessBlobEvents" 10 | endpointUrl="https://"$ngrockSubdomain".ngrok.io/runtime/webhooks/EventGrid?functionName="$functionName 11 | deadLetterContainerName="deadletter" 12 | filesContainerName="files" 13 | subjectBeginsWith="/blobServices/default/containers/"$filesContainerName 14 | 15 | # check if the storage account exists 16 | echo "Checking if ["$storageAccountName"] storage account actually exists..." 17 | 18 | set +e 19 | ( 20 | az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup &> /dev/null 21 | ) 22 | 23 | if [ $? != 0 ]; then 24 | echo "No ["$storageAccountName"] storage account actually exists" 25 | set -e 26 | ( 27 | # create the storage account 28 | az storage account create \ 29 | --name $storageAccountName \ 30 | --resource-group $storageAccountResourceGroup \ 31 | --location $location \ 32 | --sku Standard_LRS \ 33 | --kind BlobStorage \ 34 | --access-tier Hot 1> /dev/null 35 | ) 36 | echo "["$storageAccountName"] storage account successfully created" 37 | else 38 | echo "["$storageAccountName"] storage account already exists" 39 | fi 40 | 41 | # get storage account connection string 42 | echo "Retrieving the connection string for ["$storageAccountName"] storage account..." 43 | connectionString=$(az storage account show-connection-string --name $storageAccountName --resource-group $storageAccountResourceGroup --query connectionString --output tsv) 44 | 45 | if [ -n $connectionString ]; then 46 | echo "The connection string for ["$storageAccountName"] storage account is ["$connectionString"]" 47 | else 48 | echo "Failed to retrieve the connection string for ["$storageAccountName"] storage account" 49 | return 50 | fi 51 | 52 | # checking if deadletter container exists 53 | echo "Checking if ["$deadLetterContainerName"] container already exists..." 54 | set +e 55 | ( 56 | az storage container show --name $deadLetterContainerName --connection-string $connectionString &> /dev/null 57 | ) 58 | 59 | if [ $? != 0 ]; then 60 | echo "No ["$deadLetterContainerName"] container actually exists in ["$storageAccountName"] storage account" 61 | set -e 62 | ( 63 | # create deadletter container 64 | az storage container create \ 65 | --name $deadLetterContainerName \ 66 | --public-access off \ 67 | --connection-string $connectionString 1> /dev/null 68 | ) 69 | echo "["$deadLetterContainerName"] container successfully created in ["$storageAccountName"] storage account" 70 | else 71 | echo "A container called ["$deadLetterContainerName"] already exists in ["$storageAccountName"] storage account" 72 | fi 73 | 74 | # checking if files container exists 75 | echo "Checking if ["$filesContainerName"] container already exists..." 76 | set +e 77 | ( 78 | az storage container show --name $filesContainerName --connection-string $connectionString &> /dev/null 79 | ) 80 | 81 | if [ $? != 0 ]; then 82 | echo "No ["$filesContainerName"] container actually exists in ["$storageAccountName"] storage account" 83 | set -e 84 | ( 85 | # create files container 86 | az storage container create \ 87 | --name $filesContainerName \ 88 | --public-access off \ 89 | --connection-string $connectionString 1> /dev/null 90 | ) 91 | echo "["$filesContainerName"] container successfully created in ["$storageAccountName"] storage account" 92 | else 93 | echo "A container called ["$filesContainerName"] already exists in ["$storageAccountName"] storage account" 94 | fi 95 | 96 | # retrieve resource id for the storage account 97 | echo "Retrieving the resource id for ["$storageAccountName"] storage account..." 98 | storageAccountId=$(az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup --query id --output tsv 2> /dev/null) 99 | 100 | if [ -n $storageAccountId ]; then 101 | echo "Resource id for ["$storageAccountName"] storage account successfully retrieved: ["$storageAccountId"]" 102 | else 103 | echo "Failed to retrieve resource id for ["$storageAccountName"] storage account" 104 | return 105 | fi 106 | 107 | echo "Checking if Azure CLI eventgrid extension is installed..." 108 | set +e 109 | ( 110 | az extension show --name eventgrid --query name --output tsv &> /dev/null 111 | ) 112 | 113 | if [ $? != 0 ]; then 114 | echo "The Azure CLI eventgrid extension was not found. Installing the extension..." 115 | az extension add --name eventgrid 116 | else 117 | echo "Azure CLI eventgrid extension successfully found. Updating the extension to the latest version..." 118 | az extension update --name eventgrid 119 | fi 120 | 121 | # checking if the subscription already exists 122 | echo "Checking if ["$subscriptionName"] Event Grid subscription already exists for ["$storageAccountName"] storage account..." 123 | set +e 124 | ( 125 | az eventgrid event-subscription show --name $subscriptionName --source-resource-id $storageAccountId &> /dev/null 126 | ) 127 | 128 | if [ $? != 0 ]; then 129 | echo "No ["$subscriptionName"] Event Grid subscription actually exists for ["$storageAccountName"] storage account" 130 | echo "Creating a subscription for the ["$endpointUrl"] ngrock local endpoint..." 131 | 132 | set +e 133 | ( 134 | az eventgrid event-subscription create \ 135 | --source-resource-id $storageAccountId \ 136 | --name $subscriptionName \ 137 | --endpoint-type webhook \ 138 | --endpoint $endpointUrl \ 139 | --subject-begins-with $subjectBeginsWith \ 140 | --deadletter-endpoint $storageAccountId/blobServices/default/containers/$deadLetterContainerName 1> /dev/null 141 | ) 142 | 143 | if [ $? == 0 ]; then 144 | echo "["$subscriptionName"] Event Grid subscription successfully created" 145 | fi 146 | else 147 | echo "An Event Grid subscription called ["$subscriptionName"] already exists for ["$storageAccountName"] storage account" 148 | fi -------------------------------------------------------------------------------- /Scripts/create-event-grid-subscription-for-web-app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # variables 4 | location="WestEurope" 5 | storageAccountName="babofiles" 6 | storageAccountResourceGroup="BaboFilesResourceGroup" 7 | subscriptionName='BaboFilesWebApp' 8 | webAppSubdomain="baboeventgridviewer" 9 | functionName="ProcessBlobEvents" 10 | endpointUrl="https://"$webAppSubdomain".azurewebsites.net/api/updates" 11 | deadLetterContainerName="deadletter" 12 | filesContainerName="files" 13 | 14 | # check if the storage account exists 15 | echo "Checking if ["$storageAccountName"] storage account actually exists..." 16 | 17 | set +e 18 | ( 19 | az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup &> /dev/null 20 | ) 21 | 22 | if [ $? != 0 ]; then 23 | echo "No ["$storageAccountName"] storage account actually exists" 24 | set -e 25 | ( 26 | # create the storage account 27 | az storage account create \ 28 | --name $storageAccountName \ 29 | --resource-group $storageAccountResourceGroup \ 30 | --location $location \ 31 | --sku Standard_LRS \ 32 | --kind BlobStorage \ 33 | --access-tier Hot 1> /dev/null 34 | ) 35 | echo "["$storageAccountName"] storage account successfully created" 36 | else 37 | echo "["$storageAccountName"] storage account already exists" 38 | fi 39 | 40 | # get storage account connection string 41 | echo "Retrieving the connection string for ["$storageAccountName"] storage account..." 42 | connectionString=$(az storage account show-connection-string --name $storageAccountName --resource-group $storageAccountResourceGroup --query connectionString --output tsv) 43 | 44 | if [ -n $connectionString ]; then 45 | echo "The connection string for ["$storageAccountName"] storage account is ["$connectionString"]" 46 | else 47 | echo "Failed to retrieve the connection string for ["$storageAccountName"] storage account" 48 | return 49 | fi 50 | 51 | # checking if deadletter container exists 52 | echo "Checking if ["$deadLetterContainerName"] container already exists..." 53 | set +e 54 | ( 55 | az storage container show --name $deadLetterContainerName --connection-string $connectionString &> /dev/null 56 | ) 57 | 58 | if [ $? != 0 ]; then 59 | echo "No ["$deadLetterContainerName"] container actually exists in ["$storageAccountName"] storage account" 60 | set -e 61 | ( 62 | # create deadletter container 63 | az storage container create \ 64 | --name $deadLetterContainerName \ 65 | --public-access off \ 66 | --connection-string $connectionString 1> /dev/null 67 | ) 68 | echo "["$deadLetterContainerName"] container successfully created in ["$storageAccountName"] storage account" 69 | else 70 | echo "A container called ["$deadLetterContainerName"] already exists in ["$storageAccountName"] storage account" 71 | fi 72 | 73 | # checking if files container exists 74 | echo "Checking if ["$filesContainerName"] container already exists..." 75 | set +e 76 | ( 77 | az storage container show --name $filesContainerName --connection-string $connectionString &> /dev/null 78 | ) 79 | 80 | if [ $? != 0 ]; then 81 | echo "No ["$filesContainerName"] container actually exists in ["$storageAccountName"] storage account" 82 | set -e 83 | ( 84 | # create files container 85 | az storage container create \ 86 | --name $filesContainerName \ 87 | --public-access off \ 88 | --connection-string $connectionString 1> /dev/null 89 | ) 90 | echo "["$filesContainerName"] container successfully created in ["$storageAccountName"] storage account" 91 | else 92 | echo "A container called ["$filesContainerName"] already exists in ["$storageAccountName"] storage account" 93 | fi 94 | 95 | # retrieve resource id for the storage account 96 | echo "Retrieving the resource id for ["$storageAccountName"] storage account..." 97 | storageAccountId=$(az storage account show --name $storageAccountName --resource-group $storageAccountResourceGroup --query id --output tsv 2> /dev/null) 98 | 99 | if [ -n $storageAccountId ]; then 100 | echo "Resource id for ["$storageAccountName"] storage account successfully retrieved: ["$storageAccountId"]" 101 | else 102 | echo "Failed to retrieve resource id for ["$storageAccountName"] storage account" 103 | return 104 | fi 105 | 106 | echo "Checking if Azure CLI eventgrid extension is installed..." 107 | set +e 108 | ( 109 | az extension show --name eventgrid --query name --output tsv &> /dev/null 110 | ) 111 | 112 | if [ $? != 0 ]; then 113 | echo "The Azure CLI eventgrid extension was not found. Installing the extension..." 114 | az extension add --name eventgrid 115 | else 116 | echo "Azure CLI eventgrid extension successfully found. Updating the extension to the latest version..." 117 | az extension update --name eventgrid 118 | fi 119 | 120 | # checking if the subscription already exists 121 | echo "Checking if ["$subscriptionName"] Event Grid subscription already exists for ["$storageAccountName"] storage account..." 122 | set +e 123 | ( 124 | az eventgrid event-subscription show --name $subscriptionName --source-resource-id $storageAccountId &> /dev/null 125 | ) 126 | 127 | if [ $? != 0 ]; then 128 | echo "No ["$subscriptionName"] Event Grid subscription actually exists for ["$storageAccountName"] storage account" 129 | echo "Creating the subscription for the ["$endpointUrl"] endpoint of the Azure Event Grid Viewer web app..." 130 | set +e 131 | ( 132 | az eventgrid event-subscription create \ 133 | --source-resource-id $storageAccountId \ 134 | --name $subscriptionName \ 135 | --endpoint-type webhook \ 136 | --endpoint $endpointUrl \ 137 | --deadletter-endpoint $storageAccountId/blobServices/default/containers/$deadLetterContainerName 1> /dev/null 138 | ) 139 | 140 | if [ $? == 0 ]; then 141 | echo "["$subscriptionName"] Event Grid subscription successfully created" 142 | fi 143 | else 144 | echo "An Event Grid subscription called ["$subscriptionName"] already exists for ["$storageAccountName"] storage account" 145 | fi --------------------------------------------------------------------------------