mysystem-cli 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +66 -0
- package/copy-templates.js +47 -0
- package/dist/commands/init.js +15 -2
- package/dist/utils/detector.js +84 -32
- package/package.json +2 -2
- package/src/commands/init.ts +15 -2
- package/src/utils/detector.ts +86 -29
- package/templates/docker/fastapi.Dockerfile +32 -0
- package/templates/docker/nextjs.Dockerfile +45 -0
- package/templates/docker/node.Dockerfile +32 -0
- package/templates/docker/react.Dockerfile +38 -0
- package/templates/github/deploy-ec2.yml +163 -0
- package/templates/github/deploy.yml +94 -0
- package/templates/github/destroy.yml +43 -0
- package/templates/terraform/alb.tf +69 -0
- package/templates/terraform/bootstrap-oidc.yaml +69 -0
- package/templates/terraform/budget.tf +40 -0
- package/templates/terraform/db.tf +46 -0
- package/templates/terraform/dns.tf +89 -0
- package/templates/terraform/ecs.tf +110 -0
- package/templates/terraform/outputs.tf +14 -0
- package/templates/terraform/provider.tf +21 -0
- package/templates/terraform/rds_proxy.tf +157 -0
- package/templates/terraform/redis.tf +69 -0
- package/templates/terraform/security.tf +156 -0
- package/templates/terraform/variables.tf +46 -0
- package/templates/terraform/vpc.tf +68 -0
- package/templates/terraform/waf.tf +97 -0
- package/templates/terraform-ec2/budget.tf +40 -0
- package/templates/terraform-ec2/dns.tf +85 -0
- package/templates/terraform-ec2/ec2.tf +124 -0
- package/templates/terraform-ec2/ecr.tf +10 -0
- package/templates/terraform-ec2/outputs.tf +19 -0
- package/templates/terraform-ec2/provider.tf +22 -0
- package/templates/terraform-ec2/variables.tf +58 -0
- package/templates/terraform-ec2/vpc.tf +50 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# --- Amazon ECR (Container Registry) ---
|
|
2
|
+
resource "aws_ecr_repository" "app" {
|
|
3
|
+
name = var.app_name
|
|
4
|
+
image_tag_mutability = "MUTABLE"
|
|
5
|
+
force_destroy = true
|
|
6
|
+
|
|
7
|
+
image_scanning_configuration {
|
|
8
|
+
scan_on_push = true
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# --- ECS Cluster ---
|
|
13
|
+
resource "aws_ecs_cluster" "main" {
|
|
14
|
+
name = "${var.app_name}-cluster"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# --- CloudWatch Logs ---
|
|
18
|
+
resource "aws_cloudwatch_log_group" "ecs" {
|
|
19
|
+
name = "/ecs/${var.app_name}"
|
|
20
|
+
retention_in_days = 7
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# --- ECS Task Definition ---
|
|
24
|
+
resource "aws_ecs_task_definition" "app" {
|
|
25
|
+
family = var.app_name
|
|
26
|
+
network_mode = "awsvpc"
|
|
27
|
+
requires_compatibilities = ["FARGATE"]
|
|
28
|
+
cpu = var.container_cpu
|
|
29
|
+
memory = var.container_memory
|
|
30
|
+
execution_role_arn = aws_iam_role.ecs_execution.arn
|
|
31
|
+
task_role_arn = aws_iam_role.ecs_task.arn
|
|
32
|
+
|
|
33
|
+
container_definitions = jsonencode([
|
|
34
|
+
{
|
|
35
|
+
name = var.app_name
|
|
36
|
+
# We use a placeholder image for the initial provision, so terraform succeeds.
|
|
37
|
+
# The CI/CD pipeline will overwrite this image during actual deployment.
|
|
38
|
+
image = "${aws_ecr_repository.app.repository_url}:latest"
|
|
39
|
+
essential = true
|
|
40
|
+
portMappings = [
|
|
41
|
+
{
|
|
42
|
+
containerPort = var.container_port
|
|
43
|
+
hostPort = var.container_port
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
logConfiguration = {
|
|
47
|
+
logDriver = "awslogs"
|
|
48
|
+
options = {
|
|
49
|
+
"awslogs-group" = aws_cloudwatch_log_group.ecs.name
|
|
50
|
+
"awslogs-region" = var.aws_region
|
|
51
|
+
"awslogs-stream-prefix" = "ecs"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
environment = concat(
|
|
55
|
+
[
|
|
56
|
+
{
|
|
57
|
+
name = "PORT"
|
|
58
|
+
value = tostring(var.container_port)
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name = "NODE_ENV"
|
|
62
|
+
value = var.environment
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
var.enable_redis ? [
|
|
66
|
+
{
|
|
67
|
+
name = "REDIS_URL"
|
|
68
|
+
value = "redis://${aws_elasticache_replication_group.redis[0].primary_endpoint_address}:6379"
|
|
69
|
+
}
|
|
70
|
+
] : [],
|
|
71
|
+
var.sentry_dsn != "" ? [
|
|
72
|
+
{
|
|
73
|
+
name = "SENTRY_DSN"
|
|
74
|
+
value = var.sentry_dsn
|
|
75
|
+
}
|
|
76
|
+
] : []
|
|
77
|
+
)
|
|
78
|
+
# Securely load database URL parameter if database is enabled
|
|
79
|
+
secrets = var.enable_database ? [
|
|
80
|
+
{
|
|
81
|
+
name = "DATABASE_URL"
|
|
82
|
+
valueFrom = aws_ssm_parameter.db_url[0].arn
|
|
83
|
+
}
|
|
84
|
+
] : []
|
|
85
|
+
}
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# --- ECS Service ---
|
|
90
|
+
resource "aws_ecs_service" "main" {
|
|
91
|
+
name = var.app_name
|
|
92
|
+
cluster = aws_ecs_cluster.main.id
|
|
93
|
+
task_definition = aws_ecs_task_definition.app.arn
|
|
94
|
+
desired_count = 1
|
|
95
|
+
launch_type = "FARGATE"
|
|
96
|
+
|
|
97
|
+
network_configuration {
|
|
98
|
+
subnets = aws_subnet.public[*].id # Required for pulling ECR images without NAT Gateway
|
|
99
|
+
security_groups = [aws_security_group.ecs.id]
|
|
100
|
+
assign_public_ip = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
load_balancer {
|
|
104
|
+
target_group_arn = aws_lb_target_group.app.arn
|
|
105
|
+
container_name = var.app_name
|
|
106
|
+
container_port = var.container_port
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
depends_on = [aws_lb_listener.http]
|
|
110
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
output "ecr_repository_url" {
|
|
2
|
+
value = aws_ecr_repository.app.repository_url
|
|
3
|
+
description = "URL of the ECR repository"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "application_url" {
|
|
7
|
+
value = "http://${aws_lb.main.dns_name}"
|
|
8
|
+
description = "Public URL of the Application Load Balancer"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
output "database_endpoint" {
|
|
12
|
+
value = var.enable_database ? aws_db_instance.postgres[0].endpoint : "None"
|
|
13
|
+
description = "RDS database endpoint"
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_version = ">= 1.5.0"
|
|
3
|
+
required_providers {
|
|
4
|
+
aws = {
|
|
5
|
+
source = "hashicorp/aws"
|
|
6
|
+
version = "~> 5.0"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
provider "aws" {
|
|
12
|
+
region = var.aws_region
|
|
13
|
+
|
|
14
|
+
default_tags {
|
|
15
|
+
tags = {
|
|
16
|
+
Environment = var.environment
|
|
17
|
+
ManagedBy = "MySystem"
|
|
18
|
+
Project = var.app_name
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# AWS RDS Proxy (Managed PgBouncer Connection Pooler)
|
|
2
|
+
# Pools database connections to allow the application to scale to thousands of concurrent users
|
|
3
|
+
# without exhausting database connection limits or memory.
|
|
4
|
+
|
|
5
|
+
variable "enable_rds_proxy" {
|
|
6
|
+
type = bool
|
|
7
|
+
description = "Whether to provision an RDS Proxy connection pooler"
|
|
8
|
+
default = false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
# 1. AWS Secrets Manager Secret to store RDS credentials (required for RDS Proxy)
|
|
12
|
+
resource "aws_secretsmanager_secret" "db_credentials" {
|
|
13
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
14
|
+
name = "${var.app_name}-db-credentials"
|
|
15
|
+
recovery_window_in_days = 0 # force deletion on destroy for clean cleanup
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
resource "aws_secretsmanager_secret_version" "db_credentials" {
|
|
19
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
20
|
+
secret_id = aws_secretsmanager_secret.db_credentials[0].id
|
|
21
|
+
secret_string = jsonencode({
|
|
22
|
+
username = aws_db_instance.postgres[0].username
|
|
23
|
+
password = random_password.db_password[0].result
|
|
24
|
+
engine = "postgres"
|
|
25
|
+
host = aws_db_instance.postgres[0].address
|
|
26
|
+
port = 5432
|
|
27
|
+
dbInstanceIdentifier = aws_db_instance.postgres[0].identifier
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# 2. IAM Role for RDS Proxy to access Secrets Manager
|
|
32
|
+
resource "aws_iam_role" "rds_proxy" {
|
|
33
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
34
|
+
name = "${var.app_name}-rds-proxy-role"
|
|
35
|
+
|
|
36
|
+
assume_role_policy = jsonencode({
|
|
37
|
+
Version = "2012-10-17"
|
|
38
|
+
Statement = [
|
|
39
|
+
{
|
|
40
|
+
Action = "sts:AssumeRole"
|
|
41
|
+
Effect = "Allow"
|
|
42
|
+
Principal = {
|
|
43
|
+
Service = "rds.amazonaws.com"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
resource "aws_iam_policy" "rds_proxy" {
|
|
51
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
52
|
+
name = "${var.app_name}-rds-proxy-policy"
|
|
53
|
+
description = "Allows RDS Proxy to read secrets from Secrets Manager"
|
|
54
|
+
|
|
55
|
+
policy = jsonencode({
|
|
56
|
+
Version = "2012-10-17"
|
|
57
|
+
Statement = [
|
|
58
|
+
{
|
|
59
|
+
Action = [
|
|
60
|
+
"secretsmanager:GetSecretValue",
|
|
61
|
+
"secretsmanager:DescribeSecret"
|
|
62
|
+
]
|
|
63
|
+
Effect = "Allow"
|
|
64
|
+
Resource = [aws_secretsmanager_secret.db_credentials[0].arn]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
resource "aws_iam_role_policy_attachment" "rds_proxy" {
|
|
71
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
72
|
+
role = aws_iam_role.rds_proxy[0].name
|
|
73
|
+
policy_arn = aws_iam_policy.rds_proxy[0].arn
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# 3. RDS Proxy Security Group
|
|
77
|
+
resource "aws_security_group" "rds_proxy" {
|
|
78
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
79
|
+
name = "${var.app_name}-rds-proxy-sg"
|
|
80
|
+
description = "Security Group for RDS Proxy"
|
|
81
|
+
vpc_id = aws_vpc.main.id
|
|
82
|
+
|
|
83
|
+
# Allow ECS tasks to connect to the RDS Proxy
|
|
84
|
+
ingress {
|
|
85
|
+
description = "Allow DB traffic from ECS tasks"
|
|
86
|
+
from_port = 5432
|
|
87
|
+
to_port = 5432
|
|
88
|
+
protocol = "tcp"
|
|
89
|
+
security_groups = [aws_security_group.ecs.id]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
egress {
|
|
93
|
+
from_port = 0
|
|
94
|
+
to_port = 0
|
|
95
|
+
protocol = "-1"
|
|
96
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
tags = {
|
|
100
|
+
Name = "${var.app_name}-rds-proxy-sg"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Allow RDS database to accept connections from the RDS Proxy
|
|
105
|
+
resource "aws_security_group_rule" "db_allow_rds_proxy" {
|
|
106
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
107
|
+
type = "ingress"
|
|
108
|
+
from_port = 5432
|
|
109
|
+
to_port = 5432
|
|
110
|
+
protocol = "tcp"
|
|
111
|
+
security_group_id = aws_security_group.db[0].id
|
|
112
|
+
source_security_group_id = aws_security_group.rds_proxy[0].id
|
|
113
|
+
description = "Allow traffic from RDS Proxy"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# 4. RDS Proxy
|
|
117
|
+
resource "aws_db_proxy" "postgres" {
|
|
118
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
119
|
+
name = "${var.app_name}-db-proxy"
|
|
120
|
+
debug_logging = false
|
|
121
|
+
engine_family = "POSTGRESQL"
|
|
122
|
+
idle_client_timeout = 1800
|
|
123
|
+
require_tls = true
|
|
124
|
+
role_arn = aws_iam_role.rds_proxy[0].arn
|
|
125
|
+
vpc_security_group_ids = [aws_security_group.rds_proxy[0].id]
|
|
126
|
+
vpc_subnet_ids = aws_subnet.private[*].id
|
|
127
|
+
|
|
128
|
+
auth {
|
|
129
|
+
auth_scheme = "SECRETS"
|
|
130
|
+
description = "Database authentication via Secrets Manager"
|
|
131
|
+
iam_auth = "DISABLED"
|
|
132
|
+
secret_arn = aws_secretsmanager_secret.db_credentials[0].arn
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
tags = {
|
|
136
|
+
Name = "${var.app_name}-db-proxy"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# 5. Connect RDS Proxy to the database instance
|
|
141
|
+
resource "aws_db_proxy_default_target_group" "postgres" {
|
|
142
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
143
|
+
db_proxy_name = aws_db_proxy.postgres[0].name
|
|
144
|
+
|
|
145
|
+
connection_pool_config {
|
|
146
|
+
max_connections_percent = 100
|
|
147
|
+
max_idle_connections_percent = 50
|
|
148
|
+
connection_borrow_timeout = 120
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
resource "aws_db_proxy_target" "postgres" {
|
|
153
|
+
count = var.enable_database && var.enable_rds_proxy ? 1 : 0
|
|
154
|
+
db_proxy_name = aws_db_proxy.postgres[0].name
|
|
155
|
+
target_group_name = aws_db_proxy_default_target_group.postgres[0].name
|
|
156
|
+
db_instance_identifier = aws_db_instance.postgres[0].id
|
|
157
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Amazon ElastiCache Redis Cluster
|
|
2
|
+
# Provides high-performance sub-millisecond caching and session/queue management.
|
|
3
|
+
|
|
4
|
+
variable "enable_redis" {
|
|
5
|
+
type = bool
|
|
6
|
+
description = "Whether to provision an ElastiCache Redis cluster"
|
|
7
|
+
default = false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
# Redis Security Group
|
|
11
|
+
resource "aws_security_group" "redis" {
|
|
12
|
+
count = var.enable_redis ? 1 : 0
|
|
13
|
+
name = "${var.app_name}-redis-sg"
|
|
14
|
+
description = "Access to Redis from ECS tasks only"
|
|
15
|
+
vpc_id = aws_vpc.main.id
|
|
16
|
+
|
|
17
|
+
ingress {
|
|
18
|
+
description = "Allow Redis access from ECS tasks"
|
|
19
|
+
from_port = 6379
|
|
20
|
+
to_port = 6379
|
|
21
|
+
protocol = "tcp"
|
|
22
|
+
security_groups = [aws_security_group.ecs.id]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
egress {
|
|
26
|
+
from_port = 0
|
|
27
|
+
to_port = 0
|
|
28
|
+
protocol = "-1"
|
|
29
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
tags = {
|
|
33
|
+
Name = "${var.app_name}-redis-sg"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Redis Subnet Group (places Redis in private subnets)
|
|
38
|
+
resource "aws_elasticache_subnet_group" "redis" {
|
|
39
|
+
count = var.enable_redis ? 1 : 0
|
|
40
|
+
name = "${var.app_name}-redis-subnet-group"
|
|
41
|
+
subnet_ids = aws_subnet.private[*].id
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Redis Replication Group (Cluster)
|
|
45
|
+
resource "aws_elasticache_replication_group" "redis" {
|
|
46
|
+
count = var.enable_redis ? 1 : 0
|
|
47
|
+
replication_group_id = "${var.app_name}-redis"
|
|
48
|
+
description = "Redis cluster for ${var.app_name}"
|
|
49
|
+
node_type = "cache.t4g.micro" # cost-effective Graviton node
|
|
50
|
+
num_cache_clusters = 1
|
|
51
|
+
parameter_group_name = "default.redis7"
|
|
52
|
+
port = 6379
|
|
53
|
+
subnet_group_name = aws_elasticache_subnet_group.redis[0].name
|
|
54
|
+
security_group_ids = [aws_security_group.redis[0].id]
|
|
55
|
+
automatic_failover_enabled = false # Disable for single node to save cost
|
|
56
|
+
|
|
57
|
+
tags = {
|
|
58
|
+
Name = "${var.app_name}-redis"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Save Redis URL in SSM Parameter Store
|
|
63
|
+
resource "aws_ssm_parameter" "redis_url" {
|
|
64
|
+
count = var.enable_redis ? 1 : 0
|
|
65
|
+
name = "/${var.app_name}/redis_url"
|
|
66
|
+
type = "SecureString"
|
|
67
|
+
description = "Redis URL for the application"
|
|
68
|
+
value = "redis://${aws_elasticache_replication_group.redis[0].primary_endpoint_address}:6379"
|
|
69
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# --- Security Groups ---
|
|
2
|
+
|
|
3
|
+
# ALB Security Group
|
|
4
|
+
resource "aws_security_group" "alb" {
|
|
5
|
+
name = "${var.app_name}-alb-sg"
|
|
6
|
+
description = "Allow inbound HTTP/HTTPS traffic to ALB"
|
|
7
|
+
vpc_id = aws_vpc.main.id
|
|
8
|
+
|
|
9
|
+
ingress {
|
|
10
|
+
description = "Allow HTTP"
|
|
11
|
+
from_port = 80
|
|
12
|
+
to_port = 80
|
|
13
|
+
protocol = "tcp"
|
|
14
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
ingress {
|
|
18
|
+
description = "Allow HTTPS"
|
|
19
|
+
from_port = 443
|
|
20
|
+
to_port = 443
|
|
21
|
+
protocol = "tcp"
|
|
22
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
egress {
|
|
26
|
+
from_port = 0
|
|
27
|
+
to_port = 0
|
|
28
|
+
protocol = "-1"
|
|
29
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
tags = {
|
|
33
|
+
Name = "${var.app_name}-alb-sg"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# ECS Tasks Security Group
|
|
38
|
+
resource "aws_security_group" "ecs" {
|
|
39
|
+
name = "${var.app_name}-ecs-tasks-sg"
|
|
40
|
+
description = "Access to ECS tasks only from ALB"
|
|
41
|
+
vpc_id = aws_vpc.main.id
|
|
42
|
+
|
|
43
|
+
ingress {
|
|
44
|
+
description = "Allow traffic from ALB only"
|
|
45
|
+
from_port = var.container_port
|
|
46
|
+
to_port = var.container_port
|
|
47
|
+
protocol = "tcp"
|
|
48
|
+
security_groups = [aws_security_group.alb.id]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
egress {
|
|
52
|
+
from_port = 0
|
|
53
|
+
to_port = 0
|
|
54
|
+
protocol = "-1"
|
|
55
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
tags = {
|
|
59
|
+
Name = "${var.app_name}-ecs-tasks-sg"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# RDS Security Group
|
|
64
|
+
resource "aws_security_group" "db" {
|
|
65
|
+
count = var.enable_database ? 1 : 0
|
|
66
|
+
name = "${var.app_name}-db-sg"
|
|
67
|
+
description = "Access to RDS from ECS tasks only"
|
|
68
|
+
vpc_id = aws_vpc.main.id
|
|
69
|
+
|
|
70
|
+
ingress {
|
|
71
|
+
description = "Allow PostgreSQL access from ECS tasks"
|
|
72
|
+
from_port = 5432
|
|
73
|
+
to_port = 5432
|
|
74
|
+
protocol = "tcp"
|
|
75
|
+
security_groups = [aws_security_group.ecs.id]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
egress {
|
|
79
|
+
from_port = 0
|
|
80
|
+
to_port = 0
|
|
81
|
+
protocol = "-1"
|
|
82
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
tags = {
|
|
86
|
+
Name = "${var.app_name}-db-sg"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# --- IAM Roles ---
|
|
91
|
+
|
|
92
|
+
# ECS Task Execution Role (Required to pull ECR images & push logs to CloudWatch)
|
|
93
|
+
resource "aws_iam_role" "ecs_execution" {
|
|
94
|
+
name = "${var.app_name}-ecs-execution-role"
|
|
95
|
+
|
|
96
|
+
assume_role_policy = jsonencode({
|
|
97
|
+
Version = "2012-10-17"
|
|
98
|
+
Statement = [
|
|
99
|
+
{
|
|
100
|
+
Action = "sts:AssumeRole"
|
|
101
|
+
Effect = "Allow"
|
|
102
|
+
Principal = {
|
|
103
|
+
Service = "ecs-tasks.amazonaws.com"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
resource "aws_iam_role_policy_attachment" "ecs_execution" {
|
|
111
|
+
role = aws_iam_role.ecs_execution.name
|
|
112
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# ECS Task Role (Permissions for the application itself)
|
|
116
|
+
resource "aws_iam_role" "ecs_task" {
|
|
117
|
+
name = "${var.app_name}-ecs-task-role"
|
|
118
|
+
|
|
119
|
+
assume_role_policy = jsonencode({
|
|
120
|
+
Version = "2012-10-17"
|
|
121
|
+
Statement = [
|
|
122
|
+
{
|
|
123
|
+
Action = "sts:AssumeRole"
|
|
124
|
+
Effect = "Allow"
|
|
125
|
+
Principal = {
|
|
126
|
+
Service = "ecs-tasks.amazonaws.com"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Add standard policy for ECS task (e.g. basic S3 or secrets reading if needed, can be expanded)
|
|
134
|
+
resource "aws_iam_policy" "ecs_task_policy" {
|
|
135
|
+
name = "${var.app_name}-ecs-task-policy"
|
|
136
|
+
description = "Basic policy for ECS task execution"
|
|
137
|
+
|
|
138
|
+
policy = jsonencode({
|
|
139
|
+
Version = "2012-10-17"
|
|
140
|
+
Statement = [
|
|
141
|
+
{
|
|
142
|
+
Action = [
|
|
143
|
+
"ssm:GetParameters",
|
|
144
|
+
"secretsmanager:GetSecretValue"
|
|
145
|
+
]
|
|
146
|
+
Effect = "Allow"
|
|
147
|
+
Resource = "*"
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
resource "aws_iam_role_policy_attachment" "ecs_task" {
|
|
154
|
+
role = aws_iam_role.ecs_task.name
|
|
155
|
+
policy_arn = aws_iam_policy.ecs_task_policy.arn
|
|
156
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
variable "aws_region" {
|
|
2
|
+
type = string
|
|
3
|
+
description = "AWS region to deploy resources"
|
|
4
|
+
default = "us-east-1"
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
variable "app_name" {
|
|
8
|
+
type = string
|
|
9
|
+
description = "Application name (used for naming resources)"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
variable "environment" {
|
|
13
|
+
type = string
|
|
14
|
+
description = "Deployment environment"
|
|
15
|
+
default = "production"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
variable "container_port" {
|
|
19
|
+
type = number
|
|
20
|
+
description = "Port the application container listens on"
|
|
21
|
+
default = 3000
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
variable "container_cpu" {
|
|
25
|
+
type = number
|
|
26
|
+
description = "CPU units for the ECS task (1024 = 1 vCPU)"
|
|
27
|
+
default = 256
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
variable "container_memory" {
|
|
31
|
+
type = number
|
|
32
|
+
description = "Memory for the ECS task in MB"
|
|
33
|
+
default = 512
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
variable "enable_database" {
|
|
37
|
+
type = bool
|
|
38
|
+
description = "Whether to provision an RDS PostgreSQL database"
|
|
39
|
+
default = true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
variable "sentry_dsn" {
|
|
43
|
+
type = string
|
|
44
|
+
description = "Sentry Data Source Name (DSN) for error tracking"
|
|
45
|
+
default = ""
|
|
46
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
data "aws_availability_zones" "available" {
|
|
2
|
+
state = "available"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
resource "aws_vpc" "main" {
|
|
6
|
+
cidr_block = "10.0.0.0/16"
|
|
7
|
+
enable_dns_hostnames = true
|
|
8
|
+
enable_dns_support = true
|
|
9
|
+
|
|
10
|
+
tags = {
|
|
11
|
+
Name = "${var.app_name}-vpc"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
resource "aws_subnet" "public" {
|
|
16
|
+
count = 2
|
|
17
|
+
vpc_id = aws_vpc.main.id
|
|
18
|
+
cidr_block = "10.0.${count.index}.0/24"
|
|
19
|
+
availability_zone = data.aws_availability_zones.available.names[count.index]
|
|
20
|
+
map_public_ip_on_launch = true
|
|
21
|
+
|
|
22
|
+
tags = {
|
|
23
|
+
Name = "${var.app_name}-public-subnet-${count.index}"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resource "aws_subnet" "private" {
|
|
28
|
+
count = 2
|
|
29
|
+
vpc_id = aws_vpc.main.id
|
|
30
|
+
cidr_block = "10.0.${count.index + 10}.0/24"
|
|
31
|
+
availability_zone = data.aws_availability_zones.available.names[count.index]
|
|
32
|
+
|
|
33
|
+
tags = {
|
|
34
|
+
Name = "${var.app_name}-private-subnet-${count.index}"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resource "aws_internet_gateway" "gw" {
|
|
39
|
+
vpc_id = aws_vpc.main.id
|
|
40
|
+
|
|
41
|
+
tags = {
|
|
42
|
+
Name = "${var.app_name}-igw"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
resource "aws_route_table" "public" {
|
|
47
|
+
vpc_id = aws_vpc.main.id
|
|
48
|
+
|
|
49
|
+
route {
|
|
50
|
+
cidr_block = "0.0.0.0/0"
|
|
51
|
+
gateway_id = aws_internet_gateway.gw.id
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
tags = {
|
|
55
|
+
Name = "${var.app_name}-public-rt"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
resource "aws_route_table_association" "public" {
|
|
60
|
+
count = 2
|
|
61
|
+
subnet_id = aws_subnet.public[count.index].id
|
|
62
|
+
route_table_id = aws_route_table.public.id
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Note: We do not provision a NAT Gateway by default to save ~$32/month in costs.
|
|
66
|
+
# Fargate tasks run in the public subnets but are secured via Security Groups
|
|
67
|
+
# allowing traffic ONLY from the Application Load Balancer (ALB).
|
|
68
|
+
# This provides enterprise-level routing security at near-zero baseline network cost.
|