Infrastructure as code (#3)
* Moves website to website/ * Adds terraform gitignores * Terraform with AWS provider * Initialises Terraform * Locals and variables for provider * Fetches SSL certificate from ACM * S3 static website bucket * CloudFront distribution * Route53 records * Deployment workflow uses secret S3 bucket suffix * Adds README --------- Co-authored-by: Joe Carstairs <65492573+Sycamost@users.noreply.github.com>
This commit is contained in:
5
.github/workflows/build-and-deploy.yml
vendored
5
.github/workflows/build-and-deploy.yml
vendored
@@ -1,8 +1,6 @@
|
||||
name: Build and deploy
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
push:
|
||||
branches: ["main"]
|
||||
workflow_dispatch:
|
||||
@@ -20,6 +18,7 @@ jobs:
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
S3_BUCKET_SUFFIX: ${{ secrets.S3_BUCKET_SUFFIX }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -43,4 +42,4 @@ jobs:
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
|
||||
aws configure set region eu-west-2
|
||||
aws s3 sync ./dist/ s3://joeac-personal-website --delete
|
||||
aws s3 sync ./dist/ "s3://joeac.net-$S3_BUCKET_SUFFIX" --delete
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -19,3 +19,9 @@ pnpm-debug.log*
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
.terraform/
|
||||
**/*.tfvars
|
||||
**/*.tfstate
|
||||
**/*.tfstate.backup
|
||||
|
||||
|
||||
28
README.md
28
README.md
@@ -2,6 +2,34 @@
|
||||
|
||||
Joe Carstairs' personal website
|
||||
|
||||
Structure:
|
||||
|
||||
├website: My public-facing website
|
||||
└infrastructure: The infrastructure of my website as code
|
||||
|
||||
## Infrastructure
|
||||
|
||||
The infrastructure has these components:
|
||||
|
||||
- AWS Route53Domains (for domain name registration)
|
||||
- AWS Route53 (for domain name resolution)
|
||||
- AWS CloudFront (for path-based routing)
|
||||
- AWS S3 (for static website hosting)
|
||||
|
||||
The CloudFront bit is needed, because S3 static website hosting can only accept
|
||||
HTTP requests. CloudFront manages receiving HTTPS requests and forwarding them
|
||||
to HTTP.
|
||||
|
||||
The S3 bucket includes a secret string of random characters. This is because
|
||||
when you set up static website hosting, the S3 API becomes open to the internet,
|
||||
and there's no way to turn this off. So you are theoretically open to DDoS
|
||||
attacks, for which you will be charged. Including a random string in the bucket
|
||||
name makes it less likely that an attacker will find the bucket to send requests
|
||||
to.
|
||||
|
||||
The secret is stored in a GitHub secret called `S3_BUCKET_SUFFIX` so that it can
|
||||
be accessed by GitHub Actions workflows.
|
||||
|
||||
## Invalidating the CloudFront cache
|
||||
|
||||
When you update pages, you’ll need to invalidate the CloudFront cache in order
|
||||
|
||||
25
infrastructure/.terraform.lock.hcl
generated
Normal file
25
infrastructure/.terraform.lock.hcl
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "4.67.0"
|
||||
constraints = "~> 4.16"
|
||||
hashes = [
|
||||
"h1:LfOuBkdYCzQhtiRvVIxdP/KGJODa3cRsKjn8xKCTbVY=",
|
||||
"zh:0843017ecc24385f2b45f2c5fce79dc25b258e50d516877b3affee3bef34f060",
|
||||
"zh:19876066cfa60de91834ec569a6448dab8c2518b8a71b5ca870b2444febddac6",
|
||||
"zh:24995686b2ad88c1ffaa242e36eee791fc6070e6144f418048c4ce24d0ba5183",
|
||||
"zh:4a002990b9f4d6d225d82cb2fb8805789ffef791999ee5d9cb1fef579aeff8f1",
|
||||
"zh:559a2b5ace06b878c6de3ecf19b94fbae3512562f7a51e930674b16c2f606e29",
|
||||
"zh:6a07da13b86b9753b95d4d8218f6dae874cf34699bca1470d6effbb4dee7f4b7",
|
||||
"zh:768b3bfd126c3b77dc975c7c0e5db3207e4f9997cf41aa3385c63206242ba043",
|
||||
"zh:7be5177e698d4b547083cc738b977742d70ed68487ce6f49ecd0c94dbf9d1362",
|
||||
"zh:8b562a818915fb0d85959257095251a05c76f3467caa3ba95c583ba5fe043f9b",
|
||||
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
|
||||
"zh:9c385d03a958b54e2afd5279cd8c7cbdd2d6ca5c7d6a333e61092331f38af7cf",
|
||||
"zh:b3ca45f2821a89af417787df8289cb4314b273d29555ad3b2a5ab98bb4816b3b",
|
||||
"zh:da3c317f1db2469615ab40aa6baba63b5643bae7110ff855277a1fb9d8eb4f2c",
|
||||
"zh:dc6430622a8dc5cdab359a8704aec81d3825ea1d305bbb3bbd032b1c6adfae0c",
|
||||
"zh:fac0d2ddeadf9ec53da87922f666e1e73a603a611c57bcbc4b86ac2821619b1d",
|
||||
]
|
||||
}
|
||||
4
infrastructure/acm.tf
Normal file
4
infrastructure/acm.tf
Normal file
@@ -0,0 +1,4 @@
|
||||
data "aws_acm_certificate" "joeac_ssl_certificate" {
|
||||
domain = local.domain
|
||||
statuses = ["ISSUED"]
|
||||
}
|
||||
59
infrastructure/cloudfront.tf
Normal file
59
infrastructure/cloudfront.tf
Normal file
@@ -0,0 +1,59 @@
|
||||
resource "aws_cloudfront_distribution" "joeac" {
|
||||
enabled = true
|
||||
is_ipv6_enabled = true
|
||||
default_root_object = "index.html"
|
||||
price_class = "PriceClass_100"
|
||||
|
||||
aliases = ["joeac.net"]
|
||||
|
||||
origin {
|
||||
domain_name = aws_s3_bucket_website_configuration.website.website_endpoint
|
||||
origin_id = local.website_origin_id
|
||||
|
||||
custom_origin_config {
|
||||
http_port = 80
|
||||
https_port = 443
|
||||
origin_protocol_policy = "http-only"
|
||||
origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"]
|
||||
}
|
||||
}
|
||||
|
||||
default_cache_behavior {
|
||||
allowed_methods = ["GET", "HEAD", "OPTIONS"]
|
||||
cached_methods = ["GET", "HEAD", "OPTIONS"]
|
||||
target_origin_id = local.website_origin_id
|
||||
cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id
|
||||
viewer_protocol_policy = "redirect-to-https"
|
||||
}
|
||||
|
||||
restrictions {
|
||||
geo_restriction {
|
||||
restriction_type = "none"
|
||||
locations = []
|
||||
}
|
||||
}
|
||||
|
||||
viewer_certificate {
|
||||
acm_certificate_arn = data.aws_acm_certificate.joeac_ssl_certificate.arn
|
||||
ssl_support_method = "sni-only"
|
||||
minimum_protocol_version = "TLSv1.2_2021"
|
||||
}
|
||||
|
||||
depends_on = [aws_cloudfront_origin_access_control.website]
|
||||
}
|
||||
|
||||
resource "aws_cloudfront_origin_access_control" "website" {
|
||||
name = "website"
|
||||
origin_access_control_origin_type = "s3"
|
||||
signing_behavior = "always"
|
||||
signing_protocol = "sigv4"
|
||||
}
|
||||
|
||||
data "aws_cloudfront_cache_policy" "caching_optimized" {
|
||||
name = "Managed-CachingOptimized"
|
||||
}
|
||||
|
||||
locals {
|
||||
website_origin_id = "website"
|
||||
}
|
||||
|
||||
5
infrastructure/locals.tf
Normal file
5
infrastructure/locals.tf
Normal file
@@ -0,0 +1,5 @@
|
||||
locals {
|
||||
aws_region = "us-east-1"
|
||||
domain = "joeac.net"
|
||||
}
|
||||
|
||||
11
infrastructure/main.tf
Normal file
11
infrastructure/main.tf
Normal file
@@ -0,0 +1,11 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 4.16"
|
||||
}
|
||||
}
|
||||
|
||||
required_version = ">= 1.2.0"
|
||||
}
|
||||
|
||||
12
infrastructure/providers.tf
Normal file
12
infrastructure/providers.tf
Normal file
@@ -0,0 +1,12 @@
|
||||
provider "aws" {
|
||||
region = local.aws_region
|
||||
access_key = var.aws_access_key
|
||||
secret_key = var.aws_secret_key
|
||||
default_tags {
|
||||
tags = {
|
||||
project = "joeac-website"
|
||||
owner = "terraform"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
infrastructure/route53.tf
Normal file
35
infrastructure/route53.tf
Normal file
@@ -0,0 +1,35 @@
|
||||
# This hosted zone must have NS records which point to the same nameservers as
|
||||
# those listed with the domain registrar. Right now, this means manually going
|
||||
# into Route53Domains, finding the nameservers, and manually copying these into
|
||||
# the NS records for this hosted zone.
|
||||
resource "aws_route53_zone" "joeac_zone" {
|
||||
name = "joeac.net"
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "cloudfront" {
|
||||
# This is the subdomain for the record. Specify the root domain by leaving it blank
|
||||
name = ""
|
||||
|
||||
zone_id = aws_route53_zone.joeac_zone.id
|
||||
type = "A"
|
||||
|
||||
alias {
|
||||
name = aws_cloudfront_distribution.joeac.domain_name
|
||||
zone_id = aws_cloudfront_distribution.joeac.hosted_zone_id
|
||||
evaluate_target_health = false
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "cloudfront_aaaa" {
|
||||
# This is the subdomain for the record. Specify the root domain by leaving it blank
|
||||
name = ""
|
||||
|
||||
zone_id = aws_route53_zone.joeac_zone.id
|
||||
type = "AAAA"
|
||||
|
||||
alias {
|
||||
name = aws_cloudfront_distribution.joeac.domain_name
|
||||
zone_id = aws_cloudfront_distribution.joeac.hosted_zone_id
|
||||
evaluate_target_health = false
|
||||
}
|
||||
}
|
||||
78
infrastructure/s3.tf
Normal file
78
infrastructure/s3.tf
Normal file
@@ -0,0 +1,78 @@
|
||||
resource "aws_s3_bucket" "website" {
|
||||
bucket = local.bucket_name
|
||||
}
|
||||
|
||||
locals {
|
||||
bucket_name = "${local.domain}-${var.secret_s3_bucket_suffix}"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_website_configuration" "website" {
|
||||
bucket = aws_s3_bucket.website.id
|
||||
|
||||
index_document {
|
||||
suffix = "index.html"
|
||||
}
|
||||
|
||||
error_document {
|
||||
key = "error/index.html"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_ownership_controls" "website" {
|
||||
bucket = aws_s3_bucket.website.id
|
||||
|
||||
rule {
|
||||
object_ownership = "BucketOwnerPreferred"
|
||||
}
|
||||
|
||||
depends_on = [aws_s3_bucket_public_access_block.website]
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_public_access_block" "website" {
|
||||
bucket = aws_s3_bucket.website.id
|
||||
|
||||
block_public_acls = false
|
||||
block_public_policy = false
|
||||
ignore_public_acls = false
|
||||
restrict_public_buckets = false
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_acl" "website" {
|
||||
bucket = aws_s3_bucket.website.id
|
||||
|
||||
acl = "public-read"
|
||||
|
||||
depends_on = [aws_s3_bucket_ownership_controls.website]
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "website" {
|
||||
bucket = aws_s3_bucket.website.id
|
||||
|
||||
versioning_configuration {
|
||||
status = "Disabled"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_policy" "website" {
|
||||
bucket = aws_s3_bucket.website.id
|
||||
policy = data.aws_iam_policy_document.website.json
|
||||
}
|
||||
|
||||
# TODO: can we restrict access to just from the CloudFront distro?
|
||||
data "aws_iam_policy_document" "website" {
|
||||
statement {
|
||||
sid = "AllowPublicRead"
|
||||
effect = "Allow"
|
||||
resources = [
|
||||
"arn:aws:s3:::${local.bucket_name}",
|
||||
"arn:aws:s3:::${local.bucket_name}/*",
|
||||
]
|
||||
actions = ["S3:GetObject"]
|
||||
principals {
|
||||
type = "*"
|
||||
identifiers = ["*"]
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [aws_s3_bucket_public_access_block.website, aws_s3_bucket.website]
|
||||
}
|
||||
28
infrastructure/variables.tf
Normal file
28
infrastructure/variables.tf
Normal file
@@ -0,0 +1,28 @@
|
||||
variable "aws_access_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "An AWS access key with permission to provision all relevant resources"
|
||||
}
|
||||
|
||||
variable "aws_secret_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "The secret corresponding to the provided AWS access key"
|
||||
}
|
||||
|
||||
variable "secret_s3_bucket_suffix" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "This string should be a long string of up to 54 random characters. It will be appended to the S3 bucket name to mitigate the risk of DDoS attacks."
|
||||
nullable = false
|
||||
|
||||
validation {
|
||||
condition = length(var.secret_s3_bucket_suffix) > 12
|
||||
error_message = "This string should be at least 12 characters"
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = length(var.secret_s3_bucket_suffix) <= 54
|
||||
error_message = "This string should be no more than 54 characters long"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 949 KiB After Width: | Height: | Size: 949 KiB |
|
Before Width: | Height: | Size: 949 KiB After Width: | Height: | Size: 949 KiB |
0
src/env.d.ts → website/src/env.d.ts
vendored
0
src/env.d.ts → website/src/env.d.ts
vendored
Reference in New Issue
Block a user