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.
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.
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.
See examples here
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 }
}
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 }
}
Created and maintained by Jim Andreasen.
MIT Licensed. See LICENSE for full details.
Name | Version |
---|---|
terraform | >= 0.14 |
acme | 2.13.1 |
azurerm | >= 3.14.0 |
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 |
Name | Source | Version |
---|---|---|
file_extensions | reifnir/mime-map/null | n/a |
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({ |
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) |
{ |
no |
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 |
Released 2023-04-30
Released 2023-04-30
- Bumped ACME provider version to 2.13.1.
Released 2022-07-15
default_hostname
andcustom_domain_verification_id
weren't implemented in theazurerm_linux_function_app
object until azurerm version 3.14.0. Now that they are, got rid of thatazurerm_function_app
data object that was creating so much noise each time plan was run.
Released 2022-07-04
- Fixed issue where static files weren't being updated when their content changed.
Released 2022-07-04
- Added changelog notes.
Released 2022-07-04
- Realized that the
azure_function_defualt_url
output was still misspelled and tried to correct it toazure_function_default_url
before anyone used v2.0.0.
Released 2022-07-04
- When creating multiple objects, such as the DNS verification TXT records, switched from
count
tofor_each
. Usingcount
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. Usingfor_each
instead gives more stable state names. Ex:module.custom_dns_static_site.azurerm_app_service_custom_hostname_binding.static_site["www"]
instead ofmodule.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
andazurerm_app_service_plan
resource for the newerazurerm_linux_function_app
andazurerm_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.
- Unfortunately, there are still a couple of bugs in