Skip to content

Latest commit

 

History

History
236 lines (168 loc) · 14.3 KB

README.md

File metadata and controls

236 lines (168 loc) · 14.3 KB

terraform-azurerm-static-site

LICENSE

This Terraform module stands up a static website and supports custom domain names and generates Let's Encrypt TLS certs. It currently only supports Azure DNS Zones, but I'll implement more DNS providers if someone requests it.

Summary

Find the Terraform Module publicly hosted here!

  • Hosts static resources in a Storage Account using the static website hosting capability

  • Stands up an Azure Functions application to act as a reverse proxy over the Storage Account's static website

If the custom_dns variable is populated...

  • Generates a Let's Encrypt TLS certificate with all of the domain names configured

  • Creates DNS entries for the configured domains

  • Binds those domain names and the Let's Encrypt certificate to the Azure Functions application

  • Certificates are valid for 90 days. Applying Terraform again will renew the certificate if there are fewer than 30 days before it expires.

Detail

It does this by first pushing all of the blob resources into a new storage account as a static website. This is an okay place to stop if you don't mind your resources being accessed with a domain like this: https://sastaticsiteexammiahdsnu.z13.web.core.windows.net/.

Because it is currently impossible to bind custom TLS certificates to an Azure Storage Account or CDN, a minimal Azure Functions application is created to act as a reverse proxy for the static site. There are zero functions in the entire Azure Functions application. The following is the entire contents of the Function:

{
  "$schema": "http://json.schemastore.org/proxies",
  "proxies": {
    "files": {
      "matchCondition": {
        "route": "{*path}",
        "methods": ["GET"]
      },
      "backendUri": "${storage_account_static_website_url}{path}"
    }
  }
}

In order for the files hosted in the Storage account to return the correct content-type, we're using to map the values from jshttp/mime-db via this Terraform module.

These mappings can be replaced or added onto in the case that you have more exotic file types by passing that into the custom_mime_mappings variable.

Usage example

See examples here

Basic example without custom DNS/TLS certificate...

module "simple_example_static_site" {
  source                   = "../../"
  name                     = "some-unique-azure-functions-app-name"
  static_content_directory = "${path.root}/static-content"
  error_404_document       = "error_404.html"
  tags                     = { "ManagedBy" = "Terraform }
}

Basic example with custom DNS/TLS certificate...

module "custom_dns_static_site" {
  source                   = "../../"
  name                     = local.azure_function_name
  static_content_directory = "${path.root}/static-content"
  error_404_document       = "error_404.html"

  custom_dns = {
    # @ is interpreted as a naked domain. This would create the following FQDNs:
    #   - example.com
    #   - www.example.com
    #   - deeper.subdomain.example.com
    hostnames                  = ["@", "www", "deeper.subdomain"]
    dns_provider               = "azure"
    dns_zone_id                = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-dns-zones/providers/Microsoft.Network/dnszones/example.com"
    lets_encrypt_contact_email = "[email protected]"
    
    # Because you don't want to save secrets in your Terraform code
    azure_client_id     = var.azure_client_id
    azure_client_secret = var.azure_client_secret
  }

