My weekend goal was to finally publish something to my domain. I’ve been playing around with Terraform and have been looking for an excuse to build an actual project so I decided to pull the trigger and finally commit to it.
While thinking about what to make exactly, I came to the realization that I don’t have a lot of content for a typical portfolio but I share work on other services like Github and Twitter. I know I also don’t want to spend a bunch of time thinking about what to put on it.
What I ended up building was a self-updating static website. The web service summarizes my latest activity from Github, Twitter and Medium, generates a static website, and publishes the content to my domain. Besides serving as an over-engineered, non-transferable business card, it’s a convenient launcher for the apps I use most when added to the iOS home screen.
Here’s the final product:
jschr.io — Add to iOS home screen
I built it with React, Webpack and Terraform. If you want to skip right to the code, its available on Github along with instructions on how to deploy your own version.
Terraform is a tool for creating infrastructure as code with declarative configuration files. One major benefit of writing your infrastructure as code is for free, you get all the power of version control, code reviews and collaboration.
With Terraform’s declarative configuration files you can use variables and modules to make it a really easy to create and manage your infrastructure. Terraform is similar to AWS’ CloudFormation, except that it has a growing list of providers you can use to create resources in many other cloud services.
Heres what a Terraform config looks like:
Let’s start with an overview of the app’s structure:
The first step and where most of the magic happens is creating the webpack config. This project wouldn’t have been nearly as easy without this awesome static website generator plugin for webpack.
Here’s a simple webpack config using the plugin:
Tip #1: Webpack supports Typescript for config files if it has a .ts extension and ts-node is installed locally for your project.
Every property passed in through the locals option of the static site generator plugin will be sent to the server-side render function.
Using the locals option is how we will pass in the app’s props whenever we generate a new static website.
To make our site more dynamic, the next step is hooking it up to some real data. You might pull data from a local markdown file or fetch it from an API. Before being able to fetch anything, we’ll need to make a couple changes to the webpack config.
In order to make use of async/await we are going convert our config to export an async function, which works out-of-the box with webpack. We can then fetch our latest activity from Github and render the app with the getProps function.
Tip #2: Webpack supports exporting a function from the config. It will receive any env options you .
The full source of the webpack config can be found in the repo. The same config is used to start the dev server and to generate a new static website in our Lambda function.
After rendering the initial page load, the browser needs to bootstrap the React app with the same props that were used to render the html. This happens in the mount function and we need to make sure that it’s only called when running in the browser.
Note that index.ts runs in both node and browser contexts so you need to be extra cautious when importing libraries that depend on the browser APIs.
Now that we’ve setup server-side rendering and the browser client, let’s add some styling.
Adding glamor to the project involves two-steps:
Glamor has it’s own render function for server-side rendering that returns the initial html of the app and any styles that were created during the render. There are a couple gotchas related to the order of imports you need to be aware of when server-side rendering with glamor.
Now we just need to rehydrate the server state in the browser:
Here’s where you’d start adding a bunch of components but I’m going to skip over creating and styling components and move on to the actual Lambda function. You can see the components I made for my website in the repo if you’re curious.
Here’s the entire Lambda function:
Tip #3: You can use memory-fs to to output the webpack build to memory instead of the file system
After the compilation step the Lambda will upload the files to S3 and invalidate the CloudFront distribution. Alternatively to setting up CloudFront, you can configure the bucket to be a static website.
Now we’re ready to start creating the infrastructure. I’m going to use AWS Lambda, S3 and CloudFront for hosting the website and Mailgun for sending and receiving email with the domain.
Here’s an overview of the Terraform structure:
Terraform is executed from the directory of the environment you want to deploy. In this case we only have one environment and running terraform apply from env-dev will deploy the dev infrastructure.
After each deploy, Terraform sill save the state of the the created resources into a terraform.tfstate file inside the current directory. When working in larger teams, you way want to use remote state.
In order to deploy the environment, we need to create it’s terraform variable file. Here’s a condensed version of ours:
When you run terraform apply, it will run every configuration file in the current directory. Since we are running the command from env-dev there is only one — dev.tf.
The dev configuration file authenticates with AWS and Mailgun then creates the app’s infrastructure.
Terraform modules let us create re-usable components of our infrastructure. Terraform can use modules from the filesystem, Github and other remote sources. This project uses one local module for the entire app but you can compose your infrastructure from as many modules as you’d like.
To create the app module we’ll start by defining it’s input variables:
This setup allows us to easily create more environments in the future like env-staging and env-production.
Now we can start creating all the resources for our app.
Building the project will package up our app to be uploaded to Lambda. When we deploy, Terraform will determine whether or not to deploy a new version of the handler using the package’s hash.
Create a private S3 bucket for our domain.
We’ll use CloudWatch Events to trigger our Lambda every 15 minutes.
Create a CloudFront distribution for our S3 bucket.
Assuming you’ve registered your domain in Route 53, we’ll use data sources to fetch the hosted zone for our domain by name and create the DNS entries for our website.
The fastest way to start sending and receiving email through our domain is to create a Mailgun account. Retrieve your api key from your account profile and add it to dev.tfvars.
Thanks to Terraform’s Mailgun provider we can create a new domain resource with Mailgun’s API right from our config file:
The resulting terraform output of creating the Mailgun domain will look something like this:
Ideally, we could tell Terraform to create a DNS entry for each receiving record and sending record but Terraform doesn’t currently support using count with computed values.
Since the number of records is known, we can manually create each record as a workaround:
You will need to trigger domain verification in Mailgun after the first deploy if you don’t want to wait up to 24 hours before you can start receiving emails.
The final step is adding a Mailgun route to forward emails to our main account. Here’s what mine looks like:
Mailgun catch-all route for email forwarding.
Making changes to infrastructure and spinning up new environments has never been easier with Terraform. I don’t think I’ll be logging into the AWS console anytime soon.
You’re more than welcome to create, modify and deploy your own versions of the source code. I’d love to hear any suggestions you have for improvements or cool features I could add for a followup post.