Padmanav commited on
Commit
b63e780
·
1 Parent(s): 3614ecc

infra(terraform): add EKS cluster, Elasticache Redis, and ECR modules for cloud deployment

Browse files
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
+ }