From a9db14a036b8559710caa7b35033b48821dddf0a Mon Sep 17 00:00:00 2001 From: Greg Hendrickson Date: Sat, 20 Dec 2025 11:49:58 -0800 Subject: [PATCH] feat: Migrate to AWS CloudFront and update resume (#11) * variables -> env * update * mv to vars * update year in experience * mv to src folder * remove old files * Migrate to AWS CloudFront and update resume --- .github/workflows/deployment-aws.yaml | 31 +++ .gitignore | 9 + src/index.html | 15 +- terraform/.terraform.lock.hcl | 25 +++ terraform/github-role-policy.json | 27 +++ terraform/github-role-trust-policy.json | 20 ++ terraform/main.tf | 254 ++++++++++++++++++++++++ 7 files changed, 374 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/deployment-aws.yaml create mode 100644 .gitignore create mode 100644 terraform/.terraform.lock.hcl create mode 100644 terraform/github-role-policy.json create mode 100644 terraform/github-role-trust-policy.json create mode 100644 terraform/main.tf diff --git a/.github/workflows/deployment-aws.yaml b/.github/workflows/deployment-aws.yaml new file mode 100644 index 0000000..4bacc16 --- /dev/null +++ b/.github/workflows/deployment-aws.yaml @@ -0,0 +1,31 @@ +run-name: "☁️ AWS › Deploy" +name: "☁️ AWS › Deploy" + +on: [push] + +permissions: + id-token: write + contents: read + +jobs: + job-publish: + name: "📦 Publish to AWS S3/CloudFront" + runs-on: ubuntu-latest + steps: + - name: "Checkout" + id: task_publish_checkout + uses: actions/checkout@v4 + + - name: "Configure AWS Credentials" + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: us-east-1 + + - name: "Sync to S3" + run: | + aws s3 sync src/ s3://gregh.dev/ --delete --cache-control "public, max-age=3600" + + - name: "Invalidate CloudFront Cache" + run: | + aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd73d39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +*.pdf +*.iso +*.dmg +*.tar +truenas-*/ +.terraform/ +*.tfstate +*.tfstate.backup diff --git a/src/index.html b/src/index.html index efe3904..c0f8c3d 100644 --- a/src/index.html +++ b/src/index.html @@ -95,6 +95,7 @@

Feb 2024 - Present

@@ -126,8 +127,8 @@ @@ -147,10 +148,10 @@

Infrastructure

@@ -194,7 +195,7 @@

Kubernetes Platform Engineering

Architected and implemented a EKS-based and EC2 Kubernetes Deployments integrated with GitOps workflows ArgoCD, - cloudflare waf, and automated scaling policies. Reduced deployment time by 70%. + AWS WAF, and automated scaling policies. Reduced deployment time by 70%.

diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..cdc1668 --- /dev/null +++ b/terraform/.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 = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/github-role-policy.json b/terraform/github-role-policy.json new file mode 100644 index 0000000..bd01e58 --- /dev/null +++ b/terraform/github-role-policy.json @@ -0,0 +1,27 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::gregh.dev", + "arn:aws:s3:::gregh.dev/*" + ] + }, + { + "Effect": "Allow", + "Action": [ + "cloudfront:CreateInvalidation", + "cloudfront:GetInvalidation", + "cloudfront:ListInvalidations" + ], + "Resource": "arn:aws:cloudfront::471112517070:distribution/ERJQO1UEJ8IM7" + } + ] +} diff --git a/terraform/github-role-trust-policy.json b/terraform/github-role-trust-policy.json new file mode 100644 index 0000000..83b256f --- /dev/null +++ b/terraform/github-role-trust-policy.json @@ -0,0 +1,20 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::471112517070:oidc-provider/token.actions.githubusercontent.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" + }, + "StringLike": { + "token.actions.githubusercontent.com:sub": "repo:ghndrx/gregh.dev:*" + } + } + } + ] +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..231d0c1 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,254 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + bucket = "gregh-terraform-state" + key = "gregh-dev/terraform.tfstate" + region = "us-east-1" + encrypt = true + dynamodb_table = "gregh-terraform-locks" + } +} + +provider "aws" { + region = "us-east-1" + profile = "production" +} + +# S3 bucket for static website +resource "aws_s3_bucket" "website" { + bucket = "gregh.dev" + + tags = { + Name = "gregh.dev" + Environment = "production" + } +} + +resource "aws_s3_bucket_public_access_block" "website" { + bucket = aws_s3_bucket.website.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_versioning" "website" { + bucket = aws_s3_bucket.website.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "website" { + bucket = aws_s3_bucket.website.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +# CloudFront Origin Access Control +resource "aws_cloudfront_origin_access_control" "website" { + name = "gregh-dev-oac" + description = "OAC for gregh.dev" + origin_access_control_origin_type = "s3" + signing_behavior = "always" + signing_protocol = "sigv4" +} + +# S3 bucket policy to allow CloudFront access +resource "aws_s3_bucket_policy" "website" { + bucket = aws_s3_bucket.website.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowCloudFrontServicePrincipal" + Effect = "Allow" + Principal = { + Service = "cloudfront.amazonaws.com" + } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.website.arn}/*" + Condition = { + StringEquals = { + "AWS:SourceArn" = aws_cloudfront_distribution.website.arn + } + } + } + ] + }) +} + +# ACM Certificate for CloudFront (must be in us-east-1) +resource "aws_acm_certificate" "website" { + domain_name = "gregh.dev" + subject_alternative_names = ["www.gregh.dev"] + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "gregh.dev" + } +} + +# CloudFront distribution +resource "aws_cloudfront_distribution" "website" { + enabled = true + is_ipv6_enabled = true + comment = "gregh.dev static website" + default_root_object = "index.html" + price_class = "PriceClass_100" + aliases = ["gregh.dev", "www.gregh.dev"] + + origin { + domain_name = aws_s3_bucket.website.bucket_regional_domain_name + origin_id = "S3-gregh.dev" + origin_access_control_id = aws_cloudfront_origin_access_control.website.id + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-gregh.dev" + viewer_protocol_policy = "redirect-to-https" + compress = true + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + min_ttl = 0 + default_ttl = 3600 + max_ttl = 86400 + } + + custom_error_response { + error_code = 404 + response_code = 404 + response_page_path = "/index.html" + } + + custom_error_response { + error_code = 403 + response_code = 404 + response_page_path = "/index.html" + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate.website.arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" + } + + tags = { + Name = "gregh.dev" + Environment = "production" + } +} + +# Route53 Hosted Zone +resource "aws_route53_zone" "website" { + name = "gregh.dev" + + tags = { + Name = "gregh.dev" + Environment = "production" + } +} + +# Route53 records for ACM validation +resource "aws_route53_record" "cert_validation" { + for_each = { + for dvo in aws_acm_certificate.website.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = aws_route53_zone.website.zone_id +} + +# ACM certificate validation +resource "aws_acm_certificate_validation" "website" { + certificate_arn = aws_acm_certificate.website.arn + validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] +} + +# Route53 A record for apex domain +resource "aws_route53_record" "website_apex" { + zone_id = aws_route53_zone.website.zone_id + name = "gregh.dev" + type = "A" + + alias { + name = aws_cloudfront_distribution.website.domain_name + zone_id = aws_cloudfront_distribution.website.hosted_zone_id + evaluate_target_health = false + } +} + +# Route53 A record for www subdomain +resource "aws_route53_record" "website_www" { + zone_id = aws_route53_zone.website.zone_id + name = "www.gregh.dev" + type = "A" + + alias { + name = aws_cloudfront_distribution.website.domain_name + zone_id = aws_cloudfront_distribution.website.hosted_zone_id + evaluate_target_health = false + } +} + +# Outputs +output "nameservers" { + value = aws_route53_zone.website.name_servers + description = "Route53 nameservers - update these at your domain registrar" +} + +output "s3_bucket_name" { + value = aws_s3_bucket.website.id + description = "Name of the S3 bucket" +} + +output "cloudfront_distribution_id" { + value = aws_cloudfront_distribution.website.id + description = "CloudFront distribution ID" +} + +output "cloudfront_domain_name" { + value = aws_cloudfront_distribution.website.domain_name + description = "CloudFront distribution domain name" +}