mysystem-cli 1.0.0 → 1.0.1
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/package.json +2 -2
- 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,69 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: '2010-09-09'
|
|
2
|
+
Description: Bootstrap IAM Role for GitHub Actions OIDC connection to AWS (MySystem).
|
|
3
|
+
|
|
4
|
+
Parameters:
|
|
5
|
+
GitHubOrg:
|
|
6
|
+
Type: String
|
|
7
|
+
Description: Your GitHub Organization or Username (case-sensitive)
|
|
8
|
+
GitHubRepo:
|
|
9
|
+
Type: String
|
|
10
|
+
Description: Your GitHub Repository Name (case-sensitive)
|
|
11
|
+
|
|
12
|
+
Resources:
|
|
13
|
+
# OIDC Provider for GitHub (Created only if it doesn't exist, we assume it's created or we create it here)
|
|
14
|
+
# Note: Since there can only be one OIDC provider per account, we handle it. If it fails, users can use an existing one.
|
|
15
|
+
GithubOidcProvider:
|
|
16
|
+
Type: AWS::IAM::OIDCProvider
|
|
17
|
+
Properties:
|
|
18
|
+
Url: https://token.actions.githubusercontent.com
|
|
19
|
+
ClientIdList:
|
|
20
|
+
- sts.amazonaws.com
|
|
21
|
+
ThumbprintList:
|
|
22
|
+
- 6938fd4d98bab03faadb97b34396831e3780aea1 # GitHub Actions Thumbprint
|
|
23
|
+
|
|
24
|
+
# IAM Role that GitHub Actions will assume to deploy the app
|
|
25
|
+
MySystemDeployRole:
|
|
26
|
+
Type: AWS::IAM::Role
|
|
27
|
+
Properties:
|
|
28
|
+
RoleName: !Sub 'MySystemDeployRole-${GitHubRepo}'
|
|
29
|
+
Description: IAM Role assumed by GitHub Actions to deploy infrastructure and code.
|
|
30
|
+
AssumeRolePolicyDocument:
|
|
31
|
+
Version: '2012-10-17'
|
|
32
|
+
Statement:
|
|
33
|
+
- Effect: Allow
|
|
34
|
+
Principal:
|
|
35
|
+
Federated: !Ref GithubOidcProvider
|
|
36
|
+
Action: sts:AssumeRoleWithWebIdentity
|
|
37
|
+
Condition:
|
|
38
|
+
StringEquals:
|
|
39
|
+
token.actions.githubusercontent.com:aud: sts.amazonaws.com
|
|
40
|
+
StringLike:
|
|
41
|
+
token.actions.githubusercontent.com:sub: !Sub 'repo:${GitHubOrg}/${GitHubRepo}:*'
|
|
42
|
+
# Attach Policies to allow Terraform to provision VPC, ECS, RDS, ALB, ECR etc.
|
|
43
|
+
ManagedPolicyArns:
|
|
44
|
+
- arn:aws:iam::aws:policy/PowerUserAccess
|
|
45
|
+
Policies:
|
|
46
|
+
- PolicyName: MySystemAdditionalIAMPolicy
|
|
47
|
+
PolicyDocument:
|
|
48
|
+
Version: '2012-10-17'
|
|
49
|
+
Statement:
|
|
50
|
+
# PowerUserAccess does not allow managing IAM roles. Terraform needs to create ECS Task Execution Roles.
|
|
51
|
+
- Effect: Allow
|
|
52
|
+
Action:
|
|
53
|
+
- iam:CreateRole
|
|
54
|
+
- iam:DeleteRole
|
|
55
|
+
- iam:GetRole
|
|
56
|
+
- iam:PassRole
|
|
57
|
+
- iam:PutRolePolicy
|
|
58
|
+
- iam:DeleteRolePolicy
|
|
59
|
+
- iam:GetRolePolicy
|
|
60
|
+
- iam:AttachRolePolicy
|
|
61
|
+
- iam:DetachRolePolicy
|
|
62
|
+
Resource:
|
|
63
|
+
- !Sub 'arn:aws:iam::${AWS::AccountId}:role/*-ecs-execution-role'
|
|
64
|
+
- !Sub 'arn:aws:iam::${AWS::AccountId}:role/*-ecs-task-role'
|
|
65
|
+
|
|
66
|
+
Outputs:
|
|
67
|
+
RoleARN:
|
|
68
|
+
Description: The ARN of the IAM Role for GitHub Actions. Copy this to your GitHub Repository Secret `AWS_ROLE_ARN`.
|
|
69
|
+
Value: !GetAtt MySystemDeployRole.Arn
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# AWS Cost Budget Alert
|
|
2
|
+
# Sends an email notification if the forecasted or actual AWS monthly spend exceeds the specified threshold.
|
|
3
|
+
|
|
4
|
+
variable "billing_email" {
|
|
5
|
+
type = string
|
|
6
|
+
description = "Email address to send AWS budget and billing alerts"
|
|
7
|
+
default = ""
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
variable "budget_limit" {
|
|
11
|
+
type = string
|
|
12
|
+
description = "Monthly budget limit in USD"
|
|
13
|
+
default = "20"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
resource "aws_budgets_budget" "monthly_cost" {
|
|
17
|
+
count = var.billing_email != "" ? 1 : 0
|
|
18
|
+
name = "${var.app_name}-monthly-budget"
|
|
19
|
+
budget_type = "COST"
|
|
20
|
+
limit_amount = var.budget_limit
|
|
21
|
+
limit_unit = "USD"
|
|
22
|
+
time_period_start = "2026-01-01_00:00" # Arbitrary start date in the past/present
|
|
23
|
+
time_unit = "MONTHLY"
|
|
24
|
+
|
|
25
|
+
notification {
|
|
26
|
+
comparison_operator = "GREATER_THAN"
|
|
27
|
+
threshold = 80
|
|
28
|
+
threshold_type = "PERCENTAGE"
|
|
29
|
+
notification_type = "ACTUAL"
|
|
30
|
+
subscriber_email_addresses = [var.billing_email]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
notification {
|
|
34
|
+
comparison_operator = "GREATER_THAN"
|
|
35
|
+
threshold = 100
|
|
36
|
+
threshold_type = "PERCENTAGE"
|
|
37
|
+
notification_type = "FORECASTED"
|
|
38
|
+
subscriber_email_addresses = [var.billing_email]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
resource "random_password" "db_password" {
|
|
2
|
+
count = var.enable_database ? 1 : 0
|
|
3
|
+
length = 16
|
|
4
|
+
special = false
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
resource "aws_db_subnet_group" "main" {
|
|
8
|
+
count = var.enable_database ? 1 : 0
|
|
9
|
+
name = "${var.app_name}-db-subnet-group"
|
|
10
|
+
subnet_ids = aws_subnet.private[*].id
|
|
11
|
+
|
|
12
|
+
tags = {
|
|
13
|
+
Name = "${var.app_name}-db-subnet-group"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
resource "aws_db_instance" "postgres" {
|
|
18
|
+
count = var.enable_database ? 1 : 0
|
|
19
|
+
identifier = "${var.app_name}-db"
|
|
20
|
+
allocated_storage = 20
|
|
21
|
+
max_allocated_storage = 100
|
|
22
|
+
engine = "postgres"
|
|
23
|
+
engine_version = "15"
|
|
24
|
+
instance_class = "db.t4g.micro" # cost-effective burstable Graviton instance
|
|
25
|
+
db_name = replace(var.app_name, "-", "_")
|
|
26
|
+
username = "mysystem_admin"
|
|
27
|
+
password = random_password.db_password[0].result
|
|
28
|
+
db_subnet_group_name = aws_db_subnet_group.main[0].name
|
|
29
|
+
vpc_security_group_ids = [aws_security_group.db[0].id]
|
|
30
|
+
skip_final_snapshot = true
|
|
31
|
+
publicly_accessible = false
|
|
32
|
+
|
|
33
|
+
tags = {
|
|
34
|
+
Name = "${var.app_name}-db-postgres"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Save password in SSM Parameter Store so the developer can retrieve it if needed,
|
|
39
|
+
# and so ECS tasks can fetch it securely.
|
|
40
|
+
resource "aws_ssm_parameter" "db_url" {
|
|
41
|
+
count = var.enable_database ? 1 : 0
|
|
42
|
+
name = "/${var.app_name}/database_url"
|
|
43
|
+
type = "SecureString"
|
|
44
|
+
description = "Database URL for the application"
|
|
45
|
+
value = "postgresql://${aws_db_instance.postgres[0].username}:${random_password.db_password[0].result}@${var.enable_rds_proxy ? aws_db_proxy.postgres[0].endpoint : aws_db_instance.postgres[0].endpoint}/${aws_db_instance.postgres[0].db_name}"
|
|
46
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# AWS ACM (SSL Certificate) and Custom Domain Routing
|
|
2
|
+
# Supports automated Route 53 setups or external DNS providers (GoDaddy, Cloudflare, Namecheap)
|
|
3
|
+
|
|
4
|
+
variable "enable_custom_domain" {
|
|
5
|
+
type = bool
|
|
6
|
+
description = "Enable custom domain and HTTPS SSL certificate"
|
|
7
|
+
default = false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
variable "domain_name" {
|
|
11
|
+
type = string
|
|
12
|
+
description = "The custom domain name (e.g., app.example.com)"
|
|
13
|
+
default = ""
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
variable "dns_provider" {
|
|
17
|
+
type = string
|
|
18
|
+
description = "DNS provider for domain validation ('route53' or 'external')"
|
|
19
|
+
default = "external"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# 1. SSL/TLS Certificate via ACM
|
|
23
|
+
resource "aws_acm_certificate" "cert" {
|
|
24
|
+
count = var.enable_custom_domain ? 1 : 0
|
|
25
|
+
domain_name = var.domain_name
|
|
26
|
+
validation_method = "DNS"
|
|
27
|
+
|
|
28
|
+
lifecycle {
|
|
29
|
+
create_before_destroy = true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
tags = {
|
|
33
|
+
Name = "${var.app_name}-cert"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# --- Route 53 DNS Configuration (Automated Setup) ---
|
|
38
|
+
data "aws_route53_zone" "primary" {
|
|
39
|
+
count = var.enable_custom_domain && var.dns_provider == "route53" ? 1 : 0
|
|
40
|
+
name = join(".", slice(split(".", var.domain_name), length(split(".", var.domain_name)) - 2, length(split(".", var.domain_name))))
|
|
41
|
+
private_zone = false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Create DNS validation record in Route53
|
|
45
|
+
resource "aws_route53_record" "cert_validation" {
|
|
46
|
+
count = var.enable_custom_domain && var.dns_provider == "route53" ? 1 : 0
|
|
47
|
+
name = tolist(aws_acm_certificate.cert[0].domain_validation_options)[0].resource_record_name
|
|
48
|
+
type = tolist(aws_acm_certificate.cert[0].domain_validation_options)[0].resource_record_type
|
|
49
|
+
zone_id = data.aws_route53_zone.primary[0].zone_id
|
|
50
|
+
records = [tolist(aws_acm_certificate.cert[0].domain_validation_options)[0].resource_record_value]
|
|
51
|
+
ttl = 60
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Validate certificate in ACM (Wait for validation to complete)
|
|
55
|
+
resource "aws_acm_certificate_validation" "cert" {
|
|
56
|
+
count = var.enable_custom_domain && var.dns_provider == "route53" ? 1 : 0
|
|
57
|
+
certificate_arn = aws_acm_certificate.cert[0].arn
|
|
58
|
+
validation_record_fqdns = [aws_route53_record.cert_validation[0].fqdn]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Create A record in Route53 pointing to the Load Balancer
|
|
62
|
+
resource "aws_route53_record" "app" {
|
|
63
|
+
count = var.enable_custom_domain && var.dns_provider == "route53" ? 1 : 0
|
|
64
|
+
zone_id = data.aws_route53_zone.primary[0].zone_id
|
|
65
|
+
name = var.domain_name
|
|
66
|
+
type = "A"
|
|
67
|
+
|
|
68
|
+
alias {
|
|
69
|
+
name = aws_lb.main.dns_name
|
|
70
|
+
zone_id = aws_lb.main.zone_id
|
|
71
|
+
evaluate_target_health = true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# --- Outputs for External DNS Setup (GoDaddy, Cloudflare, etc.) ---
|
|
76
|
+
output "dns_validation_name" {
|
|
77
|
+
value = var.enable_custom_domain ? tolist(aws_acm_certificate.cert[0].domain_validation_options)[0].resource_record_name : "None"
|
|
78
|
+
description = "CNAME Name to add to your DNS provider for SSL certificate validation"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
output "dns_validation_value" {
|
|
82
|
+
value = var.enable_custom_domain ? tolist(aws_acm_certificate.cert[0].domain_validation_options)[0].resource_record_value : "None"
|
|
83
|
+
description = "CNAME Value/Alias to add to your DNS provider for SSL certificate validation"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
output "app_cname_alias" {
|
|
87
|
+
value = aws_lb.main.dns_name
|
|
88
|
+
description = "Point your domain CNAME record to this target to route traffic to the app"
|
|
89
|
+
}
|
|
@@ -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
|
+
}
|