Elm AWS CloudFront
Elm is a functional programming language that compiles to JavaScript
. AWS CloudFront is a content delivery network that can run JavaScript
functions at the edge of the network and customize the user request and response events.
In this note we will build Lambda@Edge functions in CloudFront using Elm
.
Yeah, yeah, but your scientists were so preoccupied with whether or not they could that they didn't stop to think if they should.
-- Dr. Ian Malcolm, Jurassic Park
AWS CloudFront
AWS CloudFront is a content delivery network (CDN) that can route user requests to defined origins such as S3 Bucket or an HTTP server.
It speeds the distribution of our static or dynamic content like .html
pages, .js
files, images and other assets.
AWS Lambda@Edge
AWS Lambda@Edge are lambda functions that allows us to run computations and customize CloudFront request and response events. As examples, we can enrich the response headers, forward the request to an authenticated origin, redirect the response to a different location.
AWS Lambda@Edge supports both Node.js
and Python
.
Elm Headless Worker
In one of our previous notes "elm headless worker" we created a simple request/response HTTP endpoint using Elm
with ports and a little JavaScript
snippet to run on the Node.js
platform.
Since Lambda@Edge is a simple handler with a CloudFront event object and a callback function, we can source all the logic into Elm
via ports:
const app = Elm.MyModule.init();
exports.handler = (event, context, callback) => {
const caller = (output) => {
callback(null, output);
app.ports.outputEvent.unsubscribe(caller);
}
// listen to Elm output and pass it to callback
app.ports.outputEvent.subscribe(caller);
// pass CloudFront to Elm
app.ports.inputEvent.send(event);
}
In this snippet we're passing the entire event
object into Elm
and when we receive a response we trigger the callback with the output data, this way all the custom logic we can do in a Lambda@Edge can be done in just Elm
.
Using elm-aws-cloudfront Package
Given the complexity of the event
object passed into Elm
and the output callback, we have to create a set of encoders, decoders and handlers for the different origins, for that we created the Elm package marcodaniels/elm-aws-cloudfront
.
With this package we can set the response cache header with the following example:
main : Program () (Model ()) Msg
main =
cloudFront
( originResponse
(\{ response, request } _ ->
response
|> withHeader { key = "cache-control", value = "public, max-age=100" }
|> toResponse
)
)
( inputEvent, outputEvent )
Let's dive into more details on how this package is structured and can help us building Lambda@Edge functions.
CloudFront Module
The CloudFront module provides the "main" function of our application as cloudFront
and its definition looks like:
cloudFront :
(flags -> Maybe InputOrigin -> OutputEvent)
-> ( (Decode.Value -> Msg) -> Sub Msg, Encode.Value -> Cmd Msg )
-> Program flags (Model flags) Msg
It requires a function to map the origin event into a result output (Lambda Module) and a tuple of input and output ports ( inputEvent, outputEvent )
with the correspondent types ((Decode.Value -> Msg) -> Sub Msg, Encode.Value -> Cmd Msg)
.
The origin mapping function can take in some initial flags, this can be used to pass some default values and/or environment variables.
In the end it just returns an Elm Worker.
Lambda Module
In the CloudFront Lambda module we can use the originRequest
and originResponse
functions to map and customize the origin event and toRequest
and toResponse
functions to transform the event into a result output.
originRequest
(\{ request } _ ->
{ request | uri = "new" }
|> toRequest
)
Headers Module
The CloudFront Header module exposes the functions withHeader
and withHeaders
to add or update HTTP headers on the origin event. In the case if the header already exists it will be the last entry value that "wins".
response |> withHeaders
[ { key = "x-frame-options", value = "DENY" }
, { key = "x-content-type-options", value = "nosniff" }
, { key = "x-xss-protection", value = "1; mode=block" }
]
Elm immutable values
In some Lambda@Edge example functions we can see objects like request, response and headers being directly accessed and modified into the callback
function.
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headerName = 'Auth-Header'
// direct update to "request.headers[...]"
request.headers[headerName.toLowerCase()] = ...
// direct update to "request.querystring"
request.querystring = ...;
callback(null, request);
};
This is not allowed in Elm
, as all values in Elm are immutable we have to create a new return object - the functions withHeader
and withHeaders
handle that by default.
To create the same example as above with Elm
:
originRequest
(\{ request } _ ->
-- returning the original "request" object
-- assigning a new value to the "querystring" property
{ request | querystring = Just "..." }
-- adding new header
|> withHeader {key = "auth-header", value = "..." }
|> toRequest
)
Building elm-aws-cloudfront applications
The package "elm-aws-cloudfront" requires the clients to setup and export a JavaScript
handler together with the bundled Elm
output, so it can be executed in the Node.js
platform in Lambda@Edge.
To ease this process we've created a Nix derivation that can compile the Elm
code, include the JavaScript
handler and bundle its contents into one single .js
file to be deployed into Lambda@Edge.
In the following example we are building and bundling two Lambda@Edge handlers:
cloudfront.buildElmAWSCloudFront {
src = ./.;
elmSrc = ./elm-srcs.nix;
elmRegistryDat = ./registry.dat;
lambdas = [
{ module = ./src/MyModuleOne.elm; }
{
module = ./src/MyModuleTwo.elm;
flags = [ ''token:"token-here"'' ''url:"url-here"'' ];
}
];
}
The Nix derivation also allows us to pass initial flags into the Elm
module.
In order for the derivation to build, it requires elm-srcs.nix
and registry.dat
files to be generated with elm2nix and the Elm
ports definition to be:
port inputEvent : (Decode.Value -> msg) -> Sub msg
port outputEvent : Encode.Value -> Cmd msg
All the updates to the Nix derivation are released with the nix-...
prefix in the elm-aws-cloudfront GitHub repository.
You're still here?
Thank you so much for reading this!
Creating elm-aws-cloudfront v1
has been quite a journey of many doubts. As the first version it covers most use cases of a Lambda@Edge but, we will definitely keep working on other improvements.
This exact website we're looking at is using elm-aws-cloudfront
to handle all the .html
and .js
files as well as all the images, you can see the implementation in GitHub.
Until then!