From 88c6e8a6585c00f0a94e27f1351e77ef18518196 Mon Sep 17 00:00:00 2001 From: Joe Carstairs <65492573+joeacarstairs@users.noreply.github.com> Date: Fri, 10 May 2024 21:57:35 +0100 Subject: [PATCH] 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> --- .github/workflows/build-and-deploy.yml | 46 --------------- README.md | 44 ++++----------- infrastructure/.terraform.lock.hcl | 39 ++++++------- infrastructure/acm.tf | 4 -- infrastructure/app.tf | 53 +++++++++++++++++ infrastructure/cloudfront.tf | 59 ------------------- infrastructure/locals.tf | 4 +- infrastructure/main.tf | 8 +-- infrastructure/providers.tf | 12 +--- infrastructure/route53.tf | 35 ------------ infrastructure/s3.tf | 78 -------------------------- infrastructure/variables.tf | 27 +-------- 12 files changed, 94 insertions(+), 315 deletions(-) delete mode 100644 .github/workflows/build-and-deploy.yml delete mode 100644 infrastructure/acm.tf create mode 100644 infrastructure/app.tf delete mode 100644 infrastructure/cloudfront.tf delete mode 100644 infrastructure/route53.tf delete mode 100644 infrastructure/s3.tf 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: +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, 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/*" +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" }