Publishing static sites on AWS

One of the good things about blogging using your own resources is how cost sensitive became yourself. In the past I always had used a wordpress installation for creating my personal blogs. The installation and configuration of wordpress is pretty straight forward: you install php, mysql some web server and you are good to go. However, you almost always need an instance of a virtual machine to run the underlying services. This cost real money and if you don't know your blog audience reach, your users can experience outages and you can have troubles like spending to much money on network transfers or depleting a service quota if you are in a VPS/cheap hosting. So unless you need do some kind of transaction or computation in your blog, probably using a wordpress is not necessary and all you need is a static site like the one I'm using right now.

For publishing in the web static content you can use plain old html or something more sophisticated CMS like Hugo, Jekyll, Pelican or others. In this article I'm going to use a very simple index.html file:

<html>
    <title>The static site title</title>
    <body>The content of the static site</body>
</html>

To accomplish publishing this static site I will use AWS services because I think it's the best cloud provider giving all tools necessary for automate every step of this process. However, in this example I will not automate two of the steps because I think are not complex and you will do it just one time.

The final solution will look like the following diagram:

The static site aws service diagram

The detail of each service is the following:

Route53

This service is the managed DNS offered by AWS. It provides high availability and integration with others AWS services like API Gateway, ELB, Cloudfront and others. This article assumes that you have a domain configured here already.

Cloudfront

Cloudfront is the AWS content delivery network (CDN) where resources get cached in an edge location near of your users. It has a ton of options and distribution types. In this example I'm going to use a web distribution.

ACM

AWS Certificate Manager is a service where you can request and manage certificates for the services you are using on AWS. If you have a certificate you can import it but in this example I'm going to request it because is free of charge.

S3

S3 is the king of distributed cloud storage where you can store your data without breaking the bank. S3 is seamlessly integrated with Cloudfront for web distributions. For this example, the intex.html is going to be saved inside a S3 bucket.

CDK and Cloudformation

AWS has a infrastructure as code template definition under the umbrella of Cloudformation service. You can define your infra in yaml or json for automating the process of creating and modifying AWS resources. However Cloudformation is not very friendly and write templates can become very fast in a not so great experience. To overcome this limitation the community helped to create CDK. This development kit basically generates Cloudformation templates but using a programming language like Python, Typescript, Java and .NET.

For this example, I'm going to write most of the infrastructure in CDK using python. You must have CDK running on your computer. More info here

The code

Environment varibles

I like to use variables instead of fixed values. In CDK you can use context keys to retrieve values that can be reused. For this you can define your key values in cdk.json file. In this case I defined the following keys:

    "bucket_site_name": "yourbucketname",
    "distribution_name": "yourdistributionname",
    "ssl_domain_name": "yourssldomain",
    "route53_zone_id": "yourzoneID",
    "cert_arn": "arn:aws:acm:us-east-1:1234567890:certificate/yourcustomcert"

To get a value from a given key you can use the try_get_context from the node object. For example:

    arn = self.node.try_get_context("cert_arn")

Bucket creation

To create the S3 bucket

        bucket = s3.Bucket(
            self,
            id="myBucket",
            bucket_name=self.node.try_get_context("bucket_site_name")
        )

If for some reason you must this code on an existing bucket the code should use from_bucket_name method instead:

            bucket = s3.Bucket.from_bucket_name(
            self,
            id="myBucket",
            bucket_name=self.node.try_get_context("bucket_site_name")

Hosted zone and ssl certificate

This code assumes that you have a hosted zone with a public domain name and a ssl certicate previously requested for the domain name for the site that you want to publish:

        hosted_zone = route53.HostedZone.from_hosted_zone_attributes(
            self,
            id="myHostedZone",
            hosted_zone_id=self.node.try_get_context("route53_zone_id"),
            zone_name=self.node.try_get_context("ssl_domain_name")
        )

        ssl_certificate = acm.Certificate.from_certificate_arn(
            self,
            id="MyCertificate",
            certificate_arn=arn
        )

Cloudfront configuration

In order to create the distribution, the ssl must be associated to it. Also you need to associate a Origin Access Identity to the bucket. More info here.

The following code configures the distribution asuming that the object root is a file called index.html

        viewer_certificate = cloudfront.ViewerCertificate.from_acm_certificate(certificate=ssl_certificate)

        origin_access_identity = cloudfront.OriginAccessIdentity(self, id="OriginAccessIdentity")
        bucket.grant_read(origin_access_identity)

        distribution = cloudfront.CloudFrontWebDistribution(
            self,
            id="myCloudfrontWebDistribution",
            origin_configs=[
                cloudfront.SourceConfiguration(
                    s3_origin_source=cloudfront.S3OriginConfig(
                        s3_bucket_source=bucket,
                        origin_access_identity=origin_access_identity),
                    behaviors=[
                        cloudfront.Behavior(
                            is_default_behavior=True,
                            default_ttl=core.Duration.hours(1)
                        )
                    ]
                )
            ],
            viewer_certificate=viewer_certificate,
            default_root_object="index.html",
        )

Route 53 alias

To add the distribution to a record in a hosted zone you must create an alias:

        route53.ARecord(
            self,
            id="AliasRecord",
            zone=hosted_zone,
            target=route53.RecordTarget.from_alias(
                route53_targets.CloudFrontTarget(distribution))
        )

And that's all, if you have your static content in the bucket, the only thing left is doing a cdk deploy and your site should be available for everyone in the internet. You can view this code in the cdk project you can review it here.