Photo by imgix on Unsplash

How to Create, Automate & Deploy a Static Blog Using Publii, AWS(S3, Route53, CloudFront), Terraform & NameCheap Part 2

On this second part we will be spinning up AWS infrastructure to house our static content from our blog. In other words we are going to create our own little but infinitely scalable webserver using serverless technologies in AWS.

We start by writing out our provider.tf:

provider "aws" {

access_key ="${var.aws_access_key}"

secret_key ="${var.aws_secret_key}"

region ="${var.aws_region}"

}

If you want a more in depth explanation of what is provider.tf, please read the first part of this tutorial. Now after that we set up our variables:

variable "aws_access_key" {}

variable "aws_secret_key" {}

variable "aws_region" {}

variable "domain_name" {

default ="yourdomain.com"

}

variable "alternative_domain_name" {

default ="www.yourdomain.com"

}

variable "sub_domain_name" {

default ="*.yourdomain.com"

}

variable "website_bucket_name" {

default ="yourdomain.com"

}

variable "certificate_arn" {}

In this variables.tf, we are adding values for our S3 bucket names and grabbing the ARN from the previous certificate created in step 1. The rest is the same as step 1 variables.tf except the region. You can use whatever region you want at in this part. Also, remember to grab the certificate ARN for your domain and make it a value of certificate_arn variable in terraform.tfvars. Now that the provider and variable Terraform files have been made, we can proceed with creating the S3 buckets.

# AWS S3 bucket for static hosting

resource "aws_s3_bucket" "website" {

bucket ="${var.website_bucket_name}"

acl ="public-read"

tags ={

Name="whatever you want"

Environment="production"

}

cors_rule {

allowed_headers =["*"]

allowed_methods =["PUT","POST"]

allowed_origins =["*"]

expose_headers =["ETag"]

max_age_seconds =3000

}

policy =<<EOF

{

"Version": "2008-10-17",

"Statement": [

{

"Sid": "PublicReadForGetBucketObjects",

"Effect": "Allow",

"Principal": {

"AWS": "*"

},

"Action": "s3:GetObject",

"Resource": "arn:aws:s3:::${var.website_bucket_name}/*"

}

]

}

EOF

website {

index_document ="index.html"

error_document ="error.html"

}

}

# AWS S3 bucket for www-redirect

resource "aws_s3_bucket" "website_redirect" {

bucket ="www.${var.website_bucket_name}"

acl ="public-read"

website {

redirect_all_requests_to ="${var.website_bucket_name}"

}

}

The above code is called buckets.tf. This script is creating our S3 buckets that will host the static website. The first lines of code are creating a bucket using the website_bucket_name variable in variables.tf. The ACL is set to public so that people can see the website and the CSS that styles it. Moreover, we include a bucket policy to read the objects publicly and assign the main webpage as index.html. The second resource creates another bucket that will have the www in the front of the domain with a public ACL and a website redirect. This means anyone who types www.yourdomain.com will be forwarded to yourdormain.com instantly. Now that we are done coding for our buckets let's get our hosted zones set up.

resource "aws_route53_zone" "main" {

name ="${var.domain_name}"

comment ="Managed by Terraform"

tags ={

Environment="production"

}

}

resource "aws_route53_record" "main-a-record" {

zone_id ="${aws_route53_zone.main.zone_id}"

name ="${var.domain_name}"

type ="A"

alias {

name ="${aws_cloudfront_distribution.s3_distribution.domain_name}"

zone_id ="${aws_cloudfront_distribution.s3_distribution.hosted_zone_id}"

evaluate_target_health =false

}

}

resource "aws_route53_record" "www-a-record" {

zone_id ="${aws_route53_zone.main.zone_id}"

name ="www"

type ="A"

alias {

name ="${aws_cloudfront_distribution.s3_distribution.domain_name}"

zone_id ="${aws_cloudfront_distribution.s3_distribution.hosted_zone_id}"

evaluate_target_health =false

}

}

I always call this hosted-zone.tf. In this script we are creating DNS records so that the website can be found by your web browser. The aws_route53_zone is invoking route 53 to create a hosted zone using the value of your domain name and tags it for cost measure in AWS billing. The two "aws_route53_record" resources are creating the main A record for yourdomain.com and www.yourdomain.com using your CloudFront CDN address. Now that our DNS code is set up we have to do code the last part and that is CloudFront.

