Azure

Self-hosting on Azure

To self-host Membrane on Azure, you need to set up Azure Blob Storage.

Everything else is the same as in the main guide.

Setting up Azure Blob Storage

This guide uses terraform to manage the infrastructure.

  1. Create a storage account and storage containers:
resource "azurerm_storage_account" "main" {
   name                             = "${var.environment}integrationapp"
   resource_group_name              = var.resource_group_name
   location                         = var.resource_group_location
   account_tier                     = "Standard"
   account_replication_type         = "LRS"
   min_tls_version                  = "TLS1_2"
   cross_tenant_replication_enabled = true

   blob_properties {
      cors_rule {
         allowed_headers    = ["*"]
         allowed_methods    = ["GET"]
         allowed_origins    = ["*"]
         exposed_headers    = ["*"]
         max_age_in_seconds = 3000
      }
   }

   tags = local.common_tags
}

# Container for temporary files
resource "azurerm_storage_container" "tmp" {
   name                  = "integration-app-${var.environment}-temp"
   storage_account_id    = azurerm_storage_account.main.id
   container_access_type = "private"
}

# Container for connectors
resource "azurerm_storage_container" "connectors" {
   name                  = "integration-app-${var.environment}-connectors"
   storage_account_id    = azurerm_storage_account.main.id
   container_access_type = "private"
}

# Note: For static website hosting, Azure automatically creates a $web container
# We'll use that container for static files instead of creating a separate one

# Static website configuration
resource "azurerm_storage_account_static_website" "main" {
   storage_account_id = azurerm_storage_account.main.id
   index_document     = "index.html"
   error_404_document = "404.html"
}

# Lifecycle management for tmp container
resource "azurerm_storage_management_policy" "tmp" {
   storage_account_id = azurerm_storage_account.main.id

   rule {
      name    = "cleanup"
      enabled = true

      filters {
         blob_types   = ["blockBlob"]
         prefix_match = ["tmp/"]
      }

      actions {
         base_blob {
            delete_after_days_since_modification_greater_than = 7
         }
      }
   }
}
  1. Configure CDN FrontDoor for serving static files (Optional)
resource "azurerm_cdn_frontdoor_profile" "static" {
   name                = "${var.environment}-afd-static"
   resource_group_name = var.resource_group_name
   sku_name            = "Standard_AzureFrontDoor"

   tags = {
      Service = "core-azure"
   }
}

resource "azurerm_cdn_frontdoor_endpoint" "static" {
   name                     = "${var.environment}-afd-static-endpoint"
   cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.static.id
}

resource "azurerm_cdn_frontdoor_origin_group" "static" {
   name                     = "${var.environment}-afd-static-origin-group"
   cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.static.id

   load_balancing {
      sample_size                 = 4
      successful_samples_required = 3
   }
}

resource "azurerm_cdn_frontdoor_origin" "static" {
   name                           = "${var.environment}-afd-static-origin"
   cdn_frontdoor_origin_group_id  = azurerm_cdn_frontdoor_origin_group.static.id
   enabled                        = true
   host_name                      = "${azurerm_storage_account.main.name}.z13.web.core.windows.net"
   http_port                      = 80
   https_port                     = 443
   origin_host_header             = "${azurerm_storage_account.main.name}.z13.web.core.windows.net"
   priority                       = 1
   weight                         = 1000
   certificate_name_check_enabled = true
}

resource "azurerm_cdn_frontdoor_route" "static" {
   name                            = "${var.environment}-afd-static-route"
   cdn_frontdoor_endpoint_id       = azurerm_cdn_frontdoor_endpoint.static.id
   cdn_frontdoor_origin_group_id   = azurerm_cdn_frontdoor_origin_group.static.id
   cdn_frontdoor_origin_ids        = [azurerm_cdn_frontdoor_origin.static.id]
   enabled                         = true
   forwarding_protocol             = "HttpsOnly"
   https_redirect_enabled          = true
   patterns_to_match               = ["/*"]
   supported_protocols             = ["Http", "Https"]
   link_to_default_domain          = true
   cdn_frontdoor_custom_domain_ids = [azurerm_cdn_frontdoor_custom_domain.static.id]
   cdn_frontdoor_rule_set_ids      = [azurerm_cdn_frontdoor_rule_set.static.id]
}

resource "azurerm_cdn_frontdoor_custom_domain" "static" {
   name                     = "staticazureintmembrane"
   cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.static.id
   host_name                = "static.${var.dns_zone_name}"

   tls {
      certificate_type = "ManagedCertificate"
   }
}

resource "azurerm_cdn_frontdoor_custom_domain_association" "static" {
   cdn_frontdoor_custom_domain_id = azurerm_cdn_frontdoor_custom_domain.static.id
   cdn_frontdoor_route_ids        = [azurerm_cdn_frontdoor_route.static.id]
}

resource "azurerm_cdn_frontdoor_rule_set" "static" {
   name                     = "${var.environment}staticrules"
   cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.static.id
}

resource "azurerm_cdn_frontdoor_rule" "compression" {
   name                      = "enablecompression"
   cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.static.id
   order                     = 1
   behavior_on_match         = "Continue"

   conditions {
      request_method_condition {
         match_values     = ["GET"]
         operator         = "Equal"
         negate_condition = false
      }
   }

   actions {
      route_configuration_override_action {
         compression_enabled           = true
         cache_behavior                = "OverrideIfOriginMissing"
         cache_duration                = "1.00:00:00" # 1 day
         query_string_caching_behavior = "IgnoreQueryString"
      }
   }
}

resource "azurerm_cdn_frontdoor_rule" "cache_static_assets" {
   name                      = "cachestaticassets"
   cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.static.id
   order                     = 2
   behavior_on_match         = "Continue"

   conditions {
      request_uri_condition {
         match_values     = ["*.js", "*.css", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.ico", "*.woff", "*.woff2"]
         operator         = "EndsWith"
         negate_condition = false
      }
   }

   actions {
      route_configuration_override_action {
         cache_behavior                = "OverrideAlways"
         cache_duration                = "7.00:00:00" # 7 days for static assets
         query_string_caching_behavior = "IgnoreQueryString"
      }
   }
}
  1. Configure custom domain DNS managed zone for BASE_STATIC_URI (Optional)
resource "azurerm_dns_txt_record" "afd_static_validation" {
   name                = "_dnsauth.static"
   zone_name           = var.dns_zone_name
   resource_group_name = var.resource_group_name
   ttl                 = 3600

   record {
      value = azurerm_cdn_frontdoor_custom_domain.static.validation_token
   }
}

resource "azurerm_dns_cname_record" "static" {
   name                = "static"
   zone_name           = var.dns_zone_name
   resource_group_name = var.resource_group_name
   ttl                 = 3600
   record              = azurerm_cdn_frontdoor_endpoint.static.host_name
}