Spaces:
Running
Running
infra(terraform): add EKS cluster, Elasticache Redis, and ECR modules for cloud deployment
Browse files- terraform/main.tf +158 -0
- terraform/modules/ecr/main.tf +33 -0
- terraform/modules/eks/main.tf +132 -0
- terraform/modules/redis/main.tf +68 -0
- terraform/outputs.tf +37 -0
- terraform/variables.tf +70 -0
terraform/main.tf
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
terraform {
|
| 2 |
+
required_version = ">= 1.6.0"
|
| 3 |
+
|
| 4 |
+
required_providers {
|
| 5 |
+
aws = {
|
| 6 |
+
source = "hashicorp/aws"
|
| 7 |
+
version = "~> 5.0"
|
| 8 |
+
}
|
| 9 |
+
kubernetes = {
|
| 10 |
+
source = "hashicorp/kubernetes"
|
| 11 |
+
version = "~> 2.27"
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
# Remote state — replace bucket/key/region with your values before applying.
|
| 16 |
+
# Remove this block entirely to use local state during development.
|
| 17 |
+
backend "s3" {
|
| 18 |
+
bucket = "your-terraform-state-bucket"
|
| 19 |
+
key = "ai-code-review-agent/terraform.tfstate"
|
| 20 |
+
region = "us-east-1"
|
| 21 |
+
encrypt = true
|
| 22 |
+
dynamodb_table = "terraform-state-lock"
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
provider "aws" {
|
| 27 |
+
region = var.aws_region
|
| 28 |
+
|
| 29 |
+
default_tags {
|
| 30 |
+
tags = var.tags
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
# ── networking ─────────────────────────────────────────────────────────────
|
| 35 |
+
# Minimal VPC: 2 public + 2 private subnets across 2 AZs.
|
| 36 |
+
# EKS nodes and Elasticache live in private subnets.
|
| 37 |
+
# NAT Gateway allows outbound internet (for git clone, LLM API calls).
|
| 38 |
+
|
| 39 |
+
data "aws_availability_zones" "available" {
|
| 40 |
+
state = "available"
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
resource "aws_vpc" "main" {
|
| 44 |
+
cidr_block = "10.0.0.0/16"
|
| 45 |
+
enable_dns_hostnames = true
|
| 46 |
+
enable_dns_support = true
|
| 47 |
+
|
| 48 |
+
tags = { Name = "${var.cluster_name}-vpc" }
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
resource "aws_subnet" "public" {
|
| 52 |
+
count = 2
|
| 53 |
+
vpc_id = aws_vpc.main.id
|
| 54 |
+
cidr_block = cidrsubnet("10.0.0.0/16", 8, count.index)
|
| 55 |
+
availability_zone = data.aws_availability_zones.available.names[count.index]
|
| 56 |
+
map_public_ip_on_launch = true
|
| 57 |
+
|
| 58 |
+
tags = {
|
| 59 |
+
Name = "${var.cluster_name}-public-${count.index}"
|
| 60 |
+
"kubernetes.io/role/elb" = "1"
|
| 61 |
+
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
resource "aws_subnet" "private" {
|
| 66 |
+
count = 2
|
| 67 |
+
vpc_id = aws_vpc.main.id
|
| 68 |
+
cidr_block = cidrsubnet("10.0.0.0/16", 8, count.index + 10)
|
| 69 |
+
availability_zone = data.aws_availability_zones.available.names[count.index]
|
| 70 |
+
|
| 71 |
+
tags = {
|
| 72 |
+
Name = "${var.cluster_name}-private-${count.index}"
|
| 73 |
+
"kubernetes.io/role/internal-elb" = "1"
|
| 74 |
+
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
resource "aws_internet_gateway" "main" {
|
| 79 |
+
vpc_id = aws_vpc.main.id
|
| 80 |
+
tags = { Name = "${var.cluster_name}-igw" }
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
resource "aws_eip" "nat" {
|
| 84 |
+
domain = "vpc"
|
| 85 |
+
tags = { Name = "${var.cluster_name}-nat-eip" }
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
resource "aws_nat_gateway" "main" {
|
| 89 |
+
allocation_id = aws_eip.nat.id
|
| 90 |
+
subnet_id = aws_subnet.public[0].id
|
| 91 |
+
tags = { Name = "${var.cluster_name}-nat" }
|
| 92 |
+
depends_on = [aws_internet_gateway.main]
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
resource "aws_route_table" "public" {
|
| 96 |
+
vpc_id = aws_vpc.main.id
|
| 97 |
+
route {
|
| 98 |
+
cidr_block = "0.0.0.0/0"
|
| 99 |
+
gateway_id = aws_internet_gateway.main.id
|
| 100 |
+
}
|
| 101 |
+
tags = { Name = "${var.cluster_name}-public-rt" }
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
resource "aws_route_table" "private" {
|
| 105 |
+
vpc_id = aws_vpc.main.id
|
| 106 |
+
route {
|
| 107 |
+
cidr_block = "0.0.0.0/0"
|
| 108 |
+
nat_gateway_id = aws_nat_gateway.main.id
|
| 109 |
+
}
|
| 110 |
+
tags = { Name = "${var.cluster_name}-private-rt" }
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
resource "aws_route_table_association" "public" {
|
| 114 |
+
count = 2
|
| 115 |
+
subnet_id = aws_subnet.public[count.index].id
|
| 116 |
+
route_table_id = aws_route_table.public.id
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
resource "aws_route_table_association" "private" {
|
| 120 |
+
count = 2
|
| 121 |
+
subnet_id = aws_subnet.private[count.index].id
|
| 122 |
+
route_table_id = aws_route_table.private.id
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
# ── modules ────────────────────────────────────────────────────────────────
|
| 126 |
+
|
| 127 |
+
module "eks" {
|
| 128 |
+
source = "./modules/eks"
|
| 129 |
+
|
| 130 |
+
cluster_name = var.cluster_name
|
| 131 |
+
cluster_version = var.cluster_version
|
| 132 |
+
vpc_id = aws_vpc.main.id
|
| 133 |
+
private_subnet_ids = aws_subnet.private[*].id
|
| 134 |
+
node_instance_type = var.node_instance_type
|
| 135 |
+
node_min_size = var.node_min_size
|
| 136 |
+
node_max_size = var.node_max_size
|
| 137 |
+
node_desired_size = var.node_desired_size
|
| 138 |
+
tags = var.tags
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
module "redis" {
|
| 142 |
+
source = "./modules/redis"
|
| 143 |
+
|
| 144 |
+
cluster_name = var.cluster_name
|
| 145 |
+
vpc_id = aws_vpc.main.id
|
| 146 |
+
private_subnet_ids = aws_subnet.private[*].id
|
| 147 |
+
eks_security_group_id = module.eks.node_security_group_id
|
| 148 |
+
node_type = var.redis_node_type
|
| 149 |
+
num_cache_nodes = var.redis_num_cache_nodes
|
| 150 |
+
tags = var.tags
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
module "ecr" {
|
| 154 |
+
source = "./modules/ecr"
|
| 155 |
+
|
| 156 |
+
repository_name = var.ecr_repository_name
|
| 157 |
+
tags = var.tags
|
| 158 |
+
}
|
terraform/modules/ecr/main.tf
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
variable "repository_name" { type = string }
|
| 2 |
+
variable "tags" { type = map(string) }
|
| 3 |
+
|
| 4 |
+
resource "aws_ecr_repository" "main" {
|
| 5 |
+
name = var.repository_name
|
| 6 |
+
image_tag_mutability = "MUTABLE"
|
| 7 |
+
|
| 8 |
+
image_scanning_configuration {
|
| 9 |
+
scan_on_push = true # free ECR basic scanning on every docker push
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
tags = var.tags
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
# Keep only the 10 most recent images to control storage cost
|
| 16 |
+
resource "aws_ecr_lifecycle_policy" "main" {
|
| 17 |
+
repository = aws_ecr_repository.main.name
|
| 18 |
+
|
| 19 |
+
policy = jsonencode({
|
| 20 |
+
rules = [{
|
| 21 |
+
rulePriority = 1
|
| 22 |
+
description = "Keep last 10 images"
|
| 23 |
+
selection = {
|
| 24 |
+
tagStatus = "any"
|
| 25 |
+
countType = "imageCountMoreThan"
|
| 26 |
+
countNumber = 10
|
| 27 |
+
}
|
| 28 |
+
action = { type = "expire" }
|
| 29 |
+
}]
|
| 30 |
+
})
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
output "repository_url" { value = aws_ecr_repository.main.repository_url }
|
terraform/modules/eks/main.tf
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
variable "cluster_name" { type = string }
|
| 2 |
+
variable "cluster_version" { type = string }
|
| 3 |
+
variable "vpc_id" { type = string }
|
| 4 |
+
variable "private_subnet_ids" { type = list(string) }
|
| 5 |
+
variable "node_instance_type" { type = string }
|
| 6 |
+
variable "node_min_size" { type = number }
|
| 7 |
+
variable "node_max_size" { type = number }
|
| 8 |
+
variable "node_desired_size" { type = number }
|
| 9 |
+
variable "tags" { type = map(string) }
|
| 10 |
+
|
| 11 |
+
# ── IAM roles ──────────────────────────────────────────────────────────────
|
| 12 |
+
|
| 13 |
+
resource "aws_iam_role" "cluster" {
|
| 14 |
+
name = "${var.cluster_name}-cluster-role"
|
| 15 |
+
|
| 16 |
+
assume_role_policy = jsonencode({
|
| 17 |
+
Version = "2012-10-17"
|
| 18 |
+
Statement = [{
|
| 19 |
+
Effect = "Allow"
|
| 20 |
+
Principal = { Service = "eks.amazonaws.com" }
|
| 21 |
+
Action = "sts:AssumeRole"
|
| 22 |
+
}]
|
| 23 |
+
})
|
| 24 |
+
|
| 25 |
+
tags = var.tags
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
resource "aws_iam_role_policy_attachment" "cluster_policy" {
|
| 29 |
+
role = aws_iam_role.cluster.name
|
| 30 |
+
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
resource "aws_iam_role" "node_group" {
|
| 34 |
+
name = "${var.cluster_name}-node-role"
|
| 35 |
+
|
| 36 |
+
assume_role_policy = jsonencode({
|
| 37 |
+
Version = "2012-10-17"
|
| 38 |
+
Statement = [{
|
| 39 |
+
Effect = "Allow"
|
| 40 |
+
Principal = { Service = "ec2.amazonaws.com" }
|
| 41 |
+
Action = "sts:AssumeRole"
|
| 42 |
+
}]
|
| 43 |
+
})
|
| 44 |
+
|
| 45 |
+
tags = var.tags
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
resource "aws_iam_role_policy_attachment" "node_worker_policy" {
|
| 49 |
+
role = aws_iam_role.node_group.name
|
| 50 |
+
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
resource "aws_iam_role_policy_attachment" "node_cni_policy" {
|
| 54 |
+
role = aws_iam_role.node_group.name
|
| 55 |
+
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
resource "aws_iam_role_policy_attachment" "node_ecr_policy" {
|
| 59 |
+
role = aws_iam_role.node_group.name
|
| 60 |
+
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# ── security group ─────────────────────────────────────────────────────────
|
| 64 |
+
|
| 65 |
+
resource "aws_security_group" "node" {
|
| 66 |
+
name = "${var.cluster_name}-node-sg"
|
| 67 |
+
description = "Security group for EKS worker nodes"
|
| 68 |
+
vpc_id = var.vpc_id
|
| 69 |
+
|
| 70 |
+
egress {
|
| 71 |
+
from_port = 0
|
| 72 |
+
to_port = 0
|
| 73 |
+
protocol = "-1"
|
| 74 |
+
cidr_blocks = ["0.0.0.0/0"]
|
| 75 |
+
description = "Allow all outbound (git clone, LLM API, ECR pull)"
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
tags = merge(var.tags, { Name = "${var.cluster_name}-node-sg" })
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
# ── cluster ────────────────────────────────────────────────────────────────
|
| 82 |
+
|
| 83 |
+
resource "aws_eks_cluster" "main" {
|
| 84 |
+
name = var.cluster_name
|
| 85 |
+
version = var.cluster_version
|
| 86 |
+
role_arn = aws_iam_role.cluster.arn
|
| 87 |
+
|
| 88 |
+
vpc_config {
|
| 89 |
+
subnet_ids = var.private_subnet_ids
|
| 90 |
+
security_group_ids = [aws_security_group.node.id]
|
| 91 |
+
endpoint_private_access = true
|
| 92 |
+
endpoint_public_access = true # set false + VPN for production hardening
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
depends_on = [aws_iam_role_policy_attachment.cluster_policy]
|
| 96 |
+
tags = var.tags
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
# ── managed node group ─────────────────────────────────────────────────────
|
| 100 |
+
|
| 101 |
+
resource "aws_eks_node_group" "main" {
|
| 102 |
+
cluster_name = aws_eks_cluster.main.name
|
| 103 |
+
node_group_name = "${var.cluster_name}-nodes"
|
| 104 |
+
node_role_arn = aws_iam_role.node_group.arn
|
| 105 |
+
subnet_ids = var.private_subnet_ids
|
| 106 |
+
instance_types = [var.node_instance_type]
|
| 107 |
+
|
| 108 |
+
scaling_config {
|
| 109 |
+
min_size = var.node_min_size
|
| 110 |
+
max_size = var.node_max_size
|
| 111 |
+
desired_size = var.node_desired_size
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
update_config {
|
| 115 |
+
max_unavailable = 1 # rolling update: one node down at a time
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
depends_on = [
|
| 119 |
+
aws_iam_role_policy_attachment.node_worker_policy,
|
| 120 |
+
aws_iam_role_policy_attachment.node_cni_policy,
|
| 121 |
+
aws_iam_role_policy_attachment.node_ecr_policy,
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
tags = var.tags
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
# ── outputs ────────────────────────────────────────────────────────────────
|
| 128 |
+
|
| 129 |
+
output "cluster_endpoint" { value = aws_eks_cluster.main.endpoint }
|
| 130 |
+
output "cluster_name" { value = aws_eks_cluster.main.name }
|
| 131 |
+
output "cluster_certificate_authority_data" { value = aws_eks_cluster.main.certificate_authority[0].data }
|
| 132 |
+
output "node_security_group_id" { value = aws_security_group.node.id }
|
terraform/modules/redis/main.tf
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
variable "cluster_name" { type = string }
|
| 2 |
+
variable "vpc_id" { type = string }
|
| 3 |
+
variable "private_subnet_ids" { type = list(string) }
|
| 4 |
+
variable "eks_security_group_id" { type = string }
|
| 5 |
+
variable "node_type" { type = string }
|
| 6 |
+
variable "num_cache_nodes" { type = number }
|
| 7 |
+
variable "tags" { type = map(string) }
|
| 8 |
+
|
| 9 |
+
# ── subnet group ───────────────────────────────────────────────────────────
|
| 10 |
+
|
| 11 |
+
resource "aws_elasticache_subnet_group" "redis" {
|
| 12 |
+
name = "${var.cluster_name}-redis-subnet-group"
|
| 13 |
+
subnet_ids = var.private_subnet_ids
|
| 14 |
+
tags = var.tags
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
# ── security group ─────────────────────────────────────────────────────────
|
| 18 |
+
# Only EKS worker nodes can reach Redis on port 6379.
|
| 19 |
+
|
| 20 |
+
resource "aws_security_group" "redis" {
|
| 21 |
+
name = "${var.cluster_name}-redis-sg"
|
| 22 |
+
description = "Allow Redis access from EKS worker nodes only"
|
| 23 |
+
vpc_id = var.vpc_id
|
| 24 |
+
|
| 25 |
+
ingress {
|
| 26 |
+
from_port = 6379
|
| 27 |
+
to_port = 6379
|
| 28 |
+
protocol = "tcp"
|
| 29 |
+
security_groups = [var.eks_security_group_id]
|
| 30 |
+
description = "Redis from EKS nodes"
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
egress {
|
| 34 |
+
from_port = 0
|
| 35 |
+
to_port = 0
|
| 36 |
+
protocol = "-1"
|
| 37 |
+
cidr_blocks = ["0.0.0.0/0"]
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
tags = merge(var.tags, { Name = "${var.cluster_name}-redis-sg" })
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
# ── elasticache cluster ────────────────────────────────────────────────────
|
| 44 |
+
|
| 45 |
+
resource "aws_elasticache_cluster" "redis" {
|
| 46 |
+
cluster_id = "${var.cluster_name}-redis"
|
| 47 |
+
engine = "redis"
|
| 48 |
+
engine_version = "7.0"
|
| 49 |
+
node_type = var.node_type
|
| 50 |
+
num_cache_nodes = var.num_cache_nodes
|
| 51 |
+
parameter_group_name = "default.redis7"
|
| 52 |
+
port = 6379
|
| 53 |
+
subnet_group_name = aws_elasticache_subnet_group.redis.name
|
| 54 |
+
security_group_ids = [aws_security_group.redis.id]
|
| 55 |
+
|
| 56 |
+
# Automatic minor version upgrades during maintenance window
|
| 57 |
+
auto_minor_version_upgrade = true
|
| 58 |
+
maintenance_window = "sun:05:00-sun:06:00"
|
| 59 |
+
snapshot_retention_limit = 1 # 1-day snapshot for recovery
|
| 60 |
+
snapshot_window = "04:00-05:00"
|
| 61 |
+
|
| 62 |
+
tags = var.tags
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
# ── outputs ────────────────────────────────────────────────────────────────
|
| 66 |
+
|
| 67 |
+
output "primary_endpoint_address" { value = aws_elasticache_cluster.redis.cache_nodes[0].address }
|
| 68 |
+
output "port" { value = aws_elasticache_cluster.redis.port }
|
terraform/outputs.tf
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
output "cluster_endpoint" {
|
| 2 |
+
description = "EKS cluster API server endpoint"
|
| 3 |
+
value = module.eks.cluster_endpoint
|
| 4 |
+
sensitive = false
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
output "cluster_name" {
|
| 8 |
+
description = "EKS cluster name — pass to kubectl and helm"
|
| 9 |
+
value = module.eks.cluster_name
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
output "cluster_certificate_authority_data" {
|
| 13 |
+
description = "Base64-encoded CA cert for kubectl config"
|
| 14 |
+
value = module.eks.cluster_certificate_authority_data
|
| 15 |
+
sensitive = true
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
output "ecr_repository_url" {
|
| 19 |
+
description = "Full ECR repository URL for docker push"
|
| 20 |
+
value = module.ecr.repository_url
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
output "redis_endpoint" {
|
| 24 |
+
description = "Elasticache Redis primary endpoint — set as REDIS_URL in k8s secret"
|
| 25 |
+
value = module.redis.primary_endpoint_address
|
| 26 |
+
sensitive = true
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
output "redis_port" {
|
| 30 |
+
description = "Elasticache Redis port"
|
| 31 |
+
value = module.redis.port
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
output "kubeconfig_command" {
|
| 35 |
+
description = "Run this command to update your local kubeconfig"
|
| 36 |
+
value = "aws eks update-kubeconfig --region ${var.aws_region} --name ${var.cluster_name}"
|
| 37 |
+
}
|
terraform/variables.tf
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
variable "aws_region" {
|
| 2 |
+
description = "AWS region to deploy into"
|
| 3 |
+
type = string
|
| 4 |
+
default = "us-east-1"
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
variable "cluster_name" {
|
| 8 |
+
description = "EKS cluster name"
|
| 9 |
+
type = string
|
| 10 |
+
default = "ai-code-review-agent"
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
variable "cluster_version" {
|
| 14 |
+
description = "Kubernetes version"
|
| 15 |
+
type = string
|
| 16 |
+
default = "1.29"
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
variable "node_instance_type" {
|
| 20 |
+
description = "EC2 instance type for EKS worker nodes"
|
| 21 |
+
type = string
|
| 22 |
+
default = "t3.medium" # 2 vCPU, 4 GB — adequate for API pods
|
| 23 |
+
# use t3.large for worker pods running LLM calls
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
variable "node_min_size" {
|
| 27 |
+
description = "Minimum number of EKS worker nodes"
|
| 28 |
+
type = number
|
| 29 |
+
default = 2
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
variable "node_max_size" {
|
| 33 |
+
description = "Maximum number of EKS worker nodes"
|
| 34 |
+
type = number
|
| 35 |
+
default = 10
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
variable "node_desired_size" {
|
| 39 |
+
description = "Desired number of EKS worker nodes at launch"
|
| 40 |
+
type = number
|
| 41 |
+
default = 2
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
variable "redis_node_type" {
|
| 45 |
+
description = "Elasticache node type for the Redis task queue and result cache"
|
| 46 |
+
type = string
|
| 47 |
+
default = "cache.t3.micro"
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
variable "redis_num_cache_nodes" {
|
| 51 |
+
description = "Number of Elasticache nodes (1 = single node, no replication)"
|
| 52 |
+
type = number
|
| 53 |
+
default = 1
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
variable "ecr_repository_name" {
|
| 57 |
+
description = "Name of the ECR repository for the application image"
|
| 58 |
+
type = string
|
| 59 |
+
default = "ai-code-review-agent"
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
variable "tags" {
|
| 63 |
+
description = "Tags applied to all resources"
|
| 64 |
+
type = map(string)
|
| 65 |
+
default = {
|
| 66 |
+
Project = "ai-code-review-agent"
|
| 67 |
+
Environment = "production"
|
| 68 |
+
ManagedBy = "terraform"
|
| 69 |
+
}
|
| 70 |
+
}
|