################################################################################ # Environment: aws # Main — SentryAgent.ai AgentIdP on AWS # # Architecture: # Internet → Route 53 → ALB (public subnets, HTTPS/443) → # ECS Fargate tasks (private subnets) → # RDS PostgreSQL 14 (private subnets, Multi-AZ) + # ElastiCache Redis 7 (private subnets, primary + replica) # # All secrets stored in AWS Secrets Manager — ECS tasks pull at launch time. # No sensitive values in state (except where Terraform internals require it). ################################################################################ terraform { required_version = ">= 1.6.0" required_providers { aws = { source = "hashicorp/aws" version = ">= 5.40.0" } random = { source = "hashicorp/random" version = ">= 3.6.0" } } # Remote state — configure your backend here. # Example using S3 + DynamoDB state locking: # # backend "s3" { # bucket = "sentryagent-terraform-state" # key = "agentidp/aws/production/terraform.tfstate" # region = "us-east-1" # encrypt = true # dynamodb_table = "sentryagent-terraform-locks" # } } provider "aws" { region = var.region default_tags { tags = { environment = var.environment project = var.project managed_by = "terraform" } } } ################################################################################ # Data sources ################################################################################ data "aws_caller_identity" "current" {} data "aws_region" "current" {} ################################################################################ # VPC ################################################################################ resource "aws_vpc" "main" { cidr_block = var.vpc_cidr enable_dns_support = true enable_dns_hostnames = true tags = { Name = "${var.project}-${var.environment}-vpc" } } resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id tags = { Name = "${var.project}-${var.environment}-igw" } } ################################################################################ # Subnets ################################################################################ resource "aws_subnet" "public" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = var.public_subnet_cidrs[count.index] availability_zone = var.availability_zones[count.index] map_public_ip_on_launch = false tags = { Name = "${var.project}-${var.environment}-public-${var.availability_zones[count.index]}" tier = "public" } } resource "aws_subnet" "private" { count = length(var.availability_zones) vpc_id = aws_vpc.main.id cidr_block = var.private_subnet_cidrs[count.index] availability_zone = var.availability_zones[count.index] tags = { Name = "${var.project}-${var.environment}-private-${var.availability_zones[count.index]}" tier = "private" } } ################################################################################ # NAT Gateways — one per AZ for HA outbound from private subnets # ECS tasks need outbound internet to pull ECR images and reach Secrets Manager. ################################################################################ resource "aws_eip" "nat" { count = length(var.availability_zones) domain = "vpc" tags = { Name = "${var.project}-${var.environment}-nat-eip-${var.availability_zones[count.index]}" } depends_on = [aws_internet_gateway.main] } resource "aws_nat_gateway" "main" { count = length(var.availability_zones) allocation_id = aws_eip.nat[count.index].id subnet_id = aws_subnet.public[count.index].id tags = { Name = "${var.project}-${var.environment}-nat-${var.availability_zones[count.index]}" } 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}-${var.environment}-public-rt" } } 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" "private" { count = length(var.availability_zones) 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}-${var.environment}-private-rt-${var.availability_zones[count.index]}" } } resource "aws_route_table_association" "private" { count = length(aws_subnet.private) subnet_id = aws_subnet.private[count.index].id route_table_id = aws_route_table.private[count.index].id } ################################################################################ # VPC Endpoints — allow ECS tasks to reach AWS services without NAT ################################################################################ resource "aws_vpc_endpoint" "secretsmanager" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.secretsmanager" vpc_endpoint_type = "Interface" subnet_ids = aws_subnet.private[*].id private_dns_enabled = true tags = { Name = "${var.project}-${var.environment}-secretsmanager-endpoint" } } resource "aws_vpc_endpoint" "ecr_api" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.ecr.api" vpc_endpoint_type = "Interface" subnet_ids = aws_subnet.private[*].id private_dns_enabled = true tags = { Name = "${var.project}-${var.environment}-ecr-api-endpoint" } } resource "aws_vpc_endpoint" "ecr_dkr" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.ecr.dkr" vpc_endpoint_type = "Interface" subnet_ids = aws_subnet.private[*].id private_dns_enabled = true tags = { Name = "${var.project}-${var.environment}-ecr-dkr-endpoint" } } resource "aws_vpc_endpoint" "s3" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.s3" vpc_endpoint_type = "Gateway" route_table_ids = aws_route_table.private[*].id tags = { Name = "${var.project}-${var.environment}-s3-endpoint" } } resource "aws_vpc_endpoint" "cloudwatch_logs" { vpc_id = aws_vpc.main.id service_name = "com.amazonaws.${var.region}.logs" vpc_endpoint_type = "Interface" subnet_ids = aws_subnet.private[*].id private_dns_enabled = true tags = { Name = "${var.project}-${var.environment}-logs-endpoint" } } ################################################################################ # IAM — ECS Task Execution Role # Allows ECS to pull images from ECR, write logs, and fetch secrets. ################################################################################ data "aws_iam_policy_document" "ecs_task_execution_assume" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } } } resource "aws_iam_role" "ecs_task_execution" { name = "${var.project}-${var.environment}-ecs-execution-role" assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_assume.json tags = { environment = var.environment project = var.project } } resource "aws_iam_role_policy_attachment" "ecs_task_execution_managed" { role = aws_iam_role.ecs_task_execution.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } # Allow the execution role to fetch the specific secrets it needs data "aws_iam_policy_document" "ecs_task_execution_secrets" { statement { sid = "GetAppSecrets" effect = "Allow" actions = [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret" ] resources = concat( [ aws_secretsmanager_secret.database_url.arn, aws_secretsmanager_secret.redis_url.arn, aws_secretsmanager_secret.jwt_private_key.arn, aws_secretsmanager_secret.jwt_public_key.arn, ], var.vault_token != "" ? [aws_secretsmanager_secret.vault_token[0].arn] : [] ) } } resource "aws_iam_role_policy" "ecs_task_execution_secrets" { name = "${var.project}-${var.environment}-secrets-policy" role = aws_iam_role.ecs_task_execution.id policy = data.aws_iam_policy_document.ecs_task_execution_secrets.json } ################################################################################ # IAM — ECS Task Role # Permissions granted to the running application container. ################################################################################ resource "aws_iam_role" "ecs_task" { name = "${var.project}-${var.environment}-ecs-task-role" assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_assume.json tags = { environment = var.environment project = var.project } } # ECS task role policy — extend as needed for other AWS service calls. data "aws_iam_policy_document" "ecs_task" { statement { sid = "AllowCloudWatchMetrics" effect = "Allow" actions = [ "cloudwatch:PutMetricData" ] resources = ["*"] } } resource "aws_iam_role_policy" "ecs_task" { name = "${var.project}-${var.environment}-task-policy" role = aws_iam_role.ecs_task.id policy = data.aws_iam_policy_document.ecs_task.json } ################################################################################ # IAM — RDS Enhanced Monitoring Role ################################################################################ data "aws_iam_policy_document" "rds_monitoring_assume" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["monitoring.rds.amazonaws.com"] } } } resource "aws_iam_role" "rds_monitoring" { name = "${var.project}-${var.environment}-rds-monitoring-role" assume_role_policy = data.aws_iam_policy_document.rds_monitoring_assume.json tags = { environment = var.environment project = var.project } } resource "aws_iam_role_policy_attachment" "rds_monitoring" { role = aws_iam_role.rds_monitoring.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole" } ################################################################################ # AWS Secrets Manager — store all sensitive values ################################################################################ resource "aws_secretsmanager_secret" "database_url" { name = "/${var.project}/${var.environment}/database-url" description = "PostgreSQL DATABASE_URL for AgentIdP" recovery_window_in_days = 7 tags = { environment = var.environment project = var.project } } resource "aws_secretsmanager_secret_version" "database_url" { secret_id = aws_secretsmanager_secret.database_url.id # Build the DATABASE_URL using the RDS endpoint output. # The password is passed in as var.db_password so it never appears in plaintext # in any .tf file — only in this encrypted secret version. secret_string = "postgresql://${var.project}:${var.db_password}@${module.rds.endpoint}:${module.rds.port}/${module.rds.db_name}?sslmode=require" depends_on = [module.rds] } resource "aws_secretsmanager_secret" "redis_url" { name = "/${var.project}/${var.environment}/redis-url" description = "Redis REDIS_URL for AgentIdP" recovery_window_in_days = 7 tags = { environment = var.environment project = var.project } } resource "aws_secretsmanager_secret_version" "redis_url" { secret_id = aws_secretsmanager_secret.redis_url.id # ElastiCache Redis with TLS uses the rediss:// scheme and requires an AUTH token. secret_string = "rediss://:${var.redis_auth_token}@${module.redis.primary_endpoint}:${module.redis.port}" depends_on = [module.redis] } resource "aws_secretsmanager_secret" "jwt_private_key" { name = "/${var.project}/${var.environment}/jwt-private-key" description = "RSA-2048 private key for signing AgentIdP JWTs" recovery_window_in_days = 7 tags = { environment = var.environment project = var.project } } resource "aws_secretsmanager_secret_version" "jwt_private_key" { secret_id = aws_secretsmanager_secret.jwt_private_key.id secret_string = var.jwt_private_key } resource "aws_secretsmanager_secret" "jwt_public_key" { name = "/${var.project}/${var.environment}/jwt-public-key" description = "RSA-2048 public key for verifying AgentIdP JWTs" recovery_window_in_days = 7 tags = { environment = var.environment project = var.project } } resource "aws_secretsmanager_secret_version" "jwt_public_key" { secret_id = aws_secretsmanager_secret.jwt_public_key.id secret_string = var.jwt_public_key } resource "aws_secretsmanager_secret" "vault_token" { count = var.vault_token != "" ? 1 : 0 name = "/${var.project}/${var.environment}/vault-token" description = "HashiCorp Vault token for AgentIdP" recovery_window_in_days = 7 tags = { environment = var.environment project = var.project } } resource "aws_secretsmanager_secret_version" "vault_token" { count = var.vault_token != "" ? 1 : 0 secret_id = aws_secretsmanager_secret.vault_token[0].id secret_string = var.vault_token } ################################################################################ # Module: Load Balancer ################################################################################ module "lb" { source = "../../modules/lb" environment = var.environment project = var.project vpc_id = aws_vpc.main.id subnet_ids = aws_subnet.public[*].id certificate_arn = var.certificate_arn target_group_port = 3000 enable_deletion_protection = true access_logs_bucket = var.alb_access_logs_bucket } ################################################################################ # Module: RDS PostgreSQL ################################################################################ module "rds" { source = "../../modules/rds" environment = var.environment project = var.project vpc_id = aws_vpc.main.id subnet_ids = aws_subnet.private[*].id # The app SG is created by the agentidp module; we wire it after both modules # are instantiated using a separate security group rule (see below). allowed_security_group_ids = [] db_name = "sentryagent_idp" db_username = var.project db_password = var.db_password instance_class = var.rds_instance_class allocated_storage = 50 max_allocated_storage = 500 multi_az = true backup_retention_days = var.rds_backup_retention_days deletion_protection = var.rds_deletion_protection skip_final_snapshot = var.rds_skip_final_snapshot monitoring_role_arn = aws_iam_role.rds_monitoring.arn monitoring_interval = 60 performance_insights_enabled = true } ################################################################################ # Module: Redis ################################################################################ module "redis" { source = "../../modules/redis" environment = var.environment project = var.project vpc_id = aws_vpc.main.id subnet_ids = aws_subnet.private[*].id # Same pattern as RDS — app SG wired after agentidp module creates it. allowed_security_group_ids = [] node_type = var.redis_node_type num_cache_clusters = 2 automatic_failover_enabled = true multi_az_enabled = true at_rest_encryption_enabled = true transit_encryption_enabled = true auth_token = var.redis_auth_token snapshot_retention_limit = 7 } ################################################################################ # Module: AgentIdP (ECS Fargate) ################################################################################ module "agentidp" { source = "../../modules/agentidp" provider_type = "aws" environment = var.environment project = var.project app_image = "sentryagent/agentidp:${var.app_image_tag}" app_port = 3000 aws_region = var.region aws_vpc_id = aws_vpc.main.id aws_subnet_ids = aws_subnet.private[*].id aws_target_group_arn = module.lb.target_group_arn aws_execution_role_arn = aws_iam_role.ecs_task_execution.arn aws_task_role_arn = aws_iam_role.ecs_task.arn aws_log_group_name = "/ecs/${var.project}-${var.environment}" aws_desired_count = var.ecs_desired_count aws_cpu = 512 aws_memory = 1024 aws_cors_origin = var.cors_origin aws_policy_dir = "/app/policies" aws_vault_addr = var.vault_addr aws_vault_mount = var.vault_mount aws_secret_database_url_arn = aws_secretsmanager_secret.database_url.arn aws_secret_redis_url_arn = aws_secretsmanager_secret.redis_url.arn aws_secret_jwt_private_key_arn = aws_secretsmanager_secret.jwt_private_key.arn aws_secret_jwt_public_key_arn = aws_secretsmanager_secret.jwt_public_key.arn aws_secret_vault_token_arn = var.vault_token != "" ? aws_secretsmanager_secret.vault_token[0].arn : "" depends_on = [ aws_secretsmanager_secret_version.database_url, aws_secretsmanager_secret_version.redis_url, aws_secretsmanager_secret_version.jwt_private_key, aws_secretsmanager_secret_version.jwt_public_key, ] } ################################################################################ # Cross-module security group wiring # # The app SG (from agentidp module) must be allowed into RDS and Redis. # These rules are created after both modules are fully instantiated to avoid # circular references in the module dependency graph. ################################################################################ resource "aws_security_group_rule" "rds_from_app" { type = "ingress" description = "PostgreSQL from ECS app tasks" from_port = 5432 to_port = 5432 protocol = "tcp" source_security_group_id = module.agentidp.aws_app_security_group_id security_group_id = module.rds.security_group_id } resource "aws_security_group_rule" "redis_from_app" { type = "ingress" description = "Redis from ECS app tasks" from_port = 6379 to_port = 6379 protocol = "tcp" source_security_group_id = module.agentidp.aws_app_security_group_id security_group_id = module.redis.security_group_id } # Allow the ALB to reach ECS tasks on the app port resource "aws_security_group_rule" "app_from_alb" { type = "ingress" description = "App port from ALB" from_port = 3000 to_port = 3000 protocol = "tcp" source_security_group_id = module.lb.alb_security_group_id security_group_id = module.agentidp.aws_app_security_group_id } ################################################################################ # Route 53 — alias record pointing the domain to the ALB ################################################################################ data "aws_route53_zone" "main" { name = join(".", slice(split(".", var.domain_name), 1, length(split(".", var.domain_name)))) private_zone = false } resource "aws_route53_record" "app" { zone_id = data.aws_route53_zone.main.zone_id name = var.domain_name type = "A" alias { name = module.lb.alb_dns_name zone_id = module.lb.alb_zone_id evaluate_target_health = true } }