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,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.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# AWS WAFv2 Web ACL (Web Application Firewall) for ALB
|
|
2
|
+
# Enforces enterprise-grade security rules at the network edge to block SQLi, XSS, and common exploits.
|
|
3
|
+
|
|
4
|
+
resource "aws_wafv2_web_acl" "main" {
|
|
5
|
+
name = "${var.app_name}-waf"
|
|
6
|
+
description = "WAF Web ACL protecting ${var.app_name} Load Balancer"
|
|
7
|
+
scope = "REGIONAL"
|
|
8
|
+
|
|
9
|
+
default_action {
|
|
10
|
+
allow {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
# Rule 1: AWS Managed Common Rule Set (OWASP Top 10 protections)
|
|
14
|
+
rule {
|
|
15
|
+
name = "AWS-AWSManagedRulesCommonRuleSet"
|
|
16
|
+
priority = 10
|
|
17
|
+
|
|
18
|
+
override_action {
|
|
19
|
+
none {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
statement {
|
|
23
|
+
managed_rule_group_statement {
|
|
24
|
+
name = "AWSManagedRulesCommonRuleSet"
|
|
25
|
+
vendor_name = "AWS"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
visibility_config {
|
|
30
|
+
cloudwatch_metrics_enabled = true
|
|
31
|
+
metric_name = "${var.app_name}-common-rules"
|
|
32
|
+
sampled_requests_enabled = true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Rule 2: AWS Managed SQL Injection Protection Rule Set
|
|
37
|
+
rule {
|
|
38
|
+
name = "AWS-AWSManagedRulesSQLiRuleSet"
|
|
39
|
+
priority = 20
|
|
40
|
+
|
|
41
|
+
override_action {
|
|
42
|
+
none {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
statement {
|
|
46
|
+
managed_rule_group_statement {
|
|
47
|
+
name = "AWSManagedRulesSQLiRuleSet"
|
|
48
|
+
vendor_name = "AWS"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
visibility_config {
|
|
53
|
+
cloudwatch_metrics_enabled = true
|
|
54
|
+
metric_name = "${var.app_name}-sqli-rules"
|
|
55
|
+
sampled_requests_enabled = true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Rule 3: AWS Managed Known Bad Inputs Rule Set
|
|
60
|
+
rule {
|
|
61
|
+
name = "AWS-AWSManagedRulesKnownBadInputsRuleSet"
|
|
62
|
+
priority = 30
|
|
63
|
+
|
|
64
|
+
override_action {
|
|
65
|
+
none {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
statement {
|
|
69
|
+
managed_rule_group_statement {
|
|
70
|
+
name = "AWSManagedRulesKnownBadInputsRuleSet"
|
|
71
|
+
vendor_name = "AWS"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
visibility_config {
|
|
76
|
+
cloudwatch_metrics_enabled = true
|
|
77
|
+
metric_name = "${var.app_name}-bad-inputs"
|
|
78
|
+
sampled_requests_enabled = true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
visibility_config {
|
|
83
|
+
cloudwatch_metrics_enabled = true
|
|
84
|
+
metric_name = "${var.app_name}-web-acl"
|
|
85
|
+
sampled_requests_enabled = true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
tags = {
|
|
89
|
+
Name = "${var.app_name}-waf"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Associate the WAF Web ACL with the Application Load Balancer
|
|
94
|
+
resource "aws_wafv2_web_acl_association" "alb" {
|
|
95
|
+
resource_arn = aws_lb.main.arn
|
|
96
|
+
web_acl_arn = aws_wafv2_web_acl.main.arn
|
|
97
|
+
}
|
|
@@ -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"
|
|
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,85 @@
|
|
|
1
|
+
# AWS ACM (SSL Certificate) and Custom Domain Routing for EC2
|
|
2
|
+
# Routes custom domains to the EC2 Elastic IP address
|
|
3
|
+
|
|
4
|
+
variable "enable_custom_domain" {
|
|
5
|
+
type = bool
|
|
6
|
+
description = "Enable custom domain and SSL certificates"
|
|
7
|
+
default = false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
variable "domain_name" {
|
|
11
|
+
type = string
|
|
12
|
+
description = "Custom domain name (e.g. app.myproduct.com)"
|
|
13
|
+
default = ""
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
variable "dns_provider" {
|
|
17
|
+
type = string
|
|
18
|
+
description = "DNS provider ('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
|
+
# 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
|
|
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 DNS A Record pointing to the EC2 Elastic IP
|
|
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
|
+
ttl = 300
|
|
68
|
+
records = [aws_eip.app.public_ip]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# --- Outputs for External DNS (GoDaddy, Cloudflare, etc.) ---
|
|
72
|
+
output "dns_validation_name" {
|
|
73
|
+
value = var.enable_custom_domain ? tolist(aws_acm_certificate.cert[0].domain_validation_options)[0].resource_record_name : "None"
|
|
74
|
+
description = "CNAME Name to add to your DNS provider for SSL certificate validation"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
output "dns_validation_value" {
|
|
78
|
+
value = var.enable_custom_domain ? tolist(aws_acm_certificate.cert[0].domain_validation_options)[0].resource_record_value : "None"
|
|
79
|
+
description = "CNAME Value/Alias to add to your DNS provider for SSL certificate validation"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
output "app_dns_a_record_ip" {
|
|
83
|
+
value = aws_eip.app.public_ip
|
|
84
|
+
description = "Point your domain A Record (IP Address) to this public Elastic IP in your DNS provider"
|
|
85
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# --- IAM Role for EC2 (SSM & ECR Access) ---
|
|
2
|
+
resource "aws_iam_role" "ec2" {
|
|
3
|
+
name = "${var.app_name}-ec2-role"
|
|
4
|
+
|
|
5
|
+
assume_role_policy = jsonencode({
|
|
6
|
+
Version = "2012-10-17"
|
|
7
|
+
Statement = [
|
|
8
|
+
{
|
|
9
|
+
Action = "sts:AssumeRole"
|
|
10
|
+
Effect = "Allow"
|
|
11
|
+
Principal = {
|
|
12
|
+
Service = "ec2.amazonaws.com"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Attach policy to allow AWS SSM Session Manager connection (SSH-free secure console access)
|
|
20
|
+
resource "aws_iam_role_policy_attachment" "ssm" {
|
|
21
|
+
role = aws_iam_role.ec2.name
|
|
22
|
+
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Attach policy to allow EC2 to pull images from ECR
|
|
26
|
+
resource "aws_iam_role_policy_attachment" "ecr_pull" {
|
|
27
|
+
role = aws_iam_role.ec2.name
|
|
28
|
+
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
resource "aws_iam_instance_profile" "ec2" {
|
|
32
|
+
name = "${var.app_name}-ec2-profile"
|
|
33
|
+
role = aws_iam_role.ec2.name
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# --- Security Group ---
|
|
37
|
+
resource "aws_security_group" "ec2" {
|
|
38
|
+
name = "${var.app_name}-ec2-sg"
|
|
39
|
+
description = "Allow inbound web traffic"
|
|
40
|
+
vpc_id = aws_vpc.main.id
|
|
41
|
+
|
|
42
|
+
ingress {
|
|
43
|
+
description = "Allow HTTP"
|
|
44
|
+
from_port = 80
|
|
45
|
+
to_port = 80
|
|
46
|
+
protocol = "tcp"
|
|
47
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ingress {
|
|
51
|
+
description = "Allow HTTPS"
|
|
52
|
+
from_port = 443
|
|
53
|
+
to_port = 443
|
|
54
|
+
protocol = "tcp"
|
|
55
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
egress {
|
|
59
|
+
from_port = 0
|
|
60
|
+
to_port = 0
|
|
61
|
+
protocol = "-1"
|
|
62
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
tags = {
|
|
66
|
+
Name = "${var.app_name}-ec2-sg"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# --- AMI Lookup (Amazon Linux 2) ---
|
|
71
|
+
data "aws_ami" "amazon_linux_2" {
|
|
72
|
+
most_recent = true
|
|
73
|
+
owners = ["amazon"]
|
|
74
|
+
|
|
75
|
+
filter {
|
|
76
|
+
name = "name"
|
|
77
|
+
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# --- EC2 Instance ---
|
|
82
|
+
resource "aws_instance" "app" {
|
|
83
|
+
ami = data.aws_ami.amazon_linux_2.id
|
|
84
|
+
instance_type = var.instance_type
|
|
85
|
+
subnet_id = aws_subnet.public.id
|
|
86
|
+
vpc_security_group_ids = [aws_security_group.ec2.id]
|
|
87
|
+
iam_instance_profile = aws_iam_instance_profile.ec2.name
|
|
88
|
+
|
|
89
|
+
# Startup script: installs Docker & Docker Compose
|
|
90
|
+
user_data = <<-EOF
|
|
91
|
+
#!/bin/bash
|
|
92
|
+
# Update system packages
|
|
93
|
+
yum update -y
|
|
94
|
+
|
|
95
|
+
# Install Docker
|
|
96
|
+
amazon-linux-extras install docker -y
|
|
97
|
+
systemctl start docker
|
|
98
|
+
systemctl enable docker
|
|
99
|
+
usermod -a -G docker ec2-user
|
|
100
|
+
|
|
101
|
+
# Install Docker Compose v2
|
|
102
|
+
mkdir -p /usr/local/lib/docker/cli-plugins
|
|
103
|
+
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o /usr/local/lib/docker/cli-plugins/docker-compose
|
|
104
|
+
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
|
105
|
+
|
|
106
|
+
# Create directory for app files
|
|
107
|
+
mkdir -p /home/ec2-user/app
|
|
108
|
+
chown -R ec2-user:ec2-user /home/ec2-user/app
|
|
109
|
+
EOF
|
|
110
|
+
|
|
111
|
+
tags = {
|
|
112
|
+
Name = var.app_name
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Assign an Elastic IP to the instance so the public IP address is static/permanent
|
|
117
|
+
resource "aws_eip" "app" {
|
|
118
|
+
instance = aws_instance.app.id
|
|
119
|
+
domain = "vpc"
|
|
120
|
+
|
|
121
|
+
tags = {
|
|
122
|
+
Name = "${var.app_name}-eip"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
output "ecr_repository_url" {
|
|
2
|
+
value = aws_ecr_repository.app.repository_url
|
|
3
|
+
description = "URL of the ECR repository"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "instance_public_ip" {
|
|
7
|
+
value = aws_eip.app.public_ip
|
|
8
|
+
description = "Permanent public Elastic IP of the EC2 instance"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
output "instance_id" {
|
|
12
|
+
value = aws_instance.app.id
|
|
13
|
+
description = "The ID of the EC2 instance (used for SSM deploys)"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
output "application_url" {
|
|
17
|
+
value = "http://${aws_eip.app.public_ip}"
|
|
18
|
+
description = "The web address of your application"
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
Tier = "Hobby"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|