# AWS Cloudfront for caching

resource "aws_cloudfront_distribution" "s3_distribution" {

origin {

domain_name ="${aws_s3_bucket.website.bucket}.s3.amazonaws.com"

origin_id ="website"

}

enabled =true

is_ipv6_enabled =true

comment ="Managed by Terraform for importknowledge.com,"

default_root_object ="index.html"

aliases =["${var.domain_name}","${var.sub_domain_name}"]

default_cache_behavior {

allowed_methods =["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]

cached_methods =["GET", "HEAD"]

target_origin_id ="website"

compress ="true"

forwarded_values {

query_string =false

cookies {

forward ="none"

}

}

viewer_protocol_policy ="redirect-to-https"

min_ttl =0

default_ttl =3600

max_ttl =86400

}

price_class ="PriceClass_All"

restrictions {

geo_restriction {

restriction_type ="none"

}

}

tags ={

Environment="production"

}

viewer_certificate {

cloudfront_default_certificate =true

acm_certificate_arn ="${var.certificate_arn}"

minimum_protocol_version ="TLSv1"

ssl_support_method ="sni-only"

}

}

So what is going on in this script? The aws_cloudfront_distribution resource is requesting the use of CloudFront to use your main bucket as the place to cache from. That is

"${aws_s3_bucket.website.bucket}.s3.amazonaws.com". In the Aliases you want to include both domain names so that cCloudFront is aware of the website redirect. It is important to set viewer_protocol_policy to redirect-to-https. The price_class is best set to: PriceClass_All so that CloudFront caches your content all over the world. The last resource uses the certificate created in step 1 to sign the security of the website. Now that we have all of the code created to spin up our AWS infrastructure it is time to run it.
The terminal output is long for a photo so heres what you should see:
Refreshing Terraform state in-memory prior to plan...

The refreshed state will be used to calculate this plan, but will not be

persisted to local or remote state storage.
------------------------------------------------------------------------

An execution plan has been generated and is shown below.

Resource actions are indicated with the following symbols:

  + create




Terraform will perform the following actions:




  # aws_cloudfront_distribution.s3_distribution will be created

  + resource "aws_cloudfront_distribution" "s3_distribution" {

      + aliases                        = [

          + "*.yourdomain.com",

          + "yourdomain.com",

        ]

      + arn                            = (known after apply)

      + caller_reference               = (known after apply)

      + comment                        = "Managed by Terraform,"

      + default_root_object            = "index.html"

      + domain_name                    = (known after apply)

      + enabled                        = true

      + etag                           = (known after apply)

      + hosted_zone_id                 = (known after apply)

      + http_version                   = "http2"

      + id                             = (known after apply)

      + in_progress_validation_batches = (known after apply)

      + is_ipv6_enabled                = true

      + last_modified_time             = (known after apply)

      + price_class                    = "PriceClass_All"

      + retain_on_delete               = false

      + status                         = (known after apply)

      + tags                           = {

          + "Environment" = "production"

        }

      + trusted_signers                = (known after apply)

      + wait_for_deployment            = true




      + default_cache_behavior {

          + allowed_methods        = [

              + "DELETE",

              + "GET",

              + "HEAD",

              + "OPTIONS",

              + "PATCH",

              + "POST",

              + "PUT",

            ]

          + cached_methods         = [

              + "GET",

              + "HEAD",

            ]

          + compress               = true

          + default_ttl            = 3600

          + max_ttl                = 86400

          + min_ttl                = 0

          + target_origin_id       = "website"

          + viewer_protocol_policy = "redirect-to-https"




          + forwarded_values {

              + query_string = false




              + cookies {

                  + forward = "none"

                }

            }

        }




      + origin {

          + domain_name = "yourdomain.com.s3.amazonaws.com"

          + origin_id   = "website"

        }




      + restrictions {

          + geo_restriction {

              + restriction_type = "none"

            }

        }




      + viewer_certificate {

          + acm_certificate_arn            = "arn:aws:acm:us-east-1:randomstrings"

          + cloudfront_default_certificate = true

          + minimum_protocol_version       = "TLSv1"

          + ssl_support_method             = "sni-only"

        }

    }




  # aws_route53_record.main-a-record will be created

  + resource "aws_route53_record" "main-a-record" {

      + allow_overwrite = (known after apply)

      + fqdn            = (known after apply)

      + id              = (known after apply)

      + name            = "yourdomain.com"

      + type            = "A"

      + zone_id         = (known after apply)




      + alias {

          + evaluate_target_health = false

          + name                   = (known after apply)

          + zone_id                = (known after apply)

        }

    }




  # aws_route53_record.www-a-record will be created

  + resource "aws_route53_record" "www-a-record" {

      + allow_overwrite = (known after apply)

      + fqdn            = (known after apply)

      + id              = (known after apply)

      + name            = "www"

      + type            = "A"

      + zone_id         = (known after apply)




      + alias {

          + evaluate_target_health = false

          + name                   = (known after apply)

          + zone_id                = (known after apply)

        }

    }




  # aws_route53_zone.main will be created

  + resource "aws_route53_zone" "main" {

      + comment       = "Managed by Terraform"

      + force_destroy = false

      + id            = (known after apply)

      + name          = "yourdomian.com"

      + name_servers  = (known after apply)

      + tags          = {

          + "Environment" = "environment name"

        }

      + zone_id       = (known after apply)

    }




  # aws_s3_bucket.website will be created

  + resource "aws_s3_bucket" "website" {

      + acceleration_status         = (known after apply)

      + acl                         = "public-read"

      + arn                         = (known after apply)

      + bucket                      = "yourdomain.com"

      + bucket_domain_name          = (known after apply)

      + bucket_regional_domain_name = (known after apply)

      + force_destroy               = false

      + hosted_zone_id              = (known after apply)

      + id                          = (known after apply)

      + policy                      = jsonencode(

            {

              + Statement = [

                  + {

                      + Action    = "s3:GetObject"

                      + Effect    = "Allow"

                      + Principal = {

                          + AWS = "*"

                        }

                      + Resource  = "arn:aws:s3:::yourdomain.com/*"

                      + Sid       = "PublicReadForGetBucketObjects"

                    },

                ]

              + Version   = "2008-10-17"

            }

        )

      + region                      = (known after apply)

      + request_payer               = (known after apply)

      + tags                        = {

          + "Environment" = "production"

          + "Name"        = "yourdomain_website"

        }

      + website_domain              = (known after apply)

      + website_endpoint            = (known after apply)




      + cors_rule {

          + allowed_headers = [

              + "*",

            ]

          + allowed_methods = [

              + "PUT",

              + "POST",

            ]

          + allowed_origins = [

              + "*",

            ]

          + expose_headers  = [

              + "ETag",

            ]

          + max_age_seconds = 3000

        }




      + versioning {

          + enabled    = (known after apply)

          + mfa_delete = (known after apply)

        }




      + website {

          + error_document = "error.html"

          + index_document = "index.html"

        }

    }




  # aws_s3_bucket.website_redirect will be created

  + resource "aws_s3_bucket" "website_redirect" {

      + acceleration_status         = (known after apply)

      + acl                         = "public-read"

      + arn                         = (known after apply)

      + bucket                      = "www.yourdomain.com"

      + bucket_domain_name          = (known after apply)

      + bucket_regional_domain_name = (known after apply)

      + force_destroy               = false

      + hosted_zone_id              = (known after apply)

      + id                          = (known after apply)

      + region                      = (known after apply)

      + request_payer               = (known after apply)

      + website_domain              = (known after apply)

      + website_endpoint            = (known after apply)




      + versioning {

          + enabled    = (known after apply)

          + mfa_delete = (known after apply)

        }




      + website {

          + redirect_all_requests_to = "yourdomain.com"

        }

    }




Plan: 6 to add, 0 to change, 0 to destroy.

After you confirm the plan looks ok then you write terraform apply and watch the magic happen for the next few minutes. Once terraform finishes successfully (which can take more than 3 minutes) our entire infrastructure is ready to be used. Now how do i get my Namecheap domain to talk to AWS services I created? We just have to take the NS record from Route53 and copy them to the Namecheap DNS for your domain.

Copy the NS records to NameCheap

And just find in your domain in Namecheap the NAMESERVERS area and post your NS records there. FYI, eliminate the last dot from the NS record when copying it to Namecheap.

Thats it! the only thing missing now is our blog!