Deploy a static website on AWS for FREE!
Tags: aws, staticwebsite, webdev, iac
Published: 2023-04-19 15:02:10

Yes, you read it right! It's possible to host a static website, for FREE, on AWS. 

> Before starting you need a hosted zone in Route53 for the website's domain you're deploying.

To do that we're going to deploy our static files on S3, create a CloudFront distribution, create a valid TLS certificate in ACM, and finally we'll add an alias record in Route53; let's start! 

So first we need to create a CDK app, I'll not take too much time on this as there are tons of articles around the web on how to do that, therefore once you've done it we can start coding! 

We'll divide the app into two stacks 

1. One stack will create the TLS certificate;
2. The other stack will create an S3 bucket and the CloudFront distribution;

This is because we need to create the TLS certificate in the same region as the CloudFront distribution (us-east-1) and for some reason, CDK handles well the CloudFront distribution creation in that region, regardless if the stack defines the region or not, rather than the TLS certificate stack need a region to be specified. 

So let's start by defining the stack for the TLS certificate, we need first to define a `cdk_environment` variable which holds info about the environment in which CDK will be executed (region and account number). For the region, we will use `us-east-1` so the certificate will be compatible with CloudFront. Then we need to pass to the `super` constructor the env variable we just declared, and an attribute `cross_region_references=True`, this little one will help us to reference the certificate's ARN in the other stack. 

Everything else is just CDK construct to define the TLS certificate and its validation method (in the stack below is Route 53). 

```python
class SSLCertificateStack(Stack):
    def __init__(self, scope: Construct, domain: str, **kwargs) -> None:
        cdk_environment = Environment(
            region="us-east-1", account=os.getenv("CDK_DEFAULT_ACCOUNT")
        )
        super().__init__(
            scope,
            f"SSLCertificateStack-{domain.replace('.', '-')}",
            env=cdk_environment,
            cross_region_references=True,
            **kwargs,
        )

        website_hosted_zone = HostedZone.from_lookup(
            self, "domain_hosted_zone", domain_name=domain
        )

        self.certificate = Certificate(
            self,
            "website_certificate",
            domain_name=domain,
            subject_alternative_names=[f"www.{domain}"],
            validation=CertificateValidation.from_dns(website_hosted_zone),
        )
```

Let's now define the other stack for S3 and the CloudFront distribution. Again we will need to define a `cdk_environment` variable which will hold the info on where to deploy the resources defined in the stack, including the website assets. In this scenario, we deploy in a different region than before, `eu-west-1`. Again we need to pass the magic `cross_region_references=True` to get the output from the precedent stack. 

```python
class WebsiteStack(Stack):
    def __init__(
        self, scope: Construct, domain: str, tls_certificate: Certificate, website_assets_path: str, **kwargs
    ) -> None:
        cdk_environment = Environment(
            region="eu-west-1", account=os.getenv("CDK_DEFAULT_ACCOUNT")
        )

        super().__init__(
            scope,
            f"WebsiteDeploy-{domain.replace('.', '-')}",
            env=cdk_environment,
            cross_region_references=True,
            **kwargs,
        )

        domains = [domain, f"www.{domain}"]

        website_bucket = s3.Bucket(
            self,
            "website_bucket",
            access_control=BucketAccessControl.PRIVATE,
            encryption=BucketEncryption.S3_MANAGED,
        )

        website_bucket.apply_removal_policy(RemovalPolicy.DESTROY)

        BucketDeployment(
            self,
            "website_deployment",
            destination_bucket=website_bucket,
            sources=[Source.asset(website_assets_path)],
        )

        oai = OriginAccessIdentity(self, "origin_access_identity")
        oai.apply_removal_policy(RemovalPolicy.DESTROY)

        website_bucket.grant_read(identity=oai)

        distribution = Distribution(
            self,
            "website_distribution",
            default_root_object="index.html",
            default_behavior=BehaviorOptions(
                origin=S3Origin(website_bucket, origin_access_identity=oai),
                viewer_protocol_policy=ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
            ),
            domain_names=domains,
            certificate=tls_certificate,
        )

        CfnOutput(
            self,
            "distribution_url",
            value=distribution.domain_name,
            export_name=f"DistributionDomainName-{domain.replace('.', '-')}",
        )

        CfnOutput(
            self,
            "bucket_arn",
            value=website_bucket.bucket_arn,
            export_name=f"WebsiteBuckerARN-{domain.replace('.', '-')}",
        )
```

Notes of attention are 
- an OAC (Origin Access Control) for S3 which allow requests through the CloudFront distribution
- the CloudFront distribution Viewer Protocol Policy to redirect all the requests to HTTPs
- the two outputs which can be used to test the website deployment (see `distribution_url`)

Now left to do is add two records in Route53, both pointing to the CloudFront distribution. For instance, say we're deploying example.com, we can add: 
- `example.com`
- `www.example.com`

Now, if you want to take a look at the whole code, here is the [repo](https://github.com/panilo/cdk_static_website_deployment) :) 

## What is free

- S3 offers the first 5 GB of data for free, if you have less than 5 GB around S3 it should be good. More info [here](https://aws.amazon.com/s3/pricing/).
- CloudFront distribution offers the first tera of data out for free, see [here](https://aws.amazon.com/cloudfront/pricing/).
- AWS ACM TLS certificates are completely free, see [here](https://aws.amazon.com/certificate-manager/pricing/).

The only service you pay for is Route 53, min .50 cents per month (see [here](https://aws.amazon.com/route53/pricing/)).


Happy to answer any questions, please let me know if you'd like to see more pictures and if this article is clear :)

Cheers
Danilo