Terraform AWS / EKS Templates
Most AWS Kubernetes Terraform work creates a VPC, private/public subnets, EKS control plane, managed node groups, IAM roles, security groups, OIDC/IRSA roles, and add-on dependencies. Keep cluster creation, add-on Helm releases, and app workloads clearly separated.
Provider And Backend
This block pins Terraform and AWS provider versions and configures S3 remote state with locking. Use it at the root of an AWS environment so plans are reproducible and multiple engineers do not apply against local state.
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "client-prod-terraform-state"
key = "aws/eks/prod.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "terraform"
Owner = "platform"
}
}
}VPC And Subnet Tags
EKS load balancers depend on subnet tags. Public subnets usually get kubernetes.io/role/elb=1; private subnets get kubernetes.io/role/internal-elb=1.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "${var.name}-${var.environment}"
cidr = "10.40.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.40.1.0/24", "10.40.2.0/24", "10.40.3.0/24"]
public_subnets = ["10.40.101.0/24", "10.40.102.0/24", "10.40.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = false
enable_dns_hostnames = true
public_subnet_tags = {
"kubernetes.io/role/elb" = "1"
}
private_subnet_tags = {
"kubernetes.io/role/internal-elb" = "1"
}
}EKS Cluster And Node Groups
This module creates the EKS control plane and managed node groups. Use separate node groups for different workload classes such as general, spot, GPU, or isolated platform workloads.
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "${var.name}-${var.environment}"
cluster_version = var.cluster_version
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
enable_irsa = true
cluster_endpoint_public_access = true
cluster_endpoint_private_access = true
enable_cluster_creator_admin_permissions = true
eks_managed_node_groups = {
general = {
min_size = 2
max_size = 6
desired_size = 3
instance_types = ["m6i.large"]
capacity_type = "ON_DEMAND"
labels = {
role = "general"
}
}
spot = {
min_size = 0
max_size = 10
desired_size = 2
instance_types = ["m6i.large", "m5.large"]
capacity_type = "SPOT"
taints = {
spot = {
key = "capacity"
value = "spot"
effect = "NO_SCHEDULE"
}
}
}
}
}Security Group Pattern
This pattern creates an explicit security group plus separate ingress and egress rule resources. Use it for load balancers or platform endpoints where reviewers need to see exactly which ports and CIDRs are allowed.
resource "aws_security_group" "app_lb" {
name = "${var.name}-${var.environment}-app-lb"
description = "Ingress from internet to public ALB"
vpc_id = module.vpc.vpc_id
}
resource "aws_vpc_security_group_ingress_rule" "app_lb_https" {
security_group_id = aws_security_group.app_lb.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 443
to_port = 443
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_egress_rule" "app_lb_all_egress" {
security_group_id = aws_security_group.app_lb.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}IRSA Role For A Pod
IRSA maps a Kubernetes ServiceAccount to a narrow IAM role. Use this for ExternalDNS, AWS Load Balancer Controller, EBS CSI, External Secrets, or app pods that need AWS API access.
module "external_dns_irsa" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
version = "~> 5.0"
role_name = "${var.name}-${var.environment}-external-dns"
attach_external_dns_policy = true
external_dns_hosted_zone_arns = ["arn:aws:route53:::hostedzone/Z1234567890"]
oidc_providers = {
main = {
provider_arn = module.eks.oidc_provider_arn
namespace_service_accounts = ["external-dns:external-dns"]
}
}
}Custom IAM Policy
This policy grants narrow read access to one S3 bucket. Use this shape when an app, controller, or CI role needs a custom permission set that is smaller than a managed AWS policy.
data "aws_iam_policy_document" "app_s3_read" {
statement {
actions = ["s3:GetObject", "s3:ListBucket"]
resources = [
"arn:aws:s3:::client-prod-app-config",
"arn:aws:s3:::client-prod-app-config/*"
]
}
}
resource "aws_iam_policy" "app_s3_read" {
name = "${var.name}-${var.environment}-app-s3-read"
policy = data.aws_iam_policy_document.app_s3_read.json
}Add-On Ownership
| Add-on | Terraform owns | Helm/GitOps owns |
|---|---|---|
| AWS Load Balancer Controller | IRSA role and policy | Helm release and values |
| ExternalDNS | Route53-scoped IRSA role | Deployment, flags, domain filters |
| EBS CSI | IAM role, KMS policy | EKS add-on or Helm chart |
| cert-manager | Route53 DNS-01 permissions | cert-manager chart and Issuers |