Add Terraform AWS infrastructure and enhanced CI/CD pipeline (FRE-4574)
- Terraform modules: VPC, ECS Fargate, RDS PostgreSQL, ElastiCache Redis, S3, Secrets Manager, CloudWatch - Multi-environment support: staging and production configs - ECS auto-scaling: CPU-based scaling with configurable min/max - CI/CD: pnpm caching, Docker Buildx, Trivy security scanning, Terraform plan on PR - Deploy: ECS service updates with automatic rollback on health check failure - Backup: automated RDS snapshots, S3 versioning, ElastiCache snapshots - Monitoring: CloudWatch dashboards, CPU/memory/5xx alarms - Rollback script for manual service rollback - Infrastructure documentation with architecture overview
This commit is contained in:
183
infra/modules/cloudwatch/main.tf
Normal file
183
infra/modules/cloudwatch/main.tf
Normal file
@@ -0,0 +1,183 @@
|
||||
variable "environment" {
|
||||
description = "Deployment environment"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "cluster_name" {
|
||||
description = "ECS cluster name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "rds_identifier" {
|
||||
description = "RDS instance identifier"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "cache_endpoint" {
|
||||
description = "ElastiCache endpoint"
|
||||
type = string
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_dashboard" "main" {
|
||||
dashboard_name = "${var.project_name}-${var.environment}-dashboard"
|
||||
|
||||
dashboard_body = jsonencode({
|
||||
widgets = [
|
||||
{
|
||||
type = "metric"
|
||||
properties = {
|
||||
title = "ECS CPU Utilization"
|
||||
metrics = [
|
||||
["AWS/ECS", "CPUUtilization", "ClusterName", var.cluster_name]
|
||||
]
|
||||
view = "timeSeries"
|
||||
stacked = false
|
||||
region = "us-east-1"
|
||||
period = 300
|
||||
}
|
||||
},
|
||||
{
|
||||
type = "metric"
|
||||
properties = {
|
||||
title = "ECS Memory Utilization"
|
||||
metrics = [
|
||||
["AWS/ECS", "MemoryUtilization", "ClusterName", var.cluster_name]
|
||||
]
|
||||
view = "timeSeries"
|
||||
stacked = false
|
||||
region = "us-east-1"
|
||||
period = 300
|
||||
}
|
||||
},
|
||||
{
|
||||
type = "metric"
|
||||
properties = {
|
||||
title = "RDS CPU Utilization"
|
||||
metrics = [
|
||||
["AWS/RDS", "CPUUtilization", "DBInstanceIdentifier", var.rds_identifier]
|
||||
]
|
||||
view = "timeSeries"
|
||||
stacked = false
|
||||
region = "us-east-1"
|
||||
period = 300
|
||||
}
|
||||
},
|
||||
{
|
||||
type = "metric"
|
||||
properties = {
|
||||
title = "ALB Request Count"
|
||||
metrics = [
|
||||
["AWS/ApplicationELB", "RequestCount", "LoadBalancer", "${var.cluster_name}-alb"]
|
||||
]
|
||||
view = "timeSeries"
|
||||
stacked = false
|
||||
region = "us-east-1"
|
||||
period = 60
|
||||
}
|
||||
},
|
||||
{
|
||||
type = "metric"
|
||||
properties = {
|
||||
title = "ALB 5xx Errors"
|
||||
metrics = [
|
||||
["AWS/ApplicationELB", "HTTPCode_Elb_5XX_Count", "LoadBalancer", "${var.cluster_name}-alb"]
|
||||
]
|
||||
view = "timeSeries"
|
||||
stacked = false
|
||||
region = "us-east-1"
|
||||
period = 60
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_metric_alarm" "ecs_cpu_high" {
|
||||
alarm_name = "${var.project_name}-${var.environment}-ecs-cpu-high"
|
||||
comparison_operator = "GreaterThanThreshold"
|
||||
evaluation_periods = 2
|
||||
metric_name = "CPUUtilization"
|
||||
namespace = "AWS/ECS"
|
||||
period = 300
|
||||
statistic = "Average"
|
||||
threshold = 80
|
||||
alarm_description = "ECS CPU utilization above 80%"
|
||||
|
||||
dimensions = {
|
||||
ClusterName = var.cluster_name
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_metric_alarm" "ecs_memory_high" {
|
||||
alarm_name = "${var.project_name}-${var.environment}-ecs-memory-high"
|
||||
comparison_operator = "GreaterThanThreshold"
|
||||
evaluation_periods = 2
|
||||
metric_name = "MemoryUtilization"
|
||||
namespace = "AWS/ECS"
|
||||
period = 300
|
||||
statistic = "Average"
|
||||
threshold = 85
|
||||
alarm_description = "ECS memory utilization above 85%"
|
||||
|
||||
dimensions = {
|
||||
ClusterName = var.cluster_name
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_metric_alarm" "alb_5xx" {
|
||||
alarm_name = "${var.project_name}-${var.environment}-alb-5xx"
|
||||
comparison_operator = "GreaterThanThreshold"
|
||||
evaluation_periods = 3
|
||||
metric_name = "HTTPCode_Elb_5XX_Count"
|
||||
namespace = "AWS/ApplicationELB"
|
||||
period = 60
|
||||
statistic = "Sum"
|
||||
threshold = 10
|
||||
alarm_description = "ALB 5xx errors above 10 per minute"
|
||||
|
||||
dimensions = {
|
||||
LoadBalancer = "${var.cluster_name}-alb"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_metric_alarm" "rds_cpu_high" {
|
||||
alarm_name = "${var.project_name}-${var.environment}-rds-cpu-high"
|
||||
comparison_operator = "GreaterThanThreshold"
|
||||
evaluation_periods = 2
|
||||
metric_name = "CPUUtilization"
|
||||
namespace = "AWS/RDS"
|
||||
period = 300
|
||||
statistic = "Average"
|
||||
threshold = 75
|
||||
alarm_description = "RDS CPU utilization above 75%"
|
||||
|
||||
dimensions = {
|
||||
DBInstanceIdentifier = var.rds_identifier
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_metric_alarm" "rds_free_storage" {
|
||||
alarm_name = "${var.project_name}-${var.environment}-rds-free-storage"
|
||||
comparison_operator = "LessThanThreshold"
|
||||
evaluation_periods = 2
|
||||
metric_name = "FreeStorageSpace"
|
||||
namespace = "AWS/RDS"
|
||||
period = 300
|
||||
statistic = "Average"
|
||||
threshold = 524288000
|
||||
alarm_description = "RDS free storage below 500MB"
|
||||
|
||||
dimensions = {
|
||||
DBInstanceIdentifier = var.rds_identifier
|
||||
}
|
||||
}
|
||||
|
||||
output "dashboard_url" {
|
||||
description = "CloudWatch dashboard URL"
|
||||
value = "https://us-east-1.console.aws.amazon.com/cloudwatch/home#dashboards/dashboard/${var.project_name}-${var.environment}-dashboard"
|
||||
}
|
||||
355
infra/modules/ecs/main.tf
Normal file
355
infra/modules/ecs/main.tf
Normal file
@@ -0,0 +1,355 @@
|
||||
variable "environment" {
|
||||
description = "Deployment environment"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "cluster_name" {
|
||||
description = "ECS cluster name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_id" {
|
||||
description = "VPC ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "subnet_ids" {
|
||||
description = "Private subnet IDs"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "security_group_ids" {
|
||||
description = "Security group IDs"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "services" {
|
||||
description = "ECS services to deploy"
|
||||
type = map(object({
|
||||
cpu = number
|
||||
memory = number
|
||||
port = number
|
||||
}))
|
||||
}
|
||||
|
||||
variable "container_images" {
|
||||
description = "Container image tags"
|
||||
type = map(string)
|
||||
}
|
||||
|
||||
variable "secrets_arn" {
|
||||
description = "Secrets Manager ARN"
|
||||
type = string
|
||||
}
|
||||
|
||||
resource "aws_ecs_cluster" "main" {
|
||||
name = var.cluster_name
|
||||
|
||||
settings {
|
||||
name = "containerInsights"
|
||||
value = "enabled"
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = var.cluster_name
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_ecs_cluster_capacity_providers" "main" {
|
||||
cluster_name = aws_ecs_cluster.main.name
|
||||
|
||||
capacity_providers = ["FARGATE"]
|
||||
|
||||
default_capacity_provider_strategy {
|
||||
base = 1
|
||||
weight = 100
|
||||
capacity_provider = "FARGATE"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_ecs_task_definition" "services" {
|
||||
for_each = var.services
|
||||
|
||||
family = "${var.cluster_name}-${each.key}"
|
||||
|
||||
container_definitions = jsonencode([
|
||||
{
|
||||
name = each.key
|
||||
image = "ghcr.io/shieldai/shieldai-${each.key}:${var.container_images[each.key]}"
|
||||
cpu = each.cpu
|
||||
memory = each.memory
|
||||
essential = true
|
||||
|
||||
portMappings = [
|
||||
{
|
||||
containerPort = each.port
|
||||
hostPort = each.port
|
||||
protocol = "tcp"
|
||||
}
|
||||
]
|
||||
|
||||
environment = [
|
||||
{
|
||||
name = "NODE_ENV"
|
||||
value = var.environment
|
||||
},
|
||||
{
|
||||
name = "PORT"
|
||||
value = tostring(each.port)
|
||||
}
|
||||
]
|
||||
|
||||
secrets = [
|
||||
{
|
||||
name = "DATABASE_URL"
|
||||
valueFrom = "${var.secrets_arn}:DATABASE_URL::"
|
||||
},
|
||||
{
|
||||
name = "REDIS_URL"
|
||||
valueFrom = "${var.secrets_arn}:REDIS_URL::"
|
||||
},
|
||||
{
|
||||
name = "HIBP_API_KEY"
|
||||
valueFrom = "${var.secrets_arn}:HIBP_API_KEY::"
|
||||
},
|
||||
{
|
||||
name = "RESEND_API_KEY"
|
||||
valueFrom = "${var.secrets_arn}:RESEND_API_KEY::"
|
||||
}
|
||||
]
|
||||
|
||||
logConfiguration = {
|
||||
logDriver = "awslogs"
|
||||
options = {
|
||||
"awslogs-group" = "/ecs/${var.cluster_name}-${each.key}"
|
||||
"awslogs-region" = "us-east-1"
|
||||
"awslogs-stream-prefix" = each.key
|
||||
}
|
||||
}
|
||||
|
||||
healthCheck = {
|
||||
command = ["CMD-SHELL", "wget -q --spider http://localhost:${each.port}/health || exit 1"]
|
||||
interval = 30
|
||||
timeout = 5
|
||||
retries = 3
|
||||
startPeriod = 60
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
network_mode = "awsvpc"
|
||||
memory = each.memory
|
||||
cpu = each.cpu
|
||||
requires_compatibilities = ["FARGATE"]
|
||||
|
||||
execution_role_arn = aws_iam_role.execution[each.key].arn
|
||||
task_role_arn = aws_iam_role.task[each.key].arn
|
||||
|
||||
tags = {
|
||||
Name = "${var.cluster_name}-${each.key}"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "execution" {
|
||||
for_each = var.services
|
||||
|
||||
name = "${var.cluster_name}-${each.key}-execution"
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Principal = {
|
||||
Service = "ecs-tasks.amazonaws.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
managed_policy_arns = [
|
||||
"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "task" {
|
||||
for_each = var.services
|
||||
|
||||
name = "${var.cluster_name}-${each.key}-task"
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Principal = {
|
||||
Service = "ecs-tasks.amazonaws.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
managed_policy_arns = [
|
||||
"arn:aws:iam::aws:policy/SecretsManagerReadOnly"
|
||||
]
|
||||
|
||||
inline_policy {
|
||||
name = "elasticache-access"
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"elasticache:DescribeCacheClusters",
|
||||
"elasticache:DescribeCacheSubnetGroups"
|
||||
]
|
||||
Resource = "*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_ecs_service" "services" {
|
||||
for_each = var.services
|
||||
|
||||
name = "${var.cluster_name}-${each.key}"
|
||||
cluster = aws_ecs_cluster.main.id
|
||||
task_definition = aws_ecs_task_definition.services[each.key].arn
|
||||
desired_count = var.environment == "production" ? 3 : 1
|
||||
|
||||
launch_desired_count = "FARGATE"
|
||||
|
||||
network_configuration {
|
||||
subnets = var.subnet_ids
|
||||
security_groups = var.security_group_ids
|
||||
assign_public_ip = false
|
||||
}
|
||||
|
||||
load_balancer {
|
||||
target_group_arn = aws_lb_target_group.services[each.key].arn
|
||||
container_name = each.key
|
||||
container_port = each.port
|
||||
}
|
||||
|
||||
auto_scaling {
|
||||
max_capacity = var.environment == "production" ? 10 : 3
|
||||
min_capacity = var.environment == "production" ? 2 : 1
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "${var.cluster_name}-${each.key}"
|
||||
Service = each.key
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
aws_lb_listener.services
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_lb" "main" {
|
||||
name = "${var.cluster_name}-alb"
|
||||
internal = false
|
||||
load_balancer_type = "application"
|
||||
security_groups = var.security_group_ids
|
||||
subnets = var.subnet_ids
|
||||
|
||||
tags = {
|
||||
Name = "${var.cluster_name}-alb"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_lb_target_group" "services" {
|
||||
for_each = var.services
|
||||
|
||||
name = "${var.cluster_name}-${each.key}-tg"
|
||||
port = each.port
|
||||
protocol = "HTTP"
|
||||
vpc_id = var.vpc_id
|
||||
|
||||
health_check {
|
||||
enabled = true
|
||||
healthy_threshold = 3
|
||||
interval = 30
|
||||
matcher = "200"
|
||||
path = "/health"
|
||||
port = "traffic-port"
|
||||
protocol = "HTTP"
|
||||
timeout = 5
|
||||
unhealthy_threshold = 3
|
||||
}
|
||||
|
||||
stickiness {
|
||||
type = "lb_cookie"
|
||||
cookie_duration = 86400
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_lb_listener" "services" {
|
||||
for_each = var.services
|
||||
|
||||
load_balancer_arn = aws_lb.main.arn
|
||||
port = 80
|
||||
protocol = "HTTP"
|
||||
|
||||
default_action {
|
||||
type = "forward"
|
||||
target_group_arn = aws_lb_target_group.services[each.key].arn
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_appautoscaling_target" "services" {
|
||||
for_each = var.services
|
||||
|
||||
service_namespace = "ecs"
|
||||
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.services[each.key].name}"
|
||||
scalable_dimension = "ecs:service:DesiredCount"
|
||||
min_capacity = var.environment == "production" ? 2 : 1
|
||||
max_capacity = var.environment == "production" ? 10 : 3
|
||||
}
|
||||
|
||||
resource "aws_appautoscaling_policy" "cpu" {
|
||||
for_each = var.services
|
||||
|
||||
name = "${var.cluster_name}-${each.key}-cpu-scaling"
|
||||
service_namespace = "ecs"
|
||||
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.services[each.key].name}"
|
||||
scalable_dimension = "ecs:service:DesiredCount"
|
||||
|
||||
target_tracking_scaling_policy_configuration {
|
||||
target_value = 70.0
|
||||
scale_in_cooldown = 60
|
||||
scale_out_cooldown = 30
|
||||
|
||||
customized_metric_specification {
|
||||
metric_name = "CPUUtilization"
|
||||
namespace = "AWS/ECS"
|
||||
statistic = "Average"
|
||||
dimensions = [{ name = "ClusterName", value = aws_ecs_cluster.main.name }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_cloudwatch_log_group" "services" {
|
||||
for_each = var.services
|
||||
|
||||
name = "/ecs/${var.cluster_name}-${each.key}"
|
||||
retention_in_days = var.environment == "production" ? 30 : 7
|
||||
|
||||
tags = {
|
||||
Name = "${var.cluster_name}-${each.key}-logs"
|
||||
}
|
||||
}
|
||||
|
||||
output "cluster_arn" {
|
||||
description = "ECS cluster ARN"
|
||||
value = aws_ecs_cluster.main.arn
|
||||
}
|
||||
|
||||
output "alb_dns_name" {
|
||||
description = "ALB DNS name"
|
||||
value = aws_lb.main.dns_name
|
||||
}
|
||||
80
infra/modules/elasticache/main.tf
Normal file
80
infra/modules/elasticache/main.tf
Normal file
@@ -0,0 +1,80 @@
|
||||
variable "environment" {
|
||||
description = "Deployment environment"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_id" {
|
||||
description = "VPC ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "subnet_ids" {
|
||||
description = "Private subnet IDs"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "security_group_id" {
|
||||
description = "ElastiCache security group ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "node_type" {
|
||||
description = "Cache node type"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "num_nodes" {
|
||||
description = "Number of cache nodes"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project name"
|
||||
type = string
|
||||
}
|
||||
|
||||
resource "aws_elasticache_subnet_group" "main" {
|
||||
name = "${var.project_name}-${var.environment}-redis-subnet"
|
||||
subnet_ids = var.subnet_ids
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-redis-subnet"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_elasticache_replication_group" "main" {
|
||||
replication_group_id = "${var.project_name}-${var.environment}-redis"
|
||||
description = "${var.project_name} Redis cluster (${var.environment})"
|
||||
|
||||
node_type = var.node_type
|
||||
num_cache_clusters = var.num_nodes
|
||||
engine = "redis"
|
||||
engine_version = "7.0"
|
||||
|
||||
transit_encryption_enabled = true
|
||||
at_rest_encryption_enabled = true
|
||||
|
||||
port = 6379
|
||||
|
||||
subnet_group_name = aws_elasticache_subnet_group.main.name
|
||||
security_group_ids = [var.security_group_id]
|
||||
|
||||
automatic_failover_enabled = var.environment == "production"
|
||||
|
||||
snapshot_retention_limit = var.environment == "production" ? 7 : 1
|
||||
snapshot_window = "03:00-04:00"
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-redis"
|
||||
}
|
||||
}
|
||||
|
||||
output "cache_endpoint" {
|
||||
description = "ElastiCache primary endpoint"
|
||||
value = aws_elasticache_replication_group.main.primary_endpoint_address
|
||||
}
|
||||
|
||||
output "reader_endpoint" {
|
||||
description = "ElastiCache reader endpoint"
|
||||
value = aws_elasticache_replication_group.main.reader_endpoint_address
|
||||
}
|
||||
132
infra/modules/rds/main.tf
Normal file
132
infra/modules/rds/main.tf
Normal file
@@ -0,0 +1,132 @@
|
||||
variable "environment" {
|
||||
description = "Deployment environment"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_id" {
|
||||
description = "VPC ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "subnet_ids" {
|
||||
description = "Private subnet IDs"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "security_group_id" {
|
||||
description = "RDS security group ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "db_name" {
|
||||
description = "Database name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "db_instance_class" {
|
||||
description = "RDS instance class"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "multi_az" {
|
||||
description = "Multi-AZ deployment"
|
||||
type = bool
|
||||
}
|
||||
|
||||
variable "backup_retention" {
|
||||
description = "Backup retention days"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project name"
|
||||
type = string
|
||||
}
|
||||
|
||||
resource "aws_db_subnet_group" "main" {
|
||||
name = "${var.project_name}-${var.environment}-db-subnet"
|
||||
subnet_ids = var.subnet_ids
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-db-subnet"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_db_instance" "main" {
|
||||
identifier = "${var.project_name}-${var.environment}-db"
|
||||
|
||||
engine = "postgres"
|
||||
engine_version = "16.2"
|
||||
instance_class = var.db_instance_class
|
||||
allocated_storage = var.environment == "production" ? 100 : 20
|
||||
|
||||
db_name = var.db_name
|
||||
username = "shieldai"
|
||||
password = random_password.db_password.result
|
||||
|
||||
multi_az = var.multi_az
|
||||
db_subnet_group_name = aws_db_subnet_group.main.name
|
||||
vpc_security_group_ids = [var.security_group_id]
|
||||
|
||||
backup_retention_period = var.backup_retention
|
||||
backup_window = "03:00-04:00"
|
||||
maintenance_window = "sun:04:00-sun:05:00"
|
||||
|
||||
skip_final_snapshot = var.environment != "production"
|
||||
final_snapshot_identifier = "${var.project_name}-${var.environment}-final"
|
||||
|
||||
storage_encrypted = true
|
||||
storage_type = "gp3"
|
||||
iops = var.environment == "production" ? 3000 : 1000
|
||||
|
||||
deletion_protection = var.environment == "production"
|
||||
copy_tags_to_snapshot = true
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-db"
|
||||
}
|
||||
}
|
||||
|
||||
resource "random_password" "db_password" {
|
||||
length = 16
|
||||
special = true
|
||||
|
||||
keepers = {
|
||||
environment = var.environment
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret_version" "db_password" {
|
||||
secret_id = aws_secretsmanager_secret.db_password.id
|
||||
secret_string = jsonencode({
|
||||
username = "shieldai"
|
||||
password = random_password.db_password.result
|
||||
engine = "postgres"
|
||||
host = aws_db_instance.main.address
|
||||
port = aws_db_instance.main.port
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret" "db_password" {
|
||||
name = "${var.project_name}-${var.environment}-db-password"
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-db-password"
|
||||
}
|
||||
}
|
||||
|
||||
output "db_endpoint" {
|
||||
description = "RDS endpoint"
|
||||
value = aws_db_instance.main.endpoint
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "db_instance_identifier" {
|
||||
description = "RDS instance identifier"
|
||||
value = aws_db_instance.main.identifier
|
||||
}
|
||||
|
||||
output "db_password_secret_arn" {
|
||||
description = "DB password secret ARN"
|
||||
value = aws_secretsmanager_secret.db_password.arn
|
||||
}
|
||||
108
infra/modules/s3/main.tf
Normal file
108
infra/modules/s3/main.tf
Normal file
@@ -0,0 +1,108 @@
|
||||
variable "environment" {
|
||||
description = "Deployment environment"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project name"
|
||||
type = string
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "terraform_state" {
|
||||
bucket = "${var.project_name}-${var.environment}-terraform-state"
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-terraform-state"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
|
||||
rule {
|
||||
apply_server_side_encryption_by_default {
|
||||
sse_algorithm = "aws:kms"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {
|
||||
bucket = aws_s3_bucket.terraform_state.id
|
||||
|
||||
rule {
|
||||
id = "expire-noncurrent"
|
||||
status = "Enabled"
|
||||
|
||||
noncurrent_version_expiration {
|
||||
noncurrent_days = 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "artifacts" {
|
||||
bucket = "${var.project_name}-${var.environment}-artifacts"
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-artifacts"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "artifacts" {
|
||||
bucket = aws_s3_bucket.artifacts.id
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
|
||||
bucket = aws_s3_bucket.artifacts.id
|
||||
|
||||
rule {
|
||||
apply_server_side_encryption_by_default {
|
||||
sse_algorithm = "aws:kms"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "logs" {
|
||||
bucket = "${var.project_name}-${var.environment}-logs"
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-logs"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
|
||||
bucket = aws_s3_bucket.logs.id
|
||||
|
||||
rule {
|
||||
id = "expire-old-logs"
|
||||
status = "Enabled"
|
||||
|
||||
expiration {
|
||||
days = 90
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "bucket_name" {
|
||||
description = "Terraform state S3 bucket name"
|
||||
value = aws_s3_bucket.terraform_state.id
|
||||
}
|
||||
|
||||
output "artifacts_bucket_name" {
|
||||
description = "Artifacts S3 bucket name"
|
||||
value = aws_s3_bucket.artifacts.id
|
||||
}
|
||||
|
||||
output "logs_bucket_name" {
|
||||
description = "Logs S3 bucket name"
|
||||
value = aws_s3_bucket.logs.id
|
||||
}
|
||||
49
infra/modules/secrets/main.tf
Normal file
49
infra/modules/secrets/main.tf
Normal file
@@ -0,0 +1,49 @@
|
||||
variable "environment" {
|
||||
description = "Deployment environment"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project name"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "secrets" {
|
||||
description = "Secrets to store"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret" "main" {
|
||||
name = "${var.project_name}-${var.environment}-app-secrets"
|
||||
|
||||
description = "Application secrets for ${var.project_name} (${var.environment})"
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-app-secrets"
|
||||
Environment = var.environment
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_secretsmanager_secret_version" "main" {
|
||||
secret_id = aws_secretsmanager_secret.main.id
|
||||
|
||||
secret_string = jsonencode(merge({
|
||||
DATABASE_URL = "postgresql://shieldai:${var.project_name}@${var.project_name}-${var.environment}-db.${data.aws_caller_identity.current.account_id}.us-east-1.rds.amazonaws.com:5432/shieldai"
|
||||
REDIS_URL = "redis://${var.project_name}-${var.environment}-redis.${data.aws_caller_identity.current.account_id}.us-east-1.cache.amazonaws.com:6379"
|
||||
NODE_ENV = var.environment
|
||||
LOG_LEVEL = var.environment == "production" ? "info" : "debug"
|
||||
}, var.secrets))
|
||||
}
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
output "secrets_manager_arn" {
|
||||
description = "Secrets Manager ARN"
|
||||
value = aws_secretsmanager_secret.main.arn
|
||||
}
|
||||
|
||||
output "secrets_manager_name" {
|
||||
description = "Secrets Manager secret name"
|
||||
value = aws_secretsmanager_secret.main.name
|
||||
}
|
||||
235
infra/modules/vpc/main.tf
Normal file
235
infra/modules/vpc/main.tf
Normal file
@@ -0,0 +1,235 @@
|
||||
variable "environment" {
|
||||
description = "Deployment environment"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "vpc_cidr" {
|
||||
description = "CIDR block for VPC"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "az_count" {
|
||||
description = "Number of availability zones"
|
||||
type = number
|
||||
}
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project name"
|
||||
type = string
|
||||
}
|
||||
|
||||
resource "aws_vpc" "main" {
|
||||
cidr_block = var.vpc_cidr
|
||||
enable_dns_support = true
|
||||
enable_dns_hostnames = true
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-vpc"
|
||||
}
|
||||
}
|
||||
|
||||
data "aws_availability_zones" "available" {
|
||||
state = "available"
|
||||
}
|
||||
|
||||
resource "aws_subnet" "public" {
|
||||
count = var.az_count
|
||||
|
||||
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-${data.aws_availability_zones.available.names[count.index]}"
|
||||
"kubernetes.io/role/elb" = "1"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_subnet" "private" {
|
||||
count = var.az_count
|
||||
|
||||
vpc_id = aws_vpc.main.id
|
||||
cidr_block = cidrsubnet(var.vpc_cidr, 8, var.az_count + count.index)
|
||||
availability_zone = data.aws_availability_zones.available.names[count.index]
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-private-${data.aws_availability_zones.available.names[count.index]}"
|
||||
"kubernetes.io/role/internal-elb" = "1"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_internet_gateway" "main" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-igw"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_eip" "nat" {
|
||||
count = var.az_count
|
||||
|
||||
domain = "vpc"
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-nat-${count.index}"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_nat_gateway" "main" {
|
||||
count = var.az_count
|
||||
|
||||
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}"
|
||||
}
|
||||
|
||||
depends_on = [aws_internet_gateway.main]
|
||||
}
|
||||
|
||||
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.az_count
|
||||
|
||||
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}"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "public" {
|
||||
count = var.az_count
|
||||
|
||||
subnet_id = aws_subnet.public[count.index].id
|
||||
route_table_id = aws_route_table.public.id
|
||||
}
|
||||
|
||||
resource "aws_route_table_association" "private" {
|
||||
count = var.az_count
|
||||
|
||||
subnet_id = aws_subnet.private[count.index].id
|
||||
route_table_id = aws_route_table.private[count.index].id
|
||||
}
|
||||
|
||||
resource "aws_security_group" "ecs" {
|
||||
name_prefix = "${var.project_name}-${var.environment}-ecs"
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
ingress {
|
||||
from_port = 3000
|
||||
to_port = 3003
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
description = "Service ports"
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-ecs-sg"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_security_group" "rds" {
|
||||
name_prefix = "${var.project_name}-${var.environment}-rds"
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
ingress {
|
||||
from_port = 5432
|
||||
to_port = 5432
|
||||
protocol = "tcp"
|
||||
security_groups = [aws_security_group.ecs.id]
|
||||
description = "PostgreSQL from ECS"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_security_group" "elasticache" {
|
||||
name_prefix = "${var.project_name}-${var.environment}-elasticache"
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
ingress {
|
||||
from_port = 6379
|
||||
to_port = 6379
|
||||
protocol = "tcp"
|
||||
security_groups = [aws_security_group.ecs.id]
|
||||
description = "Redis from ECS"
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-${var.environment}-elasticache-sg"
|
||||
}
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "VPC ID"
|
||||
value = aws_vpc.main.id
|
||||
}
|
||||
|
||||
output "private_subnet_ids" {
|
||||
description = "Private subnet IDs"
|
||||
value = aws_subnet.private[*].id
|
||||
}
|
||||
|
||||
output "public_subnet_ids" {
|
||||
description = "Public subnet IDs"
|
||||
value = aws_subnet.public[*].id
|
||||
}
|
||||
|
||||
output "ecs_security_group_id" {
|
||||
description = "ECS security group ID"
|
||||
value = aws_security_group.ecs.id
|
||||
}
|
||||
|
||||
output "rds_security_group_id" {
|
||||
description = "RDS security group ID"
|
||||
value = aws_security_group.rds.id
|
||||
}
|
||||
|
||||
output "elasticache_security_group_id" {
|
||||
description = "ElastiCache security group ID"
|
||||
value = aws_security_group.elasticache.id
|
||||
}
|
||||
Reference in New Issue
Block a user