Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
••••••••••••••••••••
Intelligence View
⚡ tsecurity.de Intelligence

Building an Amazon EKS Cluster with raw Terraform Resources

Most guides for provisioning Amazon EKS use the AWS community Terraform module for convenience. While this is great for speed, it often abstracts away what’s actually happening behind the scenes. In this blog, we will build an EKS cluster …

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Most guides for provisioning Amazon EKS use the AWS community Terraform module for convenience. While this is great for speed, it often abstracts away what’s actually happening behind the scenes.



In this blog, we will build an EKS cluster from scratch using raw Terraform resources (without using the community module). By doing this, you’ll gain a clear understanding of the AWS networking, IAM, and compute resources that make up a Kubernetes cluster on AWS.



Finally, we’ll deploy a sample microservices application (Voting App) on the cluster and access it using port-forwarding.






Architecture



Here’s how the setup works at a high level:




  • VPC is created with 2 Availability Zones for high availability.

  • Each AZ contains both a public and a private subnet.

  • EKS worker nodes (EC2 instances) are launched in private subnets for better security.

  • A NAT Gateway is provisioned in a public subnet to allow worker nodes in private subnets to pull images and updates from the internet (e.g., from ECR, Docker Hub).

  • EKS control plane (managed by AWS) communicates with the worker nodes securely within the VPC.
    This setup ensures that your nodes are not directly exposed to the internet while still having outbound internet access via the NAT gateway.








Step 1: Create a VPC with Public and Private Subnets



EKS requires a properly configured VPC with both public and private subnets. Worker nodes generally live in private subnets, while the NAT gateway in public subnets provides outbound internet connectivity.



We also add Internet Gateway (IGW), NAT Gateway, and route tables to handle connectivity between public/private subnets.




################################################################################
# VPC
################################################################################
resource "aws_vpc" "custom_vpc" {
cidr_block = var.networking.cidr_block

tags = merge(var.common_tags, {
Name = "${var.naming_prefix}-VPC"
})
}

################################################################################
# PUBLIC SUBNETS
################################################################################
resource "aws_subnet" "public_subnets" {
count = var.networking.public_subnets == null || var.networking.public_subnets == "" ? 0 : length(var.networking.public_subnets)
vpc_id = aws_vpc.custom_vpc.id
cidr_block = var.networking.public_subnets[count.index]
availability_zone = var.networking.azs[count.index]
map_public_ip_on_launch = true

tags = merge(var.common_tags, {
Name = "${var.naming_prefix}-public-subnet-${count.index}"
"kubernetes.io/role/elb" = "1"
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
})
}

################################################################################
# PRIVATE SUBNETS
################################################################################
resource "aws_subnet" "private_subnets" {
count = var.networking.private_subnets == null || var.networking.private_subnets == "" ? 0 : length(var.networking.private_subnets)
vpc_id = aws_vpc.custom_vpc.id
cidr_block = var.networking.private_subnets[count.index]
availability_zone = var.networking.azs[count.index]
map_public_ip_on_launch = false

tags = merge(var.common_tags, {
Name = "${var.naming_prefix}-private-subnet-${count.index}"
"kubernetes.io/role/internal-elb" = "1"
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
})
}

################################################################################
# INTERNET GATEWAY
################################################################################
resource "aws_internet_gateway" "i_gateway" {
vpc_id = aws_vpc.custom_vpc.id

tags = merge(var.common_tags, {
Name = "${var.naming_prefix}-i-gateway"
})
}

################################################################################
# EIPs
################################################################################
resource "aws_eip" "elastic_ip" {
count = var.networking.private_subnets == null || var.networking.nat_gateways == false ? 0 : length(var.networking.private_subnets)
depends_on = [aws_internet_gateway.i_gateway]

tags = merge(var.common_tags, {
Name = "${var.naming_prefix}-eip-${count.index}"
})
}

