From 445da6d6c6f3847dc86bd3393730890f35b5d6f6 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:04 +0300 Subject: [PATCH 01/34] Add terraform/envs/dev/backend.tf --- terraform/envs/dev/backend.tf | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 terraform/envs/dev/backend.tf diff --git a/terraform/envs/dev/backend.tf b/terraform/envs/dev/backend.tf new file mode 100644 index 0000000..cbc5df2 --- /dev/null +++ b/terraform/envs/dev/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "test" + key = "envs/dev/terraform.tfstate" + region = "us-east-1" + dynamodb_table = "" + encrypt = true + } +} \ No newline at end of file From fed2ea4c7fbe7dae1c27fde84421d2d27c02673d Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:05 +0300 Subject: [PATCH 02/34] Add terraform/envs/dev/providers.tf --- terraform/envs/dev/providers.tf | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 terraform/envs/dev/providers.tf diff --git a/terraform/envs/dev/providers.tf b/terraform/envs/dev/providers.tf new file mode 100644 index 0000000..1332b04 --- /dev/null +++ b/terraform/envs/dev/providers.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Environment = var.environment + ManagedBy = "terraform" + Project = var.project_name + } + } +} \ No newline at end of file From 35324adc43f9cd66e598972766ccacb8e681ba25 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:06 +0300 Subject: [PATCH 03/34] Add terraform/envs/dev/main.tf --- terraform/envs/dev/main.tf | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 terraform/envs/dev/main.tf diff --git a/terraform/envs/dev/main.tf b/terraform/envs/dev/main.tf new file mode 100644 index 0000000..4a1f287 --- /dev/null +++ b/terraform/envs/dev/main.tf @@ -0,0 +1,44 @@ +# Main configuration for dev environment + +# Example VPC module usage +module "vpc" { + source = "../../modules/network" + + environment = var.environment + project_name = var.project_name + vpc_cidr = var.vpc_cidr + + # Environment-specific overrides + enable_nat_gateway = false + enable_vpn_gateway = false +} + +# Example compute module usage +module "compute" { + source = "../../modules/compute" + + environment = var.environment + project_name = var.project_name + vpc_id = module.vpc.vpc_id + private_subnets = module.vpc.private_subnets + + # Environment-specific sizing + instance_type = var.instance_type + min_size = var.min_size + max_size = var.max_size +} + +# Example database module usage +module "database" { + source = "../../modules/database" + + environment = var.environment + project_name = var.project_name + vpc_id = module.vpc.vpc_id + database_subnets = module.vpc.database_subnets + + # Environment-specific configuration + instance_class = var.db_instance_class + allocated_storage = var.db_allocated_storage + backup_retention = 1 +} \ No newline at end of file From 8049db0f60970b1d74c3498acf6a27fff40d7204 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:07 +0300 Subject: [PATCH 04/34] Add terraform/envs/dev/variables.tf --- terraform/envs/dev/variables.tf | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 terraform/envs/dev/variables.tf diff --git a/terraform/envs/dev/variables.tf b/terraform/envs/dev/variables.tf new file mode 100644 index 0000000..f1d736d --- /dev/null +++ b/terraform/envs/dev/variables.tf @@ -0,0 +1,51 @@ +variable "environment" { + description = "Environment name" + type = string +} + +variable "project_name" { + description = "Project name for resource naming" + type = string +} + +variable "aws_region" { + description = "AWS region" + type = string + default = "us-east-1" +} + +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.0.0.0/16" +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t3.micro" +} + +variable "min_size" { + description = "Minimum number of instances in ASG" + type = number + default = 1 +} + +variable "max_size" { + description = "Maximum number of instances in ASG" + type = number + default = 3 +} + +variable "db_instance_class" { + description = "RDS instance class" + type = string + default = "db.t3.micro" +} + +variable "db_allocated_storage" { + description = "RDS allocated storage in GB" + type = number + default = 20 +} \ No newline at end of file From d1999c06507ddb8e3af66dfa9a2084d8721c17d9 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:08 +0300 Subject: [PATCH 05/34] Add terraform/envs/dev/outputs.tf --- terraform/envs/dev/outputs.tf | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 terraform/envs/dev/outputs.tf diff --git a/terraform/envs/dev/outputs.tf b/terraform/envs/dev/outputs.tf new file mode 100644 index 0000000..18d24a3 --- /dev/null +++ b/terraform/envs/dev/outputs.tf @@ -0,0 +1,35 @@ +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +output "vpc_cidr_block" { + description = "CIDR block of the VPC" + value = module.vpc.vpc_cidr_block +} + +output "private_subnets" { + description = "List of IDs of private subnets" + value = module.vpc.private_subnets +} + +output "public_subnets" { + description = "List of IDs of public subnets" + value = module.vpc.public_subnets +} + +output "database_subnets" { + description = "List of IDs of database subnets" + value = module.vpc.database_subnets +} + +output "load_balancer_dns" { + description = "DNS name of the load balancer" + value = module.compute.load_balancer_dns +} + +output "database_endpoint" { + description = "RDS instance endpoint" + value = module.database.endpoint + sensitive = true +} \ No newline at end of file From d15942d64bf2a494f64f643dbcdf3ec212cf36b6 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:09 +0300 Subject: [PATCH 06/34] Add terraform/envs/stage/backend.tf --- terraform/envs/stage/backend.tf | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 terraform/envs/stage/backend.tf diff --git a/terraform/envs/stage/backend.tf b/terraform/envs/stage/backend.tf new file mode 100644 index 0000000..9486467 --- /dev/null +++ b/terraform/envs/stage/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "test" + key = "envs/stage/terraform.tfstate" + region = "us-east-1" + dynamodb_table = "" + encrypt = true + } +} \ No newline at end of file From 461f085bab5586241f25b5af976f0fc88c35fc96 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:10 +0300 Subject: [PATCH 07/34] Add terraform/envs/stage/providers.tf --- terraform/envs/stage/providers.tf | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 terraform/envs/stage/providers.tf diff --git a/terraform/envs/stage/providers.tf b/terraform/envs/stage/providers.tf new file mode 100644 index 0000000..1332b04 --- /dev/null +++ b/terraform/envs/stage/providers.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Environment = var.environment + ManagedBy = "terraform" + Project = var.project_name + } + } +} \ No newline at end of file From e9f2f52b6b927dc9e48b3572952a23719d8d543b Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:10 +0300 Subject: [PATCH 08/34] Add terraform/envs/stage/main.tf --- terraform/envs/stage/main.tf | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 terraform/envs/stage/main.tf diff --git a/terraform/envs/stage/main.tf b/terraform/envs/stage/main.tf new file mode 100644 index 0000000..5a976d7 --- /dev/null +++ b/terraform/envs/stage/main.tf @@ -0,0 +1,44 @@ +# Main configuration for stage environment + +# Example VPC module usage +module "vpc" { + source = "../../modules/network" + + environment = var.environment + project_name = var.project_name + vpc_cidr = var.vpc_cidr + + # Environment-specific overrides + enable_nat_gateway = false + enable_vpn_gateway = false +} + +# Example compute module usage +module "compute" { + source = "../../modules/compute" + + environment = var.environment + project_name = var.project_name + vpc_id = module.vpc.vpc_id + private_subnets = module.vpc.private_subnets + + # Environment-specific sizing + instance_type = var.instance_type + min_size = var.min_size + max_size = var.max_size +} + +# Example database module usage +module "database" { + source = "../../modules/database" + + environment = var.environment + project_name = var.project_name + vpc_id = module.vpc.vpc_id + database_subnets = module.vpc.database_subnets + + # Environment-specific configuration + instance_class = var.db_instance_class + allocated_storage = var.db_allocated_storage + backup_retention = 1 +} \ No newline at end of file From 7f966001e20f8429205d81cb9531bfe62b16389c Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:12 +0300 Subject: [PATCH 09/34] Add terraform/envs/stage/variables.tf --- terraform/envs/stage/variables.tf | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 terraform/envs/stage/variables.tf diff --git a/terraform/envs/stage/variables.tf b/terraform/envs/stage/variables.tf new file mode 100644 index 0000000..f1d736d --- /dev/null +++ b/terraform/envs/stage/variables.tf @@ -0,0 +1,51 @@ +variable "environment" { + description = "Environment name" + type = string +} + +variable "project_name" { + description = "Project name for resource naming" + type = string +} + +variable "aws_region" { + description = "AWS region" + type = string + default = "us-east-1" +} + +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.0.0.0/16" +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t3.micro" +} + +variable "min_size" { + description = "Minimum number of instances in ASG" + type = number + default = 1 +} + +variable "max_size" { + description = "Maximum number of instances in ASG" + type = number + default = 3 +} + +variable "db_instance_class" { + description = "RDS instance class" + type = string + default = "db.t3.micro" +} + +variable "db_allocated_storage" { + description = "RDS allocated storage in GB" + type = number + default = 20 +} \ No newline at end of file From 0784f76642edbefc3603463985dbd8c398f27c09 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:13 +0300 Subject: [PATCH 10/34] Add terraform/envs/stage/outputs.tf --- terraform/envs/stage/outputs.tf | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 terraform/envs/stage/outputs.tf diff --git a/terraform/envs/stage/outputs.tf b/terraform/envs/stage/outputs.tf new file mode 100644 index 0000000..18d24a3 --- /dev/null +++ b/terraform/envs/stage/outputs.tf @@ -0,0 +1,35 @@ +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +output "vpc_cidr_block" { + description = "CIDR block of the VPC" + value = module.vpc.vpc_cidr_block +} + +output "private_subnets" { + description = "List of IDs of private subnets" + value = module.vpc.private_subnets +} + +output "public_subnets" { + description = "List of IDs of public subnets" + value = module.vpc.public_subnets +} + +output "database_subnets" { + description = "List of IDs of database subnets" + value = module.vpc.database_subnets +} + +output "load_balancer_dns" { + description = "DNS name of the load balancer" + value = module.compute.load_balancer_dns +} + +output "database_endpoint" { + description = "RDS instance endpoint" + value = module.database.endpoint + sensitive = true +} \ No newline at end of file From b268915f7e9fe6c356b731e39d716868a4737dcf Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:14 +0300 Subject: [PATCH 11/34] Add terraform/envs/prod/backend.tf --- terraform/envs/prod/backend.tf | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 terraform/envs/prod/backend.tf diff --git a/terraform/envs/prod/backend.tf b/terraform/envs/prod/backend.tf new file mode 100644 index 0000000..6a3c960 --- /dev/null +++ b/terraform/envs/prod/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "test" + key = "envs/prod/terraform.tfstate" + region = "us-east-1" + dynamodb_table = "" + encrypt = true + } +} \ No newline at end of file From 401ed6b0a07a414789210a8c7d25730521ca7348 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:15 +0300 Subject: [PATCH 12/34] Add terraform/envs/prod/providers.tf --- terraform/envs/prod/providers.tf | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 terraform/envs/prod/providers.tf diff --git a/terraform/envs/prod/providers.tf b/terraform/envs/prod/providers.tf new file mode 100644 index 0000000..1332b04 --- /dev/null +++ b/terraform/envs/prod/providers.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Environment = var.environment + ManagedBy = "terraform" + Project = var.project_name + } + } +} \ No newline at end of file From 57df09517fda97698d406c4d8ea297d4c25dd3ed Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:15 +0300 Subject: [PATCH 13/34] Add terraform/envs/prod/main.tf --- terraform/envs/prod/main.tf | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 terraform/envs/prod/main.tf diff --git a/terraform/envs/prod/main.tf b/terraform/envs/prod/main.tf new file mode 100644 index 0000000..243aae5 --- /dev/null +++ b/terraform/envs/prod/main.tf @@ -0,0 +1,44 @@ +# Main configuration for prod environment + +# Example VPC module usage +module "vpc" { + source = "../../modules/network" + + environment = var.environment + project_name = var.project_name + vpc_cidr = var.vpc_cidr + + # Environment-specific overrides + enable_nat_gateway = true + enable_vpn_gateway = true +} + +# Example compute module usage +module "compute" { + source = "../../modules/compute" + + environment = var.environment + project_name = var.project_name + vpc_id = module.vpc.vpc_id + private_subnets = module.vpc.private_subnets + + # Environment-specific sizing + instance_type = var.instance_type + min_size = var.min_size + max_size = var.max_size +} + +# Example database module usage +module "database" { + source = "../../modules/database" + + environment = var.environment + project_name = var.project_name + vpc_id = module.vpc.vpc_id + database_subnets = module.vpc.database_subnets + + # Environment-specific configuration + instance_class = var.db_instance_class + allocated_storage = var.db_allocated_storage + backup_retention = 7 +} \ No newline at end of file From 48e05925f49f365ced56cb4cc5bd7815c784ddb7 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:16 +0300 Subject: [PATCH 14/34] Add terraform/envs/prod/variables.tf --- terraform/envs/prod/variables.tf | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 terraform/envs/prod/variables.tf diff --git a/terraform/envs/prod/variables.tf b/terraform/envs/prod/variables.tf new file mode 100644 index 0000000..f1d736d --- /dev/null +++ b/terraform/envs/prod/variables.tf @@ -0,0 +1,51 @@ +variable "environment" { + description = "Environment name" + type = string +} + +variable "project_name" { + description = "Project name for resource naming" + type = string +} + +variable "aws_region" { + description = "AWS region" + type = string + default = "us-east-1" +} + +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.0.0.0/16" +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t3.micro" +} + +variable "min_size" { + description = "Minimum number of instances in ASG" + type = number + default = 1 +} + +variable "max_size" { + description = "Maximum number of instances in ASG" + type = number + default = 3 +} + +variable "db_instance_class" { + description = "RDS instance class" + type = string + default = "db.t3.micro" +} + +variable "db_allocated_storage" { + description = "RDS allocated storage in GB" + type = number + default = 20 +} \ No newline at end of file From f307322ae7e78c9ce4ed9e03af745e1030236ae8 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:17 +0300 Subject: [PATCH 15/34] Add terraform/envs/prod/outputs.tf --- terraform/envs/prod/outputs.tf | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 terraform/envs/prod/outputs.tf diff --git a/terraform/envs/prod/outputs.tf b/terraform/envs/prod/outputs.tf new file mode 100644 index 0000000..18d24a3 --- /dev/null +++ b/terraform/envs/prod/outputs.tf @@ -0,0 +1,35 @@ +output "vpc_id" { + description = "ID of the VPC" + value = module.vpc.vpc_id +} + +output "vpc_cidr_block" { + description = "CIDR block of the VPC" + value = module.vpc.vpc_cidr_block +} + +output "private_subnets" { + description = "List of IDs of private subnets" + value = module.vpc.private_subnets +} + +output "public_subnets" { + description = "List of IDs of public subnets" + value = module.vpc.public_subnets +} + +output "database_subnets" { + description = "List of IDs of database subnets" + value = module.vpc.database_subnets +} + +output "load_balancer_dns" { + description = "DNS name of the load balancer" + value = module.compute.load_balancer_dns +} + +output "database_endpoint" { + description = "RDS instance endpoint" + value = module.database.endpoint + sensitive = true +} \ No newline at end of file From 42ed49c8b40764de5052e3d99c519825fe2aed95 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:18 +0300 Subject: [PATCH 16/34] Add terraform/modules/network/main.tf --- terraform/modules/network/main.tf | 136 ++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 terraform/modules/network/main.tf diff --git a/terraform/modules/network/main.tf b/terraform/modules/network/main.tf new file mode 100644 index 0000000..943eefc --- /dev/null +++ b/terraform/modules/network/main.tf @@ -0,0 +1,136 @@ +# VPC +resource "aws_vpc" "main" { + cidr_block = var.vpc_cidr + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "${var.project_name}-${var.environment}-vpc" + } +} + +# Internet Gateway +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "${var.project_name}-${var.environment}-igw" + } +} + +# Public Subnets +resource "aws_subnet" "public" { + count = length(var.availability_zones) + + vpc_id = aws_vpc.main.id + cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index) + availability_zone = data.aws_availability_zones.available.names[count.index] + map_public_ip_on_launch = true + + tags = { + Name = "${var.project_name}-${var.environment}-public-${count.index + 1}" + Type = "Public" + } +} + +# Private Subnets +resource "aws_subnet" "private" { + count = length(var.availability_zones) + + vpc_id = aws_vpc.main.id + cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10) + availability_zone = data.aws_availability_zones.available.names[count.index] + + tags = { + Name = "${var.project_name}-${var.environment}-private-${count.index + 1}" + Type = "Private" + } +} + +# Database Subnets +resource "aws_subnet" "database" { + count = length(var.availability_zones) + + vpc_id = aws_vpc.main.id + cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 20) + availability_zone = data.aws_availability_zones.available.names[count.index] + + tags = { + Name = "${var.project_name}-${var.environment}-database-${count.index + 1}" + Type = "Database" + } +} + +# NAT Gateways +resource "aws_eip" "nat" { + count = var.enable_nat_gateway ? length(var.availability_zones) : 0 + + domain = "vpc" + depends_on = [aws_internet_gateway.main] + + tags = { + Name = "${var.project_name}-${var.environment}-nat-eip-${count.index + 1}" + } +} + +resource "aws_nat_gateway" "main" { + count = var.enable_nat_gateway ? length(var.availability_zones) : 0 + + allocation_id = aws_eip.nat[count.index].id + subnet_id = aws_subnet.public[count.index].id + + tags = { + Name = "${var.project_name}-${var.environment}-nat-${count.index + 1}" + } + + depends_on = [aws_internet_gateway.main] +} + +# Route Tables +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } + + tags = { + Name = "${var.project_name}-${var.environment}-public-rt" + } +} + +resource "aws_route_table" "private" { + count = var.enable_nat_gateway ? length(var.availability_zones) : 0 + + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.main[count.index].id + } + + tags = { + Name = "${var.project_name}-${var.environment}-private-rt-${count.index + 1}" + } +} + +# Route Table Associations +resource "aws_route_table_association" "public" { + count = length(aws_subnet.public) + + subnet_id = aws_subnet.public[count.index].id + route_table_id = aws_route_table.public.id +} + +resource "aws_route_table_association" "private" { + count = var.enable_nat_gateway ? length(aws_subnet.private) : 0 + + subnet_id = aws_subnet.private[count.index].id + route_table_id = aws_route_table.private[count.index].id +} + +# Data Sources +data "aws_availability_zones" "available" { + state = "available" +} \ No newline at end of file From 03541af9060014da6bdfd29a93c91486b85e02b6 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:19 +0300 Subject: [PATCH 17/34] Add terraform/modules/network/variables.tf --- terraform/modules/network/variables.tf | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 terraform/modules/network/variables.tf diff --git a/terraform/modules/network/variables.tf b/terraform/modules/network/variables.tf new file mode 100644 index 0000000..128de8b --- /dev/null +++ b/terraform/modules/network/variables.tf @@ -0,0 +1,33 @@ +variable "environment" { + description = "Environment name" + type = string +} + +variable "project_name" { + description = "Project name for resource naming" + type = string +} + +variable "vpc_cidr" { + description = "CIDR block for VPC" + type = string + default = "10.0.0.0/16" +} + +variable "availability_zones" { + description = "Number of availability zones to use" + type = list(string) + default = ["a", "b"] +} + +variable "enable_nat_gateway" { + description = "Enable NAT Gateway for private subnets" + type = bool + default = true +} + +variable "enable_vpn_gateway" { + description = "Enable VPN Gateway" + type = bool + default = false +} \ No newline at end of file From 85f59ae0d41447f2ba9eca8929c79d9f92193b51 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:20 +0300 Subject: [PATCH 18/34] Add terraform/modules/network/outputs.tf --- terraform/modules/network/outputs.tf | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 terraform/modules/network/outputs.tf diff --git a/terraform/modules/network/outputs.tf b/terraform/modules/network/outputs.tf new file mode 100644 index 0000000..ce91227 --- /dev/null +++ b/terraform/modules/network/outputs.tf @@ -0,0 +1,34 @@ +output "vpc_id" { + description = "ID of the VPC" + value = aws_vpc.main.id +} + +output "vpc_cidr_block" { + description = "CIDR block of the VPC" + value = aws_vpc.main.cidr_block +} + +output "internet_gateway_id" { + description = "ID of the Internet Gateway" + value = aws_internet_gateway.main.id +} + +output "public_subnets" { + description = "List of IDs of public subnets" + value = aws_subnet.public[*].id +} + +output "private_subnets" { + description = "List of IDs of private subnets" + value = aws_subnet.private[*].id +} + +output "database_subnets" { + description = "List of IDs of database subnets" + value = aws_subnet.database[*].id +} + +output "nat_gateway_ids" { + description = "List of IDs of the NAT Gateways" + value = aws_nat_gateway.main[*].id +} \ No newline at end of file From b52274bef0966b4abab3b49b689d48e6aab62e37 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:21 +0300 Subject: [PATCH 19/34] Add terraform/modules/compute/main.tf --- terraform/modules/compute/main.tf | 161 ++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 terraform/modules/compute/main.tf diff --git a/terraform/modules/compute/main.tf b/terraform/modules/compute/main.tf new file mode 100644 index 0000000..5023554 --- /dev/null +++ b/terraform/modules/compute/main.tf @@ -0,0 +1,161 @@ +# Application Load Balancer +resource "aws_lb" "main" { + name = "${var.project_name}-${var.environment}-alb" + internal = false + load_balancer_type = "application" + security_groups = [aws_security_group.alb.id] + subnets = var.public_subnets + + enable_deletion_protection = var.environment == "prod" + + tags = { + Name = "${var.project_name}-${var.environment}-alb" + } +} + +# Security Groups +resource "aws_security_group" "alb" { + name = "${var.project_name}-${var.environment}-alb-sg" + description = "Security group for ALB" + vpc_id = var.vpc_id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-${var.environment}-alb-sg" + } +} + +resource "aws_security_group" "ec2" { + name = "${var.project_name}-${var.environment}-ec2-sg" + description = "Security group for EC2 instances" + vpc_id = var.vpc_id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + security_groups = [aws_security_group.alb.id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-${var.environment}-ec2-sg" + } +} + +# Launch Template +resource "aws_launch_template" "main" { + name_prefix = "${var.project_name}-${var.environment}-" + image_id = data.aws_ami.amazon_linux.id + instance_type = var.instance_type + + vpc_security_group_ids = [aws_security_group.ec2.id] + + user_data = base64encode(templatefile("${path.module}/user_data.sh", { + project_name = var.project_name + environment = var.environment + })) + + tag_specifications { + resource_type = "instance" + tags = { + Name = "${var.project_name}-${var.environment}" + } + } +} + +# Auto Scaling Group +resource "aws_autoscaling_group" "main" { + name = "${var.project_name}-${var.environment}-asg" + vpc_zone_identifier = var.private_subnets + target_group_arns = [aws_lb_target_group.main.arn] + health_check_type = "ELB" + + min_size = var.min_size + max_size = var.max_size + desired_capacity = var.desired_capacity + + launch_template { + id = aws_launch_template.main.id + version = "$Latest" + } + + tag { + key = "Name" + value = "${var.project_name}-${var.environment}-asg" + propagate_at_launch = false + } +} + +# Target Group +resource "aws_lb_target_group" "main" { + name = "${var.project_name}-${var.environment}-tg" + port = 80 + protocol = "HTTP" + vpc_id = var.vpc_id + + health_check { + enabled = true + healthy_threshold = 2 + interval = 30 + matcher = "200" + path = "/" + port = "traffic-port" + protocol = "HTTP" + timeout = 5 + unhealthy_threshold = 2 + } + + tags = { + Name = "${var.project_name}-${var.environment}-tg" + } +} + +# ALB Listener +resource "aws_lb_listener" "main" { + load_balancer_arn = aws_lb.main.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.main.arn + } +} + +# Data Sources +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn2-ami-hvm-*-x86_64-gp2"] + } +} \ No newline at end of file From afefec2e876f3d89640cc0803234b3fa892ad5da Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:22 +0300 Subject: [PATCH 20/34] Add terraform/modules/compute/variables.tf --- terraform/modules/compute/variables.tf | 48 ++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 terraform/modules/compute/variables.tf diff --git a/terraform/modules/compute/variables.tf b/terraform/modules/compute/variables.tf new file mode 100644 index 0000000..73c408a --- /dev/null +++ b/terraform/modules/compute/variables.tf @@ -0,0 +1,48 @@ +variable "environment" { + description = "Environment name" + type = string +} + +variable "project_name" { + description = "Project name for resource naming" + type = string +} + +variable "vpc_id" { + description = "ID of the VPC" + type = string +} + +variable "public_subnets" { + description = "List of public subnet IDs" + type = list(string) +} + +variable "private_subnets" { + description = "List of private subnet IDs" + type = list(string) +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t3.micro" +} + +variable "min_size" { + description = "Minimum number of instances in ASG" + type = number + default = 1 +} + +variable "max_size" { + description = "Maximum number of instances in ASG" + type = number + default = 3 +} + +variable "desired_capacity" { + description = "Desired number of instances in ASG" + type = number + default = 2 +} \ No newline at end of file From 00d9f2b25db7be5bf912da444aeb2d9cb8a69994 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:22 +0300 Subject: [PATCH 21/34] Add terraform/modules/compute/outputs.tf --- terraform/modules/compute/outputs.tf | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 terraform/modules/compute/outputs.tf diff --git a/terraform/modules/compute/outputs.tf b/terraform/modules/compute/outputs.tf new file mode 100644 index 0000000..1de7c6b --- /dev/null +++ b/terraform/modules/compute/outputs.tf @@ -0,0 +1,24 @@ +output "load_balancer_arn" { + description = "ARN of the load balancer" + value = aws_lb.main.arn +} + +output "load_balancer_dns" { + description = "DNS name of the load balancer" + value = aws_lb.main.dns_name +} + +output "target_group_arn" { + description = "ARN of the target group" + value = aws_lb_target_group.main.arn +} + +output "autoscaling_group_arn" { + description = "ARN of the Auto Scaling Group" + value = aws_autoscaling_group.main.arn +} + +output "security_group_ids" { + description = "List of security group IDs" + value = [aws_security_group.ec2.id, aws_security_group.alb.id] +} \ No newline at end of file From 55c0ad9681cb6942737eea817b5408373d6e8c38 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:23 +0300 Subject: [PATCH 22/34] Add terraform/modules/database/main.tf --- terraform/modules/database/main.tf | 68 ++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 terraform/modules/database/main.tf diff --git a/terraform/modules/database/main.tf b/terraform/modules/database/main.tf new file mode 100644 index 0000000..494f544 --- /dev/null +++ b/terraform/modules/database/main.tf @@ -0,0 +1,68 @@ +# DB Subnet Group +resource "aws_db_subnet_group" "main" { + name = "${var.project_name}-${var.environment}-db-subnet-group" + subnet_ids = var.database_subnets + + tags = { + Name = "${var.project_name}-${var.environment}-db-subnet-group" + } +} + +# Security Group for RDS +resource "aws_security_group" "rds" { + name = "${var.project_name}-${var.environment}-rds-sg" + description = "Security group for RDS database" + vpc_id = var.vpc_id + + ingress { + from_port = 3306 + to_port = 3306 + protocol = "tcp" + cidr_blocks = [var.vpc_cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-${var.environment}-rds-sg" + } +} + +# RDS Instance +resource "aws_db_instance" "main" { + identifier = "${var.project_name}-${var.environment}-db" + + engine = var.engine + engine_version = var.engine_version + instance_class = var.instance_class + + allocated_storage = var.allocated_storage + max_allocated_storage = var.max_allocated_storage + storage_type = var.storage_type + storage_encrypted = true + + db_name = var.database_name + username = var.username + password = var.password + + vpc_security_group_ids = [aws_security_group.rds.id] + db_subnet_group_name = aws_db_subnet_group.main.name + + backup_retention_period = var.backup_retention_period + backup_window = var.backup_window + maintenance_window = var.maintenance_window + + skip_final_snapshot = var.environment != "prod" + final_snapshot_identifier = var.environment == "prod" ? "${var.project_name}-${var.environment}-final-snapshot-${formatdate("YYYY-MM-DD-hhmm", timestamp())}" : null + + deletion_protection = var.environment == "prod" + + tags = { + Name = "${var.project_name}-${var.environment}-db" + } +} \ No newline at end of file From f9b14f63f7f62d0e5a3e116939e4b9957a89c02f Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:24 +0300 Subject: [PATCH 23/34] Add terraform/modules/database/variables.tf --- terraform/modules/database/variables.tf | 96 +++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 terraform/modules/database/variables.tf diff --git a/terraform/modules/database/variables.tf b/terraform/modules/database/variables.tf new file mode 100644 index 0000000..e1643ed --- /dev/null +++ b/terraform/modules/database/variables.tf @@ -0,0 +1,96 @@ +variable "environment" { + description = "Environment name" + type = string +} + +variable "project_name" { + description = "Project name for resource naming" + type = string +} + +variable "vpc_id" { + description = "ID of the VPC" + type = string +} + +variable "vpc_cidr_block" { + description = "CIDR block of the VPC" + type = string +} + +variable "database_subnets" { + description = "List of database subnet IDs" + type = list(string) +} + +variable "engine" { + description = "Database engine" + type = string + default = "mysql" +} + +variable "engine_version" { + description = "Database engine version" + type = string + default = "8.0" +} + +variable "instance_class" { + description = "RDS instance class" + type = string + default = "db.t3.micro" +} + +variable "allocated_storage" { + description = "Allocated storage in GB" + type = number + default = 20 +} + +variable "max_allocated_storage" { + description = "Maximum allocated storage in GB" + type = number + default = 100 +} + +variable "storage_type" { + description = "Storage type" + type = string + default = "gp2" +} + +variable "database_name" { + description = "Name of the database" + type = string + default = "app" +} + +variable "username" { + description = "Database master username" + type = string + default = "admin" +} + +variable "password" { + description = "Database master password" + type = string + sensitive = true +} + +variable "backup_retention_period" { + description = "Backup retention period in days" + type = number + default = 7 +} + +variable "backup_window" { + description = "Backup window" + type = string + default = "03:00-04:00" +} + +variable "maintenance_window" { + description = "Maintenance window" + type = string + default = "sun:04:00-sun:05:00" +} \ No newline at end of file From 9d706f15f86650493517ed79b8bcacbe905b1f92 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:25 +0300 Subject: [PATCH 24/34] Add terraform/modules/database/outputs.tf --- terraform/modules/database/outputs.tf | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 terraform/modules/database/outputs.tf diff --git a/terraform/modules/database/outputs.tf b/terraform/modules/database/outputs.tf new file mode 100644 index 0000000..9b3717a --- /dev/null +++ b/terraform/modules/database/outputs.tf @@ -0,0 +1,25 @@ +output "endpoint" { + description = "RDS instance endpoint" + value = aws_db_instance.main.endpoint +} + +output "port" { + description = "RDS instance port" + value = aws_db_instance.main.port +} + +output "database_name" { + description = "Database name" + value = aws_db_instance.main.db_name +} + +output "username" { + description = "Database master username" + value = aws_db_instance.main.username + sensitive = true +} + +output "security_group_id" { + description = "ID of the RDS security group" + value = aws_security_group.rds.id +} \ No newline at end of file From fd50ab7cc6ecd4602752b3d5b483cc0cf15ecbf5 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:26 +0300 Subject: [PATCH 25/34] Add policies/digitalocean/security.rego --- policies/digitalocean/security.rego | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 policies/digitalocean/security.rego diff --git a/policies/digitalocean/security.rego b/policies/digitalocean/security.rego new file mode 100644 index 0000000..8b9f92a --- /dev/null +++ b/policies/digitalocean/security.rego @@ -0,0 +1,87 @@ +package digitalocean.security + +# Deny droplets without private networking +deny[res] { + input.resource_type == "digitalocean_droplet" + not input.config.private_networking + res := { + "address": input.address, + "rule": "digitalocean.security.private_networking_required", + "severity": "medium", + "message": sprintf("Droplet %s should enable private networking", [input.name]) + } +} + +# Require monitoring for production droplets +deny[res] { + input.resource_type == "digitalocean_droplet" + input.config.tags[_] == "environment:production" + not input.config.monitoring + res := { + "address": input.address, + "rule": "digitalocean.security.monitoring_required", + "severity": "medium", + "message": sprintf("Production droplet %s must enable monitoring", [input.name]) + } +} + +# Deny public Spaces (object storage) +deny[res] { + input.resource_type == "digitalocean_spaces_bucket" + input.config.acl == "public-read" + res := { + "address": input.address, + "rule": "digitalocean.security.no_public_spaces", + "severity": "high", + "message": sprintf("Spaces bucket %s should not be public", [input.name]) + } +} + +deny[res] { + input.resource_type == "digitalocean_spaces_bucket" + input.config.acl == "public-read-write" + res := { + "address": input.address, + "rule": "digitalocean.security.no_public_spaces", + "severity": "critical", + "message": sprintf("Spaces bucket %s allows public write access", [input.name]) + } +} + +# Require backups for database clusters +deny[res] { + input.resource_type == "digitalocean_database_cluster" + not input.config.backup_restore.backup_hour + res := { + "address": input.address, + "rule": "digitalocean.security.database_backup_required", + "severity": "high", + "message": sprintf("Database cluster %s should enable automated backups", [input.name]) + } +} + +# Require encryption for database clusters +deny[res] { + input.resource_type == "digitalocean_database_cluster" + not input.config.storage_size_mib + input.config.engine != "redis" # Redis doesn't support encryption at rest + res := { + "address": input.address, + "rule": "digitalocean.security.database_encryption_recommended", + "severity": "medium", + "message": sprintf("Database cluster %s should consider encryption at rest", [input.name]) + } +} + +# Warn about small droplet sizes in production +warn[res] { + input.resource_type == "digitalocean_droplet" + input.config.tags[_] == "environment:production" + input.config.size in ["s-1vcpu-1gb", "s-1vcpu-2gb"] + res := { + "address": input.address, + "rule": "digitalocean.security.production_sizing", + "severity": "low", + "message": sprintf("Production droplet %s uses small size, consider scaling up", [input.name]) + } +} \ No newline at end of file From 3e3fa21a94517a48ad773945328bac33d201ea5d Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:27 +0300 Subject: [PATCH 26/34] Add policies/digitalocean/compliance.rego --- policies/digitalocean/compliance.rego | 95 +++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 policies/digitalocean/compliance.rego diff --git a/policies/digitalocean/compliance.rego b/policies/digitalocean/compliance.rego new file mode 100644 index 0000000..b7dad35 --- /dev/null +++ b/policies/digitalocean/compliance.rego @@ -0,0 +1,95 @@ +package digitalocean.compliance + +# Require specific tags for all resources +deny[res] { + input.resource_type in [ + "digitalocean_droplet", + "digitalocean_database_cluster", + "digitalocean_spaces_bucket", + "digitalocean_loadbalancer" + ] + not input.config.tags + res := { + "address": input.address, + "rule": "digitalocean.compliance.tags_required", + "severity": "medium", + "message": sprintf("Resource %s must have tags", [input.address]) + } +} + +deny[res] { + input.resource_type in [ + "digitalocean_droplet", + "digitalocean_database_cluster", + "digitalocean_spaces_bucket", + "digitalocean_loadbalancer" + ] + input.config.tags + not input.config.tags.environment + res := { + "address": input.address, + "rule": "digitalocean.compliance.environment_tag_required", + "severity": "medium", + "message": sprintf("Resource %s missing required tag: environment", [input.address]) + } +} + +deny[res] { + input.resource_type in [ + "digitalocean_droplet", + "digitalocean_database_cluster", + "digitalocean_spaces_bucket", + "digitalocean_loadbalancer" + ] + input.config.tags + not input.config.tags.project + res := { + "address": input.address, + "rule": "digitalocean.compliance.project_tag_required", + "severity": "medium", + "message": sprintf("Resource %s missing required tag: project", [input.address]) + } +} + +# Require specific regions for compliance +deny[res] { + input.resource_type in [ + "digitalocean_droplet", + "digitalocean_database_cluster" + ] + not input.config.region in ["nyc1", "nyc3", "ams3", "sgp1", "lon1", "fra1"] + res := { + "address": input.address, + "rule": "digitalocean.compliance.approved_regions", + "severity": "medium", + "message": sprintf("Resource %s must use approved regions", [input.address]) + } +} + +# Require VPC for production resources +deny[res] { + input.resource_type == "digitalocean_droplet" + input.config.tags[_] == "environment:production" + not input.config.vpc_uuid + res := { + "address": input.address, + "rule": "digitalocean.compliance.production_vpc_required", + "severity": "high", + "message": sprintf("Production droplet %s must be in a VPC", [input.name]) + } +} + +# Enforce naming conventions +deny[res] { + input.resource_type in [ + "digitalocean_droplet", + "digitalocean_database_cluster" + ] + not regex.match("^[a-z][a-z0-9-]*[a-z0-9]$", input.config.name) + res := { + "address": input.address, + "rule": "digitalocean.compliance.naming_convention", + "severity": "low", + "message": sprintf("Resource %s name should follow kebab-case convention", [input.address]) + } +} \ No newline at end of file From 9c37368400e80b3384ddf05b74223151f8bf5e7f Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:28 +0300 Subject: [PATCH 27/34] Add policy-lib/terraform.rego --- policy-lib/terraform.rego | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 policy-lib/terraform.rego diff --git a/policy-lib/terraform.rego b/policy-lib/terraform.rego new file mode 100644 index 0000000..084e56e --- /dev/null +++ b/policy-lib/terraform.rego @@ -0,0 +1,32 @@ +package terraform + +# Helper functions for Terraform plan analysis + +# Get all resources of a specific type +resources_by_type(resource_type) = resources { + resources := [resource | + resource := input.planned_values.root_module.resources[_] + resource.type == resource_type + ] +} + +# Get all resource changes of a specific type +resource_changes_by_type(resource_type) = changes { + changes := [change | + change := input.resource_changes[_] + change.type == resource_type + ] +} + +# Check if a resource has a specific tag +has_tag(resource, tag_name) { + resource.values.tags[tag_name] +} + +# Check if a resource has all required tags +has_required_tags(resource, required_tags) { + count([tag | + tag := required_tags[_] + has_tag(resource, tag) + ]) == count(required_tags) +} \ No newline at end of file From ce3616a730c07cb39c069d4d5b04be67636a2abd Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:28 +0300 Subject: [PATCH 28/34] Add policy-lib/utils.rego --- policy-lib/utils.rego | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 policy-lib/utils.rego diff --git a/policy-lib/utils.rego b/policy-lib/utils.rego new file mode 100644 index 0000000..ab7390e --- /dev/null +++ b/policy-lib/utils.rego @@ -0,0 +1,30 @@ +package utils + +# Utility functions for policy evaluation + +# Check if a CIDR allows access from anywhere +allows_public_access(cidr_blocks) { + "0.0.0.0/0" in cidr_blocks +} + +# Check if a port range includes a specific port +port_range_includes(from_port, to_port, target_port) { + from_port <= target_port + to_port >= target_port +} + +# Get severity level as number for comparison +severity_level(severity) = level { + severity_map := { + "low": 1, + "medium": 2, + "high": 3, + "critical": 4 + } + level := severity_map[severity] +} + +# Check if severity meets threshold +severity_meets_threshold(severity, threshold) { + severity_level(severity) >= severity_level(threshold) +} \ No newline at end of file From dedee87e7fce106e8c1ff62e8b6363730ed310d4 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:29 +0300 Subject: [PATCH 29/34] Add .inframorph-policy.yaml --- .inframorph-policy.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .inframorph-policy.yaml diff --git a/.inframorph-policy.yaml b/.inframorph-policy.yaml new file mode 100644 index 0000000..112596f --- /dev/null +++ b/.inframorph-policy.yaml @@ -0,0 +1,13 @@ +default_severity: medium +exceptions: +- expires_at: '2025-12-31' + id: example_exception + reason: Temporary exception for migration + rules: + - aws.security.no_ssh_from_anywhere +fail_on: +- high +- critical +providers: +- digitalocean +version: 1 From dc9eed90a6a2656b251bb8283aedae004ca3bff6 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:30 +0300 Subject: [PATCH 30/34] Add scripts/export_tfplan_json.sh --- scripts/export_tfplan_json.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 scripts/export_tfplan_json.sh diff --git a/scripts/export_tfplan_json.sh b/scripts/export_tfplan_json.sh new file mode 100644 index 0000000..83ffc11 --- /dev/null +++ b/scripts/export_tfplan_json.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Export Terraform plan to JSON format +# Usage: ./export_tfplan_json.sh [plan_file] [json_file] + +BIN_PATH="${1:-terraform/tfplan.bin}" +JSON_PATH="${2:-terraform/tfplan.json}" + +if [ ! -f "$BIN_PATH" ]; then + echo "Error: Terraform plan file not found: $BIN_PATH" + exit 1 +fi + +echo "Exporting Terraform plan to JSON..." +terraform show -json "$BIN_PATH" > "$JSON_PATH" + +echo "Plan exported to: $JSON_PATH" +echo "File size: $(wc -c < "$JSON_PATH") bytes" From 32a551b9f32ee31e1e278475be37e842adc71ffa Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:31 +0300 Subject: [PATCH 31/34] Add scripts/summarize_conftest.py --- scripts/summarize_conftest.py | 109 ++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 scripts/summarize_conftest.py diff --git a/scripts/summarize_conftest.py b/scripts/summarize_conftest.py new file mode 100644 index 0000000..3fc3770 --- /dev/null +++ b/scripts/summarize_conftest.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +Summarize Conftest output for PR/MR comments +""" + +import json +import sys +from collections import defaultdict +from typing import Dict, List, Any + +def load_conftest_results(file_path: str) -> List[Dict]: + """Load conftest results from JSON file""" + try: + with open(file_path, 'r') as f: + results = json.load(f) + return results if isinstance(results, list) else [results] + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error loading conftest results: {e}") + return [] + +def group_by_severity(results: List[Dict]) -> Dict[str, List[Dict]]: + """Group violations by severity""" + grouped = defaultdict(list) + + for result in results: + if 'failures' in result: + for failure in result['failures']: + severity = failure.get('metadata', {}).get('severity', 'unknown') + grouped[severity].append(failure) + + return dict(grouped) + +def format_violation(violation: Dict) -> str: + """Format a single violation for display""" + rule = violation.get('metadata', {}).get('rule', 'unknown') + message = violation.get('msg', 'No message provided') + + return f"- [{rule}] {message}" + +def generate_summary(grouped_violations: Dict[str, List[Dict]]) -> str: + """Generate markdown summary""" + if not grouped_violations: + return """### ✅ Policy Check Summary (OPA/Conftest) + +No policy violations found! All checks passed. +""" + + # Count violations by severity + severity_counts = {severity: len(violations) for severity, violations in grouped_violations.items()} + total_violations = sum(severity_counts.values()) + + # Sort severities by priority + severity_order = ['critical', 'high', 'medium', 'low', 'unknown'] + + summary = "### 🚨 Policy Check Summary (OPA/Conftest)\n\n" + summary += f"**Total Violations**: {total_violations}\n\n" + + # Summary counts + for severity in severity_order: + if severity in severity_counts: + count = severity_counts[severity] + emoji = {'critical': '🔴', 'high': '🟠', 'medium': '🟡', 'low': '🔵'}.get(severity, '⚪') + summary += f"- {emoji} **{severity.title()}**: {count}\n" + + summary += "\n**Violations**\n\n" + + # Detailed violations + for severity in severity_order: + if severity in grouped_violations: + violations = grouped_violations[severity] + if violations: + emoji = {'critical': '🔴', 'high': '🟠', 'medium': '🟡', 'low': '🔵'}.get(severity, '⚪') + summary += f"#### {emoji} {severity.title()} Severity\n\n" + + for violation in violations[:10]: # Limit to first 10 + summary += format_violation(violation) + "\n" + + if len(violations) > 10: + summary += f"... and {len(violations) - 10} more\n" + + summary += "\n" + + # Check if merge should be blocked + blocking_severities = ['critical', 'high'] + has_blocking_violations = any(severity in grouped_violations for severity in blocking_severities) + + if has_blocking_violations: + summary += "> ⚠️ **Merge is blocked** due to high/critical violations\n\n" + summary += "Please fix the violations above or add appropriate exceptions to `.inframorph-policy.yaml`\n" + else: + summary += "> ✅ **Merge allowed** - no blocking violations found\n" + + return summary + +def main(): + """Main function""" + if len(sys.argv) != 2: + print("Usage: python3 summarize_conftest.py ") + sys.exit(1) + + results_file = sys.argv[1] + results = load_conftest_results(results_file) + grouped = group_by_severity(results) + summary = generate_summary(grouped) + + print(summary) + +if __name__ == '__main__': + main() From d40753af0066dfd4be136aa5493300f69bb97604 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:32 +0300 Subject: [PATCH 32/34] Add scripts/check_exceptions.py --- scripts/check_exceptions.py | 134 ++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 scripts/check_exceptions.py diff --git a/scripts/check_exceptions.py b/scripts/check_exceptions.py new file mode 100644 index 0000000..b30606d --- /dev/null +++ b/scripts/check_exceptions.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +Check policy exceptions and filter violations +""" + +import json +import sys +import yaml +from datetime import datetime +from typing import Dict, List, Any + +def load_conftest_results(file_path: str) -> List[Dict]: + """Load conftest results from JSON file""" + try: + with open(file_path, 'r') as f: + results = json.load(f) + return results if isinstance(results, list) else [results] + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error loading conftest results: {e}") + return [] + +def load_policy_config(file_path: str) -> Dict[str, Any]: + """Load policy configuration""" + try: + with open(file_path, 'r') as f: + return yaml.safe_load(f) + except (FileNotFoundError, yaml.YAMLError) as e: + print(f"Error loading policy config: {e}") + return {} + +def is_exception_valid(exception: Dict[str, Any]) -> bool: + """Check if an exception is still valid (not expired)""" + expires_at = exception.get('expires_at') + if not expires_at: + return True # No expiration date + + try: + expiry_date = datetime.fromisoformat(expires_at) + return datetime.now() < expiry_date + except ValueError: + print(f"Invalid expiration date format: {expires_at}") + return False + +def should_ignore_violation(violation: Dict, exceptions: List[Dict]) -> bool: + """Check if a violation should be ignored due to exceptions""" + rule = violation.get('metadata', {}).get('rule') + address = violation.get('metadata', {}).get('address', '') + + for exception in exceptions: + if not is_exception_valid(exception): + continue + + # Check if this rule is excepted + if rule in exception.get('rules', []): + # Check if address matches (if specified) + excepted_id = exception.get('id', '') + if not excepted_id or excepted_id in address: + return True + + return False + +def filter_violations(results: List[Dict], policy_config: Dict[str, Any]) -> List[Dict]: + """Filter violations based on exceptions""" + exceptions = policy_config.get('exceptions', []) + filtered_results = [] + + for result in results: + if 'failures' not in result: + filtered_results.append(result) + continue + + filtered_failures = [] + for failure in result['failures']: + if not should_ignore_violation(failure, exceptions): + filtered_failures.append(failure) + + if filtered_failures: + filtered_result = result.copy() + filtered_result['failures'] = filtered_failures + filtered_results.append(filtered_result) + + return filtered_results + +def check_blocking_violations(results: List[Dict], policy_config: Dict[str, Any]) -> bool: + """Check if there are any blocking violations""" + fail_on = policy_config.get('fail_on', ['high', 'critical']) + + for result in results: + if 'failures' in result: + for failure in result['failures']: + severity = failure.get('metadata', {}).get('severity', 'unknown') + if severity in fail_on: + return True + + return False + +def main(): + """Main function""" + if len(sys.argv) != 3: + print("Usage: python3 check_exceptions.py ") + sys.exit(1) + + results_file = sys.argv[1] + config_file = sys.argv[2] + + # Load files + results = load_conftest_results(results_file) + policy_config = load_policy_config(config_file) + + # Filter violations + filtered_results = filter_violations(results, policy_config) + + # Check for blocking violations + has_blocking = check_blocking_violations(filtered_results, policy_config) + + if has_blocking: + print("❌ Policy check failed: blocking violations found after applying exceptions") + + # Print remaining violations + for result in filtered_results: + if 'failures' in result: + for failure in result['failures']: + severity = failure.get('metadata', {}).get('severity', 'unknown') + rule = failure.get('metadata', {}).get('rule', 'unknown') + message = failure.get('msg', 'No message') + print(f" [{severity}] {rule}: {message}") + + sys.exit(1) + else: + print("✅ Policy check passed: no blocking violations after applying exceptions") + sys.exit(0) + +if __name__ == '__main__': + main() From 47e29b36371ae58c13e3de395ac5ec1de0662372 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:33 +0300 Subject: [PATCH 33/34] Add scripts/setup_secrets.py --- scripts/setup_secrets.py | 108 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 scripts/setup_secrets.py diff --git a/scripts/setup_secrets.py b/scripts/setup_secrets.py new file mode 100644 index 0000000..4713569 --- /dev/null +++ b/scripts/setup_secrets.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +Helper script to guide users through setting up CI/CD secrets +""" + +import sys +from typing import Dict, List + +def print_github_secrets_guide(provider: str): + """Print GitHub Actions secrets setup guide""" + print("🔐 GitHub Actions Secrets Setup") + print("=" * 40) + print() + print("Navigate to your repository settings:") + print("Repository → Settings → Secrets and variables → Actions") + print() + print("Add the following repository secrets:") + print() + + if provider == 'aws': + secrets = { + 'AWS_ACCESS_KEY_ID': 'Your AWS access key ID', + 'AWS_SECRET_ACCESS_KEY': 'Your AWS secret access key', + 'AWS_DEFAULT_REGION': 'Your preferred AWS region (e.g., us-east-1)' + } + elif provider == 'gcp': + secrets = { + 'GOOGLE_CREDENTIALS': 'Service account JSON key (base64 encoded)', + 'GOOGLE_PROJECT': 'Your GCP project ID' + } + elif provider == 'azure': + secrets = { + 'ARM_CLIENT_ID': 'Azure service principal client ID', + 'ARM_CLIENT_SECRET': 'Azure service principal client secret', + 'ARM_SUBSCRIPTION_ID': 'Azure subscription ID', + 'ARM_TENANT_ID': 'Azure tenant ID' + } + elif provider == 'digitalocean': + secrets = { + 'SPACES_ACCESS_KEY_ID': 'DigitalOcean Spaces access key', + 'SPACES_SECRET_ACCESS_KEY': 'DigitalOcean Spaces secret key', + 'SPACES_REGION': 'DigitalOcean Spaces region', + 'SPACES_ENDPOINT': 'DigitalOcean Spaces endpoint URL' + } + else: + print(f"Unknown provider: {provider}") + return + + for secret_name, description in secrets.items(): + print(f"• {secret_name}") + print(f" {description}") + print() + +def print_gitlab_secrets_guide(provider: str): + """Print GitLab CI/CD variables setup guide""" + print("🔐 GitLab CI/CD Variables Setup") + print("=" * 40) + print() + print("Navigate to your project settings:") + print("Project → Settings → CI/CD → Variables") + print() + print("Add the following variables (mark sensitive ones as 'Masked'):") + print() + + # Similar structure as GitHub but with GitLab-specific guidance + if provider == 'aws': + variables = { + 'AWS_ACCESS_KEY_ID': 'Your AWS access key ID (Masked)', + 'AWS_SECRET_ACCESS_KEY': 'Your AWS secret access key (Masked)', + 'AWS_DEFAULT_REGION': 'Your preferred AWS region' + } + # ... (similar for other providers) + + for var_name, description in variables.items(): + print(f"• {var_name}") + print(f" {description}") + print() + +def main(): + """Main function""" + if len(sys.argv) < 3: + print("Usage: python3 setup_secrets.py ") + print() + print("Git providers: github, gitlab, bitbucket, azure_devops") + print("Cloud providers: aws, gcp, azure, digitalocean") + sys.exit(1) + + git_provider = sys.argv[1].lower() + cloud_provider = sys.argv[2].lower() + + print(f"Setting up secrets for {git_provider} + {cloud_provider}") + print() + + if git_provider == 'github': + print_github_secrets_guide(cloud_provider) + elif git_provider == 'gitlab': + print_gitlab_secrets_guide(cloud_provider) + else: + print(f"Guide for {git_provider} not yet implemented") + + print("💡 Security Best Practices:") + print("- Use least-privilege IAM policies") + print("- Consider using OIDC federation instead of long-lived keys") + print("- Rotate secrets regularly") + print("- Monitor secret usage in your cloud provider's audit logs") + +if __name__ == '__main__': + main() From 4d2233151f12d1a18067601f4cbee29c9828d774 Mon Sep 17 00:00:00 2001 From: Eren Ciftci Date: Mon, 1 Sep 2025 23:49:34 +0300 Subject: [PATCH 34/34] Add .github/workflows/terraform.yml --- .github/workflows/terraform.yml | 132 ++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 .github/workflows/terraform.yml diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml new file mode 100644 index 0000000..05594b7 --- /dev/null +++ b/.github/workflows/terraform.yml @@ -0,0 +1,132 @@ +name: Terraform CI/CD + +on: + pull_request: + paths: + - "terraform/**" + - "policies/**" + - ".inframorph-policy.yaml" + - "scripts/**" + push: + branches: ["main"] + paths: ["terraform/**"] + +permissions: + contents: read + pull-requests: write + id-token: write # For OIDC + +env: + TF_INPUT: false + TF_IN_AUTOMATION: true + +jobs: + plan-and-policy: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.9.5 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} # For OIDC + # Alternative: use access keys + # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_DEFAULT_REGION }} + + - name: Terraform Init (dev) + working-directory: terraform/envs/dev + run: terraform init -upgrade + + - name: Terraform Format Check + working-directory: terraform/envs/dev + run: terraform fmt -check + + - name: Terraform Validate + working-directory: terraform/envs/dev + run: terraform validate + + - name: Terraform Plan (dev) + working-directory: terraform/envs/dev + run: | + terraform plan -out=tfplan.bin -detailed-exitcode || true + echo "PLAN_EXIT_CODE=$?" >> $GITHUB_ENV + + - name: Export Plan JSON + if: env.PLAN_EXIT_CODE != '1' + run: | + bash scripts/export_tfplan_json.sh terraform/envs/dev/tfplan.bin terraform/envs/dev/tfplan.json + + - name: Install Conftest + run: | + curl -L https://github.com/open-policy-agent/conftest/releases/download/v0.56.0/conftest_Linux_x86_64.tar.gz | tar xz + sudo mv conftest /usr/local/bin/ + + - name: Policy Check + if: env.PLAN_EXIT_CODE != '1' + run: | + conftest test terraform/envs/dev/tfplan.json --policy policies --output json > conftest.json || true + python3 scripts/summarize_conftest.py conftest.json > policy_summary.md + + - name: Post PR Comment + if: github.event_name == 'pull_request' && env.PLAN_EXIT_CODE != '1' + uses: marocchino/sticky-pull-request-comment@v2 + with: + recreate: true + path: policy_summary.md + + - name: Check for Blocking Violations + if: env.PLAN_EXIT_CODE != '1' + run: | + python3 scripts/check_exceptions.py conftest.json .inframorph-policy.yaml + + - name: Upload Plan Artifact + if: env.PLAN_EXIT_CODE == '2' + uses: actions/upload-artifact@v4 + with: + name: terraform-plan + path: | + terraform/envs/dev/tfplan.bin + terraform/envs/dev/tfplan.json + policy_summary.md + + apply: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: production # Requires manual approval + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.9.5 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ secrets.AWS_DEFAULT_REGION }} + + - name: Terraform Init (prod) + working-directory: terraform/envs/prod + run: terraform init -upgrade + + - name: Terraform Plan (prod) + working-directory: terraform/envs/prod + run: terraform plan + + - name: Terraform Apply (prod) + working-directory: terraform/envs/prod + run: terraform apply -auto-approve