Andrew Betts
Posted on March 26, 2024
The variety of things you can do in edge computing these days is huge, and Fastly customers are constantly impressing me with their creativity and the complexity of some of the problems that can be solved entirely at the edge. But there are some things that almost regardless of your use case, you probably should be doing.
I've set up a lot of Fastly services, and along the way I've realized that pretty much every time, I add the same patterns to do the same useful stuff. Whether it's an e-commerce site, something statically generated, a SaaS service, or anything else, there's a set of stuff that is almost universally applicable. Let's check them off here, with examples for Fastly's Delivery (VCL) services.
(If JavaScript, Rust or Go are more your thing, try Fastly Compute services. It's free to sign up and you can do a lot of the same things I've listed here)
Shielding
If your Fastly service uses backends, i.e. you have origin servers behind Fastly, it is almost always a good idea to enable shielding, which focuses "miss" traffic into one Fastly POP before sending it to your origin. You then have two chances to get a hit in the cache, and the cache in the shield POP will typically become larger and more comprehensive than other POPs, so this can reduce traffic to your origin servers by a huge amount: up to two orders of magnitude!
Shielding needs to be enabled as a property of the backend configuration, either in the control panel or when creating a backend via the Fastly API.
Want to know where to shield? Use the shield chooser tool!
HTTP/3
Although Fastly negotiates HTTP/2 automatically if your TLS configuration supports it, at time of writing we require you to explicitly enable H3 by adding an Alt-Svc
header to your responses - but the good news is that you can enable this in the control panel:
Or drop a single line of VCL into vcl_recv
:
h3.alt_svc();
Force TLS
Another quick win is forcing all clients to use TLS. We do accept connections on plain HTTP in VCL services, so it's a good idea to redirect them to HTTPS. This can be done using a toggle in the control panel, right next to the HTTP/33 option.
Or use custom VCL:
if (req.protocol != "https") {
error 608;
}
We recommend throwing error codes in the 6xx
range and then mapping them to conventional HTTP status codes in vcl_error
, where you can also turn the parameter into a Location header:
if (obj.status == 608) {
set obj.http.location = "https://" req.http.host req.url;
set obj.status = 308;
set obj.response = "Moved permanently";
return (deliver);
}
(by the way, if you're using a Compute service, this redirect happens automatically, and no insecure traffic ever reaches your service)
HTTP Strict Transport Security
You should also return a strict-transport-security
header to ensure that clients never make an insecure connection. If you use the control panel to configure Force TLS, HSTS will be set up as well, but if you're using custom VCL, adding response headers just before sending to the client can be done in vcl_deliver
:
set resp.http.strict-transport-security = "max-age=31536000; includeSubDomains; preload"
Compression
The final option that is configurable in the control panel, enable compression under Content > Compression, and we will automatically compress any uncompressed responses from origin.
If you want to do this in VCL, take care to update the Vary
header to include "Accept-Encoding":
if (beresp.status == 200 && beresp.http.content-type ~ "^(?:text/|application/json)") {
if (req.http.Accept-Encoding == "br") {
set beresp.brotli = true;
} elsif (req.http.Accept-Encoding == "gzip") {
set beresp.gzip = true;
}
set beresp.http.Vary:Accept-Encoding = "";
}
CORS
Trying to build handling for Cross-origin resource sharing into your backend app is often a pain. Instead, handle CORS preflights at the edge and ensure clients get the right CORS rules. In vcl_recv
you can trap OPTIONS
requests:
if (req.method == "OPTIONS" && req.http.Origin) {
error 612;
}
Then, in vcl_error
, construct the preflight response:
if (obj.status == 612) {
set obj.status = 204;
set obj.http.access-control-allow-origin = req.http.origin;
set obj.http.access-control-max-age = "86400";
return(deliver);
}
Finally, in vcl_deliver
, you can add the headers specifying what is allowed, since you need to add these to all responses, not just the preflight ones:
set resp.http.access-control-allow-methods = "GET,HEAD,POST,OPTIONS";
set resp.http.access-control-allow-headers = "Content-Type, x-requested-with";
Redirects
Handling redirection of out of date URLs is a really big and easy win. Put your redirects in an edge dictionary, or if you have pattern-matching redirects, write simple VCL to map the patterns:
declare local var.dest STRING;
// Lookup in table / edge dictionary
if (table.contains(my_redirects, req.url.path)) {
set var.dest = table.lookup(my_redirects, req.url.path);
// Special case pattern
} else if (req.url.path ~ "^/products/(\w+)/([0-9]+)") {
set var.dest = "/catalog/categories/" re.group.1 "/products/" re.group.2;
}
if (var.dest) {
error 601 var.dest;
}
Then in vcl_error
, construct the redirect:
if (obj.status == 601) {
set obj.http.location = obj.response;
set obj.status = 308;
set obj.response = "Permanent redirect";
return (deliver);
}
Normalizing requests for better cache performance
If you have a clear idea of what constitutes a valid URL for your site, it's helpful to use VCL to filter out anything else:
if (fastly.ff.visits_this_service == 0 && req.restarts == 0) {
# Sort query params into alphabetical order
set req.url = querystring.sort(req.url);
# Media assets have a specific set of allowed query params
if (req.url.path ~ "/media_[0-9a-f]{40,}[/a-zA-Z0-9_-]*\.[0-9a-z]+\z") {
# width, height, format, optimize
set req.url = querystring.filter_except(req.url,
"width" querystring.filtersep()
"height" querystring.filtersep()
"format" querystring.filtersep()
"optimize"
);
# Paths ending .json have a different, specific set of allowed params
} else if (req.url.ext == "json") {
# limit, offset, sheet
set req.url = querystring.filter_except(req.url,
"limit" querystring.filtersep()
"offset" querystring.filtersep()
"sheet"
);
# Otherwise, query params are not allowed at all
} else {
set req.url = req.url.path;
}
}
Not all websites have the luxury of knowing what parameters might be on the query strings of requests - and sometimes even if you do, you don't want to turn today's reality into tomorrow's limitation. After all ossification of the web is a major problem.
Blocking unwanted traffic
If you want a comprehensive solution to block malicious traffic, check out our Next-Gen WAF at Edge, but you can also construct simple rules in VCL using Access Control Lists or even just basic if
statements:
if (fastly.ff.visits_this_service == 0 && req.restarts == 0) {
if (
req.http.user-agent ~ "/bot/" || // Header pattern
!req.http.accept || // Required header
req.http.via || // Banned header
client.geo.country_code ~ "^(fr|uk|ru)$" || // Country of origin
client.ip ~ my_acl || // IP is in an ACL
client.as.number == 12345 || // Client's ISP
) {
error 625;
}
}
Then in vcl_error
:
if (obj.status == 625) {
set obj.status = 403;
set obj.response = "Forbidden";
synthetic "Not allowed";
return(deliver);
}
Strip upstream headers
If you're using AWS S3, Google Cloud Storage or similar as a backend, they will likely return a whole bunch of response headers that you don't want. Remove them with some VCL in vcl_fetch
:
unset beresp.http.server;
unset beresp.http.x-generator;
unset beresp.http.via;
unset beresp.http.x-github-request-id;
unset beresp.http.x-amz-delete-marker;
unset beresp.http.x-amz-id-2;
unset beresp.http.x-amz-request-id;
unset beresp.http.x-amz-version-id;
unset beresp.http.x-goog-component-count;
unset beresp.http.x-goog-expiration;
unset beresp.http.x-goog-generation;
unset beresp.http.x-goog-metageneration;
unset beresp.http.x-goog-stored-content-encoding;
unset beresp.http.x-goog-stored-content-length;
Doing this in vcl_fetch
makes sense because it happens before the object is written to cache.
Debugging
By default, Fastly includes certain response headers when a Fastly-Debug
header is present in the request. It's handy to use this mechanism to add any other debugging information you want to emit. For example, you could include the final request path that was used to look up the object in cache, and the version of your Fastly service that handled the request:
if (req.http.fastly-debug) {
set resp.http.fastly-service-version = req.vcl.version;
set resp.http.fastly-req-path = req.url.path;
}
You're off to a great start!
These patterns are a great way to learn about the power of the edge to simplify and decouple layers of your service architecture, and apply to almost any use case. Implementing these also helps you understand some common best practices of VCL and set you up to implement solutions more specific to your use cases.
Whatever you create, share it with us below or at community.fastly.com.
Posted on March 26, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.