################################################################################
# NAT GATEWAYS
################################################################################
resource "aws_nat_gateway" "nats" {
count = var.networking.private_subnets == null || var.networking.nat_gateways == false ? 0 : length(var.networking.private_subnets)
subnet_id = aws_subnet.public_subnets[count.index].id
connectivity_type = "public"
allocation_id = aws_eip.elastic_ip[count.index].id
depends_on = [aws_internet_gateway.i_gateway]

tags = merge(var.common_tags, {
Name = "${var.naming_prefix}-nat-gw-${count.index}"
})
}

################################################################################
# PUBLIC ROUTE TABLE
################################################################################
resource "aws_route_table" "public_table" {
vpc_id = aws_vpc.custom_vpc.id
}

resource "aws_route" "public_routes" {
route_table_id = aws_route_table.public_table.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.i_gateway.id
}

resource "aws_route_table_association" "assoc_public_routes" {
count = length(var.networking.public_subnets)
subnet_id = aws_subnet.public_subnets[count.index].id
route_table_id = aws_route_table.public_table.id
}

################################################################################
# PRIVATE ROUTE TABLES
################################################################################
resource "aws_route_table" "private_tables" {
count = length(var.networking.azs)
vpc_id = aws_vpc.custom_vpc.id
}

resource "aws_route" "private_routes" {
count = length(var.networking.private_subnets)
route_table_id = aws_route_table.private_tables[count.index].id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nats[count.index].id
}

resource "aws_route_table_association" "assoc_private_routes" {
count = length(var.networking.private_subnets)
subnet_id = aws_subnet.private_subnets[count.index].id
route_table_id = aws_route_table.private_tables[count.index].id
}

################################################################################
# SECURITY GROUPS
################################################################################
resource "aws_security_group" "sec_groups" {
for_each = { for sec in var.security_groups : sec.name => sec }
name = "${var.naming_prefix}-${each.value.name}"
description = each.value.description
vpc_id = aws_vpc.custom_vpc.id

dynamic "ingress" {
for_each = try(each.value.ingress, [])
content {
description = ingress.value.description
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
ipv6_cidr_blocks = ingress.value.ipv6_cidr_blocks
}
}

dynamic "egress" {
for_each = try(each.value.egress, [])
content {
description = egress.value.description
from_port = egress.value.from_port
to_port = egress.value.to_port
protocol = egress.value.protocol
cidr_blocks = egress.value.cidr_blocks
ipv6_cidr_blocks = egress.value.ipv6_cidr_blocks
}
}
}









Step 2: Create IAM Roles and Policies



EKS needs roles for both the control plane and worker nodes.




  • Cluster Role – Grants permissions for EKS to manage AWS resources.

  • Node Role – Allows EC2 instances (worker nodes) to join the cluster.




################################################################################
# EKS CLUSTER ROLE
################################################################################
resource "aws_iam_role" "EKSClusterRole" {
name = "${var.naming_prefix}-EKSClusterRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "eks.amazonaws.com"
}
},
]
})
}

// This policy provides Kubernetes the permissions it requires to manage resources on your behalf.
// Kubernetes requires Ec2:CreateTags permissions to place identifying information on EC2 resources
// including but not limited to Instances, Security Groups, and Elastic Network Interfaces
resource "aws_iam_role_policy_attachment" "AmazonEKSClusterPolicy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
role = aws_iam_role.EKSClusterRole.name
}

################################################################################
# NODE GROUP ROLE
################################################################################
resource "aws_iam_role" "NodeGroupRole" {
name = "${var.naming_prefix}-EKSNodeGroupRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
},
]
})
}

//This policy allows Amazon EKS worker nodes to connect to Amazon EKS Clusters.
resource "aws_iam_role_policy_attachment" "AmazonEKSWorkerNodePolicy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
role = aws_iam_role.NodeGroupRole.name
}

// Provides read-only access to Amazon EC2 Container Registry repositories.
resource "aws_iam_role_policy_attachment" "AmazonEC2ContainerRegistryReadOnly" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
role = aws_iam_role.NodeGroupRole.name
}

// This policy provides the Amazon VPC CNI Plugin (amazon-vpc-cni-k8s) the permissions
// it requires to modify the IP address configuration on your EKS worker nodes.
// This permission set allows the CNI to list, describe, and modify ENIs on your behalf
resource "aws_iam_role_policy_attachment" "AmazonEKS_CNI_Policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
role = aws_iam_role.NodeGroupRole.name
}









