Hello, dear reader. Welcome to the first post on my blog. In the tradition of self-reference that we find ourselves,1 I'm going to write my first blog post about the infrastructure behind this blog. My blog is hosted on AWS and is composed of two S3 buckets, a route53 zone certified via acm and all appropriate records, and Cloudfront Distributions to cache and encrypt. If you are familiar with AWS, you can probably already picture the resource diagram. If you can't imagine the diagram, I have provided a less imagery version:
I'm going to walk through each component, why it exists and the code that describes it. I will assume that you are familiar with programming, and comfortable with TypeScript syntax. I will not assume specialized knowledge of AWS, networking, Pulumi or any other technology used.
So how are you creating this stuff anyway
Even a simple static website requires multiple pieces of infrastructure wired together to appear on your screen. Like most software projects, errors are easy to make and poorly tolerated. I don't want to manually click through the AWS console. I don't trust myself to get it right. I certainly don't trust myself to get it right each time. Because of this, I'm using an Infrastructure as Code (IaC) tool called Pulumi2 to provision everything. I declare3 my infrastructure in typescript, and then Pulumi makes the changes to AWS on my behalf.
The rest of this post will walk through the TypeScript code I wrote to get this site stood up. The great thing about IaC tools, is that they fully and unambiguously describe their desired state. That means that this will be a walkthrough not only of my TypeScript code, but also the infrastructure it describes.
You are welcome to follow along at home.
A new project and its secrets
I started by setting up a new project and teaching it how to talk to AWS. In Pulumi, a
project is a description of some infrastructure, while a stack is a deployment of that
infrastructure. If you are from a Object Oriented background, you can think of this as
a class and an instance.4 The file that defines a project in Pulumi is called
Pulumi.yaml
, and it looks like this:
name: iwahbe
runtime: nodejs
description: |
The infra to host iwahbe.com & www.iwahbe.com.
This file should be self explanatory: There is a project called iwahbe
, and it is
written in Typescript (which runs on NodeJS). To deploy our infrastructure, we also need a
stack. The easiest way to do this is with the Pulumi CLI. To deploy and configure my main
stack, I ran
I want Pulumi to be able to access my AWS account, and I don't want it to use ambient credentials. I added my AWS credentials to the stack config as well, but marked them as secret.
|
|
This is it for configuring the project and stack, but we still want a way to consume this
definition in the code itself. In a new file: utils.ts
, I wrote a simple class to
abstract creating providers with the appropriate config:
This class serves two purposes:
- It abstracts configuration handling and credentials out of my infrastructure code. This allows me to separate the shape of my infrastructure from details of my AWS setup.
- It projects my configuration into the Typescript type system.
Because there will only ever be one Project
in my project, I export a project
variable instead of exporting the Project
class. This gives us a singleton Project
.
;
I can then import project into the Typescript project root (index.ts
).
;
;
;
;
;
I'll explain what bucketPolicy
and uploadFiles
are later when they become relevant.
That's it for configuration, I promise. We can now start actually deploying things.
Starting simple (with S3)
The most basic and most important part of a static website is the static content. The
bucket that holds my content is called www.iwahbe.com
(as required by AWS), and makes
use of AWS's S3 static site support. We start by defining a utility function in utils.ts
to describe the commonality between our buckets:
bucket
defines the AWS name of the bucket, and must correspond to the website the bucket
is serving. acl: "public-read"
tells AWS that anyone can request read from the
bucket.5
We will pass this to both our public buckets. First the main content bucket:
;
website
tells AWS that this bucket is serving a static site, and which
document to serve on root access (index.html
) or when requesting a document that doesn't
exist (404.html
).
The second bucket provides a redirect from iwahbe.com
to www.iwahbe.com
:
;
redirectAllRequestsTo
tells AWS that this bucket is serving as a proxy for a static
website, but not serving any content directly.
AWS requires that S3 buckets that serve as static websites have their bucket
name match
the domain they are serving. This is why we have two different buckets: one bucket to
server www.iwahbe.com
and another to serve iwahbe.com
(even as a simple redirect).
Technically, this is all you need to serve a static website on AWS.6 There are some major caveats:
- You don't control the url, so it will start with a prefix like
https://my-bucket.s3-us-west-2.amazonaws.com
. - Content served from a raw S3 bucket doesn't carry a SSL certificate.
- Only HTTP is supported. HTTPS is not supported.
The rest of the blog post will add the infrastructure necessary to serve a SSL secured website from a custom URL over HTTPS. That's our goal.
A custom domain in AWS
The most obvious problem with serving directly from S3 buckets is the URL. Credible sites
are served from a memorable and custom domain, such as iwahbe.com
. It's also a
prerequisite to serving a validated site, so we will start there. The abstraction that AWS
uses for domains is a Route53 Zone, so we will start by defining a zone:
;
Remember that domain
is "iwahbe.com"
. The zone is for iwahbe.com
, our root
domain. We can use our root domain to effect subdomains, such as www.iwahbe.com
.
The Route53 zone defines a set of name servers, which I pointed my domain registrar towards. To serve authorized traffic from a domain, the domain needs to be validated. AWS provides a validation service: ACM7. Getting a ACM certificate declaring that we own a domain has 3 steps in a Pulumi program:
- Declaring that we want a certificate, and how we want to prove ownership of the domain.
- Following the instructions to establish ownership.
- Waiting for AWS to validate that we have followed the steps successfully.
We want the entire process to be done in our pulumi program, so we validate with DNS
records on the domain itself. That way, we can use Pulumi to add the DNS records requested
by ACM, all within the same pulumi up
.
According to AWS, all certificates need to be hosted in the us-east-1
region. In
Pulumi's AWS provider, we do this by
declaring a new provider with a different region
. Just like before, we use our project
singleton to help us define the provider:
;
This is how we declare the certificate:
;
subjectAlternativeNames
allows us to declare that in addition to iwahbe.com
, the
certificate should apply to any domain that ends with .iwahbe.com
. This allows us to
apply the certificate to www.iwahbe.com
.
validationMethod: "DNS"
declares that we will confirm ownership of the domain by adding
a DNS record to iwahbe.com
. Because we are managing iwahbe.com
with a Route53 Zone,
this is easy to do:
;
Here we are taking the domain validation options provided by sslCert
and feeding them
into the record. Since validation needs to wait on AWS seeing the DNS updating and
validating the certificate, we also declare a phony resource that waits on the validation
to be completed:
;
That's all we need to set up our domain in AWS. We can now use setup AWS resources that
serve content from iwahbe.com
.
Serving HTTPS from our domain
Amazon's S3 can't serve HTTPS requests. Cloudfront Distributions can, and they can serve directly from our existing S3 Buckets. Because we serve content from two buckets, we will need two distributions, one for each bucket. We will then attach A
and AAAA
records to our domain to direct incoming requests to our distributions.
We encapsulate the idea of a Cloudfront distribution directing to a bucket in a function:
Inside bucketDistribution
, we define a aws.cloudfront.Distribution
. We define the origin as the passed in source
aws.s3.Bucket
, configure the Distribution to alias the bucket and set the cert to the cert we defined above: sslCert
. Finally, we mix in any arguments that we passed in at the call site.
The Distribution looks like this:
new aws.cloudfront.Distribution
` -s3-distribution`,
,
// We depend on `certValidation` to make sure that we don't deploy a Distribution with
// a cert that hasn't been validated yet.
,
;
To point the bucket's domain to the Distribution, we need to attach Address Records (often abbreviated A Records) to our domain. This tells DNS to resolve [www.]iwahbe.com
to the address of our Distribution, instead of targeting the bucket directly. Since IPv6 is enabled, we also need to attach AAAA Records. AAAA Records are the IPv6 equivalent of A Records.
for of
The entire function looks like this:
// Create a Cloudfront Distribution that routes to an s3 Bucket. The Distribution will
// front the bucket's domain, aliasing the bucket with HTTPS and IPv6 support.
Using our newly defined bucketDistribution
, we can easily define our Distributions:
First, we define the bucket for www.iwahbe.com
. The only parts we customize are error
handling and caching behavior. Most of the configuration is taken care of by bucketDistribution
.
"www", wwwBucket, ;
Second, we define the root bucket, customizing the caching behavior and nothing else.
"root", rootBucket, ;
This is all we need to serve HTTPS content from our buckets. The program is complete.
Conclusion
That's everything I used to host this blog. By combining AWS with Pulumi, I'm able to declare my infrastructure in a clear and expressive language. I am able to deploy all my infrastructure with pulumi up
.
If you made it to the end, congratulations! I'm always happy to receive feedback at @iwahbe@hachyderm.io. If you think I did something wrong (or right), please let me know.
YAML, GNU, cURL, ect.
Pulumi is my employer.
Pulumi is declaritive, which means that a tell Pulumi what you want to exist, not what changes you want to make.
Keyword alert. Object Oriented shows up in all blog posts. Here it is for this one.
ACL stands for Access Control List.
You need to remove the https
though.
ACM stands for AWS Certificate Manager