commit 990c8971fbea6d07da877cb5dd78f959ee22b853 Author: greg Date: Sun Dec 21 18:58:29 2025 -0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..748945f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.terraform/ +*.tfstate +*.tfstate.backup diff --git a/content/images/aws.svg b/content/images/aws.svg new file mode 100644 index 0000000..39f9762 --- /dev/null +++ b/content/images/aws.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/content/images/favico.png b/content/images/favico.png new file mode 100644 index 0000000..456d85f Binary files /dev/null and b/content/images/favico.png differ diff --git a/content/images/gcp.svg b/content/images/gcp.svg new file mode 100644 index 0000000..566f249 --- /dev/null +++ b/content/images/gcp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/images/kubernetes.svg b/content/images/kubernetes.svg new file mode 100644 index 0000000..9a61c19 --- /dev/null +++ b/content/images/kubernetes.svg @@ -0,0 +1,91 @@ + + + + + Kubernetes logo with no border + + + + + + image/svg+xml + + Kubernetes logo with no border + "kubectl" is pronounced "kyoob kuttel" + + + + + + + + + + diff --git a/content/images/profile.png b/content/images/profile.png new file mode 100644 index 0000000..2d48e15 Binary files /dev/null and b/content/images/profile.png differ 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/main.tf b/terraform/main.tf new file mode 100644 index 0000000..3a1bc58 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,216 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + bucket = "gregh-terraform-state" + key = "cdn-gregh-dev/terraform.tfstate" + region = "us-east-1" + encrypt = true + use_lockfile = true + } +} + +provider "aws" { + region = "us-east-1" + profile = "production" +} + +# S3 bucket for CDN content +resource "aws_s3_bucket" "cdn" { + bucket = "cdn.cloud.gregh.dev" + + tags = { + Name = "cdn.cloud.gregh.dev" + Environment = "production" + } +} + +resource "aws_s3_bucket_public_access_block" "cdn" { + bucket = aws_s3_bucket.cdn.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_versioning" "cdn" { + bucket = aws_s3_bucket.cdn.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_cors_configuration" "cdn" { + bucket = aws_s3_bucket.cdn.id + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET", "HEAD"] + allowed_origins = ["*"] + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} + +# CloudFront Origin Access Control +resource "aws_cloudfront_origin_access_control" "cdn" { + name = "cdn-gregh-dev-oac" + description = "OAC for cdn.cloud.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" "cdn" { + bucket = aws_s3_bucket.cdn.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowCloudFrontServicePrincipal" + Effect = "Allow" + Principal = { + Service = "cloudfront.amazonaws.com" + } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.cdn.arn}/*" + Condition = { + StringEquals = { + "AWS:SourceArn" = aws_cloudfront_distribution.cdn.arn + } + } + } + ] + }) +} + +# ACM Certificate for CloudFront +resource "aws_acm_certificate" "cdn" { + domain_name = "cdn.cloud.gregh.dev" + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "cdn.cloud.gregh.dev" + } +} + +# Data source for existing hosted zone +data "aws_route53_zone" "main" { + name = "gregh.dev." +} + +# Route53 records for ACM validation +resource "aws_route53_record" "cert_validation" { + for_each = { + for dvo in aws_acm_certificate.cdn.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 = data.aws_route53_zone.main.zone_id +} + +# ACM certificate validation +resource "aws_acm_certificate_validation" "cdn" { + certificate_arn = aws_acm_certificate.cdn.arn + validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] +} + +# CloudFront distribution +resource "aws_cloudfront_distribution" "cdn" { + enabled = true + is_ipv6_enabled = true + comment = "cdn.cloud.gregh.dev content" + price_class = "PriceClass_100" + aliases = ["cdn.cloud.gregh.dev"] + + origin { + domain_name = aws_s3_bucket.cdn.bucket_regional_domain_name + origin_id = "S3-cdn.cloud.gregh.dev" + origin_access_control_id = aws_cloudfront_origin_access_control.cdn.id + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-cdn.cloud.gregh.dev" + viewer_protocol_policy = "redirect-to-https" + compress = true + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + min_ttl = 0 + default_ttl = 86400 + max_ttl = 31536000 + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate.cdn.arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" + } + + tags = { + Name = "cdn.cloud.gregh.dev" + Environment = "production" + } +} + +# Route53 A record for CDN subdomain +resource "aws_route53_record" "cdn" { + zone_id = data.aws_route53_zone.main.zone_id + name = "cdn.cloud.gregh.dev" + type = "A" + + alias { + name = aws_cloudfront_distribution.cdn.domain_name + zone_id = aws_cloudfront_distribution.cdn.hosted_zone_id + evaluate_target_health = false + } +} + +# Outputs +output "s3_bucket_name" { + value = aws_s3_bucket.cdn.id +} + +output "cloudfront_distribution_id" { + value = aws_cloudfront_distribution.cdn.id +} + +output "cdn_url" { + value = "https://${aws_route53_record.cdn.name}" +}