Step 3: Provision the EKS Cluster



Following creates the EKS control plane.




################################################################################
# EKS Cluster
################################################################################
resource "aws_eks_cluster" "eks-cluster" {
name = var.cluster_config.name
role_arn = aws_iam_role.EKSClusterRole.arn
version = var.cluster_config.version

vpc_config {
subnet_ids = flatten([var.public_subnets_id, var.private_subnets_id])
security_group_ids = flatten(var.security_groups_id)
}

depends_on = [
aws_iam_role_policy_attachment.AmazonEKSClusterPolicy
]

}






At this point, we create a managed set of EC2 worker nodes with basic required addons




################################################################################
# NODE GROUP
################################################################################
resource "aws_eks_node_group" "node-ec2" {
for_each = { for node_group in var.node_groups : node_group.name => node_group }
cluster_name = aws_eks_cluster.eks-cluster.name
node_group_name = "${var.naming_prefix}-${each.value.name}"
node_role_arn = aws_iam_role.NodeGroupRole.arn
subnet_ids = flatten(var.private_subnets_id)

scaling_config {
desired_size = try(each.value.scaling_config.desired_size, 2)
max_size = try(each.value.scaling_config.max_size, 3)
min_size = try(each.value.scaling_config.min_size, 1)
}

update_config {
max_unavailable = try(each.value.update_config.max_unavailable, 1)
}

ami_type = each.value.ami_type
instance_types = each.value.instance_types
capacity_type = each.value.capacity_type
disk_size = each.value.disk_size
version = var.cluster_config.version

tags = merge(var.common_tags, {
Name = "${var.naming_prefix}-nodes-${each.value.name}"
})

depends_on = [
aws_iam_role_policy_attachment.AmazonEKSWorkerNodePolicy,
aws_iam_role_policy_attachment.AmazonEC2ContainerRegistryReadOnly,
aws_iam_role_policy_attachment.AmazonEKS_CNI_Policy
]
}

resource "aws_iam_openid_connect_provider" "eks_oidc_provider" {
url = aws_eks_cluster.eks-cluster.identity[0].oidc[0].issuer
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"]
}


################################################################################
# EKS Addons
################################################################################
data "aws_eks_addon_version" "eks_addons" {
for_each = var.addons

addon_name = each.value.name
kubernetes_version = aws_eks_cluster.eks-cluster.version
most_recent = coalesce(each.value.most_recent, true)
}

resource "aws_eks_addon" "addons" {
for_each = var.addons

cluster_name = aws_eks_cluster.eks-cluster.name
addon_name = each.value.name
addon_version = coalesce(each.value.version, data.aws_eks_addon_version.eks_addons[each.key].version)
resolve_conflicts_on_create = "OVERWRITE"

tags = merge(var.common_tags, {
Name = "${var.naming_prefix}-addon-${each.value.name}"
})

depends_on = [aws_eks_node_group.node-ec2]
}









Step 4: Configure kubectl and Access Cluster






################################################################################
# Update kubeconfig with created cluster
################################################################################
resource "null_resource" "update_kubeconfig" {
provisioner "local-exec" {
command = "aws eks --region ${var.aws_region} update-kubeconfig --name ${var.cluster_config.name}"
}
depends_on = [module.eks]
}









Step 5: Deploy a Sample Microservices Application



We will deploy famous exmaple voting app from the https://github.com/dockersamples/example-voting-app. We will just deploy the manifests and not look into building the docker images at this moment.




  • A front-end web app in Python which lets you vote between two options

  • A Redis which collects new votes

  • A .NET worker which consumes votes and stores them in…

  • A Postgres database backed by a Docker volume

  • A Node.js web app which shows the results of the voting in real time



Note: The voting application only accepts one vote per client browser. It does not register additional votes if a vote has already been submitted from a client.





Apply the manifests and verify




