Skip to content

Commit 9ffb9df

Browse files
authored
🤖 feat: add Terraform EKS sandbox configuration (#26)
## Summary Add a complete, plain-resource Terraform configuration under `terraform/` to provision a cost-optimized Amazon EKS sandbox cluster in `eu-central-1`, plus documentation for authentication and usage. ## Background The goal is to provide a practical starting point for creating an EKS cluster in a personal AWS account with reasonable default cost controls (single NAT gateway, modest node sizing), while making setup reliable for AWS CLI v2 `aws login` users. ## Implementation - Added Terraform configuration files: - `terraform/versions.tf` (Terraform/AWS provider requirements, default tags) - `terraform/variables.tf` (cluster/network/node variables with defaults) - `terraform/vpc.tf` (VPC, 2 public + 2 private subnets, IGW, single NAT, route tables) - `terraform/iam.tf` (EKS control plane + node IAM roles/policies) - `terraform/eks.tf` (EKS cluster, managed node group, `coredns`/`kube-proxy`/`vpc-cni` add-ons) - `terraform/outputs.tf` (endpoint, kubeconfig command, region, VPC, etc.) - `terraform/.gitignore` (Terraform state/workdir ignores) - Added and updated `terraform/README.md` to include: - usage + cost estimate - safe AWS auth steps for `aws login` + `login_session` via `credential_process` - guidance to avoid leaking credentials/config in git - Addressed Codex review feedback by making networking inputs resilient: - AZ selection is now derived from the selected region (`data.aws_availability_zones`) - subnet CIDRs are now derived from the configurable `var.vpc_cidr` - Addressed Codex lockfile feedback for reproducibility: - stopped ignoring `terraform/.terraform.lock.hcl` - generated and committed provider lockfile so `terraform init` stays deterministic across machines ## Validation - `terraform -chdir=terraform fmt -recursive -check` - `terraform -chdir=terraform init -backend=false -input=false` - `terraform -chdir=terraform validate` - local PR loop checks: - `make verify-vendor` ✅ - `make test` ✅ - `make build` ✅ - `make lint` ⚠️ fails in this environment because golangci-lint was built with Go 1.24 while the repo targets Go 1.25.7 ## Risks Low-to-moderate operational risk: - Provisions real billable infrastructure (EKS, EC2 nodes, NAT Gateway). - Defaults are intentionally sandbox-oriented and cost-conscious, but costs vary by workload and data transfer. - Auth instructions are documentation-only and avoid embedding account-specific secrets. --- _Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$0.53`_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=0.53 -->
1 parent 806142a commit 9ffb9df

9 files changed

Lines changed: 484 additions & 0 deletions

File tree

terraform/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.terraform/
2+
*.tfstate
3+
*.tfstate.*
4+
*.tfplan

terraform/.terraform.lock.hcl

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

terraform/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Terraform EKS Sandbox Configuration
2+
3+
This directory provisions a cost-optimized Amazon EKS sandbox cluster in region `eu-central-1`.
4+
5+
## What this sets up
6+
7+
- A VPC (`10.0.0.0/16`) with:
8+
- 2 public subnets across the first two availability zones in the selected region
9+
- 2 private subnets across the first two availability zones in the selected region
10+
- Internet Gateway
11+
- Single NAT Gateway (lower cost than one per AZ)
12+
- IAM roles for EKS control plane and worker nodes
13+
- EKS cluster (`sandbox-eks`, Kubernetes `1.31`) with public and private API endpoint access
14+
- One managed node group:
15+
- Instance type: `t3.medium`
16+
- Desired/min/max size: `2/1/3`
17+
- Disk size: `20 GiB`
18+
- AMI type: `AL2023_x86_64_STANDARD`
19+
- EKS managed add-ons: `coredns`, `kube-proxy`, `vpc-cni`
20+
- Local Terraform state (no remote backend configured)
21+
22+
## Prerequisites
23+
24+
- Terraform `>= 1.5`
25+
- AWS CLI v2 installed
26+
- AWS identity with permissions to create VPC, IAM, EKS, and EC2 resources in your target account
27+
28+
## AWS authentication (required before `terraform plan` / `terraform apply`)
29+
30+
If you are using `aws login` and your AWS profile uses `login_session`, Terraform may not detect credentials directly. Use a Terraform-specific wrapper profile via `credential_process`.
31+
32+
1. Sign in to your normal AWS login profile:
33+
34+
```bash
35+
aws login --profile <your-login-profile>
36+
```
37+
38+
2. Add a Terraform wrapper profile in `~/.aws/config`:
39+
40+
```ini
41+
[profile terraform]
42+
credential_process = aws configure export-credentials --profile <your-login-profile> --format process
43+
region = eu-central-1
44+
```
45+
46+
3. In the shell where you run Terraform, point tooling at that profile:
47+
48+
```bash
49+
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
50+
export AWS_PROFILE=terraform
51+
export AWS_REGION=eu-central-1
52+
export AWS_SDK_LOAD_CONFIG=1
53+
```
54+
55+
4. Verify credentials before running Terraform:
56+
57+
```bash
58+
aws sts get-caller-identity
59+
```
60+
61+
> Security note: do not commit `~/.aws/config`, `~/.aws/credentials`, or any copied credential values to git.
62+
63+
## Usage
64+
65+
```bash
66+
terraform init
67+
terraform plan
68+
terraform apply
69+
```
70+
71+
## Configure kubectl
72+
73+
After `terraform apply`, run the command from the Terraform output:
74+
75+
```bash
76+
terraform output -raw kubeconfig_command
77+
```
78+
79+
Then execute the printed command, for example:
80+
81+
```bash
82+
aws eks update-kubeconfig --region eu-central-1 --name sandbox-eks
83+
```
84+
85+
## Estimated cost (rough)
86+
87+
- EKS control plane: **~$0.10/hour**
88+
- 2x `t3.medium` worker nodes: **~$0.08/hour**
89+
- 1x NAT Gateway: **~$0.045/hour**
90+
- **Total: ~ $0.225/hour (~$5.40/day)**
91+
92+
> Note: Data transfer and NAT data processing charges are additional.
93+
94+
## Cleanup
95+
96+
```bash
97+
terraform destroy
98+
```

terraform/eks.tf

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
resource "aws_eks_cluster" "this" {
2+
name = var.cluster_name
3+
role_arn = aws_iam_role.eks_cluster.arn
4+
version = var.cluster_version
5+
6+
vpc_config {
7+
subnet_ids = aws_subnet.private[*].id
8+
endpoint_public_access = true
9+
endpoint_private_access = true
10+
}
11+
12+
depends_on = [aws_iam_role_policy_attachment.eks_cluster_policy]
13+
}
14+
15+
resource "aws_eks_node_group" "this" {
16+
cluster_name = aws_eks_cluster.this.name
17+
node_group_name = "${var.cluster_name}-nodes"
18+
node_role_arn = aws_iam_role.node_group.arn
19+
subnet_ids = aws_subnet.private[*].id
20+
21+
instance_types = var.node_instance_types
22+
ami_type = "AL2023_x86_64_STANDARD"
23+
disk_size = var.node_disk_size
24+
25+
scaling_config {
26+
desired_size = var.node_desired_size
27+
min_size = var.node_min_size
28+
max_size = var.node_max_size
29+
}
30+
31+
depends_on = [
32+
aws_iam_role_policy_attachment.node_group_worker_policy,
33+
aws_iam_role_policy_attachment.node_group_cni_policy,
34+
aws_iam_role_policy_attachment.node_group_ecr_readonly,
35+
]
36+
}
37+
38+
# Core add-ons are explicitly managed for predictable cluster bootstrap behavior.
39+
resource "aws_eks_addon" "coredns" {
40+
cluster_name = aws_eks_cluster.this.name
41+
addon_name = "coredns"
42+
resolve_conflicts_on_create = "OVERWRITE"
43+
44+
depends_on = [aws_eks_node_group.this]
45+
}
46+
47+
resource "aws_eks_addon" "kube_proxy" {
48+
cluster_name = aws_eks_cluster.this.name
49+
addon_name = "kube-proxy"
50+
resolve_conflicts_on_create = "OVERWRITE"
51+
52+
depends_on = [aws_eks_node_group.this]
53+
}
54+
55+
resource "aws_eks_addon" "vpc_cni" {
56+
cluster_name = aws_eks_cluster.this.name
57+
addon_name = "vpc-cni"
58+
resolve_conflicts_on_create = "OVERWRITE"
59+
60+
depends_on = [aws_eks_node_group.this]
61+
}

terraform/iam.tf

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
data "aws_iam_policy_document" "eks_cluster_assume_role" {
2+
statement {
3+
actions = ["sts:AssumeRole"]
4+
5+
principals {
6+
type = "Service"
7+
identifiers = ["eks.amazonaws.com"]
8+
}
9+
}
10+
}
11+
12+
resource "aws_iam_role" "eks_cluster" {
13+
name = "${var.cluster_name}-cluster-role"
14+
assume_role_policy = data.aws_iam_policy_document.eks_cluster_assume_role.json
15+
16+
tags = {
17+
Name = "${var.cluster_name}-cluster-role"
18+
}
19+
}
20+
21+
resource "aws_iam_role_policy_attachment" "eks_cluster_policy" {
22+
role = aws_iam_role.eks_cluster.name
23+
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
24+
}
25+
26+
data "aws_iam_policy_document" "node_group_assume_role" {
27+
statement {
28+
actions = ["sts:AssumeRole"]
29+
30+
principals {
31+
type = "Service"
32+
identifiers = ["ec2.amazonaws.com"]
33+
}
34+
}
35+
}
36+
37+
resource "aws_iam_role" "node_group" {
38+
name = "${var.cluster_name}-node-role"
39+
assume_role_policy = data.aws_iam_policy_document.node_group_assume_role.json
40+
41+
tags = {
42+
Name = "${var.cluster_name}-node-role"
43+
}
44+
}
45+
46+
resource "aws_iam_role_policy_attachment" "node_group_worker_policy" {
47+
role = aws_iam_role.node_group.name
48+
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
49+
}
50+
51+
resource "aws_iam_role_policy_attachment" "node_group_cni_policy" {
52+
role = aws_iam_role.node_group.name
53+
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
54+
}
55+
56+
resource "aws_iam_role_policy_attachment" "node_group_ecr_readonly" {
57+
role = aws_iam_role.node_group.name
58+
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
59+
}

terraform/outputs.tf

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
output "cluster_name" {
2+
description = "Name of the EKS cluster."
3+
value = aws_eks_cluster.this.name
4+
}
5+
6+
output "cluster_endpoint" {
7+
description = "API server endpoint for the EKS cluster."
8+
value = aws_eks_cluster.this.endpoint
9+
}
10+
11+
output "cluster_certificate_authority" {
12+
description = "Base64-encoded certificate data required for kubeconfig setup."
13+
value = aws_eks_cluster.this.certificate_authority[0].data
14+
sensitive = true
15+
}
16+
17+
output "cluster_security_group_id" {
18+
description = "Cluster security group ID managed by EKS."
19+
value = aws_eks_cluster.this.vpc_config[0].cluster_security_group_id
20+
}
21+
22+
output "node_group_name" {
23+
description = "Name of the managed EKS node group."
24+
value = aws_eks_node_group.this.node_group_name
25+
}
26+
27+
output "region" {
28+
description = "AWS region where the cluster is deployed."
29+
value = var.aws_region
30+
}
31+
32+
output "kubeconfig_command" {
33+
description = "AWS CLI command to merge this cluster into local kubeconfig."
34+
value = "aws eks update-kubeconfig --region ${var.aws_region} --name ${aws_eks_cluster.this.name}"
35+
}
36+
37+
output "vpc_id" {
38+
description = "VPC ID used by the EKS cluster."
39+
value = aws_vpc.this.id
40+
}

terraform/variables.tf

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
variable "aws_region" {
2+
description = "AWS region where the EKS cluster and networking resources will be created."
3+
type = string
4+
default = "eu-central-1"
5+
}
6+
7+
variable "cluster_name" {
8+
description = "Name of the EKS cluster."
9+
type = string
10+
default = "sandbox-eks"
11+
}
12+
13+
variable "cluster_version" {
14+
description = "Kubernetes version for the EKS control plane."
15+
type = string
16+
default = "1.31"
17+
}
18+
19+
variable "vpc_cidr" {
20+
description = "CIDR block for the VPC that hosts the EKS cluster."
21+
type = string
22+
default = "10.0.0.0/16"
23+
}
24+
25+
variable "node_instance_types" {
26+
description = "EC2 instance types for the managed node group."
27+
type = list(string)
28+
default = ["t3.medium"]
29+
}
30+
31+
variable "node_desired_size" {
32+
description = "Desired number of worker nodes in the managed node group."
33+
type = number
34+
default = 2
35+
}
36+
37+
variable "node_min_size" {
38+
description = "Minimum number of worker nodes in the managed node group."
39+
type = number
40+
default = 1
41+
}
42+
43+
variable "node_max_size" {
44+
description = "Maximum number of worker nodes in the managed node group."
45+
type = number
46+
default = 3
47+
}
48+
49+
variable "node_disk_size" {
50+
description = "Disk size in GiB for each node in the managed node group."
51+
type = number
52+
default = 20
53+
}

terraform/versions.tf

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
terraform {
2+
required_version = ">= 1.5"
3+
4+
required_providers {
5+
aws = {
6+
source = "hashicorp/aws"
7+
version = "~> 5.0"
8+
}
9+
}
10+
}
11+
12+
provider "aws" {
13+
region = var.aws_region
14+
15+
default_tags {
16+
tags = {
17+
ManagedBy = "terraform"
18+
Project = "sandbox-eks"
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)