hearth/terraform/modules/eks/main.tf
Eric Garcia e78000831e Initial commit: Port infrastructure from coherence-mcp
Hearth is the infrastructure home for the letemcook ecosystem.

Ported from coherence-mcp/infra:
- Terraform modules (VPC, EKS, IAM, NLB, S3, storage)
- Kubernetes manifests (Forgejo, ingress, cert-manager, karpenter)
- Deployment scripts (phased rollout)

Status: Not deployed. EKS cluster needs to be provisioned.

Next steps:
1. Bootstrap terraform backend
2. Deploy phase 1 (foundation)
3. Deploy phase 2 (core services including Forgejo)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 06:06:13 -05:00

398 lines
11 KiB
HCL

# EKS Module - Kubernetes Cluster with Karpenter
# RFC 0039: ADR-Compliant Foundation Infrastructure
# ADR 0004: "Set It and Forget It" Auto-Scaling Architecture
#
# Architecture:
# - EKS control plane (~$73/mo)
# - Fargate profile for Karpenter (chicken-egg problem)
# - Karpenter for auto-scaling nodes
# - No managed node groups (Karpenter handles all compute)
# - FIPS-compliant node configuration
locals {
cluster_name = var.cluster_name
}
# EKS Cluster
resource "aws_eks_cluster" "main" {
name = local.cluster_name
version = var.cluster_version
role_arn = aws_iam_role.cluster.arn
vpc_config {
subnet_ids = concat(var.private_subnet_ids, var.public_subnet_ids)
endpoint_private_access = true
endpoint_public_access = var.enable_public_access
public_access_cidrs = var.public_access_cidrs
security_group_ids = [aws_security_group.cluster.id]
}
# Enable all log types for security compliance
enabled_cluster_log_types = [
"api",
"audit",
"authenticator",
"controllerManager",
"scheduler"
]
# Encryption configuration for secrets
encryption_config {
provider {
key_arn = aws_kms_key.eks.arn
}
resources = ["secrets"]
}
tags = merge(var.tags, {
Name = local.cluster_name
})
depends_on = [
aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy,
aws_iam_role_policy_attachment.cluster_AmazonEKSVPCResourceController,
aws_cloudwatch_log_group.cluster,
]
}
# CloudWatch Log Group for EKS
resource "aws_cloudwatch_log_group" "cluster" {
name = "/aws/eks/${local.cluster_name}/cluster"
retention_in_days = 30
tags = var.tags
}
# KMS Key for EKS secrets encryption
resource "aws_kms_key" "eks" {
description = "EKS cluster secrets encryption key"
deletion_window_in_days = 7
enable_key_rotation = true
tags = merge(var.tags, {
Name = "${local.cluster_name}-eks-secrets"
})
}
resource "aws_kms_alias" "eks" {
name = "alias/${local.cluster_name}-eks-secrets"
target_key_id = aws_kms_key.eks.key_id
}
# EKS Cluster IAM Role
resource "aws_iam_role" "cluster" {
name = "${local.cluster_name}-cluster"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "eks.amazonaws.com"
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSClusterPolicy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
role = aws_iam_role.cluster.name
}
resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSVPCResourceController" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
role = aws_iam_role.cluster.name
}
# Cluster Security Group
resource "aws_security_group" "cluster" {
name = "${local.cluster_name}-cluster"
description = "EKS cluster security group"
vpc_id = var.vpc_id
tags = merge(var.tags, {
Name = "${local.cluster_name}-cluster"
"kubernetes.io/cluster/${local.cluster_name}" = "owned"
})
}
resource "aws_security_group_rule" "cluster_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.cluster.id
description = "Allow all outbound traffic"
}
# Node Security Group (for Karpenter-managed nodes)
resource "aws_security_group" "node" {
name = "${local.cluster_name}-node"
description = "Security group for EKS nodes"
vpc_id = var.vpc_id
tags = merge(var.tags, {
Name = "${local.cluster_name}-node"
"kubernetes.io/cluster/${local.cluster_name}" = "owned"
"karpenter.sh/discovery" = local.cluster_name
})
}
resource "aws_security_group_rule" "node_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.node.id
description = "Allow all outbound traffic"
}
resource "aws_security_group_rule" "node_ingress_self" {
type = "ingress"
from_port = 0
to_port = 65535
protocol = "-1"
source_security_group_id = aws_security_group.node.id
security_group_id = aws_security_group.node.id
description = "Allow nodes to communicate with each other"
}
resource "aws_security_group_rule" "node_ingress_cluster" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
source_security_group_id = aws_security_group.cluster.id
security_group_id = aws_security_group.node.id
description = "Allow cluster API to communicate with nodes"
}
resource "aws_security_group_rule" "node_ingress_cluster_kubelet" {
type = "ingress"
from_port = 10250
to_port = 10250
protocol = "tcp"
source_security_group_id = aws_security_group.cluster.id
security_group_id = aws_security_group.node.id
description = "Allow cluster API to communicate with kubelet"
}
resource "aws_security_group_rule" "cluster_ingress_node" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
source_security_group_id = aws_security_group.node.id
security_group_id = aws_security_group.cluster.id
description = "Allow nodes to communicate with cluster API"
}
# Fargate Profile for Karpenter
# Karpenter needs to run on Fargate to avoid chicken-egg problem
resource "aws_eks_fargate_profile" "karpenter" {
count = var.enable_karpenter ? 1 : 0
cluster_name = aws_eks_cluster.main.name
fargate_profile_name = "karpenter"
pod_execution_role_arn = aws_iam_role.fargate.arn
subnet_ids = var.private_subnet_ids
selector {
namespace = "karpenter"
}
tags = var.tags
}
# Fargate Profile for kube-system (CoreDNS, etc)
resource "aws_eks_fargate_profile" "kube_system" {
cluster_name = aws_eks_cluster.main.name
fargate_profile_name = "kube-system"
pod_execution_role_arn = aws_iam_role.fargate.arn
subnet_ids = var.private_subnet_ids
selector {
namespace = "kube-system"
}
tags = var.tags
}
# Fargate IAM Role
resource "aws_iam_role" "fargate" {
name = "${local.cluster_name}-fargate"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "eks-fargate-pods.amazonaws.com"
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "fargate_AmazonEKSFargatePodExecutionRolePolicy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy"
role = aws_iam_role.fargate.name
}
# OIDC Provider for IRSA
data "tls_certificate" "cluster" {
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
}
resource "aws_iam_openid_connect_provider" "cluster" {
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.cluster.certificates[0].sha1_fingerprint]
url = aws_eks_cluster.main.identity[0].oidc[0].issuer
tags = var.tags
}
# Node IAM Role (for Karpenter-managed nodes)
resource "aws_iam_role" "node" {
name = "${local.cluster_name}-node"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "node_AmazonEKSWorkerNodePolicy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
role = aws_iam_role.node.name
}
resource "aws_iam_role_policy_attachment" "node_AmazonEKS_CNI_Policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
role = aws_iam_role.node.name
}
resource "aws_iam_role_policy_attachment" "node_AmazonEC2ContainerRegistryReadOnly" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
role = aws_iam_role.node.name
}
resource "aws_iam_role_policy_attachment" "node_AmazonSSMManagedInstanceCore" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.node.name
}
resource "aws_iam_instance_profile" "node" {
name = "${local.cluster_name}-node"
role = aws_iam_role.node.name
tags = var.tags
}
# EKS Addons
resource "aws_eks_addon" "vpc_cni" {
cluster_name = aws_eks_cluster.main.name
addon_name = "vpc-cni"
addon_version = var.vpc_cni_version
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "OVERWRITE"
tags = var.tags
}
resource "aws_eks_addon" "coredns" {
cluster_name = aws_eks_cluster.main.name
addon_name = "coredns"
addon_version = var.coredns_version
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "OVERWRITE"
# CoreDNS needs compute to run
depends_on = [aws_eks_fargate_profile.kube_system]
tags = var.tags
}
resource "aws_eks_addon" "kube_proxy" {
cluster_name = aws_eks_cluster.main.name
addon_name = "kube-proxy"
addon_version = var.kube_proxy_version
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "OVERWRITE"
tags = var.tags
}
resource "aws_eks_addon" "ebs_csi_driver" {
cluster_name = aws_eks_cluster.main.name
addon_name = "aws-ebs-csi-driver"
addon_version = var.ebs_csi_version
service_account_role_arn = var.ebs_csi_role_arn
resolve_conflicts_on_create = "OVERWRITE"
resolve_conflicts_on_update = "OVERWRITE"
tags = var.tags
}
# aws-auth ConfigMap for Karpenter nodes
resource "kubernetes_config_map_v1_data" "aws_auth" {
metadata {
name = "aws-auth"
namespace = "kube-system"
}
data = {
mapRoles = yamlencode([
{
rolearn = aws_iam_role.node.arn
username = "system:node:{{EC2PrivateDNSName}}"
groups = [
"system:bootstrappers",
"system:nodes",
]
},
{
rolearn = aws_iam_role.fargate.arn
username = "system:node:{{SessionName}}"
groups = [
"system:bootstrappers",
"system:nodes",
"system:node-proxier",
]
}
])
}
force = true
depends_on = [aws_eks_cluster.main]
}
# Data source for current region
data "aws_region" "current" {}
# Data source for current account
data "aws_caller_identity" "current" {}