cd /example-voting-app/k8s-specifications
kubectl apply -f .
deployment.apps/db created
service/db created
deployment.apps/redis created
service/redis created
deployment.apps/result created
service/result created
deployment.apps/vote created
service/vote created
deployment.apps/worker created


kubectl port-forward service/vote 8080:8080
kubectl port-forward service/result 8081:8081






Access app using:




Vote:   http://localhost:8080
Result: http://localhost:8081













Cleanup



Delete the k8s resources created




cd /example-voting-app/k8s-specifications
kubectl delete -f .






And then terraform destroy the EKS infrastructure if you are not using it to save costs.






Conclusion



By building an EKS cluster from scratch with Terraform—without relying on the AWS community module—we gain a clear understanding of the essential AWS resources required to run Kubernetes in production.



We saw how networking forms the foundation, how the cluster and node groups tie everything together, and finally how workloads are deployed and accessed. This exercise provides not only a strong foundation in AWS and Kubernetes but also the confidence to customize infrastructure for real-world production use cases.






References



SOC Incident Playbook: Remote Code Execution (RCE) Defense
Syntax validiert (0 Fehler)
title: Detect Exploitation - Building an Amazon EKS Cluster with raw Terraform Resources
id: 3d16769b-ba4d-4a7e-9d65-81aefadb06f8
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-25
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
Syntax validiert (0 Fehler)
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-25"
        description = "YARA Signature for "
    strings:
        $str = "Building an Amazon EKS Cluster" ascii wide
    condition:
        any of them
}
Syntax validiert (0 Fehler)
index=security sourcetype IN ("cisco:asa", "pan:traffic", "zeek_conn", "suricata", "WinEventLog:Security")
("Building an Amazon EKS Cluster with raw ")
| stats count earliest(_time) as first_seen latest(_time) as last_seen by src_ip, dest_ip, dest_host, signature
| eval first_seen=strftime(first_seen, "%Y-%m-%d %H:%M:%S"), last_seen=strftime(last_seen, "%Y-%m-%d %H:%M:%S")
| sort - count
Syntax validiert (0 Fehler)
message: "*Building an Amazon EKS Cluster with raw *"
Syntax validiert (0 Fehler)
CommonSecurityLog
| where Message has "Building an Amazon EKS Cluster with raw "
| summarize EventCount = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by SourceIP, DestinationIP, DestinationPort, Activity
| extend DetectionRule = "iShareStuff-CTI-Compiled"
| sort by EventCount desc
🎯
MITRE ATT&CK Matrix Navigator 14 Taktiken
Reconnaissance
-
Resource Development
-
Initial Access
Execution
Persistence
-
Privilege Escalation
Defense Evasion
Credential Access
-
Discovery
-
Lateral Movement
-
Collection
-
Command and Control
Exfiltration
-
Impact
tsecurity.de Cognitive Threat RAG
Fokus-Vektor:

Kognitive Analyse für identifizierte Bedrohung: Erhöhte Bedrohungslage im Bereich Building an Amazon EKS Cluster with raw .... Basierend auf 368k Vektor-Korrelationen werden sofortige Isolationsmaßnahmen für betroffene Endpunkte empfohlen.

🛡️ Angriffsfläche & Exposure

Netzwerk/Remote-Zugriff ohne Vorauthentifizierung möglich.

⚡ Empfohlene Sofortmaßnahmen
  • 1. Perimeter-Inspektion: Relevante Portfreigaben und exponierte Endpunkte unverzüglich scannen.
  • 2. Patch-Applikation: Hersteller-Hotfix einspielen oder betroffene Daemons in isolierte DMZ-Segmente überführen.
  • 3. Telemetrie & EDR-Alerts: Prozessaufrufe und Child-Processes auf anomale Shell-Spawns überwachen.
🔗 Semantisch verwandte Zero-Days MariaDB 11.7 VEC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building an Amazon EKS Cluster with raw Terraform Resources

Thematisch verwandte Begriffe: Building, Amazon, Cluster, with · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-87722 | Uncontrolled Resource Consumption (CWE-400 / CWE-1333) in regex search q…
Advisory →
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel • Rechts: nächster Artikel • unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel TTP ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...
↗ Original-Quelle