Home

Awesome

Geode Pattern Accelerator

The accelerator is designed to help developers with Azure Functions based APIs that utilize Cosmos DB as a data store to implement the geode pattern by deploying their API to geodes in distributed Azure regions.

Geode Pattern

The repository contains Terraform code that deploys geodes to a configurable set of Azure regions, each containing an Azure Function App and API Management instance that fronts it. It also deploys a Cosmos DB account with read/write regions in each of the geode locations and an Azure Front Door that load balances between the regional API deployments. Each piece of the larger architecture is deployed with dedicated monitoring resources and security measures.

The accelerator can be used with any Azure Functions based API, but also supplies a basic .NET Inventory API as a starting place for new projects.

Architecture Details

Applying the files in the ./terraform directory creates a series of resources in a new resource group. In each of the locations supplied via Terraform parameter, a Consumption tier API Management instance is deployed with an Azure Functions backend. The API Management instance has an API and Operations that exactly match the endpoints exposed by the Azure Function App. An Azure Front Door is deployed globally and is configured with routing rules that allow it to distribute traffic between the API Management instances. The Front Door serves as the single point of entry for the API.

Each geode also receives a dedicated Microsoft Entra application that is used to secure the geode's Function App. The API Management instance is deployed with Managed Identity and contains an authentication-managed-identity policy that allows it to call the Azure Functions. Traffic to the Azure Functions is thus restricted to the API Management instance in its geode. Each API Management instance is also secured so that it only accepts requests from Azure Front Door. Relevant secrets are stored in a single Key Vault instance which supports the entire resource group.

The Terraform code also creates a Cosmos DB instance with a database and containers that are necessary for the Azure Functions. Each of the locations supplied via Terraform parameter are added as read regions. Optionally, the files can be applied with multi-region write enabled, deploying each of the same read regions with write capabilities. Other important properties on the Cosmos DB like maximum throughput on the database and containers, consistency level, etc. can also be easily supplied via Terraform parameter. Traffic is restricted so that the only entities able to run Cosmos DB queries are the set of Azure Functions in the various geodes.

Monitoring resources are deployed to each of the resources in the larger API architecture, where possible. A single Log Analytics workspace is deployed to the resource group and is configured to capture logs from Front Door, Cosmos DB, Azure Functions, Key Vault, and Cosmos DB. Each Azure Function App is also deployed with a dedicated Application Insights that is used to collect and query its log, performance, and error data. Application Insights reduces throughput when applied to an API Management instance and the Consumption tier does not currently support Log Analytics, so monitoring has been omitted for API Management.

The top level resources deployed to the resource group once are as follows:

The top level resources deployed to each geode are as follows:

Use With Example Inventory API

The /src directory contains a basic API that serves as an example and place for developers to get started with the accelerator. The Inventory API is designed to work with a Cosmos DB instance with an Inventory database and Products container. The API contains two endpoints, GetProducts and GetProductById, which retrieve all Products and a specific Product, respectively, from the Products container. The endpoints themselves utilize Cosmos DB input bindings for Azure Functions and simply return the retrieved result, without any additional C# logic.

Ensure your subscription_id is set within the provider block in main.tf, then navigate to the /terraform directory and initialize the project:

terraform init

Rename the sample.terraform.tfvars to terraform.tfvars and update the parameter values accordingly.

Plan, and then apply the execution plan, supplying the parameter values via the terraform.tfvars file:

terraform apply -var-file="terraform.tfvars"

Finally, navigate to the /terraform/scripts directory and run publishfunctions.sh, passing in the relative paths for the directories that contain the Terraform and Azure Functions projects. The shell script ingests the outputs from the terraform apply and deploys the API code to each Azure Function App:

Note: The script requires that Azure Functions Core Tools 4.x be installed on the machine.

publishfunctions.sh ../ ../../src/inventory-api/inventory-api

