The Problem

You wish to provide a static maintenance splash page when all members of a pool serving a Virtual Server are currently down or disabled.

The Configuration

# cat /var/tmp/splash-page.html

<!DOCTYPE html>
<html lang="en">
<head><title>Site Maintenance</title></head>
<body>
    <!-- YOUR GREAT CONTENT HERE -->
</body>
</html>

# tmsh create sys file ifile maintenance-splash-page.html source-path file:/var/tmp/splash-page.html

# tmsh create ltm ifile maintenance-splash-page.html file-name maintenance-splash-page.html

The Code

when LB_FAILED {
    if { [active_members [LB::server pool]] == 0 } {
        HTTP::respond 503 content [ifile get maintenance-splash-page.html]
    }
}

Analysis

LB_FAILED fires when the Virtual Server load-balancing decision fails. There are a number of reasons why this may occur, but we're only interested in the case where there are no active pool members available, either because all available members have been disabled, or because they are all marked down by the monitors. The HTTP::respond sends an HTTP Response message with a 503 status code, then the contents of the so-called ifile. This is a file that has been uploaded into the BIG-IP configuration filestore. The first tmsh command loads a file from the BIG-IP local filesystem into the configuration filestore. We could have imported a file via http using the appropriate source-path url. The second tmsh command makes the file visible to the LTM module.

In order to use the HTTP::respond, an http profile must be applied to the Virtual Server.

The content could also be directly embedded in the HTTP::respond command by doing this:

when LB_FAILED {
    if { [active_members [LB::server pool]] == 0 } {
        HTTP::respond 503 content {
<!DOCTYPE html>
<html>
<head><title>Maintenance Page</title></head>
<body>
    <!-- YOUR GREAT CONTENT HERE -->
</body>
</html>
        }
    }
}

The iFile mechanism, however, is more flexible, because it permits changes to the file without needing to modify and reload the iRule. Perhaps more importantly, it supports a more flexible mechanism when you need to supply more than one file.

Elaboration

Why send a 503?  Web search crawlers should not cache this response.  You may also wish to add headers to this effect, as in:

HTTP::respond 503 content [ifile ...] Cache-Control "no-store, must-revalidate"

The recipes above work fine if all of the required assets for the response are in a single HTML page. But what if you need to send other things, like a separate css or one or more images? You need to solve two problems: 1. retrieve the file based on the requested object; and 2. set the Content-Type based on the actual contents. A Data Group can be used for this purpose:

# tmsh create ltm data-group internal splash-page-assets type string records add { "/css/maintenance.css" { data "splash-page-maintenace-css,text/css" } "/img/logo.png" { data "splash-page-logo-png,image/png" } }

Then:

when LB_FAILED {
    if { [active_members [LB::server pool]] == 0 } {
        set attr [class lookup [HTTP::path] splash-page-assets]

        if { $attr eq "" } {
            HTTP::respond 503 content [ifile get maintenance-splash-page.html]
        }
        else {
            HTTP::respond 503 content [ifile get [getfield $attr , 1]] Content-Type [getfield $attr , 2]
        }
    }
}

It's worth noting that there is a slight race-condition here. Let us say that the pool member comes up while the client is pulling in the additional maintenance page assets. In that case, LB_FAILED isn't raised and the request will go through. For this reason, it is sensible to ensure these assets exist on the pool members, as well.