Moves to DigitalOcean from AWS (#4)

* Moves to DigitalOcean from AWS

* README

* Removes deployment workflow

---------

Co-authored-by: Joe Carstairs <65492573+Sycamost@users.noreply.github.com>
This commit is contained in:
Joe Carstairs
2024-05-10 21:57:35 +01:00
committed by GitHub
parent efb931b1b8
commit 88c6e8a658
12 changed files with 94 additions and 315 deletions

View File

@@ -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

View File

@@ -9,39 +9,19 @@ Structure:
## Infrastructure
The infrastructure has these components:
The infrastructure is on DigitalOcean.
- 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 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.
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.
DigitalOcean App Platform re-deploys the website every time there's an update to
the `main` branch in this repo.
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, youll need to invalidate the CloudFront cache in order
for CloudFront to serve the new versions before the caches expire (which could
be a while). Heres 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 youve updated
- Use the relative URL, not the filepath, e.g. "/blog/" not "/blog/index.html"
- Include the trailing "/" or it wont work
- You can use wildcards to make life easier, e.g. "/blog/2024/01/29/*"
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.

View File

@@ -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",
]
}

View File

@@ -1,4 +0,0 @@
data "aws_acm_certificate" "joeac_ssl_certificate" {
domain = local.domain
statuses = ["ISSUED"]
}

53
infrastructure/app.tf Normal file
View File

@@ -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"
}
}
}
}

View File

@@ -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"
}

View File

@@ -1,5 +1,5 @@
locals {
aws_region = "us-east-1"
domain = "joeac.net"
domain = "joeac.net"
region = "lon"
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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]
}

View File

@@ -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"
}