From 445397df4003145a363afc2f1cb505c73142f52a Mon Sep 17 00:00:00 2001 From: Alex Hyett Date: Thu, 14 Jan 2021 09:15:06 +0000 Subject: [PATCH] Terraform files --- README.md | 91 ++++++++++++++++++- src/.terraform.lock.hcl | 37 ++++++++ src/acm.tf | 21 +++++ src/cloudfront.tf | 170 +++++++++++++++++++++++++++++++++++ src/providers.tf | 25 ++++++ src/route53.tf | 48 ++++++++++ src/s3.tf | 33 +++++++ src/templates/s3-policy.json | 12 +++ src/terraform.tfvars | 6 ++ src/variables.tf | 13 +++ 10 files changed, 455 insertions(+), 1 deletion(-) create mode 100755 src/.terraform.lock.hcl create mode 100644 src/acm.tf create mode 100644 src/cloudfront.tf create mode 100644 src/providers.tf create mode 100644 src/route53.tf create mode 100644 src/s3.tf create mode 100644 src/templates/s3-policy.json create mode 100644 src/terraform.tfvars create mode 100644 src/variables.tf diff --git a/README.md b/README.md index d5744d9..4c11af6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,91 @@ # terraform-s3-static-website -Terraform for hosting a secure static website on AWS using S3 + +Terraform for hosting a secure static website hosted on AWS using S3. + +The terraform will set up the following components: + +- S3 bucket for www.yourdomain.com which will host your website files. +- S3 bucket for yourdomain.com which redirects to www.yourdomain.com. +- SSL certificate for yourdomain.com. +- Cloudfront distribution for www S3 bucket. +- Cloudfront distribution for S3 bucket redirect. +- Route 53 records for: + - root + - www + +## Using this repository + +### Create your S3 bucket to store your state files. + +Create a new S3 bucket manually to store your state files. This should have the following policy set: + +``` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::1234567890:root" + }, + "Action": "s3:ListBucket", + "Resource": "arn:aws:s3:::yourdomain-terraform" + }, + { + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::1234567890:root" + }, + "Action": [ + "s3:GetObject", + "s3:PutObject" + ], + "Resource": "arn:aws:s3:::yourdomain-terraform/*" + } + ] +} +``` + +Where `1234567890` is replaced with your AWS account ID and `yourdomain-terraform` is replaced with the name of your S3 bucket. + +### Update your provider settings + +In `providers.tf` update the bucket (`yourdomain-terraform`) and region (`eu-west-1`) parameters to match your setup. Note, the `acm_provider` needs to point to `us-east-1` for Cloudfront to be able to use it. + +### Update the variables + +In `terraform.tfvars` update the variables with the name of your domain and S3 bucket. You can just replace `yourdomain` with your actual domain. + +Generally you will want to call you S3 bucket the name of your domain but if that is not possible (due to a conflict) then you can call it something else. + +## Commands to run + +- `terraform init` - To initialise the project and download any required packages. +- `terraform plan` - To see what has changed compared to your current configuration. +- `terrform apply` - To apply your changes. + +The terraform scripts are set up to validate your SSL certificate via email. So you should receive an email from AWS to `webmaster@yourdomain.com` when running apply which you need to approve in order for the SSL certificate to be validated. + +Alternatively, I have left the code for DNS validation in which can be uncommented if you don't have email set up (`acm.tf` and `route53.tf`). + +Note however, that DNS validation requires the domain nameservers to already be pointing to AWS. You won't actually know the nameservers until after the NS route 53 record has been created. Alternatively, you can follow the validation instructions from the ACM page for your domain and apply to where your nameservers are currently hosted. DNS validation can take 30 minutes or more during which the terraform script will still be running. + +## Post Changes + +Once the terraform scripts have been run successfully you will need to upload your scripts to your `www.yourdomain.com` S3 bucket. + +At the very least you need an `index.html` and a `404.html` file. + +You can upload the contents of a directory to your S3 bucket by using the command: + +``` +aws s3 sync . s3://www.yourdomain.com +``` + +Whenever you make changes to the files in your S3 bucket you need to invalidate the Cloudfront cache. + +``` +aws cloudfront create-invalidation --distribution-id E3EDVELPIKTLHJ --paths "/*"; +``` + +Where `E3EDVELPIKTLHJ` is the Cloudfront ID associated with your www S3 bucket. diff --git a/src/.terraform.lock.hcl b/src/.terraform.lock.hcl new file mode 100755 index 0000000..3e55895 --- /dev/null +++ b/src/.terraform.lock.hcl @@ -0,0 +1,37 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "3.23.0" + constraints = "~> 3.0" + hashes = [ + "h1:tSznQxPJvolDnmqqaTK9SsJ0bluTws7OAWcnc1t0ABs=", + "zh:30b0733027c00472618da998bc77967c692e238ae117c07e046fdd7336b83fa3", + "zh:3677550a8bef8e01c67cb615407dc8a69d32f4e36017033cd6f71a831c99d5de", + "zh:3c2fb4c14bfd43cf20ee25d0068ce09f1d48758408b8f1c88a096cea243612b3", + "zh:5577543322003693c4fe24a69ed0d47e58f867426fd704fac94cf5c16d3d6153", + "zh:6771f09d76ad01ffc04baa3bce7a3eed09f6a8a949274ffbd9d11756a58a4329", + "zh:7a57b79d304d17cf52ee3ddce91679f6b4289c5bdda2e31b763bf7d512e542d9", + "zh:815fb027e17bfe754b05367d20bd0694726a95a99b81e8d939ddd44e2b1f05a9", + "zh:a3d67db5ec0f4e9750eb19676a9a1aff36b0721e276a4ba789f42b991bf5951c", + "zh:cd67ff33860ad578172c19412ce608ba818e7590083197df2b793f870d6f50a3", + "zh:fbe0835055d1260fb77ad19a32a8726248ba7ac187f6c463ded90737b4cea8e6", + ] +} + +provider "registry.terraform.io/hashicorp/template" { + version = "2.2.0" + hashes = [ + "h1:0wlehNaxBX7GJQnPfQwTNvvAf38Jm0Nv7ssKGMaG6Og=", + "zh:01702196f0a0492ec07917db7aaa595843d8f171dc195f4c988d2ffca2a06386", + "zh:09aae3da826ba3d7df69efeb25d146a1de0d03e951d35019a0f80e4f58c89b53", + "zh:09ba83c0625b6fe0a954da6fbd0c355ac0b7f07f86c91a2a97849140fea49603", + "zh:0e3a6c8e16f17f19010accd0844187d524580d9fdb0731f675ffcf4afba03d16", + "zh:45f2c594b6f2f34ea663704cc72048b212fe7d16fb4cfd959365fa997228a776", + "zh:77ea3e5a0446784d77114b5e851c970a3dde1e08fa6de38210b8385d7605d451", + "zh:8a154388f3708e3df5a69122a23bdfaf760a523788a5081976b3d5616f7d30ae", + "zh:992843002f2db5a11e626b3fc23dc0c87ad3729b3b3cff08e32ffb3df97edbde", + "zh:ad906f4cebd3ec5e43d5cd6dc8f4c5c9cc3b33d2243c89c5fc18f97f7277b51d", + "zh:c979425ddb256511137ecd093e23283234da0154b7fa8b21c2687182d9aea8b2", + ] +} diff --git a/src/acm.tf b/src/acm.tf new file mode 100644 index 0000000..f853248 --- /dev/null +++ b/src/acm.tf @@ -0,0 +1,21 @@ +# SSL Certificate +resource "aws_acm_certificate" "ssl_certificate" { + provider = aws.acm_provider + domain_name = var.domain_name + subject_alternative_names = ["*.${var.domain_name}"] + validation_method = "EMAIL" + #validation_method = "DNS" + + tags = var.common_tags + + lifecycle { + create_before_destroy = true + } +} + +# Uncomment the validation_record_fqdns line if you do DNS validation instead of Email. +resource "aws_acm_certificate_validation" "cert_validation" { + provider = aws.acm_provider + certificate_arn = aws_acm_certificate.ssl_certificate.arn + #validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] +} diff --git a/src/cloudfront.tf b/src/cloudfront.tf new file mode 100644 index 0000000..b6ac6d1 --- /dev/null +++ b/src/cloudfront.tf @@ -0,0 +1,170 @@ +# Cloudfront distribution for main s3 site. +resource "aws_cloudfront_distribution" "www_s3_distribution" { + origin { + domain_name = aws_s3_bucket.www_bucket.website_endpoint + origin_id = "S3-www.${var.bucket_name}" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"] + } + } + + enabled = true + is_ipv6_enabled = true + default_root_object = "index.html" + + aliases = ["www.${var.domain_name}"] + + custom_error_response { + error_caching_min_ttl = 0 + error_code = 404 + response_code = 200 + response_page_path = "/404.html" + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-www.${var.bucket_name}" + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 31536000 + default_ttl = 31536000 + max_ttl = 31536000 + compress = true + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate_validation.cert_validation.certificate_arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.1_2016" + } + + tags = var.common_tags +} + +# Cloudfront S3 for redirect to www. +resource "aws_cloudfront_distribution" "root_s3_distribution" { + origin { + domain_name = aws_s3_bucket.root_bucket.website_endpoint + origin_id = "S3-.${var.bucket_name}" + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"] + } + } + + enabled = true + is_ipv6_enabled = true + + aliases = [var.domain_name] + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-.${var.bucket_name}" + + forwarded_values { + query_string = true + + cookies { + forward = "none" + } + + headers = ["Origin"] + } + + viewer_protocol_policy = "allow-all" + min_ttl = 0 + default_ttl = 86400 + max_ttl = 31536000 + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate_validation.cert_validation.certificate_arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.1_2016" + } + + tags = var.common_tags +} + +# Devup Cloudfront +resource "aws_cloudfront_distribution" "devup_s3_distribution" { + origin { + domain_name = aws_s3_bucket.devup_bucket.website_endpoint + origin_id = "S3-devup.${var.bucket_name}" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "http-only" + origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"] + } + } + + enabled = true + is_ipv6_enabled = true + default_root_object = "index.html" + + aliases = ["devup.${var.domain_name}"] + + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-devup.${var.bucket_name}" + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 31536000 + default_ttl = 31536000 + max_ttl = 31536000 + compress = true + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate_validation.cert_validation.certificate_arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.1_2016" + } + + tags = var.common_tags +} diff --git a/src/providers.tf b/src/providers.tf new file mode 100644 index 0000000..e5ecd26 --- /dev/null +++ b/src/providers.tf @@ -0,0 +1,25 @@ +terraform { + required_version = "~> 0.14" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 3.0" + } + } + + backend "s3" { + bucket = "yourdomain-terraform" + key = "prod/terraform.tfstate" + region = "eu-west-1" + } +} + +provider "aws" { + region = "eu-west-1" +} + +provider "aws" { + alias = "acm_provider" + region = "us-east-1" +} diff --git a/src/route53.tf b/src/route53.tf new file mode 100644 index 0000000..bdca93b --- /dev/null +++ b/src/route53.tf @@ -0,0 +1,48 @@ +# Route 53 for domain +resource "aws_route53_zone" "main" { + name = var.domain_name + tags = var.common_tags +} + +resource "aws_route53_record" "root-a" { + zone_id = aws_route53_zone.main.zone_id + name = var.domain_name + type = "A" + + alias { + name = aws_cloudfront_distribution.root_s3_distribution.domain_name + zone_id = aws_cloudfront_distribution.root_s3_distribution.hosted_zone_id + evaluate_target_health = false + } +} + +resource "aws_route53_record" "www-a" { + zone_id = aws_route53_zone.main.zone_id + name = "www.${var.domain_name}" + type = "A" + + alias { + name = aws_cloudfront_distribution.www_s3_distribution.domain_name + zone_id = aws_cloudfront_distribution.www_s3_distribution.hosted_zone_id + evaluate_target_health = false + } +} + +# Uncomment the below block if you are doing certificate validation using DNS instead of Email. +#resource "aws_route53_record" "cert_validation" { +# for_each = { +# for dvo in aws_acm_certificate.ssl_certificate.domain_validation_options : dvo.domain_name => { +# name = dvo.resource_record_name +# record = dvo.resource_record_value +# type = dvo.resource_record_type +# zone_id = aws_route53_zone.main.zone_id +# } +# } +# +# allow_overwrite = true +# name = each.value.name +# records = [each.value.record] +# ttl = 60 +# type = each.value.type +# zone_id = each.value.zone_id +#} diff --git a/src/s3.tf b/src/s3.tf new file mode 100644 index 0000000..ec90152 --- /dev/null +++ b/src/s3.tf @@ -0,0 +1,33 @@ +# S3 bucket for website. +resource "aws_s3_bucket" "www_bucket" { + bucket = "www.${var.bucket_name}" + acl = "public-read" + policy = templatefile("templates/s3-policy.json", { bucket = "www.${var.bucket_name}" }) + + cors_rule { + allowed_headers = ["Authorization", "Content-Length"] + allowed_methods = ["GET", "POST"] + allowed_origins = ["https://www.${var.domain_name}"] + max_age_seconds = 3000 + } + + website { + index_document = "index.html" + error_document = "404.html" + } + + tags = var.common_tags +} + +# S3 bucket for redirecting non-www to www. +resource "aws_s3_bucket" "root_bucket" { + bucket = var.bucket_name + acl = "public-read" + policy = templatefile("templates/s3-policy.json", { bucket = var.bucket_name }) + + website { + redirect_all_requests_to = "https://www.${var.domain_name}" + } + + tags = var.common_tags +} diff --git a/src/templates/s3-policy.json b/src/templates/s3-policy.json new file mode 100644 index 0000000..7a014b0 --- /dev/null +++ b/src/templates/s3-policy.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "PublicReadGetObject", + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::${bucket}/*" + } + ] +} \ No newline at end of file diff --git a/src/terraform.tfvars b/src/terraform.tfvars new file mode 100644 index 0000000..833fa20 --- /dev/null +++ b/src/terraform.tfvars @@ -0,0 +1,6 @@ +domain_name = "yourdomain.com" +bucket_name = "yourdomain.com" + +common_tags = { + Project = "yourdomain" +} \ No newline at end of file diff --git a/src/variables.tf b/src/variables.tf new file mode 100644 index 0000000..8d862aa --- /dev/null +++ b/src/variables.tf @@ -0,0 +1,13 @@ +variable "domain_name" { + type = string + description = "The domain name for the website." +} + +variable "bucket_name" { + type = string + description = "The name of the bucket without the www. prefix. Normally domain_name." +} + +variable "common_tags" { + description = "Common tags you want applied to all components." +}