diff --git a/.gitignore b/.gitignore index 38c6af03..bd12fb13 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,17 @@ build/ ### VS Code ### .vscode/ +### Terraform ### +.terraform/ +*.tfstate +*.tfstate.backup +*.tfvars +*.tfvars.json +*.tfplan +crash.log +.terraformrc +terraform.rc + ### Kiro ### .kiro/debug/ diff --git a/infra/eks-terraform/.gitignore b/infra/eks-terraform/.gitignore new file mode 100644 index 00000000..02dba59b --- /dev/null +++ b/infra/eks-terraform/.gitignore @@ -0,0 +1,31 @@ +# Local Terraform directories +.terraform/ + +# Terraform state files +*.tfstate +*.tfstate.backup +*.terraform.lock.hcl + +# Crash log files +crash.log + +# CLI configuration files +.terraformrc +terraform.rc + +# Sensitive override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Sensitive variable files +*.tfvars +*.tfvars.json + +# Ignore generated plan files +*.tfplan + +# Ignore AWS credential files if accidentally placed +*.pem +*.key diff --git a/infra/eks-terraform/.terraform.lock.hcl b/infra/eks-terraform/.terraform.lock.hcl new file mode 100644 index 00000000..dcb645ec --- /dev/null +++ b/infra/eks-terraform/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.49.0" + constraints = ">= 4.0.0" + hashes = [ + "h1:zBF6IqpUiJAszE5L4HpWuOgG6nSnqGRnwr08frehB40=", + "zh:11a636bb415bf780f0ad300cab83d687aebdc51381112ae7b29862e0bee43017", + "zh:2d6c4bb861c073d9900a2afc39cc1e38492c6996653e53c7a2083b526fb10ae9", + "zh:49f7ee4a7488f3d31342c5e9dbb577c40e0847a0cab152a0082e9aeef45f5c0f", + "zh:561283c9c9bd36b9d09832e50769b941eb45c43c6ab031f27c8bf78256af4af1", + "zh:576bf944e66d097b29fc45b25a5bbc53e7d4e71a486e2cb126304cf77b51fe79", + "zh:6c6bf8860773c121b9ca22743f733feea943f890fa3aba8740a59579dea16fc4", + "zh:88b963a659e42daac7384a6abb99b3383f9f6c8abad5dedafcd443536b122b84", + "zh:947e9404235ca094e39e7f3f464f99436320a168e1607c550e581fbc70553d48", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a9c5a37bd1e5e81e85ad3dc3141f1b453b96e9cb8e500bc15bfb9c488fc95dcc", + "zh:b7b8a028fb2eafb98c676f9438808318f7ff0ea76eba03a6653d0940848a31c7", + "zh:be29f31827d6d5567aaec26034e6cceb994460446778ba1d0a435fc7ccd8a9f5", + "zh:c24c60ecffe3a7d44c762e62a29a802aec604096715ad3110b7ca2207124eb3a", + "zh:e2a247c5c7815437969e87cdce1f27f323eea75b97ed53f7605fef05d6406071", + "zh:e590ce9aca3f4fd964f7bf908a24dc3bc88e0b03a8554ccc81b9edcc84690670", + ] +} diff --git a/infra/eks-terraform/.terraform/modules/eks b/infra/eks-terraform/.terraform/modules/eks new file mode 160000 index 00000000..efbe9526 --- /dev/null +++ b/infra/eks-terraform/.terraform/modules/eks @@ -0,0 +1 @@ +Subproject commit efbe9526322cef7443734c6c8b1e2c166f1cd428 diff --git a/infra/eks-terraform/NOTES.md b/infra/eks-terraform/NOTES.md new file mode 100644 index 00000000..07ae6e04 --- /dev/null +++ b/infra/eks-terraform/NOTES.md @@ -0,0 +1,7 @@ +Notes and cost warnings + +- AWS EKS control plane is NOT free: there is a per-cluster hourly fee (~$0.10/hr at time of writing). This will incur charges beyond the Free Tier. +- EC2 Free Tier covers t2.micro for 750 hours/month for 12 months for new accounts; node groups with larger instance types will incur costs. +- IAM, EBS, NAT gateway, data transfer, and other resources may also add charges. + +If you need a fully free local Kubernetes experience, consider `kind` or `minikube` instead of EKS. diff --git a/infra/eks-terraform/README.md b/infra/eks-terraform/README.md new file mode 100644 index 00000000..29317bd5 --- /dev/null +++ b/infra/eks-terraform/README.md @@ -0,0 +1,51 @@ +This folder contains Terraform to create an AWS EKS cluster in `us-east-1`. + +Important: EKS is not fully covered by AWS Free Tier. The control plane and some data transfer and resource usage will incur charges. Read the notes in `NOTES.md` before running. + +Commands: + +```bash +export AWS_PROFILE=your-profile +cd infra/eks-terraform +terraform init +terraform plan -var="aws_region=us-east-1" +terraform apply -var="aws_region=us-east-1" +``` + +Set `AWS_PROFILE` or appropriate environment variables for credentials. + +Default worker node instance type: `t3.large` (change with the `node_instance_type` variable). + +Remote state (S3) bootstrap + - You can create an S3 bucket + DynamoDB table to store remote state and enable locking using the `backend-bootstrap` helper in this folder. + +Steps: +1. Create backend resources from the `backend-bootstrap` folder: + +From repository root: + +```bash +cd infra/eks-terraform/backend-bootstrap +terraform init +terraform apply -var="aws_region=us-east-1" -var="create_dynamodb=false" +``` + +If you are already inside `infra/eks-terraform`: + +```bash +cd backend-bootstrap +terraform init +terraform apply -var="aws_region=us-east-1" -var="create_dynamodb=false" +``` + +2. Note the outputs `bucket` and `dynamodb_table`. + +3. Initialize the main EKS terraform with those backend values: + +```bash +cd .. +terraform init -backend-config="bucket=YOUR_BUCKET_NAME" -backend-config="key=eks/terraform.tfstate" -backend-config="region=us-east-1" -backend-config="dynamodb_table=YOUR_DYNAMODB_TABLE" +terraform plan -var="aws_region=us-east-1" +``` + +I cannot take control of your terminal. Run the commands above in your shell. Paste any errors here and I'll help fix them. diff --git a/infra/eks-terraform/backend-bootstrap/.gitignore b/infra/eks-terraform/backend-bootstrap/.gitignore new file mode 100644 index 00000000..e2324436 --- /dev/null +++ b/infra/eks-terraform/backend-bootstrap/.gitignore @@ -0,0 +1,30 @@ +# Local Terraform directories +.terraform/ + +# Terraform state files +*.tfstate +*.tfstate.backup + +# Crash log files +crash.log + +# CLI configuration files +.terraformrc +terraform.rc + +# Sensitive override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Sensitive variable files +*.tfvars +*.tfvars.json + +# Ignore generated plan files +*.tfplan + +# Ignore AWS credential files if accidentally placed +*.pem +*.key diff --git a/infra/eks-terraform/backend-bootstrap/.terraform.lock.hcl b/infra/eks-terraform/backend-bootstrap/.terraform.lock.hcl new file mode 100644 index 00000000..7a38829d --- /dev/null +++ b/infra/eks-terraform/backend-bootstrap/.terraform.lock.hcl @@ -0,0 +1,44 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.49.0" + hashes = [ + "h1:zBF6IqpUiJAszE5L4HpWuOgG6nSnqGRnwr08frehB40=", + "zh:11a636bb415bf780f0ad300cab83d687aebdc51381112ae7b29862e0bee43017", + "zh:2d6c4bb861c073d9900a2afc39cc1e38492c6996653e53c7a2083b526fb10ae9", + "zh:49f7ee4a7488f3d31342c5e9dbb577c40e0847a0cab152a0082e9aeef45f5c0f", + "zh:561283c9c9bd36b9d09832e50769b941eb45c43c6ab031f27c8bf78256af4af1", + "zh:576bf944e66d097b29fc45b25a5bbc53e7d4e71a486e2cb126304cf77b51fe79", + "zh:6c6bf8860773c121b9ca22743f733feea943f890fa3aba8740a59579dea16fc4", + "zh:88b963a659e42daac7384a6abb99b3383f9f6c8abad5dedafcd443536b122b84", + "zh:947e9404235ca094e39e7f3f464f99436320a168e1607c550e581fbc70553d48", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a9c5a37bd1e5e81e85ad3dc3141f1b453b96e9cb8e500bc15bfb9c488fc95dcc", + "zh:b7b8a028fb2eafb98c676f9438808318f7ff0ea76eba03a6653d0940848a31c7", + "zh:be29f31827d6d5567aaec26034e6cceb994460446778ba1d0a435fc7ccd8a9f5", + "zh:c24c60ecffe3a7d44c762e62a29a802aec604096715ad3110b7ca2207124eb3a", + "zh:e2a247c5c7815437969e87cdce1f27f323eea75b97ed53f7605fef05d6406071", + "zh:e590ce9aca3f4fd964f7bf908a24dc3bc88e0b03a8554ccc81b9edcc84690670", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.9.0" + hashes = [ + "h1:q/uaUTBdKgAmZESrwsoeDQff9uUA/cI/N5ZKNgVwa9c=", + "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", + "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", + "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", + "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0", + "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61", + "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc", + "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e", + "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef", + "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b", + "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257", + "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04", + ] +} diff --git a/infra/eks-terraform/backend-bootstrap/README.md b/infra/eks-terraform/backend-bootstrap/README.md new file mode 100644 index 00000000..4b258e00 --- /dev/null +++ b/infra/eks-terraform/backend-bootstrap/README.md @@ -0,0 +1,23 @@ +Bootstrap for remote state backend resources. + +Usage: + +```bash +cd infra/eks-terraform/backend-bootstrap +terraform init +terraform apply -var="aws_region=us-east-1" + +# After apply note outputs `bucket` and `dynamodb_table`. +``` + +Use the printed `bucket` and optionally `dynamodb_table` values when initializing the main EKS terraform backend (see main README). + +If you do not have DynamoDB or want a Free Tier friendly setup, run the bootstrap with DynamoDB disabled (default): + +```bash +cd infra/eks-terraform/backend-bootstrap +terraform init +terraform apply -var="aws_region=us-east-1" -var="create_dynamodb=false" +``` + +This will create only the S3 bucket. Without DynamoDB you will not have state locking; avoid running concurrent `terraform apply` operations. diff --git a/infra/eks-terraform/backend-bootstrap/main.tf b/infra/eks-terraform/backend-bootstrap/main.tf new file mode 100644 index 00000000..6af3c6de --- /dev/null +++ b/infra/eks-terraform/backend-bootstrap/main.tf @@ -0,0 +1,44 @@ +terraform { + required_version = ">= 1.0.0" +} + +provider "aws" { + region = var.aws_region +} + +resource "random_id" "suffix" { + byte_length = 4 +} + +resource "aws_s3_bucket" "tfstate" { + bucket = "tfstate-${var.project_name}-${random_id.suffix.hex}" + force_destroy = true + tags = { + Name = "tfstate-${var.project_name}" + } +} + +resource "aws_s3_bucket_acl" "tfstate_acl" { + bucket = aws_s3_bucket.tfstate.id + acl = "private" +} + +resource "aws_dynamodb_table" "tf_locks" { + count = var.create_dynamodb ? 1 : 0 + name = "tf-locks-${var.project_name}-${random_id.suffix.hex}" + billing_mode = "PAY_PER_REQUEST" + hash_key = "LockID" + attribute { + name = "LockID" + type = "S" + } + tags = { Name = "tf-locks-${var.project_name}" } +} + +output "bucket" { + value = aws_s3_bucket.tfstate.bucket +} + +output "dynamodb_table" { + value = try(aws_dynamodb_table.tf_locks[0].name, "") +} diff --git a/infra/eks-terraform/backend-bootstrap/variables.tf b/infra/eks-terraform/backend-bootstrap/variables.tf new file mode 100644 index 00000000..a4b9e5c9 --- /dev/null +++ b/infra/eks-terraform/backend-bootstrap/variables.tf @@ -0,0 +1,15 @@ +variable "aws_region" { + type = string + default = "us-east-1" +} + +variable "project_name" { + type = string + default = "java-on-aws-eks" +} + +variable "create_dynamodb" { + description = "Whether to create a DynamoDB table for state locking. Set to false for Free Tier / no DynamoDB support." + type = bool + default = false +} diff --git a/infra/eks-terraform/backend.tf b/infra/eks-terraform/backend.tf new file mode 100644 index 00000000..12c0dbe5 --- /dev/null +++ b/infra/eks-terraform/backend.tf @@ -0,0 +1,3 @@ +terraform { + backend "s3" {} +} diff --git a/infra/eks-terraform/main.tf b/infra/eks-terraform/main.tf new file mode 100644 index 00000000..af0286aa --- /dev/null +++ b/infra/eks-terraform/main.tf @@ -0,0 +1,155 @@ +// Minimal standalone EKS cluster (no external module) to avoid module input mismatches. + +locals { + azs = slice(data.aws_availability_zones.available.names, 0, 2) +} + +data "aws_availability_zones" "available" {} + +resource "aws_vpc" "this" { + cidr_block = "10.0.0.0/16" + tags = { + Name = "eks-vpc-${var.cluster_name}" + } +} + +resource "aws_subnet" "public" { + count = length(local.azs) + vpc_id = aws_vpc.this.id + cidr_block = cidrsubnet(aws_vpc.this.cidr_block, 8, count.index) + availability_zone = local.azs[count.index] + map_public_ip_on_launch = true + tags = { + Name = "eks-public-${count.index}" + } +} + +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.this.id + tags = { Name = "eks-igw" } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.this.id + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + tags = { Name = "eks-public-rt" } +} + +resource "aws_route_table_association" "public" { + count = length(local.azs) + subnet_id = aws_subnet.public[count.index].id + route_table_id = aws_route_table.public.id +} + +resource "aws_iam_role" "eks_cluster" { + name = "eks-cluster-role-${var.cluster_name}" + assume_role_policy = data.aws_iam_policy_document.eks_cluster_assume.json +} + +data "aws_iam_policy_document" "eks_cluster_assume" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["eks.amazonaws.com"] + } + } +} + +resource "aws_iam_role_policy_attachment" "eks_cluster_AmazonEKSClusterPolicy" { + role = aws_iam_role.eks_cluster.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" +} + +resource "aws_iam_role_policy_attachment" "eks_cluster_AmazonEKSServicePolicy" { + role = aws_iam_role.eks_cluster.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSServicePolicy" +} + +resource "aws_eks_cluster" "this" { + name = var.cluster_name + role_arn = aws_iam_role.eks_cluster.arn + + version = var.k8s_version + + vpc_config { + subnet_ids = aws_subnet.public[*].id + endpoint_public_access = true + public_access_cidrs = ["0.0.0.0/0"] + } + + depends_on = [aws_iam_role_policy_attachment.eks_cluster_AmazonEKSClusterPolicy] +} + +resource "aws_iam_role" "node_group" { + name = "eks-node-role-${var.cluster_name}" + assume_role_policy = data.aws_iam_policy_document.node_assume.json +} + +data "aws_iam_policy_document" "node_assume" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_role_policy_attachment" "node_AmazonEKSWorkerNodePolicy" { + role = aws_iam_role.node_group.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" +} + +resource "aws_iam_role_policy_attachment" "node_AmazonEC2ContainerRegistryReadOnly" { + role = aws_iam_role.node_group.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" +} + +resource "aws_iam_role_policy_attachment" "node_AmazonEKS_CNI_Policy" { + role = aws_iam_role.node_group.name + policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" +} + +resource "aws_launch_template" "node_lt" { + name_prefix = "eks-nt-${var.cluster_name}-" + image_id = data.aws_ami.eks_worker.id + instance_type = var.node_instance_type + + lifecycle { create_before_destroy = true } +} + +data "aws_ami" "eks_worker" { + most_recent = true + owners = ["602401143452"] # Amazon EKS optimized AMI owner (may vary by region) + filter { + name = "name" + values = ["amazon-eks-node-*-*"] + } +} + +resource "aws_eks_node_group" "default" { + cluster_name = aws_eks_cluster.this.name + node_group_name = "ng-${var.cluster_name}" + node_role_arn = aws_iam_role.node_group.arn + subnet_ids = aws_subnet.public[*].id + + scaling_config { + desired_size = var.node_desired_count + max_size = var.node_desired_count + 1 + min_size = 1 + } + + capacity_type = "ON_DEMAND" + + launch_template { + id = aws_launch_template.node_lt.id + version = "$Latest" + } + + depends_on = [aws_iam_role_policy_attachment.node_AmazonEKSWorkerNodePolicy] +} + diff --git a/infra/eks-terraform/outputs.tf b/infra/eks-terraform/outputs.tf new file mode 100644 index 00000000..8c14b862 --- /dev/null +++ b/infra/eks-terraform/outputs.tf @@ -0,0 +1,12 @@ +output "cluster_name" { + value = aws_eks_cluster.this.name +} + +output "kubeconfig_certificate_authority_data" { + value = aws_eks_cluster.this.certificate_authority[0].data + sensitive = true +} + +output "kubeconfig_endpoint" { + value = aws_eks_cluster.this.endpoint +} diff --git a/infra/eks-terraform/providers.tf b/infra/eks-terraform/providers.tf new file mode 100644 index 00000000..c9d7ccbd --- /dev/null +++ b/infra/eks-terraform/providers.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = var.aws_region +} diff --git a/infra/eks-terraform/variables.tf b/infra/eks-terraform/variables.tf new file mode 100644 index 00000000..f240d38b --- /dev/null +++ b/infra/eks-terraform/variables.tf @@ -0,0 +1,29 @@ +variable "aws_region" { + description = "AWS region to deploy to" + type = string + default = "us-east-1" +} + +variable "cluster_name" { + description = "EKS cluster name" + type = string + default = "demo-eks-cluster" +} + +variable "node_instance_type" { + description = "EC2 instance type for worker nodes" + type = string + default = "t3.large" +} + +variable "node_desired_count" { + description = "Desired number of worker nodes" + type = number + default = 1 +} + +variable "k8s_version" { + description = "Kubernetes version for the EKS control plane" + type = string + default = "1.28" +} diff --git a/infra/eks-terraform/versions.tf b/infra/eks-terraform/versions.tf new file mode 100644 index 00000000..c791e919 --- /dev/null +++ b/infra/eks-terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.0" + } + } +}