hearth/terraform/modules/iam/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

426 lines
11 KiB
HCL

# IAM Module - Roles and IRSA
# RFC 0039: ADR-Compliant Foundation Infrastructure
#
# Creates IAM roles for:
# - Karpenter node provisioning
# - EBS CSI Driver
# - EFS CSI Driver
# - AWS Load Balancer Controller
# - External DNS
# - cert-manager
locals {
oidc_issuer = replace(var.cluster_oidc_issuer_url, "https://", "")
}
# Karpenter Controller IAM Role
resource "aws_iam_role" "karpenter_controller" {
name = "${var.cluster_name}-karpenter-controller"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = var.cluster_oidc_provider_arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_issuer}:aud" = "sts.amazonaws.com"
"${local.oidc_issuer}:sub" = "system:serviceaccount:karpenter:karpenter"
}
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy" "karpenter_controller" {
name = "${var.cluster_name}-karpenter-controller"
role = aws_iam_role.karpenter_controller.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "Karpenter"
Effect = "Allow"
Action = [
"ec2:CreateLaunchTemplate",
"ec2:CreateFleet",
"ec2:CreateTags",
"ec2:DescribeLaunchTemplates",
"ec2:DescribeImages",
"ec2:DescribeInstances",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstanceTypeOfferings",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeSpotPriceHistory",
"ec2:DeleteLaunchTemplate",
"ec2:RunInstances",
"ec2:TerminateInstances",
]
Resource = "*"
},
{
Sid = "PassNodeRole"
Effect = "Allow"
Action = "iam:PassRole"
Resource = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.cluster_name}-node"
},
{
Sid = "EKSClusterEndpointLookup"
Effect = "Allow"
Action = "eks:DescribeCluster"
Resource = "arn:aws:eks:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:cluster/${var.cluster_name}"
},
{
Sid = "KarpenterInterruptionQueue"
Effect = "Allow"
Action = [
"sqs:DeleteMessage",
"sqs:GetQueueUrl",
"sqs:GetQueueAttributes",
"sqs:ReceiveMessage"
]
Resource = aws_sqs_queue.karpenter_interruption.arn
},
{
Sid = "PricingAPI"
Effect = "Allow"
Action = "pricing:GetProducts"
Resource = "*"
},
{
Sid = "SSM"
Effect = "Allow"
Action = "ssm:GetParameter"
Resource = "arn:aws:ssm:${data.aws_region.current.name}::parameter/aws/service/*"
}
]
})
}
# Karpenter Interruption Queue (for Spot instance termination notices)
resource "aws_sqs_queue" "karpenter_interruption" {
name = "${var.cluster_name}-karpenter-interruption"
message_retention_seconds = 300
tags = var.tags
}
resource "aws_sqs_queue_policy" "karpenter_interruption" {
queue_url = aws_sqs_queue.karpenter_interruption.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "EC2InterruptionPolicy"
Effect = "Allow"
Principal = {
Service = [
"events.amazonaws.com",
"sqs.amazonaws.com"
]
}
Action = "sqs:SendMessage"
Resource = aws_sqs_queue.karpenter_interruption.arn
}
]
})
}
# EventBridge rules for Karpenter interruption handling
resource "aws_cloudwatch_event_rule" "spot_interruption" {
name = "${var.cluster_name}-spot-interruption"
description = "Spot instance interruption handler for Karpenter"
event_pattern = jsonencode({
source = ["aws.ec2"]
detail-type = ["EC2 Spot Instance Interruption Warning"]
})
tags = var.tags
}
resource "aws_cloudwatch_event_target" "spot_interruption" {
rule = aws_cloudwatch_event_rule.spot_interruption.name
target_id = "KarpenterInterruptionQueue"
arn = aws_sqs_queue.karpenter_interruption.arn
}
resource "aws_cloudwatch_event_rule" "instance_rebalance" {
name = "${var.cluster_name}-instance-rebalance"
description = "Instance rebalance recommendation for Karpenter"
event_pattern = jsonencode({
source = ["aws.ec2"]
detail-type = ["EC2 Instance Rebalance Recommendation"]
})
tags = var.tags
}
resource "aws_cloudwatch_event_target" "instance_rebalance" {
rule = aws_cloudwatch_event_rule.instance_rebalance.name
target_id = "KarpenterInterruptionQueue"
arn = aws_sqs_queue.karpenter_interruption.arn
}
resource "aws_cloudwatch_event_rule" "instance_state_change" {
name = "${var.cluster_name}-instance-state-change"
description = "Instance state change handler for Karpenter"
event_pattern = jsonencode({
source = ["aws.ec2"]
detail-type = ["EC2 Instance State-change Notification"]
})
tags = var.tags
}
resource "aws_cloudwatch_event_target" "instance_state_change" {
rule = aws_cloudwatch_event_rule.instance_state_change.name
target_id = "KarpenterInterruptionQueue"
arn = aws_sqs_queue.karpenter_interruption.arn
}
# EBS CSI Driver IAM Role
resource "aws_iam_role" "ebs_csi" {
name = "${var.cluster_name}-ebs-csi"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = var.cluster_oidc_provider_arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_issuer}:aud" = "sts.amazonaws.com"
"${local.oidc_issuer}:sub" = "system:serviceaccount:kube-system:ebs-csi-controller-sa"
}
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "ebs_csi" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"
role = aws_iam_role.ebs_csi.name
}
# Additional EBS CSI policy for encryption
resource "aws_iam_role_policy" "ebs_csi_kms" {
name = "${var.cluster_name}-ebs-csi-kms"
role = aws_iam_role.ebs_csi.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"kms:CreateGrant",
"kms:ListGrants",
"kms:RevokeGrant",
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
]
Resource = "*"
}
]
})
}
# EFS CSI Driver IAM Role
resource "aws_iam_role" "efs_csi" {
name = "${var.cluster_name}-efs-csi"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = var.cluster_oidc_provider_arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_issuer}:aud" = "sts.amazonaws.com"
"${local.oidc_issuer}:sub" = "system:serviceaccount:kube-system:efs-csi-controller-sa"
}
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "efs_csi" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy"
role = aws_iam_role.efs_csi.name
}
# AWS Load Balancer Controller IAM Role
resource "aws_iam_role" "aws_load_balancer_controller" {
name = "${var.cluster_name}-aws-load-balancer-controller"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = var.cluster_oidc_provider_arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_issuer}:aud" = "sts.amazonaws.com"
"${local.oidc_issuer}:sub" = "system:serviceaccount:kube-system:aws-load-balancer-controller"
}
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy" "aws_load_balancer_controller" {
name = "${var.cluster_name}-aws-load-balancer-controller"
role = aws_iam_role.aws_load_balancer_controller.id
policy = file("${path.module}/policies/aws-load-balancer-controller.json")
}
# cert-manager IAM Role
resource "aws_iam_role" "cert_manager" {
name = "${var.cluster_name}-cert-manager"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = var.cluster_oidc_provider_arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_issuer}:aud" = "sts.amazonaws.com"
"${local.oidc_issuer}:sub" = "system:serviceaccount:cert-manager:cert-manager"
}
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy" "cert_manager" {
name = "${var.cluster_name}-cert-manager"
role = aws_iam_role.cert_manager.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"route53:GetChange"
]
Resource = "arn:aws:route53:::change/*"
},
{
Effect = "Allow"
Action = [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
]
Resource = "arn:aws:route53:::hostedzone/*"
},
{
Effect = "Allow"
Action = "route53:ListHostedZonesByName"
Resource = "*"
}
]
})
}
# External DNS IAM Role
resource "aws_iam_role" "external_dns" {
name = "${var.cluster_name}-external-dns"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = var.cluster_oidc_provider_arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"${local.oidc_issuer}:aud" = "sts.amazonaws.com"
"${local.oidc_issuer}:sub" = "system:serviceaccount:external-dns:external-dns"
}
}
}
]
})
tags = var.tags
}
resource "aws_iam_role_policy" "external_dns" {
name = "${var.cluster_name}-external-dns"
role = aws_iam_role.external_dns.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"route53:ChangeResourceRecordSets"
]
Resource = "arn:aws:route53:::hostedzone/*"
},
{
Effect = "Allow"
Action = [
"route53:ListHostedZones",
"route53:ListResourceRecordSets"
]
Resource = "*"
}
]
})
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}