This commit is contained in:
whilb 2025-08-16 17:54:12 -07:00
commit 97f9a95415
21 changed files with 2963 additions and 0 deletions

45
infra/backend.tf Normal file
View file

@ -0,0 +1,45 @@
# Backend infrastructure for Terraform state management
# This should be run first, before the main configuration
resource "aws_s3_bucket" "terraform_state" {
bucket = "calculator-127local-net-terraform-state"
tags = var.tags
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_state_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = var.tags
}

171
infra/main.tf Normal file
View file

@ -0,0 +1,171 @@
locals {
origin_id = "s3-calculator-origin"
bucket_name = replace(var.primary_domain, ".", "-")
}
resource "aws_s3_bucket" "site" {
bucket = local.bucket_name
tags = var.tags
}
resource "aws_s3_bucket_ownership_controls" "site" {
bucket = aws_s3_bucket.site.id
rule { object_ownership = "BucketOwnerPreferred" }
}
resource "aws_s3_bucket_public_access_block" "site" {
bucket = aws_s3_bucket.site.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_policy" "site" {
bucket = aws_s3_bucket.site.id
policy = data.aws_iam_policy_document.s3_policy.json
}
data "aws_caller_identity" "current" {}
data "aws_iam_policy_document" "s3_policy" {
statement {
sid = "AllowCloudFrontRead"
effect = "Allow"
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.site.arn}/*"]
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [aws_cloudfront_distribution.site.arn]
}
}
}
# CloudFront OAC (origin access control)
resource "aws_cloudfront_origin_access_control" "oac" {
name = "${local.bucket_name}-oac"
description = "OAC for ${local.bucket_name}"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
# ACM certificate in us-east-1 for both domains
resource "aws_acm_certificate" "cert" {
provider = aws.us_east_1
domain_name = var.primary_domain
subject_alternative_names = [var.secondary_domain]
validation_method = "DNS"
tags = var.tags
}
resource "aws_route53_record" "cert_validation" {
for_each = { for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
type = dvo.resource_record_type
value = dvo.resource_record_value
} }
zone_id = data.aws_route53_zone.main.zone_id
name = each.value.name
type = each.value.type
ttl = 60
records = [each.value.value]
}
resource "aws_acm_certificate_validation" "cert" {
provider = aws.us_east_1
certificate_arn = aws_acm_certificate.cert.arn
validation_record_fqdns = [for r in aws_route53_record.cert_validation : r.fqdn]
}
# Get the hosted zone
data "aws_route53_zone" "main" {
name = var.hosted_zone
}
# CloudFront distribution
resource "aws_cloudfront_distribution" "site" {
enabled = true
is_ipv6_enabled = true
comment = "Calculator site for ${var.primary_domain} and ${var.secondary_domain}"
default_root_object = "index.html"
aliases = [var.primary_domain, var.secondary_domain]
origin {
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
origin_id = local.origin_id
origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.origin_id
viewer_protocol_policy = "redirect-to-https"
compress = true
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate_validation.cert.certificate_arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
custom_error_response {
error_code = 403
response_code = 200
response_page_path = "/index.html"
}
custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
}
tags = var.tags
}
# Route53 alias for primary domain
resource "aws_route53_record" "primary_alias" {
zone_id = data.aws_route53_zone.main.zone_id
name = var.primary_domain
type = "A"
alias {
name = aws_cloudfront_distribution.site.domain_name
zone_id = aws_cloudfront_distribution.site.hosted_zone_id
evaluate_target_health = false
}
}
# Route53 alias for secondary domain
resource "aws_route53_record" "secondary_alias" {
zone_id = data.aws_route53_zone.main.zone_id
name = var.secondary_domain
type = "A"
alias {
name = aws_cloudfront_distribution.site.domain_name
zone_id = aws_cloudfront_distribution.site.hosted_zone_id
evaluate_target_health = false
}
}
output "bucket_name" { value = aws_s3_bucket.site.bucket }
output "distribution_id" { value = aws_cloudfront_distribution.site.id }
output "primary_domain" { value = var.primary_domain }
output "secondary_domain" { value = var.secondary_domain }
output "cloudfront_domain" { value = aws_cloudfront_distribution.site.domain_name }

4
infra/terraform.tfvars Normal file
View file

@ -0,0 +1,4 @@
primary_domain = "calculator.127local.net"
secondary_domain = "calc.127local.net"
hosted_zone = "127local.net"
aws_region = "us-west-2"

24
infra/variables.tf Normal file
View file

@ -0,0 +1,24 @@
variable "primary_domain" {
type = string
description = "Primary domain name (e.g., calculator.127local.net)"
}
variable "secondary_domain" {
type = string
description = "Secondary domain name (e.g., calc.127local.net)"
}
variable "hosted_zone" {
type = string
description = "Hosted zone name (e.g., 127local.net)"
}
variable "aws_region" {
type = string
default = "us-west-2"
}
variable "tags" {
type = map(string)
default = {
Application = "calculator.127local"
Project = "calc-127local-net"
}
}

28
infra/versions.tf Normal file
View file

@ -0,0 +1,28 @@
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.55"
}
}
# S3 backend configuration - uncomment after backend infrastructure is created
backend "s3" {
bucket = "calculator-127local-net-terraform-state"
key = "terraform.tfstate"
region = "us-west-2"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
}
# ACM cert for CloudFront must be in us-east-1
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}