diff options
| -rw-r--r-- | .github/workflows/build-and-deploy.yml | 5 | ||||
| -rw-r--r-- | .gitignore | 6 | ||||
| -rw-r--r-- | README.md | 28 | ||||
| -rw-r--r-- | infrastructure/.terraform.lock.hcl | 25 | ||||
| -rw-r--r-- | infrastructure/acm.tf | 4 | ||||
| -rw-r--r-- | infrastructure/cloudfront.tf | 59 | ||||
| -rw-r--r-- | infrastructure/locals.tf | 5 | ||||
| -rw-r--r-- | infrastructure/main.tf | 11 | ||||
| -rw-r--r-- | infrastructure/providers.tf | 12 | ||||
| -rw-r--r-- | infrastructure/route53.tf | 35 | ||||
| -rw-r--r-- | infrastructure/s3.tf | 78 | ||||
| -rw-r--r-- | infrastructure/variables.tf | 28 | ||||
| -rw-r--r-- | website/astro.config.mjs (renamed from astro.config.mjs) | 0 | ||||
| -rw-r--r-- | website/package-lock.json (renamed from package-lock.json) | 0 | ||||
| -rw-r--r-- | website/package.json (renamed from package.json) | 0 | ||||
| -rw-r--r-- | website/public/css/base.css (renamed from public/css/base.css) | 0 | ||||
| -rw-r--r-- | website/public/css/blog.css (renamed from public/css/blog.css) | 0 | ||||
| -rw-r--r-- | website/public/css/hcard.css (renamed from public/css/hcard.css) | 0 | ||||
| -rw-r--r-- | website/public/css/reset.css (renamed from public/css/reset.css) | 0 | ||||
| -rw-r--r-- | website/public/images/headshot.jpg (renamed from public/images/headshot.jpg) | bin | 971952 -> 971952 bytes | |||
| -rw-r--r-- | website/public/images/headshot_large.jpg (renamed from public/images/headshot_large.jpg) | bin | 971952 -> 971952 bytes | |||
| -rw-r--r-- | website/src/components/BaseHead.astro (renamed from src/components/BaseHead.astro) | 0 | ||||
| -rw-r--r-- | website/src/components/BlogFeed.astro (renamed from src/components/BlogFeed.astro) | 0 | ||||
| -rw-r--r-- | website/src/components/FormattedDate.astro (renamed from src/components/FormattedDate.astro) | 0 | ||||
| -rw-r--r-- | website/src/components/Me.astro (renamed from src/components/Me.astro) | 0 | ||||
| -rw-r--r-- | website/src/consts.ts (renamed from src/consts.ts) | 0 | ||||
| -rw-r--r-- | website/src/content/blog/2024/01/14/sapiens_on_religion.md (renamed from src/content/blog/2024/01/14/sapiens_on_religion.md) | 0 | ||||
| -rw-r--r-- | website/src/content/blog/2024/01/29/euhwc_toast_to_the_lasses_2024.md (renamed from src/content/blog/2024/01/29/euhwc_toast_to_the_lasses_2024.md) | 0 | ||||
| -rw-r--r-- | website/src/content/blog/2024/03/30/easter.md (renamed from src/content/blog/2024/03/30/easter.md) | 0 | ||||
| -rw-r--r-- | website/src/content/config.ts (renamed from src/content/config.ts) | 0 | ||||
| -rw-r--r-- | website/src/env.d.ts (renamed from src/env.d.ts) | 0 | ||||
| -rw-r--r-- | website/src/layouts/BlogPost.astro (renamed from src/layouts/BlogPost.astro) | 0 | ||||
| -rw-r--r-- | website/src/layouts/Page.astro (renamed from src/layouts/Page.astro) | 0 | ||||
| -rw-r--r-- | website/src/pages/blog/[...slug].astro (renamed from src/pages/blog/[...slug].astro) | 0 | ||||
| -rw-r--r-- | website/src/pages/blog/index.astro (renamed from src/pages/blog/index.astro) | 0 | ||||
| -rw-r--r-- | website/src/pages/error.astro (renamed from src/pages/error.astro) | 0 | ||||
| -rw-r--r-- | website/src/pages/index.astro (renamed from src/pages/index.astro) | 0 | ||||
| -rw-r--r-- | website/src/pages/rss.xml.js (renamed from src/pages/rss.xml.js) | 0 | ||||
| -rw-r--r-- | website/src/src/content/blog/2024/04/10/tracking_pixels.md (renamed from src/content/blog/2024/04/10/tracking_pixels.md) | 0 | ||||
| -rw-r--r-- | website/tsconfig.json (renamed from tsconfig.json) | 0 |
40 files changed, 293 insertions, 3 deletions
diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 0ea882e..8b5add3 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -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 @@ -19,3 +19,9 @@ pnpm-debug.log* # macOS-specific files .DS_Store + +.terraform/ +**/*.tfvars +**/*.tfstate +**/*.tfstate.backup + @@ -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 diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/.terraform.lock.hcl new file mode 100644 index 0000000..8790b90 --- /dev/null +++ b/infrastructure/.terraform.lock.hcl @@ -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", + ] +} diff --git a/infrastructure/acm.tf b/infrastructure/acm.tf new file mode 100644 index 0000000..f07a0eb --- /dev/null +++ b/infrastructure/acm.tf @@ -0,0 +1,4 @@ +data "aws_acm_certificate" "joeac_ssl_certificate" { + domain = local.domain + statuses = ["ISSUED"] +} diff --git a/infrastructure/cloudfront.tf b/infrastructure/cloudfront.tf new file mode 100644 index 0000000..4cdc80c --- /dev/null +++ b/infrastructure/cloudfront.tf @@ -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" +} + diff --git a/infrastructure/locals.tf b/infrastructure/locals.tf new file mode 100644 index 0000000..d4fd3bf --- /dev/null +++ b/infrastructure/locals.tf @@ -0,0 +1,5 @@ +locals { + aws_region = "us-east-1" + domain = "joeac.net" +} + diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 0000000..03b8568 --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.16" + } + } + + required_version = ">= 1.2.0" +} + diff --git a/infrastructure/providers.tf b/infrastructure/providers.tf new file mode 100644 index 0000000..8e06505 --- /dev/null +++ b/infrastructure/providers.tf @@ -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" + } + } +} + diff --git a/infrastructure/route53.tf b/infrastructure/route53.tf new file mode 100644 index 0000000..828cec2 --- /dev/null +++ b/infrastructure/route53.tf @@ -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 + } +} diff --git a/infrastructure/s3.tf b/infrastructure/s3.tf new file mode 100644 index 0000000..7b116a7 --- /dev/null +++ b/infrastructure/s3.tf @@ -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] +} diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf new file mode 100644 index 0000000..224ada4 --- /dev/null +++ b/infrastructure/variables.tf @@ -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" + } +} diff --git a/astro.config.mjs b/website/astro.config.mjs index e4257f9..e4257f9 100644 --- a/astro.config.mjs +++ b/website/astro.config.mjs diff --git a/package-lock.json b/website/package-lock.json index 50ad631..50ad631 100644 --- a/package-lock.json +++ b/website/package-lock.json diff --git a/package.json b/website/package.json index ec8cb15..ec8cb15 100644 --- a/package.json +++ b/website/package.json diff --git a/public/css/base.css b/website/public/css/base.css index a22ca34..a22ca34 100644 --- a/public/css/base.css +++ b/website/public/css/base.css diff --git a/public/css/blog.css b/website/public/css/blog.css index 88f201a..88f201a 100644 --- a/public/css/blog.css +++ b/website/public/css/blog.css diff --git a/public/css/hcard.css b/website/public/css/hcard.css index 99431d9..99431d9 100644 --- a/public/css/hcard.css +++ b/website/public/css/hcard.css diff --git a/public/css/reset.css b/website/public/css/reset.css index 5a637b3..5a637b3 100644 --- a/public/css/reset.css +++ b/website/public/css/reset.css diff --git a/public/images/headshot.jpg b/website/public/images/headshot.jpg Binary files differindex 2ca13cf..2ca13cf 100644 --- a/public/images/headshot.jpg +++ b/website/public/images/headshot.jpg diff --git a/public/images/headshot_large.jpg b/website/public/images/headshot_large.jpg Binary files differindex 2ca13cf..2ca13cf 100644 --- a/public/images/headshot_large.jpg +++ b/website/public/images/headshot_large.jpg diff --git a/src/components/BaseHead.astro b/website/src/components/BaseHead.astro index e8e44ab..e8e44ab 100644 --- a/src/components/BaseHead.astro +++ b/website/src/components/BaseHead.astro diff --git a/src/components/BlogFeed.astro b/website/src/components/BlogFeed.astro index b6f9d55..b6f9d55 100644 --- a/src/components/BlogFeed.astro +++ b/website/src/components/BlogFeed.astro diff --git a/src/components/FormattedDate.astro b/website/src/components/FormattedDate.astro index bb7a2c0..bb7a2c0 100644 --- a/src/components/FormattedDate.astro +++ b/website/src/components/FormattedDate.astro diff --git a/src/components/Me.astro b/website/src/components/Me.astro index c9af587..c9af587 100644 --- a/src/components/Me.astro +++ b/website/src/components/Me.astro diff --git a/src/consts.ts b/website/src/consts.ts index 461043d..461043d 100644 --- a/src/consts.ts +++ b/website/src/consts.ts diff --git a/src/content/blog/2024/01/14/sapiens_on_religion.md b/website/src/content/blog/2024/01/14/sapiens_on_religion.md index b484557..b484557 100644 --- a/src/content/blog/2024/01/14/sapiens_on_religion.md +++ b/website/src/content/blog/2024/01/14/sapiens_on_religion.md diff --git a/src/content/blog/2024/01/29/euhwc_toast_to_the_lasses_2024.md b/website/src/content/blog/2024/01/29/euhwc_toast_to_the_lasses_2024.md index 8641a5d..8641a5d 100644 --- a/src/content/blog/2024/01/29/euhwc_toast_to_the_lasses_2024.md +++ b/website/src/content/blog/2024/01/29/euhwc_toast_to_the_lasses_2024.md diff --git a/src/content/blog/2024/03/30/easter.md b/website/src/content/blog/2024/03/30/easter.md index 9735220..9735220 100644 --- a/src/content/blog/2024/03/30/easter.md +++ b/website/src/content/blog/2024/03/30/easter.md diff --git a/src/content/config.ts b/website/src/content/config.ts index 58dbc00..58dbc00 100644 --- a/src/content/config.ts +++ b/website/src/content/config.ts diff --git a/src/env.d.ts b/website/src/env.d.ts index acef35f..acef35f 100644 --- a/src/env.d.ts +++ b/website/src/env.d.ts diff --git a/src/layouts/BlogPost.astro b/website/src/layouts/BlogPost.astro index b709356..b709356 100644 --- a/src/layouts/BlogPost.astro +++ b/website/src/layouts/BlogPost.astro diff --git a/src/layouts/Page.astro b/website/src/layouts/Page.astro index 4e5f3bb..4e5f3bb 100644 --- a/src/layouts/Page.astro +++ b/website/src/layouts/Page.astro diff --git a/src/pages/blog/[...slug].astro b/website/src/pages/blog/[...slug].astro index 800c534..800c534 100644 --- a/src/pages/blog/[...slug].astro +++ b/website/src/pages/blog/[...slug].astro diff --git a/src/pages/blog/index.astro b/website/src/pages/blog/index.astro index 0c617bd..0c617bd 100644 --- a/src/pages/blog/index.astro +++ b/website/src/pages/blog/index.astro diff --git a/src/pages/error.astro b/website/src/pages/error.astro index fc84122..fc84122 100644 --- a/src/pages/error.astro +++ b/website/src/pages/error.astro diff --git a/src/pages/index.astro b/website/src/pages/index.astro index 74cf934..74cf934 100644 --- a/src/pages/index.astro +++ b/website/src/pages/index.astro diff --git a/src/pages/rss.xml.js b/website/src/pages/rss.xml.js index 79da596..79da596 100644 --- a/src/pages/rss.xml.js +++ b/website/src/pages/rss.xml.js diff --git a/src/content/blog/2024/04/10/tracking_pixels.md b/website/src/src/content/blog/2024/04/10/tracking_pixels.md index 15ccc55..15ccc55 100644 --- a/src/content/blog/2024/04/10/tracking_pixels.md +++ b/website/src/src/content/blog/2024/04/10/tracking_pixels.md diff --git a/tsconfig.json b/website/tsconfig.json index 41bbfbf..41bbfbf 100644 --- a/tsconfig.json +++ b/website/tsconfig.json |
