Created
March 19, 2025 14:45
-
-
Save 19h/285a771116ee464a4092e3facc3159b7 to your computer and use it in GitHub Desktop.
Kubernetes setup in terraform - Russian Elite Engineer vs American Dipshit Engineer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
terraform { | |
required_providers { | |
aws = { | |
source = "hashicorp/aws" | |
version = "~> 4.16" | |
} | |
kubernetes = { | |
source = "hashicorp/kubernetes" | |
version = "~> 2.16" | |
} | |
helm = { | |
source = "hashicorp/helm" | |
version = "~> 2.8" | |
} | |
random = { | |
source = "hashicorp/random" | |
version = "~> 3.4" | |
} | |
tls = { | |
source = "hashicorp/tls" | |
version = "~> 4.0" | |
} | |
} | |
required_version = ">= 1.2.0" | |
backend "s3" { | |
bucket = "terraform-state-k8s-production" | |
key = "terraform.tfstate" | |
region = "eu-central-1" | |
encrypt = true | |
dynamodb_table = "terraform-lock" | |
} | |
} | |
provider "aws" { | |
region = var.region | |
default_tags { | |
tags = { | |
Environment = var.environment | |
Terraform = "true" | |
Project = "k8s-cluster" | |
} | |
} | |
} | |
# Variables | |
variable "region" { | |
description = "AWS region" | |
type = string | |
default = "eu-central-1" | |
} | |
variable "environment" { | |
description = "Environment name" | |
type = string | |
default = "production" | |
} | |
variable "cluster_name" { | |
description = "EKS cluster name" | |
type = string | |
default = "production-eks" | |
} | |
variable "vpc_cidr" { | |
description = "VPC CIDR block" | |
type = string | |
default = "10.0.0.0/16" | |
} | |
variable "availability_zones" { | |
description = "List of availability zones" | |
type = list(string) | |
default = ["eu-central-1a", "eu-central-1b", "eu-central-1c"] | |
} | |
variable "instance_types" { | |
description = "EC2 instance types for worker nodes" | |
type = list(string) | |
default = ["t3.large", "t3.xlarge", "m5.large"] | |
} | |
variable "node_group_min_size" { | |
description = "Minimum size of node group" | |
type = number | |
default = 3 | |
} | |
variable "node_group_max_size" { | |
description = "Maximum size of node group" | |
type = number | |
default = 10 | |
} | |
variable "node_group_desired_size" { | |
description = "Desired size of node group" | |
type = number | |
default = 3 | |
} | |
resource "random_id" "suffix" { | |
byte_length = 4 | |
} | |
# Network Infrastructure | |
resource "aws_vpc" "main" { | |
cidr_block = var.vpc_cidr | |
enable_dns_hostnames = true | |
enable_dns_support = true | |
tags = { | |
Name = "${var.environment}-vpc-${random_id.suffix.hex}" | |
} | |
} | |
resource "aws_subnet" "public" { | |
count = length(var.availability_zones) | |
vpc_id = aws_vpc.main.id | |
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index) | |
availability_zone = var.availability_zones[count.index] | |
map_public_ip_on_launch = true | |
tags = { | |
Name = "${var.environment}-public-${var.availability_zones[count.index]}" | |
"kubernetes.io/cluster/${var.cluster_name}" = "shared" | |
"kubernetes.io/role/elb" = "1" | |
} | |
} | |
resource "aws_subnet" "private" { | |
count = length(var.availability_zones) | |
vpc_id = aws_vpc.main.id | |
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + length(var.availability_zones)) | |
availability_zone = var.availability_zones[count.index] | |
tags = { | |
Name = "${var.environment}-private-${var.availability_zones[count.index]}" | |
"kubernetes.io/cluster/${var.cluster_name}" = "shared" | |
"kubernetes.io/role/internal-elb" = "1" | |
} | |
} | |
resource "aws_internet_gateway" "main" { | |
vpc_id = aws_vpc.main.id | |
tags = { | |
Name = "${var.environment}-igw" | |
} | |
} | |
resource "aws_eip" "nat" { | |
count = length(var.availability_zones) | |
vpc = true | |
tags = { | |
Name = "${var.environment}-nat-eip-${count.index}" | |
} | |
} | |
resource "aws_nat_gateway" "main" { | |
count = length(var.availability_zones) | |
allocation_id = aws_eip.nat[count.index].id | |
subnet_id = aws_subnet.public[count.index].id | |
tags = { | |
Name = "${var.environment}-nat-${count.index}" | |
} | |
depends_on = [aws_internet_gateway.main] | |
} | |
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 = "${var.environment}-public-rt" | |
} | |
} | |
resource "aws_route_table" "private" { | |
count = length(var.availability_zones) | |
vpc_id = aws_vpc.main.id | |
route { | |
cidr_block = "0.0.0.0/0" | |
nat_gateway_id = aws_nat_gateway.main[count.index].id | |
} | |
tags = { | |
Name = "${var.environment}-private-rt-${count.index}" | |
} | |
} | |
resource "aws_route_table_association" "public" { | |
count = length(var.availability_zones) | |
subnet_id = aws_subnet.public[count.index].id | |
route_table_id = aws_route_table.public.id | |
} | |
resource "aws_route_table_association" "private" { | |
count = length(var.availability_zones) | |
subnet_id = aws_subnet.private[count.index].id | |
route_table_id = aws_route_table.private[count.index].id | |
} | |
# Security Groups | |
resource "aws_security_group" "eks_cluster" { | |
name = "${var.environment}-eks-cluster-sg" | |
description = "Security group for EKS control plane" | |
vpc_id = aws_vpc.main.id | |
egress { | |
from_port = 0 | |
to_port = 0 | |
protocol = "-1" | |
cidr_blocks = ["0.0.0.0/0"] | |
} | |
tags = { | |
Name = "${var.environment}-eks-cluster-sg" | |
} | |
} | |
resource "aws_security_group_rule" "eks_cluster_ingress_https" { | |
security_group_id = aws_security_group.eks_cluster.id | |
type = "ingress" | |
from_port = 443 | |
to_port = 443 | |
protocol = "tcp" | |
cidr_blocks = [var.vpc_cidr] | |
description = "Allow HTTPS traffic from VPC" | |
} | |
resource "aws_security_group" "eks_nodes" { | |
name = "${var.environment}-eks-nodes-sg" | |
description = "Security group for EKS worker nodes" | |
vpc_id = aws_vpc.main.id | |
egress { | |
from_port = 0 | |
to_port = 0 | |
protocol = "-1" | |
cidr_blocks = ["0.0.0.0/0"] | |
} | |
tags = { | |
Name = "${var.environment}-eks-nodes-sg" | |
"kubernetes.io/cluster/${var.cluster_name}" = "owned" | |
} | |
} | |
resource "aws_security_group_rule" "eks_nodes_ingress_self" { | |
security_group_id = aws_security_group.eks_nodes.id | |
type = "ingress" | |
from_port = 0 | |
to_port = 65535 | |
protocol = "-1" | |
source_security_group_id = aws_security_group.eks_nodes.id | |
description = "Allow all traffic between nodes" | |
} | |
resource "aws_security_group_rule" "eks_nodes_ingress_cluster" { | |
security_group_id = aws_security_group.eks_nodes.id | |
type = "ingress" | |
from_port = 1025 | |
to_port = 65535 | |
protocol = "tcp" | |
source_security_group_id = aws_security_group.eks_cluster.id | |
description = "Allow worker kubelet and pods to receive communication from the cluster control plane" | |
} | |
resource "aws_security_group_rule" "eks_cluster_ingress_nodes" { | |
security_group_id = aws_security_group.eks_cluster.id | |
type = "ingress" | |
from_port = 443 | |
to_port = 443 | |
protocol = "tcp" | |
source_security_group_id = aws_security_group.eks_nodes.id | |
description = "Allow pods to communicate with the cluster API Server" | |
} | |
# IAM for EKS | |
resource "aws_iam_role" "eks_cluster" { | |
name = "${var.environment}-eks-cluster-role" | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Principal = { | |
Service = "eks.amazonaws.com" | |
} | |
Action = "sts:AssumeRole" | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "eks_cluster_policy" { | |
role = aws_iam_role.eks_cluster.name | |
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" | |
} | |
resource "aws_iam_role_policy_attachment" "eks_service_policy" { | |
role = aws_iam_role.eks_cluster.name | |
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSServicePolicy" | |
} | |
resource "aws_iam_role" "eks_nodes" { | |
name = "${var.environment}-eks-nodes-role" | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Principal = { | |
Service = "ec2.amazonaws.com" | |
} | |
Action = "sts:AssumeRole" | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" { | |
role = aws_iam_role.eks_nodes.name | |
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" | |
} | |
resource "aws_iam_role_policy_attachment" "eks_cni_policy" { | |
role = aws_iam_role.eks_nodes.name | |
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" | |
} | |
resource "aws_iam_role_policy_attachment" "eks_container_registry_policy" { | |
role = aws_iam_role.eks_nodes.name | |
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" | |
} | |
resource "aws_iam_role_policy_attachment" "eks_cloudwatch_policy" { | |
role = aws_iam_role.eks_nodes.name | |
policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy" | |
} | |
# Allow autoscaling for nodes | |
resource "aws_iam_role_policy_attachment" "eks_autoscaler_policy" { | |
role = aws_iam_role.eks_nodes.name | |
policy_arn = aws_iam_policy.cluster_autoscaler.arn | |
} | |
resource "aws_iam_policy" "cluster_autoscaler" { | |
name = "${var.environment}-cluster-autoscaler-policy" | |
description = "Policy for cluster autoscaler" | |
policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Action = [ | |
"autoscaling:DescribeAutoScalingGroups", | |
"autoscaling:DescribeAutoScalingInstances", | |
"autoscaling:DescribeLaunchConfigurations", | |
"autoscaling:DescribeTags", | |
"autoscaling:SetDesiredCapacity", | |
"autoscaling:TerminateInstanceInAutoScalingGroup", | |
"ec2:DescribeLaunchTemplateVersions" | |
] | |
Resource = "*" | |
Effect = "Allow" | |
} | |
] | |
}) | |
} | |
# EKS Cluster | |
resource "aws_eks_cluster" "main" { | |
name = var.cluster_name | |
role_arn = aws_iam_role.eks_cluster.arn | |
version = "1.26" | |
vpc_config { | |
subnet_ids = concat(aws_subnet.private[*].id) | |
security_group_ids = [aws_security_group.eks_cluster.id] | |
endpoint_private_access = true | |
endpoint_public_access = true | |
} | |
depends_on = [ | |
aws_iam_role_policy_attachment.eks_cluster_policy, | |
aws_iam_role_policy_attachment.eks_service_policy | |
] | |
encryption_config { | |
provider { | |
key_arn = aws_kms_key.eks.arn | |
} | |
resources = ["secrets"] | |
} | |
enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"] | |
tags = { | |
Name = var.cluster_name | |
} | |
} | |
resource "aws_kms_key" "eks" { | |
description = "EKS Secret Encryption Key" | |
deletion_window_in_days = 7 | |
enable_key_rotation = true | |
tags = { | |
Name = "${var.environment}-eks-kms-key" | |
} | |
} | |
resource "aws_cloudwatch_log_group" "eks" { | |
name = "/aws/eks/${var.cluster_name}/cluster" | |
retention_in_days = 30 | |
tags = { | |
Name = "${var.environment}-eks-cloudwatch-log-group" | |
} | |
} | |
# EKS Node Groups | |
resource "aws_eks_node_group" "main" { | |
count = length(var.availability_zones) | |
cluster_name = aws_eks_cluster.main.name | |
node_group_name = "${var.environment}-node-group-${count.index}" | |
node_role_arn = aws_iam_role.eks_nodes.arn | |
subnet_ids = [aws_subnet.private[count.index].id] | |
instance_types = var.instance_types | |
ami_type = "AL2_x86_64" | |
capacity_type = "ON_DEMAND" | |
scaling_config { | |
desired_size = var.node_group_desired_size | |
max_size = var.node_group_max_size | |
min_size = var.node_group_min_size | |
} | |
update_config { | |
max_unavailable = 1 | |
} | |
# Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling. | |
# Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces. | |
depends_on = [ | |
aws_iam_role_policy_attachment.eks_worker_node_policy, | |
aws_iam_role_policy_attachment.eks_cni_policy, | |
aws_iam_role_policy_attachment.eks_container_registry_policy, | |
] | |
lifecycle { | |
create_before_destroy = true | |
} | |
tags = { | |
"k8s.io/cluster-autoscaler/enabled" = "true" | |
"k8s.io/cluster-autoscaler/${var.cluster_name}" = "owned" | |
} | |
} | |
# Configure kubectl to use the EKS cluster | |
resource "null_resource" "kubectl_config" { | |
depends_on = [aws_eks_cluster.main] | |
provisioner "local-exec" { | |
command = "aws eks update-kubeconfig --name ${var.cluster_name} --region ${var.region}" | |
} | |
} | |
# Kubernetes provider configuration | |
provider "kubernetes" { | |
host = aws_eks_cluster.main.endpoint | |
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data) | |
exec { | |
api_version = "client.authentication.k8s.io/v1beta1" | |
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name] | |
command = "aws" | |
} | |
} | |
provider "helm" { | |
kubernetes { | |
host = aws_eks_cluster.main.endpoint | |
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data) | |
exec { | |
api_version = "client.authentication.k8s.io/v1beta1" | |
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name] | |
command = "aws" | |
} | |
} | |
} | |
# Kubernetes Add-ons | |
resource "kubernetes_namespace" "monitoring" { | |
metadata { | |
name = "monitoring" | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "kubernetes_namespace" "ingress" { | |
metadata { | |
name = "ingress-nginx" | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "helm_release" "metrics_server" { | |
name = "metrics-server" | |
repository = "https://kubernetes-sigs.github.io/metrics-server/" | |
chart = "metrics-server" | |
namespace = "kube-system" | |
version = "3.8.2" | |
set { | |
name = "apiService.create" | |
value = "true" | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "helm_release" "cluster_autoscaler" { | |
name = "cluster-autoscaler" | |
repository = "https://kubernetes.github.io/autoscaler" | |
chart = "cluster-autoscaler" | |
namespace = "kube-system" | |
version = "9.24.0" | |
set { | |
name = "autoDiscovery.clusterName" | |
value = aws_eks_cluster.main.name | |
} | |
set { | |
name = "awsRegion" | |
value = var.region | |
} | |
set { | |
name = "rbac.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.eks_nodes.arn | |
} | |
depends_on = [aws_eks_node_group.main, helm_release.metrics_server] | |
} | |
resource "helm_release" "prometheus" { | |
name = "prometheus" | |
repository = "https://prometheus-community.github.io/helm-charts" | |
chart = "kube-prometheus-stack" | |
namespace = kubernetes_namespace.monitoring.metadata[0].name | |
version = "45.0.0" | |
values = [ | |
<<-EOT | |
grafana: | |
adminPassword: "StrongPassword123!" | |
persistence: | |
enabled: true | |
size: 10Gi | |
prometheus: | |
prometheusSpec: | |
retention: 15d | |
storageSpec: | |
volumeClaimTemplate: | |
spec: | |
accessModes: ["ReadWriteOnce"] | |
resources: | |
requests: | |
storage: 50Gi | |
alertmanager: | |
alertmanagerSpec: | |
storage: | |
volumeClaimTemplate: | |
spec: | |
accessModes: ["ReadWriteOnce"] | |
resources: | |
requests: | |
storage: 10Gi | |
EOT | |
] | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "helm_release" "ingress_nginx" { | |
name = "ingress-nginx" | |
repository = "https://kubernetes.github.io/ingress-nginx" | |
chart = "ingress-nginx" | |
namespace = kubernetes_namespace.ingress.metadata[0].name | |
version = "4.4.0" | |
set { | |
name = "controller.service.type" | |
value = "LoadBalancer" | |
} | |
set { | |
name = "controller.service.externalTrafficPolicy" | |
value = "Local" | |
} | |
set { | |
name = "controller.resources.requests.cpu" | |
value = "100m" | |
} | |
set { | |
name = "controller.resources.requests.memory" | |
value = "90Mi" | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "helm_release" "cert_manager" { | |
name = "cert-manager" | |
repository = "https://charts.jetstack.io" | |
chart = "cert-manager" | |
namespace = "cert-manager" | |
version = "v1.10.0" | |
create_namespace = true | |
set { | |
name = "installCRDs" | |
value = "true" | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "helm_release" "aws_load_balancer_controller" { | |
name = "aws-load-balancer-controller" | |
repository = "https://aws.github.io/eks-charts" | |
chart = "aws-load-balancer-controller" | |
namespace = "kube-system" | |
version = "1.4.6" | |
set { | |
name = "clusterName" | |
value = aws_eks_cluster.main.name | |
} | |
set { | |
name = "serviceAccount.create" | |
value = "true" | |
} | |
set { | |
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.eks_nodes.arn | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
# IAM Policy for AWS Load Balancer Controller | |
resource "aws_iam_policy" "load_balancer_controller" { | |
name = "${var.environment}-load-balancer-controller-policy" | |
description = "Policy for AWS Load Balancer Controller" | |
policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Action = [ | |
"iam:CreateServiceLinkedRole", | |
"ec2:DescribeAccountAttributes", | |
"ec2:DescribeAddresses", | |
"ec2:DescribeInternetGateways", | |
"ec2:DescribeVpcs", | |
"ec2:DescribeSubnets", | |
"ec2:DescribeSecurityGroups", | |
"ec2:DescribeInstances", | |
"ec2:DescribeNetworkInterfaces", | |
"ec2:DescribeTags", | |
"ec2:GetCoipPoolUsage", | |
"ec2:DescribeCoipPools", | |
"elasticloadbalancing:DescribeLoadBalancers", | |
"elasticloadbalancing:DescribeLoadBalancerAttributes", | |
"elasticloadbalancing:DescribeListeners", | |
"elasticloadbalancing:DescribeListenerCertificates", | |
"elasticloadbalancing:DescribeSSLPolicies", | |
"elasticloadbalancing:DescribeRules", | |
"elasticloadbalancing:DescribeTargetGroups", | |
"elasticloadbalancing:DescribeTargetGroupAttributes", | |
"elasticloadbalancing:DescribeTargetHealth", | |
"elasticloadbalancing:DescribeTags" | |
] | |
Resource = "*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"cognito-idp:DescribeUserPoolClient", | |
"acm:ListCertificates", | |
"acm:DescribeCertificate", | |
"iam:ListServerCertificates", | |
"iam:GetServerCertificate", | |
"waf-regional:GetWebACL", | |
"waf-regional:GetWebACLForResource", | |
"waf-regional:AssociateWebACL", | |
"waf-regional:DisassociateWebACL", | |
"wafv2:GetWebACL", | |
"wafv2:GetWebACLForResource", | |
"wafv2:AssociateWebACL", | |
"wafv2:DisassociateWebACL", | |
"shield:GetSubscriptionState", | |
"shield:DescribeProtection", | |
"shield:CreateProtection", | |
"shield:DeleteProtection" | |
] | |
Resource = "*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:AuthorizeSecurityGroupIngress", | |
"ec2:RevokeSecurityGroupIngress" | |
] | |
Resource = "*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:CreateSecurityGroup" | |
] | |
Resource = "*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:CreateTags" | |
] | |
Resource = "arn:aws:ec2:*:*:security-group/*" | |
Condition = { | |
StringEquals = { | |
"ec2:CreateAction" = "CreateSecurityGroup" | |
} | |
Null = { | |
"aws:RequestTag/elbv2.k8s.aws/cluster" = "false" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:CreateTags", | |
"ec2:DeleteTags" | |
] | |
Resource = "arn:aws:ec2:*:*:security-group/*" | |
Condition = { | |
Null = { | |
"aws:RequestTag/elbv2.k8s.aws/cluster" = "true" | |
"aws:ResourceTag/elbv2.k8s.aws/cluster" = "false" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:AuthorizeSecurityGroupIngress", | |
"ec2:RevokeSecurityGroupIngress", | |
"ec2:DeleteSecurityGroup" | |
] | |
Resource = "*" | |
Condition = { | |
StringEquals = { | |
"ec2:ResourceTag/elbv2.k8s.aws/cluster" = "true" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"elasticloadbalancing:CreateLoadBalancer", | |
"elasticloadbalancing:CreateTargetGroup" | |
] | |
Resource = "*" | |
Condition = { | |
Null = { | |
"aws:RequestTag/elbv2.k8s.aws/cluster" = "false" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"elasticloadbalancing:CreateListener", | |
"elasticloadbalancing:DeleteListener", | |
"elasticloadbalancing:CreateRule", | |
"elasticloadbalancing:DeleteRule" | |
] | |
Resource = "*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"elasticloadbalancing:AddTags", | |
"elasticloadbalancing:RemoveTags" | |
] | |
Resource = [ | |
"arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", | |
"arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", | |
"arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" | |
] | |
Condition = { | |
Null = { | |
"aws:RequestTag/elbv2.k8s.aws/cluster" = "true" | |
"aws:ResourceTag/elbv2.k8s.aws/cluster" = "false" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"elasticloadbalancing:ModifyLoadBalancerAttributes", | |
"elasticloadbalancing:SetIpAddressType", | |
"elasticloadbalancing:SetSecurityGroups", | |
"elasticloadbalancing:SetSubnets", | |
"elasticloadbalancing:DeleteLoadBalancer", | |
"elasticloadbalancing:ModifyTargetGroup", | |
"elasticloadbalancing:ModifyTargetGroupAttributes", | |
"elasticloadbalancing:DeleteTargetGroup" | |
] | |
Resource = "*" | |
Condition = { | |
StringEquals = { | |
"elasticloadbalancing:ResourceTag/elbv2.k8s.aws/cluster" = "true" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"elasticloadbalancing:RegisterTargets", | |
"elasticloadbalancing:DeregisterTargets" | |
] | |
Resource = "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"elasticloadbalancing:SetWebAcl", | |
"elasticloadbalancing:ModifyListener", | |
"elasticloadbalancing:AddListenerCertificates", | |
"elasticloadbalancing:RemoveListenerCertificates", | |
"elasticloadbalancing:ModifyRule" | |
] | |
Resource = "*" | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "load_balancer_controller_policy" { | |
role = aws_iam_role.eks_nodes.name | |
policy_arn = aws_iam_policy.load_balancer_controller.arn | |
} | |
# Deploy a sample app for testing | |
resource "kubernetes_namespace" "demo" { | |
metadata { | |
name = "demo" | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "kubernetes_deployment" "demo" { | |
metadata { | |
name = "nginx-demo" | |
namespace = kubernetes_namespace.demo.metadata[0].name | |
labels = { | |
app = "nginx-demo" | |
} | |
} | |
spec { | |
replicas = 2 | |
selector { | |
match_labels = { | |
app = "nginx-demo" | |
} | |
} | |
template { | |
metadata { | |
labels = { | |
app = "nginx-demo" | |
} | |
} | |
spec { | |
container { | |
image = "nginx:latest" | |
name = "nginx" | |
port { | |
container_port = 80 | |
} | |
resources { | |
limits = { | |
cpu = "200m" | |
memory = "256Mi" | |
} | |
requests = { | |
cpu = "100m" | |
memory = "128Mi" | |
} | |
} | |
} | |
readiness_probe { | |
http_get { | |
path = "/" | |
port = 80 | |
} | |
initial_delay_seconds = 10 | |
period_seconds = 5 | |
} | |
liveness_probe { | |
http_get { | |
path = "/" | |
port = 80 | |
} | |
initial_delay_seconds = 15 | |
period_seconds = 15 | |
} | |
} | |
} | |
} | |
} | |
resource "kubernetes_service" "demo" { | |
metadata { | |
name = "nginx-demo" | |
namespace = kubernetes_namespace.demo.metadata[0].name | |
} | |
spec { | |
selector = { | |
app = kubernetes_deployment.demo.spec[0].template[0].metadata[0].labels.app | |
} | |
port { | |
port = 80 | |
target_port = 80 | |
} | |
type = "ClusterIP" | |
} | |
} | |
resource "kubernetes_ingress_v1" "demo" { | |
metadata { | |
name = "nginx-demo" | |
namespace = kubernetes_namespace.demo.metadata[0].name | |
annotations = { | |
"kubernetes.io/ingress.class" = "nginx" | |
"cert-manager.io/cluster-issuer" = "letsencrypt-prod" | |
"nginx.ingress.kubernetes.io/ssl-redirect" = "true" | |
} | |
} | |
spec { | |
rule { | |
host = "demo.example.com" | |
http { | |
path { | |
path = "/" | |
path_type = "Prefix" | |
backend { | |
service { | |
name = kubernetes_service.demo.metadata[0].name | |
port { | |
number = 80 | |
} | |
} | |
} | |
} | |
} | |
} | |
tls { | |
hosts = ["demo.example.com"] | |
secret_name = "demo-tls" | |
} | |
} | |
depends_on = [helm_release.ingress_nginx, helm_release.cert_manager] | |
} | |
# Create ClusterIssuer for Let's Encrypt certificates | |
resource "kubernetes_manifest" "cluster_issuer" { | |
manifest = { | |
apiVersion = "cert-manager.io/v1" | |
kind = "ClusterIssuer" | |
metadata = { | |
name = "letsencrypt-prod" | |
} | |
spec = { | |
acme = { | |
server = "https://acme-v02.api.letsencrypt.org/directory" | |
email = "[email protected]" | |
privateKeySecretRef = { | |
name = "letsencrypt-prod" | |
} | |
solvers = [ | |
{ | |
http01 = { | |
ingress = { | |
class = "nginx" | |
} | |
} | |
} | |
] | |
} | |
} | |
} | |
depends_on = [helm_release.cert_manager] | |
} | |
# Setup External DNS | |
resource "helm_release" "external_dns" { | |
name = "external-dns" | |
repository = "https://charts.bitnami.com/bitnami" | |
chart = "external-dns" | |
namespace = "kube-system" | |
version = "6.11.0" | |
set { | |
name = "provider" | |
value = "aws" | |
} | |
set { | |
name = "aws.region" | |
value = var.region | |
} | |
set { | |
name = "domainFilters[0]" | |
value = "example.com" | |
} | |
set { | |
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.eks_nodes.arn | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
# Create IAM policy for External DNS | |
resource "aws_iam_policy" "external_dns" { | |
name = "${var.environment}-external-dns-policy" | |
description = "Policy for External DNS" | |
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 = [ | |
"*" | |
] | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "external_dns_policy" { | |
role = aws_iam_role.eks_nodes.name | |
policy_arn = aws_iam_policy.external_dns.arn | |
} | |
# Configure persistent volumes with EBS CSI driver | |
resource "helm_release" "aws_ebs_csi_driver" { | |
name = "aws-ebs-csi-driver" | |
repository = "https://kubernetes-sigs.github.io/aws-ebs-csi-driver" | |
chart = "aws-ebs-csi-driver" | |
namespace = "kube-system" | |
version = "2.16.0" | |
set { | |
name = "controller.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.eks_nodes.arn | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "aws_iam_policy" "ebs_csi_driver" { | |
name = "${var.environment}-ebs-csi-driver-policy" | |
description = "Policy for EBS CSI Driver" | |
policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:CreateSnapshot", | |
"ec2:AttachVolume", | |
"ec2:DetachVolume", | |
"ec2:ModifyVolume", | |
"ec2:DescribeAvailabilityZones", | |
"ec2:DescribeInstances", | |
"ec2:DescribeSnapshots", | |
"ec2:DescribeTags", | |
"ec2:DescribeVolumes", | |
"ec2:DescribeVolumesModifications" | |
] | |
Resource = "*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:CreateTags" | |
] | |
Resource = [ | |
"arn:aws:ec2:*:*:volume/*", | |
"arn:aws:ec2:*:*:snapshot/*" | |
] | |
Condition = { | |
StringEquals = { | |
"ec2:CreateAction" = [ | |
"CreateVolume", | |
"CreateSnapshot" | |
] | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:DeleteTags" | |
] | |
Resource = [ | |
"arn:aws:ec2:*:*:volume/*", | |
"arn:aws:ec2:*:*:snapshot/*" | |
] | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:CreateVolume" | |
] | |
Resource = "*" | |
Condition = { | |
StringLike = { | |
"aws:RequestTag/kubernetes.io/cluster/*" = "owned" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:CreateVolume" | |
] | |
Resource = "*" | |
Condition = { | |
StringLike = { | |
"aws:RequestTag/CSIVolumeName" = "*" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:DeleteVolume" | |
] | |
Resource = "*" | |
Condition = { | |
StringLike = { | |
"ec2:ResourceTag/CSIVolumeName" = "*" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:DeleteVolume" | |
] | |
Resource = "*" | |
Condition = { | |
StringLike = { | |
"ec2:ResourceTag/kubernetes.io/cluster/*" = "owned" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:DeleteSnapshot" | |
] | |
Resource = "*" | |
Condition = { | |
StringLike = { | |
"ec2:ResourceTag/CSIVolumeSnapshotName" = "*" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:DeleteSnapshot" | |
] | |
Resource = "*" | |
Condition = { | |
StringLike = { | |
"ec2:ResourceTag/kubernetes.io/cluster/*" = "owned" | |
} | |
} | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "ebs_csi_driver_policy" { | |
role = aws_iam_role.eks_nodes.name | |
policy_arn = aws_iam_policy.ebs_csi_driver.arn | |
} | |
# Create storage class for gp3 EBS volumes | |
resource "kubernetes_storage_class" "gp3" { | |
metadata { | |
name = "gp3" | |
annotations = { | |
"storageclass.kubernetes.io/is-default-class" = "true" | |
} | |
} | |
storage_provisioner = "ebs.csi.aws.com" | |
reclaim_policy = "Delete" | |
allow_volume_expansion = true | |
volume_binding_mode = "WaitForFirstConsumer" | |
parameters = { | |
type = "gp3" | |
fsType = "ext4" | |
} | |
depends_on = [helm_release.aws_ebs_csi_driver] | |
} | |
# Backup solution with Velero | |
resource "kubernetes_namespace" "velero" { | |
metadata { | |
name = "velero" | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "aws_s3_bucket" "velero" { | |
bucket = "${var.environment}-velero-backup-${random_id.suffix.hex}" | |
tags = { | |
Name = "${var.environment}-velero-backup-bucket" | |
} | |
} | |
resource "aws_s3_bucket_versioning" "velero" { | |
bucket = aws_s3_bucket.velero.id | |
versioning_configuration { | |
status = "Enabled" | |
} | |
} | |
resource "aws_s3_bucket_lifecycle_configuration" "velero" { | |
bucket = aws_s3_bucket.velero.id | |
rule { | |
id = "expire-old-backups" | |
status = "Enabled" | |
expiration { | |
days = 90 | |
} | |
noncurrent_version_expiration { | |
noncurrent_days = 30 | |
} | |
} | |
} | |
resource "aws_iam_policy" "velero" { | |
name = "${var.environment}-velero-policy" | |
description = "Policy for Velero backup and restore" | |
policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:DescribeVolumes", | |
"ec2:DescribeSnapshots", | |
"ec2:CreateTags", | |
"ec2:CreateVolume", | |
"ec2:CreateSnapshot", | |
"ec2:DeleteSnapshot" | |
] | |
Resource = "*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"s3:GetObject", | |
"s3:DeleteObject", | |
"s3:PutObject", | |
"s3:AbortMultipartUpload", | |
"s3:ListMultipartUploadParts" | |
] | |
Resource = [ | |
"${aws_s3_bucket.velero.arn}/*" | |
] | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"s3:ListBucket" | |
] | |
Resource = [ | |
"${aws_s3_bucket.velero.arn}" | |
] | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "velero_policy" { | |
role = aws_iam_role.eks_nodes.name | |
policy_arn = aws_iam_policy.velero.arn | |
} | |
resource "helm_release" "velero" { | |
name = "velero" | |
repository = "https://vmware-tanzu.github.io/helm-charts" | |
chart = "velero" | |
namespace = kubernetes_namespace.velero.metadata[0].name | |
version = "4.0.0" | |
set { | |
name = "configuration.provider" | |
value = "aws" | |
} | |
set { | |
name = "configuration.backupStorageLocation.bucket" | |
value = aws_s3_bucket.velero.id | |
} | |
set { | |
name = "configuration.backupStorageLocation.config.region" | |
value = var.region | |
} | |
set { | |
name = "initContainers[0].name" | |
value = "velero-plugin-for-aws" | |
} | |
set { | |
name = "initContainers[0].image" | |
value = "velero/velero-plugin-for-aws:v1.5.0" | |
} | |
set { | |
name = "initContainers[0].volumeMounts[0].mountPath" | |
value = "/target" | |
} | |
set { | |
name = "initContainers[0].volumeMounts[0].name" | |
value = "plugins" | |
} | |
set { | |
name = "serviceAccount.server.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.eks_nodes.arn | |
} | |
set { | |
name = "snapshotsEnabled" | |
value = "true" | |
} | |
set { | |
name = "deployRestic" | |
value = "true" | |
} | |
set { | |
name = "restic.privileged" | |
value = "true" | |
} | |
depends_on = [aws_eks_node_group.main, aws_s3_bucket.velero] | |
} | |
# Setup periodic backups with Velero | |
resource "kubernetes_manifest" "backup_schedule" { | |
manifest = { | |
apiVersion = "velero.io/v1" | |
kind = "Schedule" | |
metadata = { | |
name = "daily-backup" | |
namespace = kubernetes_namespace.velero.metadata[0].name | |
} | |
spec = { | |
schedule = "0 1 * * *" | |
template = { | |
ttl = "720h" | |
includedNamespaces = ["*"] | |
excludedNamespaces = ["kube-system", "velero"] | |
includeClusterResources = true | |
storageLocation = "default" | |
volumeSnapshotLocations = ["default"] | |
} | |
} | |
} | |
depends_on = [helm_release.velero] | |
} | |
# NetworkPolicy to restrict communication between namespaces | |
resource "kubernetes_network_policy" "default_deny" { | |
metadata { | |
name = "default-deny" | |
namespace = kubernetes_namespace.demo.metadata[0].name | |
} | |
spec { | |
pod_selector {} | |
policy_types = ["Ingress", "Egress"] | |
} | |
depends_on = [aws_eks_node_group.main] | |
} | |
resource "kubernetes_network_policy" "allow_ingress" { | |
metadata { | |
name = "allow-ingress-controllers" | |
namespace = kubernetes_namespace.demo.metadata[0].name | |
} | |
spec { | |
pod_selector { | |
match_labels = { | |
app = "nginx-demo" | |
} | |
} | |
ingress { | |
from { | |
namespace_selector { | |
match_labels = { | |
"kubernetes.io/metadata.name" = "ingress-nginx" | |
} | |
} | |
} | |
} | |
policy_types = ["Ingress"] | |
} | |
depends_on = [aws_eks_node_group.main, kubernetes_namespace.ingress] | |
} | |
resource "kubernetes_network_policy" "allow_monitoring" { | |
metadata { | |
name = "allow-monitoring" | |
namespace = kubernetes_namespace.demo.metadata[0].name | |
} | |
spec { | |
pod_selector {} | |
ingress { | |
from { | |
namespace_selector { | |
match_labels = { | |
"kubernetes.io/metadata.name" = "monitoring" | |
} | |
} | |
} | |
} | |
policy_types = ["Ingress"] | |
} | |
depends_on = [aws_eks_node_group.main, kubernetes_namespace.monitoring] | |
} | |
# Output values | |
output "cluster_endpoint" { | |
description = "EKS cluster endpoint" | |
value = aws_eks_cluster.main.endpoint | |
} | |
output "cluster_security_group_id" { | |
description = "Security group ID attached to the EKS cluster" | |
value = aws_security_group.eks_cluster.id | |
} | |
output "cluster_name" { | |
description = "EKS cluster name" | |
value = aws_eks_cluster.main.name | |
} | |
output "cluster_certificate_authority_data" { | |
description = "Base64 encoded certificate data required to communicate with the cluster" | |
value = aws_eks_cluster.main.certificate_authority[0].data | |
sensitive = true | |
} | |
output "cluster_oidc_issuer_url" { | |
description = "The URL on the EKS cluster for OpenID Connect" | |
value = aws_eks_cluster.main.identity[0].oidc[0].issuer | |
} | |
output "node_security_group_id" { | |
description = "Security group ID attached to the EKS nodes" | |
value = aws_security_group.eks_nodes.id | |
} | |
output "kubectl_config_command" { | |
description = "kubectl configuration command" | |
value = "aws eks update-kubeconfig --name ${aws_eks_cluster.main.name} --region ${var.region}" | |
} | |
output "prometheus_grafana_url" { | |
description = "URL for Grafana dashboard" | |
value = "https://grafana.example.com" | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
terraform { | |
required_providers { | |
aws = { | |
source = "hashicorp/aws" | |
version = "~> 5.31.0" | |
} | |
kubernetes = { | |
source = "hashicorp/kubernetes" | |
version = "~> 2.23.0" | |
} | |
helm = { | |
source = "hashicorp/helm" | |
version = "~> 2.11.0" | |
} | |
tls = { | |
source = "hashicorp/tls" | |
version = "~> 4.0.4" | |
} | |
} | |
backend "s3" { | |
bucket = "terraform-k8s-state" | |
key = "terraform/k8s/state.tfstate" | |
region = "us-west-2" | |
encrypt = true | |
dynamodb_table = "terraform-lock" | |
} | |
} | |
provider "aws" { | |
region = var.region | |
default_tags { | |
tags = { | |
Environment = var.environment | |
Project = "kubernetes-cluster" | |
ManagedBy = "terraform" | |
} | |
} | |
} | |
# Variables | |
variable "region" { | |
description = "AWS region to deploy the cluster" | |
type = string | |
default = "us-west-2" | |
} | |
variable "environment" { | |
description = "Environment name (e.g., prod, staging)" | |
type = string | |
default = "prod" | |
} | |
variable "cluster_name" { | |
description = "Name of the EKS cluster" | |
type = string | |
default = "resilient-k8s-cluster" | |
} | |
variable "cluster_version" { | |
description = "Kubernetes version" | |
type = string | |
default = "1.28" | |
} | |
variable "vpc_cidr" { | |
description = "CIDR block for the VPC" | |
type = string | |
default = "10.0.0.0/16" | |
} | |
variable "availability_zones" { | |
description = "List of availability zones" | |
type = list(string) | |
default = ["us-west-2a", "us-west-2b", "us-west-2c"] | |
} | |
variable "node_groups" { | |
description = "Node group configuration" | |
type = map(object({ | |
instance_types = list(string) | |
min_size = number | |
max_size = number | |
desired_size = number | |
disk_size = number | |
})) | |
default = { | |
critical = { | |
instance_types = ["m5.large", "m5a.large", "m5d.large"] | |
min_size = 3 | |
max_size = 5 | |
desired_size = 3 | |
disk_size = 100 | |
}, | |
applications = { | |
instance_types = ["c5.xlarge", "c5a.xlarge", "c5d.xlarge"] | |
min_size = 3 | |
max_size = 10 | |
desired_size = 3 | |
disk_size = 100 | |
} | |
} | |
} | |
# Network Infrastructure | |
resource "aws_vpc" "main" { | |
cidr_block = var.vpc_cidr | |
enable_dns_support = true | |
enable_dns_hostnames = true | |
tags = { | |
Name = "${var.cluster_name}-vpc" | |
} | |
} | |
resource "aws_subnet" "private" { | |
count = length(var.availability_zones) | |
vpc_id = aws_vpc.main.id | |
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index) | |
availability_zone = var.availability_zones[count.index] | |
tags = { | |
Name = "${var.cluster_name}-private-${var.availability_zones[count.index]}" | |
"kubernetes.io/cluster/${var.cluster_name}" = "shared" | |
"kubernetes.io/role/internal-elb" = "1" | |
} | |
} | |
resource "aws_subnet" "public" { | |
count = length(var.availability_zones) | |
vpc_id = aws_vpc.main.id | |
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index + length(var.availability_zones)) | |
availability_zone = var.availability_zones[count.index] | |
map_public_ip_on_launch = true | |
tags = { | |
Name = "${var.cluster_name}-public-${var.availability_zones[count.index]}" | |
"kubernetes.io/cluster/${var.cluster_name}" = "shared" | |
"kubernetes.io/role/elb" = "1" | |
} | |
} | |
resource "aws_internet_gateway" "main" { | |
vpc_id = aws_vpc.main.id | |
tags = { | |
Name = "${var.cluster_name}-igw" | |
} | |
} | |
resource "aws_eip" "nat" { | |
count = length(var.availability_zones) | |
domain = "vpc" | |
tags = { | |
Name = "${var.cluster_name}-nat-eip-${count.index}" | |
} | |
} | |
resource "aws_nat_gateway" "main" { | |
count = length(var.availability_zones) | |
allocation_id = aws_eip.nat[count.index].id | |
subnet_id = aws_subnet.public[count.index].id | |
tags = { | |
Name = "${var.cluster_name}-nat-${count.index}" | |
} | |
depends_on = [aws_internet_gateway.main] | |
} | |
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 = "${var.cluster_name}-public-rt" | |
} | |
} | |
resource "aws_route_table" "private" { | |
count = length(var.availability_zones) | |
vpc_id = aws_vpc.main.id | |
route { | |
cidr_block = "0.0.0.0/0" | |
nat_gateway_id = aws_nat_gateway.main[count.index].id | |
} | |
tags = { | |
Name = "${var.cluster_name}-private-rt-${count.index}" | |
} | |
} | |
resource "aws_route_table_association" "public" { | |
count = length(var.availability_zones) | |
subnet_id = aws_subnet.public[count.index].id | |
route_table_id = aws_route_table.public.id | |
} | |
resource "aws_route_table_association" "private" { | |
count = length(var.availability_zones) | |
subnet_id = aws_subnet.private[count.index].id | |
route_table_id = aws_route_table.private[count.index].id | |
} | |
# Security Groups | |
resource "aws_security_group" "cluster" { | |
name = "${var.cluster_name}-cluster-sg" | |
description = "Security group for Kubernetes cluster control plane" | |
vpc_id = aws_vpc.main.id | |
egress { | |
from_port = 0 | |
to_port = 0 | |
protocol = "-1" | |
cidr_blocks = ["0.0.0.0/0"] | |
} | |
tags = { | |
Name = "${var.cluster_name}-cluster-sg" | |
} | |
} | |
resource "aws_security_group" "nodes" { | |
name = "${var.cluster_name}-node-sg" | |
description = "Security group for Kubernetes cluster nodes" | |
vpc_id = aws_vpc.main.id | |
egress { | |
from_port = 0 | |
to_port = 0 | |
protocol = "-1" | |
cidr_blocks = ["0.0.0.0/0"] | |
} | |
tags = { | |
Name = "${var.cluster_name}-node-sg" | |
} | |
} | |
resource "aws_security_group_rule" "cluster_ingress_node_https" { | |
description = "Allow nodes to communicate with the cluster API Server" | |
type = "ingress" | |
from_port = 443 | |
to_port = 443 | |
protocol = "tcp" | |
security_group_id = aws_security_group.cluster.id | |
source_security_group_id = aws_security_group.nodes.id | |
} | |
resource "aws_security_group_rule" "nodes_ingress_self" { | |
description = "Allow nodes to communicate with each other" | |
type = "ingress" | |
from_port = 0 | |
to_port = 65535 | |
protocol = "-1" | |
security_group_id = aws_security_group.nodes.id | |
source_security_group_id = aws_security_group.nodes.id | |
} | |
resource "aws_security_group_rule" "nodes_ingress_cluster" { | |
description = "Allow worker Kubelets and pods to receive communication from the cluster control plane" | |
type = "ingress" | |
from_port = 1025 | |
to_port = 65535 | |
protocol = "tcp" | |
security_group_id = aws_security_group.nodes.id | |
source_security_group_id = aws_security_group.cluster.id | |
} | |
# IAM Roles for EKS | |
resource "aws_iam_role" "cluster" { | |
name = "${var.cluster_name}-cluster-role" | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Principal = { | |
Service = "eks.amazonaws.com" | |
} | |
Action = "sts:AssumeRole" | |
} | |
] | |
}) | |
} | |
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 | |
} | |
resource "aws_iam_role" "nodes" { | |
name = "${var.cluster_name}-node-role" | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Principal = { | |
Service = "ec2.amazonaws.com" | |
} | |
Action = "sts:AssumeRole" | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "nodes_AmazonEKSWorkerNodePolicy" { | |
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" | |
role = aws_iam_role.nodes.name | |
} | |
resource "aws_iam_role_policy_attachment" "nodes_AmazonEKS_CNI_Policy" { | |
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" | |
role = aws_iam_role.nodes.name | |
} | |
resource "aws_iam_role_policy_attachment" "nodes_AmazonEC2ContainerRegistryReadOnly" { | |
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" | |
role = aws_iam_role.nodes.name | |
} | |
resource "aws_iam_role_policy_attachment" "nodes_AmazonSSMManagedInstanceCore" { | |
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" | |
role = aws_iam_role.nodes.name | |
} | |
# EKS Cluster | |
resource "aws_eks_cluster" "main" { | |
name = var.cluster_name | |
role_arn = aws_iam_role.cluster.arn | |
version = var.cluster_version | |
vpc_config { | |
subnet_ids = concat(aws_subnet.private[*].id, aws_subnet.public[*].id) | |
security_group_ids = [aws_security_group.cluster.id] | |
endpoint_private_access = true | |
endpoint_public_access = true | |
} | |
encryption_config { | |
provider { | |
key_arn = aws_kms_key.eks.arn | |
} | |
resources = ["secrets"] | |
} | |
enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"] | |
depends_on = [ | |
aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy, | |
aws_iam_role_policy_attachment.cluster_AmazonEKSVPCResourceController, | |
aws_cloudwatch_log_group.eks | |
] | |
} | |
# KMS Key for EKS encryption | |
resource "aws_kms_key" "eks" { | |
description = "EKS Secret Encryption Key" | |
deletion_window_in_days = 7 | |
enable_key_rotation = true | |
tags = { | |
Name = "${var.cluster_name}-kms" | |
} | |
} | |
# CloudWatch Logs | |
resource "aws_cloudwatch_log_group" "eks" { | |
name = "/aws/eks/${var.cluster_name}/cluster" | |
retention_in_days = 90 | |
} | |
# EKS Node Groups | |
resource "aws_eks_node_group" "main" { | |
for_each = var.node_groups | |
cluster_name = aws_eks_cluster.main.name | |
node_group_name = "${var.cluster_name}-${each.key}" | |
node_role_arn = aws_iam_role.nodes.arn | |
subnet_ids = aws_subnet.private[*].id | |
ami_type = "AL2_x86_64" | |
capacity_type = "ON_DEMAND" | |
instance_types = each.value.instance_types | |
disk_size = each.value.disk_size | |
scaling_config { | |
desired_size = each.value.desired_size | |
min_size = each.value.min_size | |
max_size = each.value.max_size | |
} | |
update_config { | |
max_unavailable = 1 | |
} | |
remote_access { | |
ec2_ssh_key = aws_key_pair.eks.key_name | |
source_security_group_ids = [aws_security_group.cluster_ssh.id] | |
} | |
labels = { | |
"role" = each.key | |
} | |
tags = { | |
Name = "${var.cluster_name}-${each.key}" | |
} | |
depends_on = [ | |
aws_iam_role_policy_attachment.nodes_AmazonEKSWorkerNodePolicy, | |
aws_iam_role_policy_attachment.nodes_AmazonEKS_CNI_Policy, | |
aws_iam_role_policy_attachment.nodes_AmazonEC2ContainerRegistryReadOnly, | |
aws_iam_role_policy_attachment.nodes_AmazonSSMManagedInstanceCore | |
] | |
lifecycle { | |
create_before_destroy = true | |
} | |
} | |
# SSH Key for Node Access | |
resource "tls_private_key" "eks" { | |
algorithm = "RSA" | |
rsa_bits = 4096 | |
} | |
resource "aws_key_pair" "eks" { | |
key_name = "${var.cluster_name}-keypair" | |
public_key = tls_private_key.eks.public_key_openssh | |
} | |
resource "aws_secretsmanager_secret" "ssh_key" { | |
name = "${var.cluster_name}-ssh-key" | |
recovery_window_in_days = 0 | |
} | |
resource "aws_secretsmanager_secret_version" "ssh_key" { | |
secret_id = aws_secretsmanager_secret.ssh_key.id | |
secret_string = tls_private_key.eks.private_key_pem | |
} | |
resource "aws_security_group" "cluster_ssh" { | |
name = "${var.cluster_name}-ssh-sg" | |
description = "Security group for SSH access to EKS nodes" | |
vpc_id = aws_vpc.main.id | |
ingress { | |
from_port = 22 | |
to_port = 22 | |
protocol = "tcp" | |
cidr_blocks = ["10.0.0.0/8"] # Restrict to VPN/bastion hosts in production | |
} | |
egress { | |
from_port = 0 | |
to_port = 0 | |
protocol = "-1" | |
cidr_blocks = ["0.0.0.0/0"] | |
} | |
tags = { | |
Name = "${var.cluster_name}-ssh-sg" | |
} | |
} | |
# Kubernetes Provider Configuration | |
provider "kubernetes" { | |
host = aws_eks_cluster.main.endpoint | |
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data) | |
exec { | |
api_version = "client.authentication.k8s.io/v1beta1" | |
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name] | |
command = "aws" | |
} | |
} | |
provider "helm" { | |
kubernetes { | |
host = aws_eks_cluster.main.endpoint | |
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data) | |
exec { | |
api_version = "client.authentication.k8s.io/v1beta1" | |
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name] | |
command = "aws" | |
} | |
} | |
} | |
# Kubernetes Add-ons | |
# AWS Load Balancer Controller | |
resource "aws_iam_policy" "aws_load_balancer_controller" { | |
name = "${var.cluster_name}-alb-controller-policy" | |
description = "Policy for AWS Load Balancer Controller" | |
policy = jsonencode({ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"iam:CreateServiceLinkedRole" | |
], | |
"Resource": "*", | |
"Condition": { | |
"StringEquals": { | |
"iam:AWSServiceName": "elasticloadbalancing.amazonaws.com" | |
} | |
} | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ec2:DescribeAccountAttributes", | |
"ec2:DescribeAddresses", | |
"ec2:DescribeAvailabilityZones", | |
"ec2:DescribeInternetGateways", | |
"ec2:DescribeVpcs", | |
"ec2:DescribeVpcPeeringConnections", | |
"ec2:DescribeSubnets", | |
"ec2:DescribeSecurityGroups", | |
"ec2:DescribeInstances", | |
"ec2:DescribeNetworkInterfaces", | |
"ec2:DescribeTags", | |
"ec2:GetCoipPoolUsage", | |
"ec2:DescribeCoipPools", | |
"elasticloadbalancing:DescribeLoadBalancers", | |
"elasticloadbalancing:DescribeLoadBalancerAttributes", | |
"elasticloadbalancing:DescribeListeners", | |
"elasticloadbalancing:DescribeListenerCertificates", | |
"elasticloadbalancing:DescribeSSLPolicies", | |
"elasticloadbalancing:DescribeRules", | |
"elasticloadbalancing:DescribeTargetGroups", | |
"elasticloadbalancing:DescribeTargetGroupAttributes", | |
"elasticloadbalancing:DescribeTargetHealth", | |
"elasticloadbalancing:DescribeTags" | |
], | |
"Resource": "*" | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"cognito-idp:DescribeUserPoolClient", | |
"acm:ListCertificates", | |
"acm:DescribeCertificate", | |
"iam:ListServerCertificates", | |
"iam:GetServerCertificate", | |
"waf-regional:GetWebACL", | |
"waf-regional:GetWebACLForResource", | |
"waf-regional:AssociateWebACL", | |
"waf-regional:DisassociateWebACL", | |
"wafv2:GetWebACL", | |
"wafv2:GetWebACLForResource", | |
"wafv2:AssociateWebACL", | |
"wafv2:DisassociateWebACL", | |
"shield:GetSubscriptionState", | |
"shield:DescribeProtection", | |
"shield:CreateProtection", | |
"shield:DeleteProtection" | |
], | |
"Resource": "*" | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ec2:AuthorizeSecurityGroupIngress", | |
"ec2:RevokeSecurityGroupIngress" | |
], | |
"Resource": "*" | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ec2:CreateSecurityGroup" | |
], | |
"Resource": "*" | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ec2:CreateTags" | |
], | |
"Resource": "arn:aws:ec2:*:*:security-group/*", | |
"Condition": { | |
"StringEquals": { | |
"ec2:CreateAction": "CreateSecurityGroup" | |
}, | |
"Null": { | |
"aws:RequestTag/elbv2.k8s.aws/cluster": "false" | |
} | |
} | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ec2:CreateTags", | |
"ec2:DeleteTags" | |
], | |
"Resource": "arn:aws:ec2:*:*:security-group/*", | |
"Condition": { | |
"Null": { | |
"aws:RequestTag/elbv2.k8s.aws/cluster": "true", | |
"aws:ResourceTag/elbv2.k8s.aws/cluster": "false" | |
} | |
} | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"ec2:AuthorizeSecurityGroupIngress", | |
"ec2:RevokeSecurityGroupIngress", | |
"ec2:DeleteSecurityGroup" | |
], | |
"Resource": "*", | |
"Condition": { | |
"Null": { | |
"aws:ResourceTag/elbv2.k8s.aws/cluster": "false" | |
} | |
} | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"elasticloadbalancing:CreateLoadBalancer", | |
"elasticloadbalancing:CreateTargetGroup" | |
], | |
"Resource": "*", | |
"Condition": { | |
"Null": { | |
"aws:RequestTag/elbv2.k8s.aws/cluster": "false" | |
} | |
} | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"elasticloadbalancing:CreateListener", | |
"elasticloadbalancing:DeleteListener", | |
"elasticloadbalancing:CreateRule", | |
"elasticloadbalancing:DeleteRule" | |
], | |
"Resource": "*" | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"elasticloadbalancing:AddTags", | |
"elasticloadbalancing:RemoveTags" | |
], | |
"Resource": [ | |
"arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", | |
"arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", | |
"arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" | |
], | |
"Condition": { | |
"Null": { | |
"aws:RequestTag/elbv2.k8s.aws/cluster": "true", | |
"aws:ResourceTag/elbv2.k8s.aws/cluster": "false" | |
} | |
} | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"elasticloadbalancing:AddTags", | |
"elasticloadbalancing:RemoveTags" | |
], | |
"Resource": [ | |
"arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*", | |
"arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*", | |
"arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*", | |
"arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*" | |
] | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"elasticloadbalancing:ModifyLoadBalancerAttributes", | |
"elasticloadbalancing:SetIpAddressType", | |
"elasticloadbalancing:SetSecurityGroups", | |
"elasticloadbalancing:SetSubnets", | |
"elasticloadbalancing:DeleteLoadBalancer", | |
"elasticloadbalancing:ModifyTargetGroup", | |
"elasticloadbalancing:ModifyTargetGroupAttributes", | |
"elasticloadbalancing:DeleteTargetGroup" | |
], | |
"Resource": "*", | |
"Condition": { | |
"Null": { | |
"aws:ResourceTag/elbv2.k8s.aws/cluster": "false" | |
} | |
} | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"elasticloadbalancing:RegisterTargets", | |
"elasticloadbalancing:DeregisterTargets" | |
], | |
"Resource": "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*" | |
}, | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"elasticloadbalancing:SetWebAcl", | |
"elasticloadbalancing:ModifyListener", | |
"elasticloadbalancing:AddListenerCertificates", | |
"elasticloadbalancing:RemoveListenerCertificates", | |
"elasticloadbalancing:ModifyRule" | |
], | |
"Resource": "*" | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role" "aws_load_balancer_controller" { | |
name = "${var.cluster_name}-alb-controller-role" | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Principal = { | |
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}" | |
} | |
Action = "sts:AssumeRoleWithWebIdentity" | |
Condition = { | |
StringEquals = { | |
"${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller" | |
} | |
} | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "aws_load_balancer_controller" { | |
policy_arn = aws_iam_policy.aws_load_balancer_controller.arn | |
role = aws_iam_role.aws_load_balancer_controller.name | |
} | |
resource "helm_release" "aws_load_balancer_controller" { | |
name = "aws-load-balancer-controller" | |
repository = "https://aws.github.io/eks-charts" | |
chart = "aws-load-balancer-controller" | |
namespace = "kube-system" | |
version = "1.6.0" | |
set { | |
name = "clusterName" | |
value = aws_eks_cluster.main.name | |
} | |
set { | |
name = "serviceAccount.create" | |
value = "true" | |
} | |
set { | |
name = "serviceAccount.name" | |
value = "aws-load-balancer-controller" | |
} | |
set { | |
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.aws_load_balancer_controller.arn | |
} | |
depends_on = [ | |
aws_eks_node_group.main, | |
kubernetes_config_map.aws_auth | |
] | |
} | |
# AWS Auth Config Map | |
resource "kubernetes_config_map" "aws_auth" { | |
metadata { | |
name = "aws-auth" | |
namespace = "kube-system" | |
} | |
data = { | |
mapRoles = yamlencode([ | |
{ | |
rolearn = aws_iam_role.nodes.arn | |
username = "system:node:{{EC2PrivateDNSName}}" | |
groups = ["system:bootstrappers", "system:nodes"] | |
} | |
]) | |
} | |
depends_on = [ | |
aws_eks_cluster.main | |
] | |
} | |
# Cluster Autoscaler | |
resource "aws_iam_role" "cluster_autoscaler" { | |
name = "${var.cluster_name}-cluster-autoscaler-role" | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Principal = { | |
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}" | |
} | |
Action = "sts:AssumeRoleWithWebIdentity" | |
Condition = { | |
StringEquals = { | |
"${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}:sub": "system:serviceaccount:kube-system:cluster-autoscaler" | |
} | |
} | |
} | |
] | |
}) | |
} | |
resource "aws_iam_policy" "cluster_autoscaler" { | |
name = "${var.cluster_name}-cluster-autoscaler-policy" | |
description = "Policy for Cluster Autoscaler" | |
policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Action = [ | |
"autoscaling:DescribeAutoScalingGroups", | |
"autoscaling:DescribeAutoScalingInstances", | |
"autoscaling:DescribeLaunchConfigurations", | |
"autoscaling:DescribeTags", | |
"autoscaling:SetDesiredCapacity", | |
"autoscaling:TerminateInstanceInAutoScalingGroup", | |
"ec2:DescribeLaunchTemplateVersions" | |
], | |
Resource = "*" | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "cluster_autoscaler" { | |
policy_arn = aws_iam_policy.cluster_autoscaler.arn | |
role = aws_iam_role.cluster_autoscaler.name | |
} | |
resource "helm_release" "cluster_autoscaler" { | |
name = "cluster-autoscaler" | |
repository = "https://kubernetes.github.io/autoscaler" | |
chart = "cluster-autoscaler" | |
namespace = "kube-system" | |
version = "9.29.0" | |
set { | |
name = "autoDiscovery.clusterName" | |
value = aws_eks_cluster.main.name | |
} | |
set { | |
name = "awsRegion" | |
value = var.region | |
} | |
set { | |
name = "rbac.serviceAccount.create" | |
value = "true" | |
} | |
set { | |
name = "rbac.serviceAccount.name" | |
value = "cluster-autoscaler" | |
} | |
set { | |
name = "rbac.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.cluster_autoscaler.arn | |
} | |
# Improve scaling performance and reduce throttling | |
set { | |
name = "extraArgs.scan-interval" | |
value = "30s" | |
} | |
set { | |
name = "extraArgs.max-node-provision-time" | |
value = "15m0s" | |
} | |
set { | |
name = "extraArgs.scale-down-delay-after-add" | |
value = "10m" | |
} | |
depends_on = [ | |
aws_eks_node_group.main, | |
kubernetes_config_map.aws_auth | |
] | |
} | |
# External DNS | |
resource "aws_iam_role" "external_dns" { | |
name = "${var.cluster_name}-external-dns-role" | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Principal = { | |
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}" | |
} | |
Action = "sts:AssumeRoleWithWebIdentity" | |
Condition = { | |
StringEquals = { | |
"${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}:sub": "system:serviceaccount:kube-system:external-dns" | |
} | |
} | |
} | |
] | |
}) | |
} | |
resource "aws_iam_policy" "external_dns" { | |
name = "${var.cluster_name}-external-dns-policy" | |
description = "Policy for External DNS" | |
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 = [ | |
"*" | |
] | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "external_dns" { | |
policy_arn = aws_iam_policy.external_dns.arn | |
role = aws_iam_role.external_dns.name | |
} | |
resource "helm_release" "external_dns" { | |
name = "external-dns" | |
repository = "https://charts.bitnami.com/bitnami" | |
chart = "external-dns" | |
namespace = "kube-system" | |
version = "6.28.1" | |
set { | |
name = "provider" | |
value = "aws" | |
} | |
set { | |
name = "aws.region" | |
value = var.region | |
} | |
set { | |
name = "serviceAccount.create" | |
value = "true" | |
} | |
set { | |
name = "serviceAccount.name" | |
value = "external-dns" | |
} | |
set { | |
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.external_dns.arn | |
} | |
set { | |
name = "policy" | |
value = "sync" | |
} | |
set { | |
name = "txtOwnerId" | |
value = var.cluster_name | |
} | |
depends_on = [ | |
aws_eks_node_group.main, | |
kubernetes_config_map.aws_auth | |
] | |
} | |
# Metrics Server | |
resource "helm_release" "metrics_server" { | |
name = "metrics-server" | |
repository = "https://kubernetes-sigs.github.io/metrics-server/" | |
chart = "metrics-server" | |
namespace = "kube-system" | |
version = "3.11.0" | |
set { | |
name = "args[0]" | |
value = "--kubelet-insecure-tls" | |
} | |
set { | |
name = "args[1]" | |
value = "--kubelet-preferred-address-types=InternalIP" | |
} | |
depends_on = [ | |
aws_eks_node_group.main, | |
kubernetes_config_map.aws_auth | |
] | |
} | |
# Prometheus and Grafana for monitoring | |
resource "kubernetes_namespace" "monitoring" { | |
metadata { | |
name = "monitoring" | |
} | |
depends_on = [ | |
aws_eks_cluster.main | |
] | |
} | |
resource "helm_release" "prometheus_operator" { | |
name = "prometheus" | |
repository = "https://prometheus-community.github.io/helm-charts" | |
chart = "kube-prometheus-stack" | |
namespace = kubernetes_namespace.monitoring.metadata[0].name | |
version = "55.5.0" | |
set { | |
name = "grafana.enabled" | |
value = "true" | |
} | |
set { | |
name = "grafana.adminPassword" | |
value = "changeme" # In production, use aws_ssm_parameter or kubernetes_secret | |
} | |
set { | |
name = "prometheus.prometheusSpec.retention" | |
value = "15d" | |
} | |
set { | |
name = "prometheus.prometheusSpec.resources.requests.cpu" | |
value = "500m" | |
} | |
set { | |
name = "prometheus.prometheusSpec.resources.requests.memory" | |
value = "2Gi" | |
} | |
set { | |
name = "prometheus.prometheusSpec.resources.limits.cpu" | |
value = "1000m" | |
} | |
set { | |
name = "prometheus.prometheusSpec.resources.limits.memory" | |
value = "4Gi" | |
} | |
set { | |
name = "prometheusOperator.tls.enabled" | |
value = "true" | |
} | |
depends_on = [ | |
aws_eks_node_group.main, | |
kubernetes_config_map.aws_auth | |
] | |
} | |
# Create an EFS filesystem for persistent storage | |
resource "aws_efs_file_system" "eks" { | |
creation_token = "${var.cluster_name}-efs" | |
encrypted = true | |
tags = { | |
Name = "${var.cluster_name}-efs" | |
} | |
} | |
resource "aws_security_group" "efs" { | |
name = "${var.cluster_name}-efs-sg" | |
description = "Allow traffic from EKS nodes to EFS" | |
vpc_id = aws_vpc.main.id | |
ingress { | |
description = "Allow NFS traffic from EKS nodes" | |
from_port = 2049 | |
to_port = 2049 | |
protocol = "tcp" | |
security_groups = [aws_security_group.nodes.id] | |
} | |
egress { | |
from_port = 0 | |
to_port = 0 | |
protocol = "-1" | |
cidr_blocks = ["0.0.0.0/0"] | |
} | |
tags = { | |
Name = "${var.cluster_name}-efs-sg" | |
} | |
} | |
resource "aws_efs_mount_target" "eks" { | |
count = length(var.availability_zones) | |
file_system_id = aws_efs_file_system.eks.id | |
subnet_id = aws_subnet.private[count.index].id | |
security_groups = [aws_security_group.efs.id] | |
} | |
# EFS CSI Driver | |
resource "aws_iam_role" "efs_csi_driver" { | |
name = "${var.cluster_name}-efs-csi-driver-role" | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Principal = { | |
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}" | |
} | |
Action = "sts:AssumeRoleWithWebIdentity" | |
Condition = { | |
StringEquals = { | |
"${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}:sub": "system:serviceaccount:kube-system:efs-csi-controller-sa" | |
} | |
} | |
} | |
] | |
}) | |
} | |
resource "aws_iam_policy" "efs_csi_driver" { | |
name = "${var.cluster_name}-efs-csi-driver-policy" | |
description = "Policy for EFS CSI Driver" | |
policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Action = [ | |
"elasticfilesystem:DescribeAccessPoints", | |
"elasticfilesystem:DescribeFileSystems", | |
"elasticfilesystem:DescribeMountTargets", | |
"ec2:DescribeAvailabilityZones" | |
] | |
Resource = "*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"elasticfilesystem:CreateAccessPoint" | |
] | |
Resource = "*" | |
Condition = { | |
StringLike = { | |
"aws:RequestTag/kubernetes.io/cluster/*": "owned" | |
} | |
} | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"elasticfilesystem:DeleteAccessPoint" | |
] | |
Resource = "*" | |
Condition = { | |
StringEquals = { | |
"aws:ResourceTag/kubernetes.io/cluster/${var.cluster_name}": "owned" | |
} | |
} | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "efs_csi_driver" { | |
policy_arn = aws_iam_policy.efs_csi_driver.arn | |
role = aws_iam_role.efs_csi_driver.name | |
} | |
resource "helm_release" "efs_csi_driver" { | |
name = "aws-efs-csi-driver" | |
repository = "https://kubernetes-sigs.github.io/aws-efs-csi-driver/" | |
chart = "aws-efs-csi-driver" | |
namespace = "kube-system" | |
version = "2.5.2" | |
set { | |
name = "controller.serviceAccount.create" | |
value = "true" | |
} | |
set { | |
name = "controller.serviceAccount.name" | |
value = "efs-csi-controller-sa" | |
} | |
set { | |
name = "controller.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.efs_csi_driver.arn | |
} | |
depends_on = [ | |
aws_eks_node_group.main, | |
aws_efs_mount_target.eks, | |
kubernetes_config_map.aws_auth | |
] | |
} | |
# Create storage class for EFS | |
resource "kubernetes_storage_class" "efs" { | |
metadata { | |
name = "efs-sc" | |
} | |
storage_provisioner = "efs.csi.aws.com" | |
reclaim_policy = "Retain" | |
parameters = { | |
provisioningMode = "efs-ap" | |
fileSystemId = aws_efs_file_system.eks.id | |
directoryPerms = "700" | |
} | |
mount_options = ["tls"] | |
depends_on = [ | |
helm_release.efs_csi_driver | |
] | |
} | |
# Install Cert-Manager for TLS certificate management | |
resource "helm_release" "cert_manager" { | |
name = "cert-manager" | |
repository = "https://charts.jetstack.io" | |
chart = "cert-manager" | |
namespace = "cert-manager" | |
version = "v1.13.3" | |
create_namespace = true | |
set { | |
name = "installCRDs" | |
value = "true" | |
} | |
set { | |
name = "prometheus.enabled" | |
value = "true" | |
} | |
depends_on = [ | |
aws_eks_node_group.main, | |
kubernetes_config_map.aws_auth | |
] | |
} | |
# Istio service mesh for advanced networking | |
resource "kubernetes_namespace" "istio_system" { | |
metadata { | |
name = "istio-system" | |
labels = { | |
istio-injection = "disabled" | |
} | |
} | |
depends_on = [ | |
aws_eks_cluster.main | |
] | |
} | |
resource "helm_release" "istio_base" { | |
name = "istio-base" | |
repository = "https://istio-release.storage.googleapis.com/charts" | |
chart = "base" | |
namespace = kubernetes_namespace.istio_system.metadata[0].name | |
version = "1.20.0" | |
depends_on = [ | |
aws_eks_node_group.main, | |
kubernetes_config_map.aws_auth | |
] | |
} | |
resource "helm_release" "istiod" { | |
name = "istiod" | |
repository = "https://istio-release.storage.googleapis.com/charts" | |
chart = "istiod" | |
namespace = kubernetes_namespace.istio_system.metadata[0].name | |
version = "1.20.0" | |
set { | |
name = "pilot.resources.requests.cpu" | |
value = "500m" | |
} | |
set { | |
name = "pilot.resources.requests.memory" | |
value = "2Gi" | |
} | |
set { | |
name = "global.proxy.resources.requests.cpu" | |
value = "100m" | |
} | |
set { | |
name = "global.proxy.resources.requests.memory" | |
value = "128Mi" | |
} | |
set { | |
name = "global.proxy.resources.limits.cpu" | |
value = "500m" | |
} | |
set { | |
name = "global.proxy.resources.limits.memory" | |
value = "512Mi" | |
} | |
depends_on = [ | |
helm_release.istio_base | |
] | |
} | |
resource "helm_release" "istio_ingress" { | |
name = "istio-ingress" | |
repository = "https://istio-release.storage.googleapis.com/charts" | |
chart = "gateway" | |
namespace = kubernetes_namespace.istio_system.metadata[0].name | |
version = "1.20.0" | |
set { | |
name = "service.type" | |
value = "LoadBalancer" | |
} | |
set { | |
name = "service.annotations.service\\.beta\\.kubernetes\\.io/aws-load-balancer-type" | |
value = "nlb" | |
} | |
set { | |
name = "resources.requests.cpu" | |
value = "100m" | |
} | |
set { | |
name = "resources.requests.memory" | |
value = "128Mi" | |
} | |
set { | |
name = "resources.limits.cpu" | |
value = "500m" | |
} | |
set { | |
name = "resources.limits.memory" | |
value = "512Mi" | |
} | |
depends_on = [ | |
helm_release.istiod | |
] | |
} | |
# Velero for backup and restore | |
resource "kubernetes_namespace" "velero" { | |
metadata { | |
name = "velero" | |
} | |
depends_on = [ | |
aws_eks_cluster.main | |
] | |
} | |
resource "aws_s3_bucket" "velero" { | |
bucket = "${var.cluster_name}-velero-backups" | |
tags = { | |
Name = "${var.cluster_name}-velero-backups" | |
} | |
} | |
resource "aws_s3_bucket_versioning" "velero" { | |
bucket = aws_s3_bucket.velero.id | |
versioning_configuration { | |
status = "Enabled" | |
} | |
} | |
resource "aws_s3_bucket_server_side_encryption_configuration" "velero" { | |
bucket = aws_s3_bucket.velero.id | |
rule { | |
apply_server_side_encryption_by_default { | |
sse_algorithm = "AES256" | |
} | |
} | |
} | |
resource "aws_iam_role" "velero" { | |
name = "${var.cluster_name}-velero-role" | |
assume_role_policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Principal = { | |
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}" | |
} | |
Action = "sts:AssumeRoleWithWebIdentity" | |
Condition = { | |
StringEquals = { | |
"${replace(aws_eks_cluster.main.identity[0].oidc[0].issuer, "https://", "")}:sub": "system:serviceaccount:velero:velero" | |
} | |
} | |
} | |
] | |
}) | |
} | |
resource "aws_iam_policy" "velero" { | |
name = "${var.cluster_name}-velero-policy" | |
description = "Policy for Velero backup and restore" | |
policy = jsonencode({ | |
Version = "2012-10-17" | |
Statement = [ | |
{ | |
Effect = "Allow" | |
Action = [ | |
"ec2:DescribeVolumes", | |
"ec2:DescribeSnapshots", | |
"ec2:CreateTags", | |
"ec2:CreateVolume", | |
"ec2:CreateSnapshot", | |
"ec2:DeleteSnapshot" | |
] | |
Resource = "*" | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"s3:GetObject", | |
"s3:DeleteObject", | |
"s3:PutObject", | |
"s3:AbortMultipartUpload", | |
"s3:ListMultipartUploadParts" | |
] | |
Resource = [ | |
"${aws_s3_bucket.velero.arn}/*" | |
] | |
}, | |
{ | |
Effect = "Allow" | |
Action = [ | |
"s3:ListBucket" | |
] | |
Resource = [ | |
"${aws_s3_bucket.velero.arn}" | |
] | |
} | |
] | |
}) | |
} | |
resource "aws_iam_role_policy_attachment" "velero" { | |
policy_arn = aws_iam_policy.velero.arn | |
role = aws_iam_role.velero.name | |
} | |
resource "helm_release" "velero" { | |
name = "velero" | |
repository = "https://vmware-tanzu.github.io/helm-charts" | |
chart = "velero" | |
namespace = kubernetes_namespace.velero.metadata[0].name | |
version = "5.1.3" | |
set { | |
name = "credentials.useSecret" | |
value = "false" | |
} | |
set { | |
name = "serviceAccount.server.annotations.eks\\.amazonaws\\.com/role-arn" | |
value = aws_iam_role.velero.arn | |
} | |
set { | |
name = "configuration.provider" | |
value = "aws" | |
} | |
set { | |
name = "configuration.backupStorageLocation.bucket" | |
value = aws_s3_bucket.velero.id | |
} | |
set { | |
name = "configuration.backupStorageLocation.config.region" | |
value = var.region | |
} | |
set { | |
name = "configuration.volumeSnapshotLocation.config.region" | |
value = var.region | |
} | |
set { | |
name = "initContainers[0].name" | |
value = "velero-plugin-for-aws" | |
} | |
set { | |
name = "initContainers[0].image" | |
value = "velero/velero-plugin-for-aws:v1.8.0" | |
} | |
set { | |
name = "initContainers[0].volumeMounts[0].mountPath" | |
value = "/target" | |
} | |
set { | |
name = "initContainers[0].volumeMounts[0].name" | |
value = "plugins" | |
} | |
# Configure backup schedule | |
set { | |
name = "schedules.daily-backup.schedule" | |
value = "0 1 * * *" # 1:00 AM UTC every day | |
} | |
set { | |
name = "schedules.daily-backup.template.ttl" | |
value = "168h" # 7 days | |
} | |
set { | |
name = "schedules.daily-backup.template.includedNamespaces" | |
value = "{*}" | |
} | |
set { | |
name = "schedules.daily-backup.template.snapshotVolumes" | |
value = "true" | |
} | |
depends_on = [ | |
aws_eks_node_group.main, | |
kubernetes_config_map.aws_auth, | |
aws_s3_bucket.velero | |
] | |
} | |
# AWS Caller Identity Data Source | |
data "aws_caller_identity" "current" {} | |
# Outputs | |
output "cluster_endpoint" { | |
description = "Endpoint for EKS control plane" | |
value = aws_eks_cluster.main.endpoint | |
} | |
output "cluster_security_group_id" { | |
description = "Security group ID for the cluster control plane" | |
value = aws_security_group.cluster.id | |
} | |
output "kubectl_config" { | |
description = "kubectl config as generated by the aws eks get-token command" | |
value = "aws eks --region ${var.region} update-kubeconfig --name ${aws_eks_cluster.main.name}" | |
} | |
output "config_map_aws_auth" { | |
description = "The aws-auth ConfigMap used to connect to the EKS cluster" | |
value = kubernetes_config_map.aws_auth.metadata[0].name | |
} | |
output "efs_id" { | |
description = "ID of the EFS file system for persistent storage" | |
value = aws_efs_file_system.eks.id | |
} | |
output "velero_s3_bucket" { | |
description = "S3 bucket for Velero backups" | |
value = aws_s3_bucket.velero.id | |
} | |
output "private_subnets" { | |
description = "List of private subnet IDs" | |
value = aws_subnet.private[*].id | |
} | |
output "public_subnets" { | |
description = "List of public subnet IDs" | |
value = aws_subnet.public[*].id | |
} | |
output "vpc_id" { | |
description = "VPC ID for the Kubernetes cluster" | |
value = aws_vpc.main.id | |
} | |
output "node_security_group_id" { | |
description = "Security group ID for the worker nodes" | |
value = aws_security_group.nodes.id | |
} | |
output "cluster_name" { | |
description = "Kubernetes Cluster Name" | |
value = aws_eks_cluster.main.name | |
} | |
output "cluster_version" { | |
description = "Kubernetes Cluster Version" | |
value = aws_eks_cluster.main.version | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment