Hexo Serverless

Putting the Hexo CMS into a Serverless world

Prerequisites:

Optional:

  • Git repository
  • Registered domain name
  • Name Servers set to AWS Route53

Let’s face it. Building a simple website to post some info for others (or probably no one) is cumbersome and used to require a lot of effort. Thanks to static website generators and static website hosting, this is a thing of the past. Let’s setup a completely serverless and secure website using Hexo and AWS.

Start out by making the infrastructure needed to support the Hexo site we will create.

1
mkdir -p terraform && cd terraform

Create a file called providers.tf and place the following contents, using the profile from your ~/.aws/credentials file:

1
2
3
4
provider "aws" {
profile = "default" # profile you are using
region = "us-east-1" # required, but not used
}

Run the following command to initialize the terraform backend and download the required plugins terraform init :

You should see output similar to the following:

1
2
3
4
5
6
7
8
Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
...
Terraform has been successfully initialized!

...

Now that we can communicate with Terraform and AWS, let’s start building out the infrastructure to keep this site alive and secure! First though, we need to ensure we are tagging our resources!

Create a file named variables.tf and add:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
variable "domain_name" {
type = "string"
description = "Domain name to serve the Hexo content from"
default = "mydomain.com"
}
variable "s3_bucket_website_content" {
type = "string"
description = "S3 bucket name to serve the Hexo content from"
default = ""
}
variable "tags" {
type = "map"
default = {
Name = "Unnamed"
}
}

This will create a variable for the domain name we will use to serve our web site from as well as a variable for the name of the AWS S3 bucket used to store the content. In addition, a default set of tags that we should be applying to resources; nearly every AWS resource supports tags so you should be using them. If you don’t have a domain name, that’s OK. You’ll skips the setups involving Route53 and ACM and continue when adding the website content to AWS S3.

Onto adding resources into our account.

Create a file named kms.tf and put the following contents into it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
resource "aws_kms_key" "terraform_state" {
description = "Key used to encrypt data related to Terraform"
deletion_window_in_days = 10
enable_key_rotation = true
tags = "${merge(var.tags, map("Name", "Terraform Key"))}"
}

resource "aws_kms_alias" "terraform_state" {
name = "alias/terraform"
target_key_id = "${aws_kms_key.terraform_state.key_id}"
}

resource "aws_kms_key" "bucket_logging" {
description = "Key used for S3 bucket logging"
deletion_window_in_days = 10
enable_key_rotation = true
tags = "${merge(var.tags, map("Name", "S3 Bucket Logging Key"))}"
}

resource "aws_kms_alias" "bucket_logging" {
name = "alias/s3/logging"
target_key_id = "${aws_kms_key.terraform_state.key_id}"
}

What we are doing is creating a couple AWS KMS CMKs that we will use to encrypt and decrypt our data. We do care about security here from a learning and practical standpoint. Let’s run terraform plan to see what Terraform would create for us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
Terraform will perform the following actions:

# aws_kms_alias.terraform_state will be created
+ resource "aws_kms_alias" "terraform_state" {
+ arn = (known after apply)
+ id = (known after apply)
+ name = "alias/terraform"
+ target_key_arn = (known after apply)
+ target_key_id = (known after apply)
}

# aws_kms_key.terraform_state will be created
+ resource "aws_kms_key" "terraform_state" {
+ arn = (known after apply)
+ deletion_window_in_days = 10
+ description = "Key used to encrypt data related to Terraform"
+ enable_key_rotation = true
+ id = (known after apply)
+ is_enabled = true
+ key_id = (known after apply)
+ key_usage = (known after apply)
+ policy = (known after apply)
+ tags = {
+ "Name" = "Terraform Key"
}
}
...
Plan: 4 to add, 0 to change, 0 to destroy.
...

As you can see from the output, Terraform wants to create new resources; KMS CMKs and Aliases to the CMKs. It also wants to apply our name to the “Name” tag. It would be “Unnamed” had we not specified a name in our merge function. We will use these encryption keys for our Terraform state as well as AWS S3 bucket logging.

Let’s put these keys to good use by setting up a place for our Terraform state file to live that’s not on our computer. We’re going to setup what’s called a “remote state” and we will store it on AWS S3.

Create a file named s3.tf and put the following contents into it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
resource "aws_s3_bucket" "bucket_logging" {
bucket = "serverless-hexo-demo-bucket-logging"
acl = "log-delivery-write"
tags = "${merge(var.tags, map("Name", "Bucket Logging"))}"
versioning {
enabled = true
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = "${aws_kms_key.bucket_logging.arn}"
sse_algorithm = "aws:kms"
}
}
}
}

resource "aws_s3_bucket" "terraform_state" {
bucket = "serverless-hexo-demo-tf-state"
acl = "private"
tags = "${merge(var.tags, map("Name", "Terraform State"))}"
versioning {
enabled = true
}
logging {
target_bucket = "${aws_s3_bucket.bucket_logging.id}"
target_prefix = "terraform_state/"
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = "${aws_kms_key.terraform_state.arn}"
sse_algorithm = "aws:kms"
}
}
}
}

Running terraform apply will create a bucket for AWS S3 Bucket Logging logs to be stored as well as a bucket for our Terraform state data. Both buckets are encrypted with versioning and the Terraform state bucket has logging enabled.

Grab something refreshing and head on over to part 2.