Once the script exits, test the API endpoints in the Azure Front Door, using the frontdoor_hostname from the Terraform outputs (https://<frontdoor_hostname>/inventory/api/products and https://<frontdoor_hostname>/inventory/api/product/{id}).

Use With Your Own API

The accelerator contains a .NET Azure Functions based Inventory API (/src/inventory-api) that deals with storage and retrieval of Product entities in Cosmos DB. The project contains two Functions, GetProducts and GetProductById, which retrieve all Products and a specific Product, respectively, from the Products container in an Inventory database in a Cosmos DB.

Assuming you have an Azure Functions based API that uses Cosmos DB as a data store, the Inventory API can be deleted from the repository entirely and a new Azure Functions project can be moved into the repo. In order for the accelerator to work with your API, the Terraform code will need to be updated in a few key places.

The Cosmos DB database and container resources are declared on line 139 of main.tf. Update the "inventory" database and "products" container with the appropriate names and partition keys:

resource "azurerm_cosmosdb_sql_database" "inventory" {
  name                = "Inventory"
  resource_group_name = azurerm_resource_group.rg.name
  account_name        = azurerm_cosmosdb_account.cosmosaccount.name
  autoscale_settings {
    max_throughput = var.database_max_throughput
  }
}

resource "azurerm_cosmosdb_sql_container" "products" {
  name                = "Products"
  resource_group_name = azurerm_resource_group.rg.name
  account_name        = azurerm_cosmosdb_account.cosmosaccount.name
  database_name       = azurerm_cosmosdb_sql_database.inventory.name
  partition_key_paths = ["/id"]
  autoscale_settings {
    max_throughput = var.container_max_throughput
  }
}

The API Management API(s) and Operations will need to be updated to match the endpoints in your API.

On line 20 of main.tf in the geode module, the "inventory" API is declared with path, protocols, etc. specified:

resource "azurerm_api_management_api" "inventory" {
  name                  = "Inventory"
  resource_group_name   = var.resource_group_name
  api_management_name   = azurerm_api_management.apim_service.name
  revision              = "1"
  display_name          = "Inventory"
  path                  = "inventory"
  protocols             = ["https"]
  service_url           = "https://${azurerm_windows_function_app.fxn_app.default_hostname}"
  subscription_required = false
}

Rename the API and update its properties to fit your API's needs.

The two endpoints, GetProducts and GetProductById, are declared on line 35:

resource "azurerm_api_management_api_operation" "get_product_by_id" {
  operation_id        = "GetProductById"
  api_name            = azurerm_api_management_api.inventory.name
  api_management_name = azurerm_api_management.apim_service.name
  resource_group_name = var.resource_group_name
  display_name        = "GetProductById"
  method              = "GET"
  url_template        = "/api/product/{id}"
  description         = "Retrieves Product by Id"

  response {
    status_code = 200
  }

  template_parameter {
    name     = "id"
    required = true
    type     = "string"
  }
}

resource "azurerm_api_management_api_operation" "get_products" {
  operation_id        = "GetProducts"
  api_name            = azurerm_api_management_api.inventory.name
  api_management_name = azurerm_api_management.apim_service.name
  resource_group_name = var.resource_group_name
  display_name        = "GetProducts"
  method              = "GET"
  url_template        = "/api/products"
  description         = "Retrieves all Products"

  response {
    status_code = 200
  }
}

Update the Operations to match the endpoints in your API, ensuring that the url template matches exactly so requests are properly routed.

The Inventory API project relies on a CosmosDBConnection application setting, stored in the local.settings.json file. Line 42 in the circular_dependencies module declares a function_app_settings array with the necessary key value pairs from the Inventory API's local.settings.json file:

locals {
  function_app_settings = [
    {
      name        = "APPINSIGHTS_INSTRUMENTATIONKEY"
      value       = var.instrumentation_key
      slotSetting = false
    },
    {
      name        = "FUNCTIONS_EXTENSION_VERSION"
      value       = "~4"
      slotSetting = false
    },
    {
      name        = "FUNCTIONS_WORKER_RUNTIME"
      value       = "dotnet-isolated"
      slotSetting = false
    },
    {
      name        = "CosmosDBConnection"
      value       = "@Microsoft.KeyVault(SecretUri=${var.cosmos_connection_string_key_vault_secret_id})"
      slotSetting = false
    }
  ]
}

Update the array with the appropriate settings from your API's local.settings.json file.

At this point, the project has been updated to fit the new API and can now be used to globally distribute its deployment. Ensure your subscription_id is set within the provider block in main.tf, then navigate to the /terraform directory and initialize the project:

terraform init

Rename the sample.terraform.tfvars to terraform.tfvars and update the parameter values accordingly.

Plan, and then apply the execution plan, supplying the parameter values via the terraform.tfvars file:

terraform apply -var-file="terraform.tfvars"

Finally, navigate to the /terraform/scripts directory and run publishfunctions.sh, passing in the relative paths for the directories that contain the Terraform and Azure Functions projects. The shell script ingests the outputs from the terraform apply and deploys the API code to each Azure Function App:

publishfunctions.sh <TERRAFORM_DIRECTORY_RELATIVE_PATH> <FUNCTION_APP_DIRECTORY_RELATIVE_PATH>

Once the script exits, test the API endpoints in the Azure Front Door, using the frontdoor_hostname from the Terraform outputs (https://<frontdoor_hostname>/inventory/api/products and https://<frontdoor_hostname>/inventory/api/product/{id}).

Terraform Parameters

ParameterDataTypeRequiredDescription
base_namestringtrueThe base name for created resources, used for tagging as a group.
primary_locationstringtrueThe Azure region in which to deploy the Resource Group as well as Cosmos DB, API, and Key Vault resources.
additional_locationsstringtrueThe additional Azure regions in which to deploy API resources.
database_max_throughputintegerfalseThe maximum throughput of the SQL database (RU/s). Must be between 100,000 and 1,000,000. Must be set in increments of 1,000.
container_max_throughputintegerfalseThe maximum throughput of the SQL container (RU/s). Must be between 10,000 and 100,000. Must be set in increments of 1,000.
consistency_levelstringfalseThe Consistency Level to use for the CosmosDB Account - can be either BoundedStaleness, Eventual, Session, Strong or ConsistentPrefix.
availability_zonesbooleanfalseShould zone redundancy be enabled for the Cosmos DB regions?
multi_region_writebooleanfalseEnable multi-master support for the Cosmos DB account.
app_service_skustringfalseSpecifies the Azure Functions App Service SKU.
front_door_skustringfalseSpecifies the Azure Front Door SKU.

Security

All services in the API architecture are secure, aside from the Azure Front Door. The Cosmos DB is inaccessible to all entities except the Azure Function Apps through IP restriction. The Azure Functions are deployed alongside a dedicated Microsoft Entra application that is used to restrict access to the Function endpoints. The endpoints are only accessible via the API Management instance in the same geode, which have Managed Identity and a corresponding authentication-managed-identity policy that allow it to authorize requests to the Azure Functions backend. Each API Management instance in the created resource group contains additional policies that make it inaccessible from all origins except the Azure Front Door. Front Doors are created with a unique ID that is attached to all requests to its backend pools via headers. The API Management API entities contain policies that check for the unique ID in the request headers and return 401s if it is not present.

The only accessible ingress point for the API is the Azure Front Door. The accelerator is designed to deploy the Front Door without security measures in order for you to add the appropriate ones for your needs. Azure Front Door is deployed alongside a Front Door specific Web Application Firewall (WAF). WAF policies should be added to the Front Door in the main.tf file via an azurerm_cdn_frontdoor_firewall_policy resource and can secure the API from a number of vulnerabilities, provide custom access, rate limit, and more.

Monitoring

The /monitoring directory contains Kusto queries that can be run against the different services within the API architecture. As previously mentioned, the global and geode-specific resources are configured to stream logs to Log Analytics and the Azure Functions are additionally deployed with a corresponding Application Insights instance. The queries each contain a cluster() function that connects to these monitoring resources and must be updated with the baseName value provided earlier as a Terraform parameter.

The following tables list the provided queries with a brief description:

Front Door

Kusto QueryDescription
backend_errors.kqlDisplays the number of errors thrown by each backend in the Front Door's backend pool over a period of time.
incoming_requests_by_ip_address.kqlDisplays the number of incoming requests from each IP address over a period of time.
requests_per_second.kqlDisplays the requests per second handled by the Front Door over a period of time.
routed_requests_by_backend_host.kqlDisplays the number of incoming requests routed to each backend in the Front Door's backend pool over a period of time.

Azure Functions

Kusto QueryDescription
function_instance_scaleout.kqlDisplays the number of instances the Function App scaled to over a period of time.
overall_response_duration.kqlDisplays the minimum, maximum, average, and percentile duration data over a period of time for all requests.
read_response_duration.kqlDisplays the minimum, maximum, average, and percentile duration data over a period of time for read requests.
response_codes.kqlDisplays the count for each response code returned from the Function App over a period of time.
total_requests.kqlDisplays the total number of requests handled by the Function App over a period of time.
write_response_duration.kqlDisplays the minimum, maximum, average, and percentile duration data over a period of time for write requests.

Cosmos DB

Kusto QueryDescription
consumed_rus.kqlDisplays the consumed RUs for the Cosmos DB over a period of time.
read_operation_duration.kqlDisplays the minimum, maximum, average, and percentile duration data over a period of time for read requests.
total_failed_requests.kqlDisplays the total number of failed requests over a period of time.
total_read_requests.kqlDisplays the total number of read requests over a period of time.
total_successful_requests.kqlDisplays the total number of successful requests over a period of time.
total_throttled_requests.kqlDisplays the total number of throttled requests over a period of time.
total_write_requests.kqlDisplays the total number of write requests over a period of time.
write_operation_duration.kqlDisplays the minimum, maximum, average, and percentile duration data over a period of time for write requests.

Contributions

Please see our contributor guide.

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

Microsoft Patterns & Practices, Azure Architecture Center.