# Hearth Minimal Infrastructure # Single EC2 + k3s for ~1 user # Cost: ~$7.50/month terraform { required_version = ">= 1.5.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.30" } } backend "s3" { bucket = "hearth-terraform-state-181640953119" key = "hearth-minimal/terraform.tfstate" region = "us-east-1" dynamodb_table = "hearth-terraform-locks" encrypt = true profile = "hearth" } } provider "aws" { region = var.aws_region profile = "hearth" default_tags { tags = { Project = "hearth" Environment = "minimal" ManagedBy = "terraform" } } } # ----------------------------------------------------------------------------- # Data Sources # ----------------------------------------------------------------------------- data "aws_availability_zones" "available" { state = "available" } data "aws_caller_identity" "current" {} # Amazon Linux 2023 ARM64 (for t4g instances) data "aws_ami" "al2023" { most_recent = true owners = ["amazon"] filter { name = "name" values = ["al2023-ami-*-arm64"] } filter { name = "virtualization-type" values = ["hvm"] } } # ----------------------------------------------------------------------------- # VPC - Minimal single public subnet # ----------------------------------------------------------------------------- resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "hearth-minimal" } } resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id tags = { Name = "hearth-minimal" } } resource "aws_subnet" "public" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = data.aws_availability_zones.available.names[0] map_public_ip_on_launch = true tags = { Name = "hearth-minimal-public" } } resource "aws_route_table" "public" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.main.id } tags = { Name = "hearth-minimal-public" } } resource "aws_route_table_association" "public" { subnet_id = aws_subnet.public.id route_table_id = aws_route_table.public.id } # ----------------------------------------------------------------------------- # Security Group # ----------------------------------------------------------------------------- resource "aws_security_group" "forgejo" { name = "hearth-forgejo" description = "Security group for Forgejo server" vpc_id = aws_vpc.main.id # SSH for Git (Forgejo) ingress { description = "Git SSH" from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # HTTP (redirect to HTTPS) ingress { description = "HTTP" from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # HTTPS ingress { description = "HTTPS" from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # Admin SSH (restricted - update with your IP) ingress { description = "Admin SSH" from_port = 2222 to_port = 2222 protocol = "tcp" cidr_blocks = var.admin_cidr_blocks } # Kubernetes API (for local kubectl, restricted) ingress { description = "Kubernetes API" from_port = 6443 to_port = 6443 protocol = "tcp" cidr_blocks = var.admin_cidr_blocks } # DNS (UDP) - PowerDNS authoritative server ingress { description = "DNS UDP" from_port = 53 to_port = 53 protocol = "udp" cidr_blocks = ["0.0.0.0/0"] } # DNS (TCP) - PowerDNS authoritative server ingress { description = "DNS TCP" from_port = 53 to_port = 53 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } # All outbound egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "hearth-forgejo" } } # ----------------------------------------------------------------------------- # IAM Role for EC2 (S3 backup access) # ----------------------------------------------------------------------------- resource "aws_iam_role" "forgejo" { name = "hearth-forgejo" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } } ] }) } resource "aws_iam_role_policy" "forgejo_backup" { name = "forgejo-backup" role = aws_iam_role.forgejo.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:PutObject", "s3:GetObject", "s3:ListBucket", "s3:DeleteObject" ] Resource = [ aws_s3_bucket.backups.arn, "${aws_s3_bucket.backups.arn}/*" ] }, { Effect = "Allow" Action = [ "ec2:CreateSnapshot", "ec2:DescribeSnapshots", "ec2:DeleteSnapshot" ] Resource = "*" } ] }) } resource "aws_iam_instance_profile" "forgejo" { name = "hearth-forgejo" role = aws_iam_role.forgejo.name } # ----------------------------------------------------------------------------- # S3 Bucket for Backups # ----------------------------------------------------------------------------- resource "aws_s3_bucket" "backups" { bucket = "hearth-backups-${data.aws_caller_identity.current.account_id}" tags = { Name = "hearth-backups" } } resource "aws_s3_bucket_versioning" "backups" { bucket = aws_s3_bucket.backups.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_lifecycle_configuration" "backups" { bucket = aws_s3_bucket.backups.id rule { id = "expire-old-backups" status = "Enabled" filter { prefix = "" } # Keep 60 days of backups expiration { days = 60 } # Move to cheaper storage after 30 days (STANDARD_IA minimum) transition { days = 30 storage_class = "STANDARD_IA" } } } resource "aws_s3_bucket_server_side_encryption_configuration" "backups" { bucket = aws_s3_bucket.backups.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } } # ----------------------------------------------------------------------------- # EC2 Instance # ----------------------------------------------------------------------------- resource "aws_instance" "forgejo" { ami = data.aws_ami.al2023.id instance_type = var.instance_type subnet_id = aws_subnet.public.id iam_instance_profile = aws_iam_instance_profile.forgejo.name vpc_security_group_ids = [aws_security_group.forgejo.id] # Use spot instance for cost savings instance_market_options { market_type = "spot" spot_options { max_price = var.spot_max_price spot_instance_type = "persistent" instance_interruption_behavior = "stop" } } root_block_device { volume_size = var.volume_size volume_type = "gp3" iops = 3000 throughput = 125 delete_on_termination = false # Preserve data on instance termination encrypted = true tags = { Name = "hearth-forgejo-root" } } user_data = base64encode(templatefile("${path.module}/user-data.sh", { domain = var.domain letsencrypt_email = var.letsencrypt_email ssh_port = var.admin_ssh_port s3_bucket = aws_s3_bucket.backups.id ssh_public_key = var.ssh_public_key })) tags = { Name = "hearth-forgejo" } lifecycle { ignore_changes = [ami, user_data] # Don't replace on AMI or user-data changes } } # ----------------------------------------------------------------------------- # Elastic IP (stable DNS) # ----------------------------------------------------------------------------- resource "aws_eip" "forgejo" { instance = aws_instance.forgejo.id domain = "vpc" tags = { Name = "hearth-forgejo" } } # ----------------------------------------------------------------------------- # Outputs # ----------------------------------------------------------------------------- output "instance_id" { description = "EC2 instance ID" value = aws_instance.forgejo.id } output "public_ip" { description = "Elastic IP address" value = aws_eip.forgejo.public_ip } output "ssh_command" { description = "SSH command for admin access" value = "ssh -p ${var.admin_ssh_port} ec2-user@${aws_eip.forgejo.public_ip}" } output "forgejo_url" { description = "Forgejo web URL" value = "https://${var.domain}" } output "git_clone_url" { description = "Git clone URL format" value = "git@${var.domain}:ORG/REPO.git" } output "backup_bucket" { description = "S3 bucket for backups" value = aws_s3_bucket.backups.id } output "dns_record" { description = "DNS A record to create" value = "${var.domain} → ${aws_eip.forgejo.public_ip}" } output "powerdns_deploy_command" { description = "Command to deploy PowerDNS after instance is running" value = <<-EOT # Copy and run deployment script: scp -P ${var.admin_ssh_port} scripts/deploy-powerdns.sh ec2-user@${aws_eip.forgejo.public_ip}: ssh -p ${var.admin_ssh_port} ec2-user@${aws_eip.forgejo.public_ip} 'sudo bash deploy-powerdns.sh ${aws_eip.forgejo.public_ip}' EOT } output "dns_glue_records" { description = "Glue records to configure at GoDaddy after PowerDNS is running" value = <<-EOT For each managed domain, set nameservers and glue records: Nameservers: ns1. ns2. Glue Records: ns1 → ${aws_eip.forgejo.public_ip} ns2 → ${aws_eip.forgejo.public_ip} Domains: superviber.com, muffinlabs.ai, letemcook.com, appbasecamp.com, thanksforborrowing.com, alignment.coop EOT }