summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build-and-deploy.yml46
-rw-r--r--README.md50
-rw-r--r--infrastructure/.terraform.lock.hcl39
-rw-r--r--infrastructure/acm.tf4
-rw-r--r--infrastructure/app.tf53
-rw-r--r--infrastructure/cloudfront.tf59
-rw-r--r--infrastructure/locals.tf4
-rw-r--r--infrastructure/main.tf8
-rw-r--r--infrastructure/providers.tf12
-rw-r--r--infrastructure/route53.tf35
-rw-r--r--infrastructure/s3.tf78
-rw-r--r--infrastructure/variables.tf27
12 files changed, 97 insertions, 318 deletions
diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
deleted file mode 100644
index d2b1374..0000000
--- a/.github/workflows/build-and-deploy.yml
+++ /dev/null
@@ -1,46 +0,0 @@
-name: Build and deploy
-
-on:
- push:
- branches: ["main"]
- workflow_dispatch:
-
-# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
-# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
-concurrency:
- group: "deploy"
- cancel-in-progress: false
-
-jobs:
- build:
- name: Build with Astro and deploy to S3
- runs-on: ubuntu-latest
- 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
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: "20"
- cache: "npm"
- - name: Install dependencies
- run: npm ci
- - name: Build with Astro
- run: npm run build
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v4
- with:
- aws-region: us-east-1
- aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
- aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- - name: Sync S3 bucket
- run: |
- 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 ./website/dist/ "s3://joeac.net-$S3_BUCKET_SUFFIX" --delete
-
diff --git a/README.md b/README.md
index 13f249d..ea434b5 100644
--- a/README.md
+++ b/README.md
@@ -9,39 +9,19 @@ Structure:
## 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
-for CloudFront to serve the new versions before the caches expire (which could
-be a while). Here’s how to do it:
-
-1. Go to the CloudFront console
-2. Select the distribution for this website
-3. Go to the Invalidations tab
-4. Add a new Invalidation
-5. Include all pages you’ve updated
- - Use the relative URL, not the filepath, e.g. "/blog/" not "/blog/index.html"
- - Include the trailing "/" or it won’t work
- - You can use wildcards to make life easier, e.g. "/blog/2024/01/29/*"
+The infrastructure is on DigitalOcean.
+The website is hosted using the App Platform service from DigitalOcean. This is
+free for static websites, and is quite flexible to add in extra apps as Droplets
+or Functions at a later time if I so please.
+
+DigitalOcean App Platform re-deploys the website every time there's an update to
+the `main` branch in this repo.
+
+All the DigitalOcean infrastructure is managed using Terraform. The code for
+this is in the `infrastructure/` directory.
+
+The domain, however, is registered on AWS. The nameservers registered in AWS
+have to be kept manually up-to-date with the DigitalOcean nameservers. These
+shouldn't change, though, so this is unlikely to need intervention more than
+once.
diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/.terraform.lock.hcl
index 8790b90..3d0510b 100644
--- a/infrastructure/.terraform.lock.hcl
+++ b/infrastructure/.terraform.lock.hcl
@@ -1,25 +1,26 @@
# 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"
+provider "registry.terraform.io/digitalocean/digitalocean" {
+ version = "2.38.0"
+ constraints = "~> 2.0"
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",
+ "h1:ElG5GAiHN6paMNUG0JnQ4uCv1Y37ZUbCXQSAQwb/j8U=",
+ "zh:04d1ca6ac6d7e69635657aeac8aadb75f84018305514381f9d7bed48065df61b",
+ "zh:081258f8526b3597eeb7154d5b453c2fe36194ca1a95e0de655d7a1080224be0",
+ "zh:15e77088b8a73012d87ad29ad3cf10392642a6781a35c0923a45386fe61cda61",
+ "zh:1a0c741f9be0f22c18fec93b4200c1968c1c6e4ac02292d494ace78b16f13fea",
+ "zh:2a3a778cd982e5e09a2414f0a16c54101c56d7ab6f824ea3e55709a608f7f3ea",
+ "zh:6832b594a7e408e085bf148c5a1e2eaf94ed8f47796b917acf56078ee9362ca1",
+ "zh:78b8acad99e7035344677f70c08c3daf457cfa3f8b467e73352d14353889b8cf",
+ "zh:8a1c446f1b3b5dee097fc000cf4d341b00602ace60246af4a09e5a49666a0638",
+ "zh:8c23094d06f5b4c780dbdde21c56b9b03b275c1cc8728669ef8f3f45e0d9fb24",
+ "zh:8c5a9871c2a2e094f58723624bb3fd2dbedafc8d9cab609df79d01011de79b8a",
+ "zh:a32adff1d0fc405f1596b6dc679b3029c87177e717eb991c418fff13ec559427",
+ "zh:a3f89176401db06ccc97b89a23bb004c8ab1562ce04178d6a08125bd4b8c073e",
+ "zh:d739de876cdd6176570c610e0edca0d2133b8e21787f22caa9a72d9806055c07",
+ "zh:d8a874a0e442c10deae3217bb88375209d45f79d52cb7d2b19756863d6ab6414",
+ "zh:eac47cd0b28953404622d11d8d4049ce2a4da3bc0a0ecaf386e5f55117386bbd",
+ "zh:ec3686163d1177d41f13588bb24db9a2b3afd199ac410d8c9899f053f271515c",
]
}
diff --git a/infrastructure/acm.tf b/infrastructure/acm.tf
deleted file mode 100644
index f07a0eb..0000000
--- a/infrastructure/acm.tf
+++ /dev/null
@@ -1,4 +0,0 @@
-data "aws_acm_certificate" "joeac_ssl_certificate" {
- domain = local.domain
- statuses = ["ISSUED"]
-}
diff --git a/infrastructure/app.tf b/infrastructure/app.tf
new file mode 100644
index 0000000..24ec010
--- /dev/null
+++ b/infrastructure/app.tf
@@ -0,0 +1,53 @@
+resource "digitalocean_app" "joeac_net" {
+ project_id = "c106269c-1115-4682-8757-867368e057a4"
+
+ spec {
+ name = "joeac-net"
+ region = local.region
+ domain {
+ name = local.domain
+ }
+ features = [
+ "buildpack-stack=ubuntu-22"
+ ]
+
+ alert {
+ disabled = false
+ rule = "DEPLOYMENT_FAILED"
+ }
+
+ alert {
+ disabled = false
+ rule = "DOMAIN_FAILED"
+ }
+
+ ingress {
+ rule {
+ component {
+ name = "personal-website"
+ preserve_path_prefix = false
+ }
+
+ match {
+ path {
+ prefix = "/"
+ }
+ }
+ }
+ }
+
+ static_site {
+ build_command = "npm run build"
+ environment_slug = "node-js"
+ name = "personal-website"
+ output_dir = "website/dist"
+ source_dir = "/"
+
+ github {
+ branch = "main"
+ deploy_on_push = true
+ repo = "joeacarstairs/personal-website"
+ }
+ }
+ }
+}
diff --git a/infrastructure/cloudfront.tf b/infrastructure/cloudfront.tf
deleted file mode 100644
index 4cdc80c..0000000
--- a/infrastructure/cloudfront.tf
+++ /dev/null
@@ -1,59 +0,0 @@
-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
index d4fd3bf..d051e43 100644
--- a/infrastructure/locals.tf
+++ b/infrastructure/locals.tf
@@ -1,5 +1,5 @@
locals {
- aws_region = "us-east-1"
- domain = "joeac.net"
+ domain = "joeac.net"
+ region = "lon"
}
diff --git a/infrastructure/main.tf b/infrastructure/main.tf
index 03b8568..a1aa568 100644
--- a/infrastructure/main.tf
+++ b/infrastructure/main.tf
@@ -1,11 +1,9 @@
terraform {
required_providers {
- aws = {
- source = "hashicorp/aws"
- version = "~> 4.16"
+ digitalocean = {
+ source = "digitalocean/digitalocean"
+ version = "~> 2.0"
}
}
-
- required_version = ">= 1.2.0"
}
diff --git a/infrastructure/providers.tf b/infrastructure/providers.tf
index 8e06505..5b0e240 100644
--- a/infrastructure/providers.tf
+++ b/infrastructure/providers.tf
@@ -1,12 +1,4 @@
-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"
- }
- }
+provider "digitalocean" {
+ token = var.do_token
}
diff --git a/infrastructure/route53.tf b/infrastructure/route53.tf
deleted file mode 100644
index 828cec2..0000000
--- a/infrastructure/route53.tf
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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
deleted file mode 100644
index 7b116a7..0000000
--- a/infrastructure/s3.tf
+++ /dev/null
@@ -1,78 +0,0 @@
-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
index 224ada4..9af60f9 100644
--- a/infrastructure/variables.tf
+++ b/infrastructure/variables.tf
@@ -1,28 +1,5 @@
-variable "aws_access_key" {
+variable "do_token" {
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"
- }
+ description = "A DigitalOcean access token. Can also be provided as an environment variable"
}