Deploy a static website on AWS for FREE!
Danilo Desole
Posted on April 19, 2023
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
- One stack will create the TLS certificate;
- 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).
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.
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 :)
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.
- CloudFront distribution offers the first tera of data out for free, see here.
- AWS ACM TLS certificates are completely free, see here.
The only service you pay for is Route 53, min .50 cents per month (see here).
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
Posted on April 19, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.