  tags = { "ManagedBy" = "Terraform }
}

Author

Created and maintained by Jim Andreasen.

github.com/reifnir

gitlab.com/jim.andreasen

[email protected]

License

MIT Licensed. See LICENSE for full details.

Requirements

Name Version
terraform >= 0.14
acme 2.13.1
azurerm >= 3.14.0

Providers

Name Version
acme 2.13.1
archive n/a
azurerm >= 3.14.0
dns n/a
local n/a
random n/a
tls n/a

Modules

Name Source Version
file_extensions reifnir/mime-map/null n/a

Resources

Name Type
acme_certificate.certificate resource
acme_registration.reg resource
azurerm_app_service_certificate.custom_hostname resource
azurerm_app_service_certificate_binding.custom_hostname resource
azurerm_app_service_custom_hostname_binding.static_site resource
azurerm_dns_a_record.naked_domain resource
azurerm_dns_cname_record.cnames_to_function resource
azurerm_dns_txt_record.function_domain_verification resource
azurerm_linux_function_app.static_site resource
azurerm_resource_group.static_site resource
azurerm_service_plan.static_site resource
azurerm_storage_account.static_site resource
azurerm_storage_blob.function resource
azurerm_storage_blob.static_files resource
azurerm_storage_container.function_packages resource
local_file.proxies resource
random_password.pfx resource
random_string.storage_account_name resource
tls_private_key.reg_private_key resource
archive_file.azure_function_package data source
azurerm_client_config.current data source
azurerm_dns_zone.custom data source
azurerm_storage_account_sas.package data source
dns_a_record_set.function data source

Inputs

Name Description Type Default Required
custom_dns Information required to wire-up custom DNS for your static site. When setting hostnames, be sure to enter the full DNS. Note that the Azure client secret is necessary for completing ACME DNS verification when generating a Let's Encrypt TLS certificate.
object({
dns_provider = string
dns_zone_id = string
hostnames = set(string)
lets_encrypt_contact_email = string
azure_client_id = string
azure_client_secret = string
})
null no
custom_mime_mappings Add or replace content-type mappings by setting this value. Ex: { "text" = "text/plain", "new" = "text/derp" } map(string) null no
error_404_document The resource path to a custom webpage that should be used when a request is made for a resource that doesn't exist in the supplied directory of static content. Ex: 'error_404.html' string "" no
index_document The webpage that Azure Storage serves for requests to the root of a website or any subfolder. For example, index.html. This value is case-sensitive. string "index.html" no
location Azure region in which resources will be located string "eastus" no
name Slug is added to the name of most resources. This is also the name of the Azure Functions application and MUST be unique across all of Azure. string n/a yes
static_content_directory This is the path to the directory containing static resources. string n/a yes
tags n/a map(string)
{
"ManagedBy": "Terraform"
}
no

Outputs

Name Description
azure_function_default_url The Azure Functions application's default URL
custom_dns_domains List of any custom DNS domains that were created bound to the static site
storage_account_primary_web_endpoint The storage account's self-hosted static site URL

Change Log

3.3.1

Released 2023-04-30

3.3.0

Released 2023-04-30

  • Bumped ACME provider version to 2.13.1.

3.2.0

Released 2022-07-15

  • default_hostname and custom_domain_verification_id weren't implemented in the azurerm_linux_function_app object until azurerm version 3.14.0. Now that they are, got rid of that azurerm_function_app data object that was creating so much noise each time plan was run.

3.1.0

Released 2022-07-04

  • Fixed issue where static files weren't being updated when their content changed.

3.0.1

Released 2022-07-04

  • Added changelog notes.

3.0.0

Released 2022-07-04

  • Realized that the azure_function_defualt_url output was still misspelled and tried to correct it to azure_function_default_url before anyone used v2.0.0.

2.0.0

Released 2022-07-04

  • When creating multiple objects, such as the DNS verification TXT records, switched from count to for_each. Using count can be buggy when items in a list are changed. If you're creating objects and their state index changes, Terraform will try to update both at once or remove/add at the same time leading to conflicts where you have to retry a number of times. Using for_each instead gives more stable state names. Ex: module.custom_dns_static_site.azurerm_app_service_custom_hostname_binding.static_site["www"] instead of module.custom_dns_static_site.azurerm_app_service_custom_hostname_binding.static_site[0].
  • Marked the module compatible with versions beyond 0.14.
  • Bumped the ACME provider used to generate Let's Encrypt certs from 2.4.0 to the current version, 2.9.0.
  • Now using azurerm Terraform provider version 3 and later.
  • Moved away from deprecated objects azurerm_function_app and azurerm_app_service_plan resource for the newer azurerm_linux_function_app and azurerm_service_plan.
    • Unfortunately, there are still a couple of bugs in azurerm_linux_function_app in that some properties (custom_domain_verification_id, default_hostname) are not yet implemented. There are already a couple of MRs that are just awaiting responses from Hashicorp here and here (I did that one).
    • In order to use the newer resources, I needed to add a deprecated azurerm_function_app data reference as a workaround. This will be removed as soon as the missing functionality is released.