├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitignore ├── .markdownlint.jsonc ├── .vscode └── extensions.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── azure-pipelines.yml ├── deploy.sh ├── img ├── azcustomtopicevents.png ├── azdo_run.png ├── azloggedevents.png └── azstorevents.png ├── infrastructure ├── terraform │ ├── .gitignore │ ├── functions │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── main.tf │ ├── provider.tf │ └── variables.tf └── terraform_backend │ └── azure_backend.tf └── src ├── .gitignore ├── FunctionApp ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── Functions │ └── EventGridFunction.cs ├── Sample.FunctionApp.csproj ├── host.json └── sample.local.settings.json └── Sample.FunctionApp.sln /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 4 | #------------------------------------------------------------------------------------------------------------- 5 | 6 | FROM mcr.microsoft.com/azure-functions/dotnet:3.0-dotnet3-core-tools 7 | 8 | # [Optional] Install Terrafrom 9 | ARG INSTALL_TERRAFORM="true" 10 | ARG TERRAFORM_VERSION=1.0.0 11 | ARG TFLINT_VERSION=0.29.0 12 | 13 | # Avoid warnings by switching to noninteractive 14 | ENV DEBIAN_FRONTEND=noninteractive 15 | 16 | # Configure apt and install packages 17 | RUN apt-get update \ 18 | && apt-get -y install --no-install-recommends apt-utils dialog \ 19 | # 20 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 21 | && apt-get -y install \ 22 | unzip \ 23 | # 24 | # [Optional] Install Terraform, tflint, and graphviz 25 | && if [ "$INSTALL_TERRAFORM" = "true" ]; then \ 26 | # 27 | mkdir -p /tmp/docker-downloads \ 28 | && curl -sSL -o /tmp/docker-downloads/terraform.zip https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip \ 29 | && unzip /tmp/docker-downloads/terraform.zip \ 30 | && mv terraform /usr/local/bin \ 31 | && curl -sSL -o /tmp/docker-downloads/tflint.zip https://github.com/wata727/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip \ 32 | && unzip /tmp/docker-downloads/tflint.zip \ 33 | && mv tflint /usr/local/bin \ 34 | && cd ~ \ 35 | && rm -rf /tmp/docker-downloads \ 36 | && apt-get install -y graphviz; \ 37 | fi \ 38 | # 39 | # Clean up 40 | && apt-get autoremove -y \ 41 | && apt-get clean -y \ 42 | && rm -rf /var/lib/apt/lists/* 43 | 44 | # Opt out of Func CLI telemetry gathering 45 | #ENV FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT=true 46 | 47 | # Switch back to dialog for any ad-hoc use of apt-get 48 | ENV DEBIAN_FRONTEND=dialog -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure Functions + Terraform", 3 | "dockerFile": "Dockerfile", 4 | // Use 'settings' to set *default* container specific settings.json values on container create. 5 | // You can edit these settings after create using File > Preferences > Settings > Remote. 6 | "settings": { 7 | "terminal.integrated.profiles.linux": { 8 | "bash": { 9 | "path": "/bin/bash" 10 | } 11 | }, 12 | "terraform.languageServer": { 13 | "enabled": true 14 | }, 15 | "terraform.templateDirectory": "infrastructure/terraform" 16 | }, 17 | // Use 'appPort' to create a container with published ports. If the port isn't working, be sure 18 | // your server accepts connections from all interfaces (0.0.0.0 or '*'), not just localhost. 19 | "appPort": [ 20 | 7071 21 | ], 22 | // [Optional] To reuse of your local HTTPS dev cert, first export it locally using this command: 23 | // * Windows PowerShell: 24 | // dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" 25 | // * macOS/Linux terminal: 26 | // dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" 27 | // 28 | // Next, after running the command above, uncomment lines in the 'mounts' and 'remoteEnv' lines below, 29 | // and open / rebuild the container so the settings take effect. 30 | // 31 | "mounts": [ 32 | // "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind", 33 | "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" 34 | ], 35 | // Uncomment the next line to have VS Code connect as an existing non-root user in the container. 36 | // On Linux, by default, the container user's UID/GID will be updated to match your local user. See 37 | // https://aka.ms/vscode-remote/containers/non-root for details on adding a non-root user if none exist. 38 | // "remoteUser": "vscode", 39 | // Add the IDs of extensions you want installed when the container is created in the array below. 40 | "extensions": [ 41 | "hashicorp.terraform", 42 | "ms-azuretools.vscode-azureterraform", 43 | "ms-azuretools.vscode-azurestorage", 44 | "ms-azuretools.vscode-azureeventgrid", 45 | "ms-azuretools.vscode-azurefunctions", 46 | "ms-azure-devops.azure-pipelines", 47 | "ms-vscode.azurecli", 48 | "ms-vscode.azure-account", 49 | "ms-dotnettools.csharp" 50 | ] 51 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/terraform,azurefunctions 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=terraform,azurefunctions 4 | 5 | ### AzureFunctions ### 6 | # Azure Functions localsettings file 7 | local.settings.json 8 | 9 | ### Terraform ### 10 | # Local .terraform directories 11 | **/.terraform/* 12 | 13 | # .tfstate files 14 | *.tfstate 15 | *.tfstate.* 16 | 17 | # tflock files 18 | *.terraform.lock* 19 | 20 | # Crash log files 21 | crash.log 22 | 23 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 24 | # .tfvars files are managed as part of configuration and so should be included in 25 | # version control. 26 | # 27 | # example.tfvars 28 | 29 | # Ignore override files as they are usually used to override resources locally and so 30 | # are not checked in 31 | override.tf 32 | override.tf.json 33 | *_override.tf 34 | *_override.tf.json 35 | 36 | # Include override files you do wish to add to version control using negated pattern 37 | # !example_override.tf 38 | 39 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 40 | # example: *tfplan* 41 | 42 | # End of https://www.toptal.com/developers/gitignore/api/terraform,azurefunctions 43 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "line-length": false, 4 | "no-inline-html": false 5 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-vscode-remote.remote-containers" 4 | ] 5 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - csharp 5 | - yaml 6 | products: 7 | - dotnet 8 | - azure-functions 9 | - azure-event-grid 10 | - azure-storage 11 | - azure-blob-storage 12 | - azure-devops 13 | --- 14 | 15 | # Subscribing an Azure Function to Event Grid Events via Terraform 16 | 17 | This sample will show you how to create Terraform scripts that create an Azure Function and Subscribe it to Event Grid Storage events. 18 | This is especially interesting as, for Event Grid Subscriptions, the target endpoint must answer EG's "[Subscription Validation Event](https://docs.microsoft.com/en-us/azure/event-grid/security-authentication#validation-details)" which it cannot do until it is deployed. So this method - affectionally coined a "Terraform Sandwich" - shows how to do just that. 19 | 20 | ## Deploying locally 21 | 22 | 1. Open the repo in its VS Code Dev Container (this ensures you have all the right versions of the necessary tooling) 23 | 1. run `./deploy.sh ` 24 | 25 | ### What it does 26 | 27 | - Logs in to Azure and connects to the target subscription 28 | - Tells terraform to deploy everything **except** the event grid subscription piece 29 | - Deploys the function app out to Azure so it's ready to answer the subscription wire-up that Terraform will do next 30 | - Tells terraform to deploy **everything**, which issues the necessary changes to Azure to add the event grid subscription to an 'inbox' storage account 31 | 32 | ## Deploying via Azure DevOps 33 | 34 | By importing the [azure-piplines.yaml](./azure-pipelines.yaml) file in to an Azure DevOps pipeline, you'll get the same process as the above local execution. 35 | > Note: Be sure to change [the `PREFIX` variable](./azure-pipelines.yml#L10) to something unique to you to avoid naming collisions on storage & function apps 36 | 37 | You'll need to create a Service Connection in Azure DevOps: 38 | 39 | 1. Click **Project settings** (bottom left) 40 | 1. Click **Service connections** 41 | 1. Click **Create service connection** 42 | 1. Select **Azure Resource Manager** 43 | 1. Select **Service Principal (automatic)** 44 | 1. Enter **Service Connection name** `my-azure` (Note: you can leave **Resource group** blank) 45 | 1. Ensure **Grant access permissions to all pipelines** is checked 46 | 47 | Run the pipeline: 48 | 49 | Azure DevOps successful run 50 | 51 | ## Running the sample 52 | 53 | This sample has an Azure Function that subscribes to Blob Storage Events and then simply passes the event on to a custom Event Grid Topic. The receiving and sending of events is accomplished via the Event Grid Binding for Azure Functions. 54 | 55 | To exercise the sample: 56 | 57 | 1. Open the 'inbox' storage account created by the deployment 58 | 1. Create a new container 59 | 1. Upload a file in to the container 60 | 61 | Next, go to the Azure Portal, and the Storage Account created by the deployment. Click the 'Events' area and you will see one or more events have come through: 62 | 63 | Azure Portal Storage Events area 64 | 65 | Then go to the custom topic created by deployment, and you'll see that one or more events have been posted to it: 66 | 67 | Azure Portal Custom Topic Events area 68 | 69 | Finally, if you wish to see the output from the Function, go to the Application Insights resource created by deployment and look through the logged TRACE events: 70 | 71 | Azure Portal Custom Topic Events area 72 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | pool: 5 | vmImage: 'ubuntu-latest' 6 | 7 | variables: 8 | LOCATION: westus 9 | # Uncomment & change this to something more unique before running 10 | # PREFIX: azuresample 11 | 12 | jobs: 13 | - job: SetupAzureTFBackend 14 | displayName: Setup TFState Azure Backend 15 | dependsOn: [] 16 | steps: 17 | - task: AzureCLI@2 18 | displayName: Create resource group 19 | inputs: 20 | azureSubscription: 'my-azure' 21 | scriptType: 'bash' 22 | scriptLocation: 'inlineScript' 23 | inlineScript: 'az group create --name $(PREFIX)-sample-rg --location $(LOCATION) --tags sample=azure-functions-event-grid-terraform' 24 | - task: AzureCLI@2 25 | displayName: Create tfstate storage account 26 | inputs: 27 | azureSubscription: 'my-azure' 28 | scriptType: 'bash' 29 | scriptLocation: 'inlineScript' 30 | inlineScript: 'az storage account create --resource-group $(PREFIX)-sample-rg --name $(PREFIX)tfstor --kind StorageV2 --tags sample=azure-functions-event-grid-terraform' 31 | 32 | - task: AzureCLI@2 33 | displayName: Create tfstate container 34 | inputs: 35 | azureSubscription: 'my-azure' 36 | scriptType: 'bash' 37 | scriptLocation: 'inlineScript' 38 | inlineScript: 'az storage container create --name tfstate --account-name $(PREFIX)tfstor' 39 | 40 | - job: Deploy 41 | displayName: Build and Deploy 42 | dependsOn: 43 | - SetupAzureTFBackend 44 | steps: 45 | - task: UseDotNet@2 46 | displayName: 'Target .NET Core 3.1' 47 | inputs: 48 | packageType: sdk 49 | version: 3.1.x 50 | 51 | - task: TerraformInstaller@0 52 | displayName: 'Install Terraform' 53 | inputs: 54 | terraformVersion: '0.12.20' 55 | 56 | - task: DotNetCoreCLI@2 57 | displayName: 'Build Function App' 58 | inputs: 59 | command: 'build' 60 | projects: '$(Build.SourcesDirectory)/src/**/*.csproj' 61 | arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)' 62 | 63 | - task: CopyFiles@2 64 | displayName: Copy Azure TF backend file to primary TF dir 65 | inputs: 66 | SourceFolder: 'infrastructure/terraform_backend' 67 | Contents: '**' 68 | TargetFolder: 'infrastructure/terraform' 69 | OverWrite: true 70 | 71 | - task: TerraformTaskV1@0 72 | displayName: 'tf init' 73 | inputs: 74 | provider: 'azurerm' 75 | command: 'init' 76 | workingDirectory: '$(System.DefaultWorkingDirectory)/infrastructure/terraform' 77 | backendServiceArm: 'my-azure' 78 | backendAzureRmResourceGroupName: '$(PREFIX)-sample-rg' 79 | backendAzureRmStorageAccountName: '$(PREFIX)tfstor' 80 | backendAzureRmContainerName: 'tfstate' 81 | backendAzureRmKey: 'terraform.tfstate' 82 | 83 | - task: TerraformTaskV1@0 84 | displayName: 'TF 🍔 "top bun" (tf apply -target module.functions -var "prefix=$(PREFIX)" -var "location=$(LOCATION)")' 85 | inputs: 86 | provider: 'azurerm' 87 | command: 'apply' 88 | commandOptions: '-target module.functions -var "prefix=$(PREFIX)" -var "location=$(LOCATION)"' 89 | workingDirectory: '$(System.DefaultWorkingDirectory)/infrastructure/terraform' 90 | environmentServiceNameAzureRM: 'my-azure' 91 | 92 | - task: AzureFunctionApp@1 93 | displayName: '🥓 Deploy Function App' 94 | inputs: 95 | azureSubscription: 'my-azure' 96 | appType: 'functionApp' 97 | appName: '$(PREFIX)-fxn' 98 | package: '$(Build.ArtifactStagingDirectory)' 99 | deploymentMethod: 'auto' 100 | 101 | - task: TerraformTaskV1@0 102 | displayName: 'TF 🍔 "bottom bun" (tf apply -var "prefix=$(PREFIX)" -var "location=$(LOCATION)")' 103 | inputs: 104 | provider: 'azurerm' 105 | command: 'apply' 106 | commandOptions: '-var "prefix=$(PREFIX)" -var "location=$(LOCATION)"' 107 | workingDirectory: '$(System.DefaultWorkingDirectory)/infrastructure/terraform' 108 | environmentServiceNameAzureRM: 'my-azure' -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ $# -lt 2 ] 4 | then 5 | echo "Usage: ./deploy.sh " 6 | exit 1 7 | fi 8 | 9 | az account set --subscription $1 &> /dev/null 10 | if [ $? -ne 0 ]; then 11 | az login > /dev/null 12 | fi 13 | 14 | [ $? -ne 0 ] && exit $? 15 | 16 | az account set --subscription $1 17 | 18 | [ $? -ne 0 ] && exit $? 19 | 20 | echo 'Deploying Terraform sandwich "top bun"...' 21 | cd infrastructure/terraform 22 | terraform init -reconfigure -upgrade=true > /dev/null 23 | terraform apply -var prefix=$2 -target module.functions -compact-warnings 24 | 25 | [ $? -ne 0 ] && exit $? 26 | 27 | cd ../../src/FunctionApp 28 | 29 | echo "Deploying Function App..." 30 | sleep 3 31 | func azure functionapp list-functions $2-fxn &> /dev/null 32 | while [ $? -ne 0 ] ; 33 | do 34 | sleep 3 35 | func azure functionapp list-functions $2-fxn &> /dev/null 36 | done 37 | 38 | func azure functionapp publish $2-fxn --csharp > /dev/null 39 | 40 | [ $? -ne 0 ] && exit $? 41 | 42 | echo 'Deploying Terraform sandwich "bottom bun"...' 43 | cd ../../infrastructure/terraform 44 | terraform apply -var prefix=$2 -compact-warnings -------------------------------------------------------------------------------- /img/azcustomtopicevents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-functions-event-grid-terraform/37299715ef04974d679332851e663edfeac14f87/img/azcustomtopicevents.png -------------------------------------------------------------------------------- /img/azdo_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-functions-event-grid-terraform/37299715ef04974d679332851e663edfeac14f87/img/azdo_run.png -------------------------------------------------------------------------------- /img/azloggedevents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-functions-event-grid-terraform/37299715ef04974d679332851e663edfeac14f87/img/azloggedevents.png -------------------------------------------------------------------------------- /img/azstorevents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-functions-event-grid-terraform/37299715ef04974d679332851e663edfeac14f87/img/azstorevents.png -------------------------------------------------------------------------------- /infrastructure/terraform/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/terraform 3 | # Edit at https://www.gitignore.io/?templates=terraform 4 | 5 | ### Terraform ### 6 | # Local .terraform directories 7 | **/.terraform/* 8 | 9 | # .tfstate files 10 | *.tfstate 11 | *.tfstate.* 12 | 13 | # Crash log files 14 | crash.log 15 | 16 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 17 | # .tfvars files are managed as part of configuration and so should be included in 18 | # version control. 19 | # 20 | # example.tfvars 21 | 22 | # Ignore override files as they are usually used to override resources locally and so 23 | # are not checked in 24 | override.tf 25 | override.tf.json 26 | *_override.tf 27 | *_override.tf.json 28 | 29 | # Include override files you do wish to add to version control using negated pattern 30 | # !example_override.tf 31 | 32 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 33 | # example: *tfplan* 34 | 35 | # End of https://www.gitignore.io/api/terraform -------------------------------------------------------------------------------- /infrastructure/terraform/functions/main.tf: -------------------------------------------------------------------------------- 1 | # Windows consumption function app 2 | resource "azurerm_app_service_plan" "fxnapp" { 3 | name = "${var.prefix}-fxn-plan" 4 | location = var.location 5 | resource_group_name = var.resource_group_name 6 | kind = "functionapp" 7 | sku { 8 | tier = "Dynamic" 9 | size = "Y1" 10 | } 11 | tags = { 12 | sample = "azure-functions-event-grid-terraform" 13 | } 14 | } 15 | 16 | # # Linux consumption function app 17 | # resource "azurerm_app_service_plan" "fxnapp" { 18 | # name = "${var.prefix}-lxfxn-plan" 19 | # location = var.location 20 | # resource_group_name = var.resource_group_name 21 | # kind = "functionapp" 22 | # reserved = true 23 | # sku { 24 | # tier = "Dynamic" 25 | # size = "Y1" 26 | # } 27 | # tags = { 28 | # sample = "azure-functions-event-grid-terraform" 29 | # } 30 | # } 31 | 32 | # # Windows Containers consumption function app 33 | # resource "azurerm_app_service_plan" "fxnapp" { 34 | # name = "${var.prefix}-wcfxn-plan" 35 | # location = var.location 36 | # resource_group_name = var.resource_group_name 37 | # kind = "functionapp" 38 | # reserved = true 39 | # is_xenon = true 40 | # sku { 41 | # tier = "Dynamic" 42 | # size = "Y1" 43 | # } 44 | # tags = { 45 | # sample = "azure-functions-event-grid-terraform" 46 | # } 47 | # } 48 | 49 | # Storage account for Azure Function 50 | resource "azurerm_storage_account" "fxnstor" { 51 | name = "${var.prefix}fxnssa" 52 | resource_group_name = var.resource_group_name 53 | location = var.location 54 | account_tier = "Standard" 55 | account_replication_type = "LRS" 56 | account_kind = "StorageV2" 57 | enable_https_traffic_only = true 58 | tags = { 59 | sample = "azure-functions-event-grid-terraform" 60 | } 61 | } 62 | 63 | resource "azurerm_function_app" "fxn" { 64 | name = "${var.prefix}-fxn" 65 | location = var.location 66 | resource_group_name = var.resource_group_name 67 | app_service_plan_id = azurerm_app_service_plan.fxnapp.id 68 | storage_account_name = azurerm_storage_account.fxnstor.name 69 | storage_account_access_key = azurerm_storage_account.fxnstor.primary_access_key 70 | version = "~3" 71 | tags = { 72 | sample = "azure-functions-event-grid-terraform" 73 | } 74 | app_settings = { 75 | APPINSIGHTS_INSTRUMENTATIONKEY = var.application_insights_instrumentation_key 76 | SAMPLE_TOPIC_END_POINT = var.sample_topic_endpoint 77 | SAMPLE_TOPIC_KEY = var.sample_topic_key 78 | } 79 | 80 | # We ignore these because they're set/changed by Function deployment 81 | lifecycle { 82 | ignore_changes = [ 83 | app_settings["WEBSITE_RUN_FROM_PACKAGE"] 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /infrastructure/terraform/functions/outputs.tf: -------------------------------------------------------------------------------- 1 | output "function_id" { 2 | value = azurerm_function_app.fxn.id 3 | } 4 | -------------------------------------------------------------------------------- /infrastructure/terraform/functions/variables.tf: -------------------------------------------------------------------------------- 1 | variable "prefix" { 2 | type = string 3 | } 4 | 5 | variable "resource_group_name" { 6 | type = string 7 | } 8 | 9 | variable "location" { 10 | type = string 11 | } 12 | 13 | variable "sample_topic_endpoint" { 14 | type = string 15 | } 16 | 17 | variable "sample_topic_key" { 18 | type = string 19 | } 20 | 21 | variable "application_insights_instrumentation_key" { 22 | type = string 23 | } 24 | -------------------------------------------------------------------------------- /infrastructure/terraform/main.tf: -------------------------------------------------------------------------------- 1 | ################################################################################## 2 | # Main Terraform file 3 | ################################################################################## 4 | 5 | resource "azurerm_resource_group" "sample" { 6 | name = "${var.prefix}-sample-rg" 7 | location = var.location 8 | tags = { 9 | sample = "azure-functions-event-grid-terraform" 10 | } 11 | } 12 | 13 | resource "azurerm_eventgrid_topic" "sample_topic" { 14 | name = "${var.prefix}-azsam-egt" 15 | location = var.location 16 | resource_group_name = azurerm_resource_group.sample.name 17 | tags = { 18 | sample = "azure-functions-event-grid-terraform" 19 | } 20 | } 21 | 22 | resource "azurerm_application_insights" "logging" { 23 | name = "${var.prefix}-ai" 24 | location = var.location 25 | resource_group_name = azurerm_resource_group.sample.name 26 | application_type = "web" 27 | retention_in_days = 90 28 | tags = { 29 | sample = "azure-functions-event-grid-terraform" 30 | } 31 | } 32 | 33 | resource "azurerm_storage_account" "inbox" { 34 | name = "${var.prefix}inboxsa" 35 | resource_group_name = azurerm_resource_group.sample.name 36 | location = var.location 37 | account_tier = "Standard" 38 | account_replication_type = "LRS" 39 | account_kind = "StorageV2" 40 | enable_https_traffic_only = true 41 | tags = { 42 | sample = "azure-functions-event-grid-terraform" 43 | } 44 | } 45 | 46 | module "functions" { 47 | source = "./functions" 48 | prefix = var.prefix 49 | resource_group_name = azurerm_resource_group.sample.name 50 | location = azurerm_resource_group.sample.location 51 | application_insights_instrumentation_key = azurerm_application_insights.logging.instrumentation_key 52 | sample_topic_endpoint = azurerm_eventgrid_topic.sample_topic.endpoint 53 | sample_topic_key = azurerm_eventgrid_topic.sample_topic.primary_access_key 54 | } 55 | 56 | resource "azurerm_eventgrid_event_subscription" "eventgrid_subscription" { 57 | name = "${var.prefix}-handlerfxn-egsub" 58 | scope = azurerm_storage_account.inbox.id 59 | labels = ["azure-functions-event-grid-terraform"] 60 | azure_function_endpoint { 61 | function_id = "${module.functions.function_id}/functions/${var.eventGridFunctionName}" 62 | 63 | # defaults, specified to avoid "no-op" changes when 'apply' is re-ran 64 | max_events_per_batch = 1 65 | preferred_batch_size_in_kilobytes = 64 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /infrastructure/terraform/provider.tf: -------------------------------------------------------------------------------- 1 | #Set the terraform required version 2 | terraform { 3 | required_version = "~> 1.0.0" 4 | # Configure the Azure Provider 5 | required_providers { 6 | # It is recommended to pin to a given version of the Provider 7 | azurerm = { 8 | source = "hashicorp/azurerm" 9 | version = "~> 2.62.0" 10 | } 11 | } 12 | } 13 | 14 | provider "azurerm" { 15 | features {} 16 | } 17 | 18 | # Make client_id, tenant_id, subscription_id and object_id variables 19 | data "azurerm_client_config" "current" {} 20 | -------------------------------------------------------------------------------- /infrastructure/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "prefix" { 2 | type = string 3 | description = "Prefix given to all resources. Try to make this unique to you" 4 | } 5 | 6 | variable "location" { 7 | type = string 8 | description = "Azure region where to create resources." 9 | default = "West US" 10 | } 11 | 12 | variable "eventGridFunctionName" { 13 | type = string 14 | description = "The name of the Function which handles Event Grid messages" 15 | default = "StorageHandler" 16 | } 17 | -------------------------------------------------------------------------------- /infrastructure/terraform_backend/azure_backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "azure" {} 3 | } -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /src/FunctionApp/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-vscode.csharp" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/FunctionApp/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to .NET Functions", 6 | "type": "coreclr", 7 | "request": "attach", 8 | "processId": "${command:azureFunctions.pickProcess}" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /src/FunctionApp/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "bin/Release/netcoreapp3.1/publish", 3 | "azureFunctions.projectLanguage": "C#", 4 | "azureFunctions.projectRuntime": "~3", 5 | "debug.internalConsoleOptions": "neverOpen", 6 | "azureFunctions.preDeployTask": "publish" 7 | } -------------------------------------------------------------------------------- /src/FunctionApp/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "clean", 6 | "command": "dotnet", 7 | "args": [ 8 | "clean", 9 | "/property:GenerateFullPaths=true", 10 | "/consoleloggerparameters:NoSummary" 11 | ], 12 | "type": "process", 13 | "problemMatcher": "$msCompile" 14 | }, 15 | { 16 | "label": "build", 17 | "command": "dotnet", 18 | "args": [ 19 | "build", 20 | "/property:GenerateFullPaths=true", 21 | "/consoleloggerparameters:NoSummary" 22 | ], 23 | "type": "process", 24 | "dependsOn": "clean", 25 | "group": { 26 | "kind": "build", 27 | "isDefault": true 28 | }, 29 | "problemMatcher": "$msCompile" 30 | }, 31 | { 32 | "label": "clean release", 33 | "command": "dotnet", 34 | "args": [ 35 | "clean", 36 | "--configuration", 37 | "Release", 38 | "/property:GenerateFullPaths=true", 39 | "/consoleloggerparameters:NoSummary" 40 | ], 41 | "type": "process", 42 | "problemMatcher": "$msCompile" 43 | }, 44 | { 45 | "label": "publish", 46 | "command": "dotnet", 47 | "args": [ 48 | "publish", 49 | "--configuration", 50 | "Release", 51 | "/property:GenerateFullPaths=true", 52 | "/consoleloggerparameters:NoSummary" 53 | ], 54 | "type": "process", 55 | "dependsOn": "clean release", 56 | "problemMatcher": "$msCompile" 57 | }, 58 | { 59 | "type": "func", 60 | "dependsOn": "build", 61 | "options": { 62 | "cwd": "${workspaceFolder}/bin/Debug/netcoreapp3.1" 63 | }, 64 | "command": "host start", 65 | "isBackground": true, 66 | "problemMatcher": "$func-watch" 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /src/FunctionApp/Functions/EventGridFunction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Azure.EventGrid.Models; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.EventGrid; 6 | using Microsoft.Extensions.Logging; 7 | using Newtonsoft.Json; 8 | 9 | namespace Sample.FunctionApp.Functions 10 | { 11 | public class EventGridFunction 12 | { 13 | [FunctionName(nameof(StorageHandler))] 14 | public async Task StorageHandler( 15 | [EventGridTrigger] EventGridEvent[] eventGridEvents, 16 | [EventGrid(TopicEndpointUri = @"SAMPLE_TOPIC_END_POINT", TopicKeySetting = @"SAMPLE_TOPIC_KEY")] IAsyncCollector outputEvents, 17 | ILogger logger) 18 | { 19 | if (eventGridEvents == null) 20 | { 21 | throw new ArgumentNullException("Null request received"); 22 | } 23 | 24 | foreach (var ege in eventGridEvents) 25 | { 26 | logger.LogTrace($@"Got event grid message: {JsonConvert.SerializeObject(ege, Formatting.Indented)}"); 27 | 28 | ege.Topic = null; // have to reset topic for the message send to work 29 | ege.DataVersion = "1"; // required to be set 30 | 31 | await outputEvents.AddAsync(ege); 32 | 33 | logger.LogTrace($@"Sent to output topic. {JsonConvert.SerializeObject(ege, Formatting.Indented)}"); 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/FunctionApp/Sample.FunctionApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.1 4 | v3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | PreserveNewest 14 | 15 | 16 | PreserveNewest 17 | Never 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/FunctionApp/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "logLevel": { 5 | "default": "Trace" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/FunctionApp/sample.local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet", 6 | 7 | "SAMPLE_TOPIC_END_POINT": "", 8 | "SAMPLE_TOPIC_KEY": "" 9 | } 10 | } -------------------------------------------------------------------------------- /src/Sample.FunctionApp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29709.97 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.FunctionApp", "FunctionApp\Sample.FunctionApp.csproj", "{53DFCD77-905E-4E6A-AF41-94DC6162338C}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {53DFCD77-905E-4E6A-AF41-94DC6162338C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {53DFCD77-905E-4E6A-AF41-94DC6162338C}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {53DFCD77-905E-4E6A-AF41-94DC6162338C}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {53DFCD77-905E-4E6A-AF41-94DC6162338C}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {766C8593-9118-421F-AEC9-16A6030515E2} 24 | EndGlobalSection 25 | EndGlobal 26 | --------------------------------------------------------------------------------