Quick, Pretty and Easy Maintenance Page using Cloudflare Workers & Terraform
Tags: webdev, sre, devops, opensource
Published: 2020-08-01 22:26:07

Maintenance pages are a neat way to inform your users that there are operational changes that require downtime. Cloudflare Workers allows you to execute Javascript and serve HTML close to your users and when Cloudflare manages your DNS it makes it easy to create a Worker and let it manage all traffic during a maintenance period. This makes a great solution for smaller teams/applications that do not want to invest time into creating and displaying maintenance pages. Terraform is a tool that enables provisioning infrastructure via code and it integrates great with Cloudflare APIs.

A preview of the [final implementation can be found here](https://hodovi.cc/maintenance/), and I've created [a Terraform module for creating the maintenence page](https://github.com/adinhodovic/terraform-cloudflare-maintenance).

## Cloudflare Worker

The Worker's javascript is simple and it is found and explained in depth in this [blog post](https://www.resdevops.com/2018/03/20/cloudflare-workers-maintenance-mode-static-page/). The snippet forwards the request to the Web Server if the connecting IP is whitelisted, otherwise it returns a html response that you've defined which in our case is the maintenance page.

```javascript
async function fetchAndReplace(request) {

  let modifiedHeaders = new Headers()

  modifiedHeaders.set('Content-Type', 'text/html')
  modifiedHeaders.append('Pragma', 'no-cache')


  //Allow users from trusted into site
  if (white_list.indexOf(request.headers.get("cf-connecting-ip")) > -1)
  {
    //Fire all other requests directly to our WebServers
    return fetch(request)
  }
  else //Return maint page if you're not calling from a trusted IP
  {
    // Return modified response.
    return new Response(maintenancePage, {
      status: 503,
      headers: modifiedHeaders
    })
  }
}

let maintenancePage = `
<!doctype html>
<title>Site Maintenance</title>
<div class="content">
    <h1>We&rsquo;ll be back soon!</h1>
    <p>We&rsquo;re very sorry for the inconvenience but we&rsquo;re performing maintenance. Please check back soon...</p>
    <p>&mdash; Awesome Team</p>
</div>
`
```

## Terraform

We'll now use Terraform to create the script and to create the worker route. Creating a worker script with Terraform can be done with the `cloud_worker_script` resource:

```terraform
resource "cloudflare_worker_script" "this" {
  name    = "maintenance"
  content = file("/maintenance.js")
}
```

This will create a Cloudflare worker script which we can reference with the `cloudflare_worker_route`. The route resource will deploy the worker in a zone with a specified url pattern:

```terraform
data "cloudflare_zones" "this" {
  filter {
    name = "hodovic.cc/*"
  }
}

resource "cloudflare_worker_route" "this" {
  zone_id     = lookup(data.cloudflare_zones.this.zones[0], "id")
  pattern     = "hodovi.cc/maintenance/*"
  script_name = cloudflare_worker_script.this.name
}
```

All users that visit the `/maintenance/` path will be shown the maintenance page except for the whitelisted IPs.

## Terraform Module

The solution presented above is neat, although we'd like a resuable module. Therefore, I've created a [Terraform module](https://github.com/adinhodovic/terraform-cloudflare-maintenance) that does the creation and deployment of a Cloudflare Worker. However, for the module to be reusable we need dynamic html content as:

- Logo
- Font
- Company/Team name
- Favicon
- Email address for support

Cloudflare supports key/value storage which we can define in the Terraform module. We'll adjust the previous `cloudflare_worker_script` to add the key/value storages:

```terraform
resource "cloudflare_worker_script" "this" {
  name    = "maintenance"
  content = file(format("%s/maintenance.js", path.module))

  plain_text_binding {
    name = "COMPANY_NAME"
    text = var.company_name
  }

  plain_text_binding {
    name = "WHITELIST_IPS"
    text = var.whitelist_ips
  }

  plain_text_binding {
    name = "LOGO_URL"
    text = var.logo_url
  }

  plain_text_binding {
    name = "FAVICON_URL"
    text = var.favicon_url
  }

  plain_text_binding {
    name = "FONT"
    text = var.font
  }

  plain_text_binding {
    name = "EMAIL"
    text = var.email
  }
}
```

We can create a HTML snippet and pass the environment variables:

```javascript
let maintPage = (company_name, logo_url) => `
<body>
    <div class="content">
        <img class="logo" src="${logo_url}" alt="${company_name}">
        <div class="info">
            <h1>Our site is currently down for maintanence</h1>
            <p>We apologize for any inconveniences caused and we will be online as soon as possible. Please check again in a little while. Thank you!</p>
            <p>&mdash; ${company_name}</p>
        </div>
        <img class="image-main" src="https://i.imgur.com/0uJkCM8.png" alt="Maintenance image">
        <hr />
        <a href="mailto:${email}?subject=Maintenance">You can reach us at: ${email}</a>
    </div>
</body>
`;
```

We'll also create dynamic CSS for fonts:

```css
body {
    text-align: center;
    font-family: "${font}", sans-serif;
    color: #0C1231;
}
```

The html/css snippets are more complex and dynamic and the full source code can be found on [Github](https://github.com/adinhodovic/terraform-cloudflare-maintenance).

The module supports several variables:

- Logo
- Font (Google fonts)
- Email
- Team name
- Whitelisting IPs

You can create it using Terraform's `module`:

```terraform
module "hodovi_cc_maintenance" {
  source          = "git::git@github.com:adinhodovic/terraform-cloudflare-maintenance.git?ref=v0.1.3"
  enabled         = false
  cloudflare_zone = "hodovi.cc"
  pattern         = "hodovi.cc/maintenance/*"
  company_name    = "HoneyLogic"
  email           = "support@honeylogic.io"
  font            = "Poppins"
  logo_url        = "https://s3.eu-west-1.amazonaws.com/honeylogic.io/media/images/Honeylogic-blue.original.png"
  favicon_url     = "https://s3.eu-west-1.amazonaws.com/honeylogic.io/media/images/Honeylogic_-_icon.original.height-80.png"
}
```

A preview of the maintenance page can be found [here](https://hodovi.cc/maintenance/), fully adjustable to your team needs!