HelmChart values schema changed in newer Traefik versions causing installation failures. Replaced with direct Deployment + RBAC manifests which work reliably with Traefik v3.2. Also adds SSH public key variable for admin access. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
397 lines
8.8 KiB
HCL
397 lines
8.8 KiB
HCL
# 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
|
|
}
|
|
|
|
# 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] # Don't replace on AMI updates
|
|
}
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 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}"
|
|
}
|