nodejs-quickstart-structure 2.2.1 → 2.3.0
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/CHANGELOG.md +7 -1
- package/README.md +14 -13
- package/bin/index.js +2 -1
- package/lib/generator.js +10 -1
- package/lib/modules/project-setup.js +41 -0
- package/lib/modules/terraform-setup.js +131 -0
- package/lib/prompts.js +16 -1
- package/package.json +1 -1
- package/templates/clean-architecture/js/src/usecases/CreateUser.js.ejs +5 -2
- package/templates/clean-architecture/ts/src/usecases/createUser.ts.ejs +4 -2
- package/templates/common/caching/clean/js/CreateUser.js.ejs +4 -2
- package/templates/common/caching/clean/ts/createUser.ts.ejs +4 -2
- package/templates/common/terraform/main.tf +52 -0
- package/templates/common/terraform/modules/cache/main.tf +41 -0
- package/templates/common/terraform/modules/cache/outputs.tf +7 -0
- package/templates/common/terraform/modules/cache/variables.tf +4 -0
- package/templates/common/terraform/modules/compute/main.tf +69 -0
- package/templates/common/terraform/modules/compute/outputs.tf +7 -0
- package/templates/common/terraform/modules/compute/variables.tf +20 -0
- package/templates/common/terraform/modules/database/main.tf +57 -0
- package/templates/common/terraform/modules/database/outputs.tf +16 -0
- package/templates/common/terraform/modules/database/variables.tf +27 -0
- package/templates/common/terraform/modules/security/main.tf +130 -0
- package/templates/common/terraform/modules/security/outputs.tf +15 -0
- package/templates/common/terraform/modules/security/variables.tf +12 -0
- package/templates/common/terraform/modules/vpc/main.tf +134 -0
- package/templates/common/terraform/modules/vpc/outputs.tf +19 -0
- package/templates/common/terraform/modules/vpc/variables.tf +45 -0
- package/templates/common/terraform/outputs.tf +29 -0
- package/templates/common/terraform/provider.tf +17 -0
- package/templates/common/terraform/variables.tf +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
## [2.3.0] - 2026-05-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Terraform Infrastructure Integration (IaC)**: Integrated modular, production-ready AWS Terraform templates with Multi-AZ VPC network isolation, WAF, ALB, RDS, and ElastiCache.
|
|
7
|
+
- **Expanded Validation Matrix**: Scaled the mathematical validation matrix to **23,760+ unique project scenarios** (representing a 3x expansion), ensuring 100% template rendering accuracy across all permutations including the new Infrastructure options.
|
|
8
|
+
|
|
3
9
|
## [2.2.1] - 2026-05-12
|
|
4
10
|
|
|
5
11
|
### Added
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ A powerful ecosystem to scaffold production-ready Node.js microservices with bui
|
|
|
17
17
|
- [What's New](#whats-new-in-v21-the-authentication-release)
|
|
18
18
|
- [Key Features](#key-features)
|
|
19
19
|
- [Professional Standards](#professional-standards)
|
|
20
|
-
- [
|
|
20
|
+
- [23,760+ Project Combinations](#23760-project-combinations)
|
|
21
21
|
- [Configuration Options](#configuration-options)
|
|
22
22
|
- [Generated Project Structure](#generated-project-structure)
|
|
23
23
|
- [Documentation](#documentation)
|
|
@@ -27,8 +27,8 @@ A powerful ecosystem to scaffold production-ready Node.js microservices with bui
|
|
|
27
27
|
|
|
28
28
|
| **Path A: Next-Gen Web UI** (Recommended) | **Path B: Interactive CLI** |
|
|
29
29
|
| :--- | :--- |
|
|
30
|
-
| <a href="https://
|
|
31
|
-
| [Try Visual Configurator →](https://
|
|
30
|
+
| <a href="https://nodejs-quickstart-generator.netlify.app/#configurator"><img src="docs/public/v2-preview.png" width="100%" alt="UI Preview"></a> | <img src="docs/demo.gif" width="100%" alt="CLI Demo"> |
|
|
31
|
+
| [Try Visual Configurator →](https://nodejs-quickstart-generator.netlify.app/#configurator) | [See CLI Commands ↓](#path-b-interactive-cli) |
|
|
32
32
|
| **Visual Preview**: Real-time folder simulation. | **Fast & Direct**: Quickly scaffold in terminal. |
|
|
33
33
|
| **Zero-Prompt**: Paste a tailored command. | **AI-Ready**: Generates `.cursorrules`. |
|
|
34
34
|
|
|
@@ -50,12 +50,12 @@ nodejs-quickstart init
|
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
-
## What's New in v2.
|
|
53
|
+
## What's New in v2.3 (The Infrastructure & Terraform Release)
|
|
54
54
|
|
|
55
|
-
The v2.
|
|
55
|
+
The v2.3.0 release brings professional-grade Infrastructure as Code (IaC) to your generated projects:
|
|
56
56
|
|
|
57
|
-
- **
|
|
58
|
-
- **Massive Matrix Expansion**: Now supporting **
|
|
57
|
+
- **Terraform Infrastructure Integration (IaC)**: Support for modular, production-ready AWS Terraform scaffolding with Multi-AZ VPC network isolation, WAF, application load balancers, database layers, and ElastiCache.
|
|
58
|
+
- **Massive Matrix Expansion**: Now supporting **23,760+ unique project scenarios**, mathematically validated for template consistency.
|
|
59
59
|
|
|
60
60
|
---
|
|
61
61
|
|
|
@@ -85,14 +85,14 @@ We don't just generate boilerplate; we generate **production-ready** foundations
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
-
##
|
|
88
|
+
## 23,760+ Project Combinations
|
|
89
89
|
|
|
90
90
|
The CLI supports a massive number of configurations to fit your exact needs:
|
|
91
91
|
|
|
92
|
-
- **
|
|
93
|
-
- **MVC Architecture**:
|
|
94
|
-
- **Clean Architecture**:
|
|
95
|
-
- **
|
|
92
|
+
- **2,160 Core Combinations**:
|
|
93
|
+
- **MVC Architecture**: 1,620 variants (Languages × View Engines × Databases × Communication Patterns × Caching × Auth × Infrastructure)
|
|
94
|
+
- **Clean Architecture**: 540 variants (Languages × Databases × Communication Patterns × Caching × Auth × Infrastructure)
|
|
95
|
+
- **23,760+ Total Scenarios**:
|
|
96
96
|
- Every combination can be generated across 5 CI/CD providers.
|
|
97
97
|
- Optional **Enterprise-Grade Security Hardening** doubles the scenarios.
|
|
98
98
|
- Every single scenario is verified to be compatible with our **80% Coverage Threshold** policy.
|
|
@@ -112,6 +112,7 @@ The CLI will guide you through:
|
|
|
112
112
|
8. **Auth**: `None` | `JWT` | `OAuth2 (Google/GitHub) + JWT`
|
|
113
113
|
9. **CI/CD**: `GitHub Actions` | `Jenkins` | `GitLab CI` | `CircleCI` | `Bitbucket Pipelines`
|
|
114
114
|
10. **Security**: (Optional) Snyk & SonarCloud Hardening
|
|
115
|
+
11. **Infrastructure**: (Optional IaC) `None` | `Standard` (Single EC2) | `Production` (WAF + ALB + Multi-AZ VPC)
|
|
115
116
|
|
|
116
117
|
---
|
|
117
118
|
|
|
@@ -139,7 +140,7 @@ Depending on your choices, the structure adapts. Here is a **TypeScript + Clean
|
|
|
139
140
|
|
|
140
141
|
## Documentation
|
|
141
142
|
|
|
142
|
-
For full guides, architecture deep-dives, and feature references, visit our **[Official Documentation Site](https://
|
|
143
|
+
For full guides, architecture deep-dives, and feature references, visit our **[Official Documentation Site](https://nodejs-quickstart-generator.netlify.app/guide/getting-started.html)**.
|
|
143
144
|
|
|
144
145
|
---
|
|
145
146
|
|
package/bin/index.js
CHANGED
|
@@ -15,7 +15,7 @@ const program = new Command();
|
|
|
15
15
|
|
|
16
16
|
program
|
|
17
17
|
.name('nodejs-quickstart')
|
|
18
|
-
.description('🚀 CLI to scaffold production-ready Node.js microservices.\n\nGenerates projects with:\n- MVC or Clean Architecture\n- REST, GraphQL or Kafka\n- MySQL, PostgreSQL, or MongoDB\n- Auth (None, JWT)\n- Docker, Flyway & Mongoose support')
|
|
18
|
+
.description('🚀 CLI to scaffold production-ready Node.js microservices.\n\nGenerates projects with:\n- MVC or Clean Architecture\n- REST, GraphQL or Kafka\n- MySQL, PostgreSQL, or MongoDB\n- Auth (None, JWT)\n- Terraform (Standard, Production)\n- Docker, Flyway & Mongoose support')
|
|
19
19
|
.version(pkg.version, '-v, --version', 'Output the current version')
|
|
20
20
|
.addHelpText('after', `\n${chalk.yellow('Example:')}\n $ nodejs-quickstart init ${chalk.gray('# Start the interactive setup')}\n`);
|
|
21
21
|
|
|
@@ -35,6 +35,7 @@ program
|
|
|
35
35
|
.option('--caching <type>', 'Caching Layer (None/Redis/Memory Cache)')
|
|
36
36
|
.option('--auth <modes...>', 'Authentication Modes (None, JWT)')
|
|
37
37
|
.option('--social-auth <providers...>', 'Social Authentication Providers (None, Google, GitHub)')
|
|
38
|
+
.option('--terraform <tier>', 'Infrastructure Tier (None, Standard, Production)')
|
|
38
39
|
.option('--advanced-options', 'Enable Advanced Options')
|
|
39
40
|
.option('--no-advanced-options', 'Disable Advanced Options')
|
|
40
41
|
.action(async (options) => {
|
package/lib/generator.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
|
-
import { setupProjectDirectory, copyBaseStructure, copyCommonFiles } from './modules/project-setup.js';
|
|
3
|
+
import { setupProjectDirectory, copyBaseStructure, copyCommonFiles, cleanGeneratedFiles } from './modules/project-setup.js';
|
|
4
4
|
import { renderPackageJson, renderDockerCompose, renderReadme, renderDockerfile, renderProfessionalConfig, setupCiCd, renderTestSample, renderEnvExample, renderPm2Config, renderAiNativeFiles } from './modules/config-files.js';
|
|
5
5
|
import { renderIndexFile, renderEnvConfig, renderErrorMiddleware, renderDynamicComponents, renderSwaggerConfig, setupViews as setupSrcViews, processAllTests } from './modules/app-setup.js';
|
|
6
6
|
import { setupDatabase } from './modules/database-setup.js';
|
|
7
7
|
import { setupKafka, setupViews } from './modules/kafka-setup.js';
|
|
8
8
|
import { setupCaching } from './modules/caching-setup.js';
|
|
9
|
+
import { setupTerraform } from './modules/terraform-setup.js';
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -23,6 +24,7 @@ export const generateProject = async (config) => {
|
|
|
23
24
|
database: 'None',
|
|
24
25
|
includeSecurity: false,
|
|
25
26
|
auth: ['None'],
|
|
27
|
+
terraform: 'None',
|
|
26
28
|
...config
|
|
27
29
|
};
|
|
28
30
|
|
|
@@ -99,6 +101,12 @@ export const generateProject = async (config) => {
|
|
|
99
101
|
// 16. PM2 Configuration
|
|
100
102
|
await renderPm2Config(templatesDir, targetDir, config);
|
|
101
103
|
|
|
104
|
+
// 16a. Terraform Setup
|
|
105
|
+
await setupTerraform(templatesDir, targetDir, config);
|
|
106
|
+
|
|
107
|
+
// 16b. Auto-format generated files (clean trailing whitespaces, collapse EJS conditional blank lines)
|
|
108
|
+
await cleanGeneratedFiles(targetDir);
|
|
109
|
+
|
|
102
110
|
// 17. Process All Tests
|
|
103
111
|
await processAllTests(targetDir, config);
|
|
104
112
|
|
|
@@ -122,6 +130,7 @@ export const generateProject = async (config) => {
|
|
|
122
130
|
✅ Security: Helmet, CORS, Rate-Limiting added${config.includeSecurity ? '\n ✅ Enterprise Security: Snyk (SCA) & SonarCloud (SAST) integration' : ''}
|
|
123
131
|
✅ Testing: Jest setup for Unit/Integration tests
|
|
124
132
|
✅ Docker: Production-ready multi-stage build
|
|
133
|
+
${config.terraform && config.terraform !== 'None' ? `✅ Infrastructure (IaC): Terraform (${config.terraform}) ready` : ''}
|
|
125
134
|
${config.ciProvider !== 'None' ? `✅ CI/CD: ${config.ciProvider} Workflow ready` : '❌ CI/CD: Skipped (User preferred)'}
|
|
126
135
|
|
|
127
136
|
----------------------------------------------------
|
|
@@ -31,3 +31,44 @@ export const copyCommonFiles = async (templatesDir, targetDir, language) => {
|
|
|
31
31
|
await fs.copy(path.join(templatesDir, 'common', 'tsconfig.eslint.json'), path.join(targetDir, 'tsconfig.eslint.json'));
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
+
|
|
35
|
+
export const cleanGeneratedFiles = async (dir) => {
|
|
36
|
+
const items = await fs.readdir(dir);
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
const fullPath = path.join(dir, item);
|
|
39
|
+
const stat = await fs.stat(fullPath);
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
if (item !== 'node_modules' && item !== '.git') {
|
|
42
|
+
await cleanGeneratedFiles(fullPath);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
const ext = path.extname(item);
|
|
46
|
+
if (['.ts', '.js', '.json', '.yml', '.yaml', '.md', '.env', '.example', '.cursorrules'].includes(ext) || item === 'Dockerfile' || item === 'Jenkinsfile') {
|
|
47
|
+
let content = await fs.readFile(fullPath, 'utf-8');
|
|
48
|
+
const original = content;
|
|
49
|
+
|
|
50
|
+
content = content.replace(/\r\n/g, '\n');
|
|
51
|
+
|
|
52
|
+
// 1. Remove trailing spaces on lines
|
|
53
|
+
content = content.replace(/[ \t]+$/gm, '');
|
|
54
|
+
|
|
55
|
+
// 2. Collapse 3+ consecutive newlines to maximum 2 newlines (double spacing)
|
|
56
|
+
content = content.replace(/\n{3,}/g, '\n\n');
|
|
57
|
+
|
|
58
|
+
// 3. Remove blank lines immediately before closing braces/brackets, preserving indentation
|
|
59
|
+
content = content.replace(/\n\s*\n(\s*)([\}|\]])/g, '\n$1$2');
|
|
60
|
+
|
|
61
|
+
// 4. Remove blank lines immediately after opening braces/brackets, preserving indentation
|
|
62
|
+
content = content.replace(/({\s*)\n\s*\n(\s*)/g, '$1\n$2');
|
|
63
|
+
content = content.replace(/(\[\s*)\n\s*\n(\s*)/g, '$1\n$2');
|
|
64
|
+
|
|
65
|
+
// 5. Ensure exactly one trailing newline at the end of the file
|
|
66
|
+
content = content.trim() + '\n';
|
|
67
|
+
|
|
68
|
+
if (content !== original) {
|
|
69
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const setupTerraform = async (templatesDir, targetDir, config) => {
|
|
6
|
+
const { terraform, projectName } = config;
|
|
7
|
+
|
|
8
|
+
if (!terraform || terraform === 'None') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const terraformSourceDir = path.join(templatesDir, 'common/terraform');
|
|
13
|
+
const terraformTargetDir = path.join(targetDir, 'terraform');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Ensure the source directory exists
|
|
17
|
+
if (!(await fs.pathExists(terraformSourceDir))) {
|
|
18
|
+
console.warn(chalk.yellow(`\n Terraform templates not found at ${terraformSourceDir}. Skipping...`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Create target directory
|
|
23
|
+
await fs.ensureDir(terraformTargetDir);
|
|
24
|
+
|
|
25
|
+
// Copy everything first
|
|
26
|
+
await fs.copy(terraformSourceDir, terraformTargetDir);
|
|
27
|
+
|
|
28
|
+
// --- Customization ---
|
|
29
|
+
|
|
30
|
+
// 1. Customize variables.tf
|
|
31
|
+
const variablesPath = path.join(terraformTargetDir, 'variables.tf');
|
|
32
|
+
if (await fs.pathExists(variablesPath)) {
|
|
33
|
+
let content = await fs.readFile(variablesPath, 'utf8');
|
|
34
|
+
content = content.replace(/default\s*=\s*"nodejs-quickstart"/, `default = "${projectName}"`);
|
|
35
|
+
|
|
36
|
+
// Map database engine
|
|
37
|
+
if (config.database === 'PostgreSQL') {
|
|
38
|
+
content = content.replace(/variable "db_engine" {[\s\S]*?default\s*=\s*"mysql"/, 'variable "db_engine" {\n description = "Database engine (mysql or postgres)"\n default = "postgres"');
|
|
39
|
+
} else if (config.database === 'MySQL') {
|
|
40
|
+
content = content.replace(/default\s*=\s*"myappdb"/, `default = "${config.dbName || 'demo'}"`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Set is_production flag
|
|
44
|
+
if (terraform === 'Production') {
|
|
45
|
+
content = content.replace(/variable "is_production" \{[\s\S]*?default\s*=\s*false/, 'variable "is_production" {\n description = "Enable production-grade features (Multi-AZ, WAF, Scaling)"\n type = bool\n default = true');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await fs.writeFile(variablesPath, content);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Handle Database "None" case
|
|
52
|
+
if (config.database === 'None') {
|
|
53
|
+
// REMOVE database module call from main.tf
|
|
54
|
+
const mainTfPath = path.join(terraformTargetDir, 'main.tf');
|
|
55
|
+
if (await fs.pathExists(mainTfPath)) {
|
|
56
|
+
let content = await fs.readFile(mainTfPath, 'utf8');
|
|
57
|
+
// Remove the database module block entirely (from comment to closing brace)
|
|
58
|
+
content = content.replace(/# --- Data Layer \(RDS Isolated\) ---[\s\S]*?module "database" \{[\s\S]*?\}\n/g, '');
|
|
59
|
+
await fs.writeFile(mainTfPath, content);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// REMOVE database outputs from outputs.tf
|
|
63
|
+
const outputsTfPath = path.join(terraformTargetDir, 'outputs.tf');
|
|
64
|
+
if (await fs.pathExists(outputsTfPath)) {
|
|
65
|
+
let content = await fs.readFile(outputsTfPath, 'utf8');
|
|
66
|
+
// Remove individual database outputs
|
|
67
|
+
content = content.replace(/output "database_[\s\S]*?\}\n/g, '');
|
|
68
|
+
await fs.writeFile(outputsTfPath, content);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// DELETE the database module folder
|
|
72
|
+
const dbModuleDir = path.join(terraformTargetDir, 'modules/database');
|
|
73
|
+
if (await fs.pathExists(dbModuleDir)) {
|
|
74
|
+
await fs.remove(dbModuleDir);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Handle Caching case (Delete if NOT Redis)
|
|
79
|
+
if (config.caching !== 'Redis') {
|
|
80
|
+
// REMOVE cache module call from main.tf
|
|
81
|
+
const mainTfPath = path.join(terraformTargetDir, 'main.tf');
|
|
82
|
+
if (await fs.pathExists(mainTfPath)) {
|
|
83
|
+
let content = await fs.readFile(mainTfPath, 'utf8');
|
|
84
|
+
// Remove the cache module block entirely
|
|
85
|
+
content = content.replace(/# --- Cache Layer \(Redis ElastiCache\) ---[\s\S]*?module "cache" \{[\s\S]*?\}\n/g, '');
|
|
86
|
+
await fs.writeFile(mainTfPath, content);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// REMOVE redis output from outputs.tf
|
|
90
|
+
const outputsTfPath = path.join(terraformTargetDir, 'outputs.tf');
|
|
91
|
+
if (await fs.pathExists(outputsTfPath)) {
|
|
92
|
+
let content = await fs.readFile(outputsTfPath, 'utf8');
|
|
93
|
+
content = content.replace(/output "redis_endpoint" \{[\s\S]*?\}\n/g, '');
|
|
94
|
+
await fs.writeFile(outputsTfPath, content);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// DELETE the cache module folder
|
|
98
|
+
const cacheModuleDir = path.join(terraformTargetDir, 'modules/cache');
|
|
99
|
+
if (await fs.pathExists(cacheModuleDir)) {
|
|
100
|
+
await fs.remove(cacheModuleDir);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// If caching IS Redis, it's already uncommented in the template, so no action needed here.
|
|
104
|
+
|
|
105
|
+
// 4. Customize Compute module based on language
|
|
106
|
+
const computeMainPath = path.join(terraformTargetDir, 'modules/compute/main.tf');
|
|
107
|
+
if (await fs.pathExists(computeMainPath)) {
|
|
108
|
+
let content = await fs.readFile(computeMainPath, 'utf8');
|
|
109
|
+
const startHint = config.language === 'TypeScript'
|
|
110
|
+
? 'npm run build && node dist/index.js'
|
|
111
|
+
: 'node src/index.js';
|
|
112
|
+
|
|
113
|
+
content = content.replace(/# docker run -d -p 3000:3000 my-node-app:latest/,
|
|
114
|
+
`# For ${config.language}: ${startHint}\n # docker run -d -p 3000:3000 my-node-app:latest`);
|
|
115
|
+
await fs.writeFile(computeMainPath, content);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 5. Handle "Standard" tier (Optional: disable WAF for cost)
|
|
119
|
+
if (terraform === 'Standard') {
|
|
120
|
+
const mainTfPath = path.join(terraformTargetDir, 'main.tf');
|
|
121
|
+
if (await fs.pathExists(mainTfPath)) {
|
|
122
|
+
let content = await fs.readFile(mainTfPath, 'utf8');
|
|
123
|
+
// In Standard tier, we might want to tell the security module to skip WAF
|
|
124
|
+
// For now, we'll keep it simple, but we could add a variable 'enable_waf'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(chalk.red(`\n Error setting up Terraform: ${error.message}`));
|
|
130
|
+
}
|
|
131
|
+
};
|
package/lib/prompts.js
CHANGED
|
@@ -89,7 +89,7 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
89
89
|
{
|
|
90
90
|
type: 'select',
|
|
91
91
|
name: 'advancedOptions',
|
|
92
|
-
message: 'Enable Advanced Options (Authentication, etc.)?',
|
|
92
|
+
message: 'Enable Advanced Options (Authentication, Terraform, etc.)?',
|
|
93
93
|
choices: ['No', 'Yes'],
|
|
94
94
|
default: 'No',
|
|
95
95
|
when: options.advancedOptions === undefined
|
|
@@ -104,6 +104,21 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
104
104
|
const advanced = options.advancedOptions !== undefined ? options.advancedOptions : answers.advancedOptions;
|
|
105
105
|
return (advanced === 'Yes' || advanced === true) && !options.auth;
|
|
106
106
|
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: 'select',
|
|
110
|
+
name: 'terraform',
|
|
111
|
+
message: 'Select Infrastructure (IaC - Terraform):',
|
|
112
|
+
choices: [
|
|
113
|
+
{ name: 'None (No infrastructure files)', value: 'None' },
|
|
114
|
+
{ name: 'Standard (Single EC2 - Cost Efficient)', value: 'Standard' },
|
|
115
|
+
{ name: 'Production (WAF + ALB + Private Subnets - High Availability)', value: 'Production' }
|
|
116
|
+
],
|
|
117
|
+
default: 'None',
|
|
118
|
+
when: (answers) => {
|
|
119
|
+
const advanced = options.advancedOptions !== undefined ? options.advancedOptions : answers.advancedOptions;
|
|
120
|
+
return (advanced === 'Yes' || advanced === true) && !options.terraform;
|
|
121
|
+
}
|
|
107
122
|
}
|
|
108
123
|
];
|
|
109
124
|
|
package/package.json
CHANGED
|
@@ -13,12 +13,15 @@ class CreateUser {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async execute(name, email, password) {
|
|
16
|
+
<%_ if (auth.includes('JWT')) { -%>
|
|
16
17
|
let finalPassword = password;
|
|
17
|
-
<%_ if (auth.includes('JWT')) { -%>
|
|
18
18
|
if (password) {
|
|
19
19
|
finalPassword = await bcrypt.hash(password, 10);
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
21
|
const user = new User(null, name, email, finalPassword);
|
|
22
|
+
<%_ } else { -%>
|
|
23
|
+
const user = new User(null, name, email, password);
|
|
24
|
+
<%_ } -%>
|
|
22
25
|
const savedUser = await this.userRepository.save(user);
|
|
23
26
|
<%_ if (caching !== 'None') { -%>
|
|
24
27
|
try {
|
|
@@ -14,13 +14,15 @@ export default class CreateUser {
|
|
|
14
14
|
constructor(private userRepository: UserRepository) {}
|
|
15
15
|
|
|
16
16
|
async execute(name: string, email: string, password?: string) {
|
|
17
|
-
let finalPassword = password;
|
|
18
17
|
<% if (auth.includes('JWT')) { -%>
|
|
18
|
+
let finalPassword = password;
|
|
19
19
|
if (password) {
|
|
20
20
|
finalPassword = await bcrypt.hash(password, 10);
|
|
21
21
|
}
|
|
22
|
-
<% } -%>
|
|
23
22
|
const user = new User(null, name, email, finalPassword);
|
|
23
|
+
<% } else { -%>
|
|
24
|
+
const user = new User(null, name, email, password);
|
|
25
|
+
<% } -%>
|
|
24
26
|
const savedUser = await this.userRepository.save(user);
|
|
25
27
|
<% if (caching !== 'None') { -%>
|
|
26
28
|
try {
|
|
@@ -15,13 +15,15 @@ class CreateUser {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
async execute(name, email, password) {
|
|
18
|
-
let finalPassword = password;
|
|
19
18
|
<% if (auth.includes('JWT')) { -%>
|
|
19
|
+
let finalPassword = password;
|
|
20
20
|
if (password) {
|
|
21
21
|
finalPassword = await bcrypt.hash(password, 10);
|
|
22
22
|
}
|
|
23
|
-
<% } -%>
|
|
24
23
|
const user = new User(null, name, email, finalPassword);
|
|
24
|
+
<% } else { -%>
|
|
25
|
+
const user = new User(null, name, email, password);
|
|
26
|
+
<% } -%>
|
|
25
27
|
const savedUser = await this.userRepository.save(user);
|
|
26
28
|
|
|
27
29
|
try {
|
|
@@ -14,13 +14,15 @@ export default class CreateUser {
|
|
|
14
14
|
constructor(private userRepository: UserRepository) {}
|
|
15
15
|
|
|
16
16
|
async execute(name: string, email: string, password?: string) {
|
|
17
|
-
let finalPassword = password;
|
|
18
17
|
<% if (auth.includes('JWT')) { -%>
|
|
18
|
+
let finalPassword = password;
|
|
19
19
|
if (password) {
|
|
20
20
|
finalPassword = await bcrypt.hash(password, 10);
|
|
21
21
|
}
|
|
22
|
-
<% } -%>
|
|
23
22
|
const user = new User(null, name, email, finalPassword);
|
|
23
|
+
<% } else { -%>
|
|
24
|
+
const user = new User(null, name, email, password);
|
|
25
|
+
<% } -%>
|
|
24
26
|
const savedUser = await this.userRepository.save(user);
|
|
25
27
|
|
|
26
28
|
try {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# --- Network Layer ---
|
|
2
|
+
module "vpc" {
|
|
3
|
+
source = "./modules/vpc"
|
|
4
|
+
project_name = var.project_name
|
|
5
|
+
environment = var.environment
|
|
6
|
+
is_production = var.is_production
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
# --- Security Layer (WAF, ALB, SGs) ---
|
|
10
|
+
module "security" {
|
|
11
|
+
source = "./modules/security"
|
|
12
|
+
project_name = var.project_name
|
|
13
|
+
environment = var.environment
|
|
14
|
+
vpc_id = module.vpc.vpc_id
|
|
15
|
+
public_subnet_ids = module.vpc.public_subnet_ids
|
|
16
|
+
enable_waf = var.is_production
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# --- Data Layer (RDS Isolated) ---
|
|
20
|
+
module "database" {
|
|
21
|
+
source = "./modules/database"
|
|
22
|
+
project_name = var.project_name
|
|
23
|
+
environment = var.environment
|
|
24
|
+
vpc_id = module.vpc.vpc_id
|
|
25
|
+
isolated_subnet_ids = module.vpc.isolated_subnet_ids
|
|
26
|
+
app_sg_id = module.security.app_sg_id
|
|
27
|
+
db_engine = var.db_engine
|
|
28
|
+
db_name = var.db_name
|
|
29
|
+
db_user = var.db_user
|
|
30
|
+
multi_az = var.is_production
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# --- Cache Layer (Redis ElastiCache) ---
|
|
34
|
+
module "cache" {
|
|
35
|
+
source = "./modules/cache"
|
|
36
|
+
project_name = var.project_name
|
|
37
|
+
vpc_id = module.vpc.vpc_id
|
|
38
|
+
private_subnet_ids = module.vpc.private_subnet_ids
|
|
39
|
+
app_sg_id = module.security.app_sg_id
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# --- Compute Layer (App EC2) ---
|
|
43
|
+
module "compute" {
|
|
44
|
+
source = "./modules/compute"
|
|
45
|
+
project_name = var.project_name
|
|
46
|
+
environment = var.environment
|
|
47
|
+
vpc_id = module.vpc.vpc_id
|
|
48
|
+
private_subnet_ids = module.vpc.private_subnet_ids
|
|
49
|
+
app_sg_id = module.security.app_sg_id
|
|
50
|
+
target_group_arn = module.security.alb_target_group_arn
|
|
51
|
+
instance_count = var.is_production ? 2 : 1
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
resource "aws_security_group" "redis_sg" {
|
|
2
|
+
name = "${var.project_name}-redis-sg"
|
|
3
|
+
description = "Allow Redis traffic from App"
|
|
4
|
+
vpc_id = var.vpc_id
|
|
5
|
+
|
|
6
|
+
ingress {
|
|
7
|
+
from_port = 6379
|
|
8
|
+
to_port = 6379
|
|
9
|
+
protocol = "tcp"
|
|
10
|
+
security_groups = [var.app_sg_id]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
egress {
|
|
14
|
+
from_port = 0
|
|
15
|
+
to_port = 0
|
|
16
|
+
protocol = "-1"
|
|
17
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
tags = { Name = "${var.project_name}-redis-sg" }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resource "aws_elasticache_subnet_group" "main" {
|
|
24
|
+
name = "${var.project_name}-redis-subnet-group"
|
|
25
|
+
subnet_ids = var.private_subnet_ids
|
|
26
|
+
|
|
27
|
+
tags = { Name = "${var.project_name}-redis-subnet-group" }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
resource "aws_elasticache_cluster" "main" {
|
|
31
|
+
cluster_id = "${var.project_name}-redis"
|
|
32
|
+
engine = "redis"
|
|
33
|
+
node_type = "cache.t3.micro"
|
|
34
|
+
num_cache_nodes = 1
|
|
35
|
+
parameter_group_name = "default.redis7"
|
|
36
|
+
port = 6379
|
|
37
|
+
subnet_group_name = aws_elasticache_subnet_group.main.name
|
|
38
|
+
security_group_ids = [aws_security_group.redis_sg.id]
|
|
39
|
+
|
|
40
|
+
tags = { Name = "${var.project_name}-redis" }
|
|
41
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# --- IAM Role for Systems Manager (SSM) ---
|
|
2
|
+
# This allows SSH-less access to the private instance
|
|
3
|
+
resource "aws_iam_role" "ssm_role" {
|
|
4
|
+
name = "${var.project_name}-ssm-role"
|
|
5
|
+
|
|
6
|
+
assume_role_policy = jsonencode({
|
|
7
|
+
Version = "2012-10-17"
|
|
8
|
+
Statement = [{
|
|
9
|
+
Action = "sts:AssumeRole"
|
|
10
|
+
Effect = "Allow"
|
|
11
|
+
Principal = { Service = "ec2.amazonaws.com" }
|
|
12
|
+
}]
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
resource "aws_iam_role_policy_attachment" "ssm_policy" {
|
|
17
|
+
role = aws_iam_role.ssm_role.name
|
|
18
|
+
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
resource "aws_iam_instance_profile" "ssm_profile" {
|
|
22
|
+
name = "${var.project_name}-ssm-profile"
|
|
23
|
+
role = aws_iam_role.ssm_role.name
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# --- EC2 Instance ---
|
|
27
|
+
data "aws_ami" "latest" {
|
|
28
|
+
most_recent = true
|
|
29
|
+
owners = ["amazon"]
|
|
30
|
+
filter {
|
|
31
|
+
name = "name"
|
|
32
|
+
values = ["amzn2-ami-hvm-*"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resource "aws_instance" "app" {
|
|
37
|
+
count = var.instance_count
|
|
38
|
+
ami = data.aws_ami.latest.id
|
|
39
|
+
instance_type = var.instance_type
|
|
40
|
+
subnet_id = var.private_subnet_ids[count.index % length(var.private_subnet_ids)]
|
|
41
|
+
vpc_security_group_ids = [var.app_sg_id]
|
|
42
|
+
iam_instance_profile = aws_iam_instance_profile.ssm_profile.name
|
|
43
|
+
|
|
44
|
+
user_data = <<-EOF
|
|
45
|
+
#!/bin/bash
|
|
46
|
+
yum update -y
|
|
47
|
+
yum install -y docker
|
|
48
|
+
systemctl start docker
|
|
49
|
+
systemctl enable docker
|
|
50
|
+
|
|
51
|
+
# Add user to docker group
|
|
52
|
+
usermod -aG docker ec2-user
|
|
53
|
+
|
|
54
|
+
# Note: For production, you would pull your image and run it here
|
|
55
|
+
# docker run -d -p 3000:3000 my-node-app:latest
|
|
56
|
+
EOF
|
|
57
|
+
|
|
58
|
+
tags = {
|
|
59
|
+
Name = "${var.project_name}-app-server-${count.index + 1}"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Attach to ALB Target Group
|
|
64
|
+
resource "aws_lb_target_group_attachment" "app" {
|
|
65
|
+
count = var.instance_count
|
|
66
|
+
target_group_arn = var.target_group_arn
|
|
67
|
+
target_id = aws_instance.app[count.index].id
|
|
68
|
+
port = 3000
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
variable "project_name" { type = string }
|
|
2
|
+
variable "environment" { type = string }
|
|
3
|
+
variable "vpc_id" { type = string }
|
|
4
|
+
variable "private_subnet_ids" { type = list(string) }
|
|
5
|
+
variable "app_sg_id" { type = string }
|
|
6
|
+
variable "instance_count" {
|
|
7
|
+
type = number
|
|
8
|
+
default = 1
|
|
9
|
+
}
|
|
10
|
+
variable "target_group_arn" { type = string }
|
|
11
|
+
|
|
12
|
+
variable "instance_type" {
|
|
13
|
+
default = "t3.micro"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
variable "ami_id" {
|
|
17
|
+
description = "Amazon Linux 2023 AMI"
|
|
18
|
+
type = string
|
|
19
|
+
default = "ami-051f8b211046e76c0" # Thay đổi tùy region
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Security Group for RDS (Only from App SG)
|
|
2
|
+
resource "aws_security_group" "db_sg" {
|
|
3
|
+
name = "${var.project_name}-db-sg"
|
|
4
|
+
description = "Allow traffic from App only"
|
|
5
|
+
vpc_id = var.vpc_id
|
|
6
|
+
|
|
7
|
+
ingress {
|
|
8
|
+
from_port = var.db_engine == "mysql" ? 3306 : 5432
|
|
9
|
+
to_port = var.db_engine == "mysql" ? 3306 : 5432
|
|
10
|
+
protocol = "tcp"
|
|
11
|
+
security_groups = [var.app_sg_id]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
egress {
|
|
15
|
+
from_port = 0
|
|
16
|
+
to_port = 0
|
|
17
|
+
protocol = "-1"
|
|
18
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
tags = { Name = "${var.project_name}-db-sg" }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Subnet Group for RDS
|
|
25
|
+
resource "aws_db_subnet_group" "main" {
|
|
26
|
+
name = "${var.project_name}-db-subnet-group"
|
|
27
|
+
subnet_ids = var.isolated_subnet_ids
|
|
28
|
+
|
|
29
|
+
tags = { Name = "${var.project_name}-db-subnet-group" }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Generate random password
|
|
33
|
+
resource "random_password" "db_password" {
|
|
34
|
+
length = 16
|
|
35
|
+
special = true
|
|
36
|
+
override_special = "!#$%&*()-_=+[]{}<>:?"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# RDS Instance
|
|
40
|
+
resource "aws_db_instance" "main" {
|
|
41
|
+
identifier = "${var.project_name}-db"
|
|
42
|
+
allocated_storage = 20
|
|
43
|
+
engine = var.db_engine
|
|
44
|
+
instance_class = var.db_instance_class
|
|
45
|
+
db_name = var.db_name
|
|
46
|
+
username = var.db_user
|
|
47
|
+
password = random_password.db_password.result
|
|
48
|
+
db_subnet_group_name = aws_db_subnet_group.main.name
|
|
49
|
+
vpc_security_group_ids = [aws_security_group.db_sg.id]
|
|
50
|
+
skip_final_snapshot = true
|
|
51
|
+
multi_az = var.multi_az
|
|
52
|
+
|
|
53
|
+
# Encryption at rest (Security Hardening)
|
|
54
|
+
storage_encrypted = true
|
|
55
|
+
|
|
56
|
+
tags = { Name = "${var.project_name}-db" }
|
|
57
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
output "db_endpoint" {
|
|
2
|
+
value = aws_db_instance.main.endpoint
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
output "db_name" {
|
|
6
|
+
value = aws_db_instance.main.db_name
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
output "db_user" {
|
|
10
|
+
value = aws_db_instance.main.username
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
output "db_password" {
|
|
14
|
+
value = random_password.db_password.result
|
|
15
|
+
sensitive = true
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
variable "project_name" { type = string }
|
|
2
|
+
variable "environment" { type = string }
|
|
3
|
+
variable "vpc_id" { type = string }
|
|
4
|
+
variable "isolated_subnet_ids" { type = list(string) }
|
|
5
|
+
variable "app_sg_id" { type = string }
|
|
6
|
+
variable "multi_az" {
|
|
7
|
+
type = bool
|
|
8
|
+
default = false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "db_engine" {
|
|
12
|
+
description = "Database engine (mysql, postgres)"
|
|
13
|
+
type = string
|
|
14
|
+
default = "mysql"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
variable "db_instance_class" {
|
|
18
|
+
default = "db.t3.micro"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
variable "db_name" {
|
|
22
|
+
default = "myappdb"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
variable "db_user" {
|
|
26
|
+
default = "admin"
|
|
27
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# --- Security Groups ---
|
|
2
|
+
|
|
3
|
+
# ALB Security Group (Public access)
|
|
4
|
+
resource "aws_security_group" "alb_sg" {
|
|
5
|
+
name = "${var.project_name}-alb-sg"
|
|
6
|
+
description = "Allow HTTP/HTTPS from everywhere"
|
|
7
|
+
vpc_id = var.vpc_id
|
|
8
|
+
|
|
9
|
+
ingress {
|
|
10
|
+
from_port = 80
|
|
11
|
+
to_port = 80
|
|
12
|
+
protocol = "tcp"
|
|
13
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
egress {
|
|
17
|
+
from_port = 0
|
|
18
|
+
to_port = 0
|
|
19
|
+
protocol = "-1"
|
|
20
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
tags = { Name = "${var.project_name}-alb-sg" }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# App Security Group (Only from ALB)
|
|
27
|
+
resource "aws_security_group" "app_sg" {
|
|
28
|
+
name = "${var.project_name}-app-sg"
|
|
29
|
+
description = "Allow traffic only from ALB"
|
|
30
|
+
vpc_id = var.vpc_id
|
|
31
|
+
|
|
32
|
+
ingress {
|
|
33
|
+
from_port = var.app_port
|
|
34
|
+
to_port = var.app_port
|
|
35
|
+
protocol = "tcp"
|
|
36
|
+
security_groups = [aws_security_group.alb_sg.id]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
egress {
|
|
40
|
+
from_port = 0
|
|
41
|
+
to_port = 0
|
|
42
|
+
protocol = "-1"
|
|
43
|
+
cidr_blocks = ["0.0.0.0/0"]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
tags = { Name = "${var.project_name}-app-sg" }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# --- Load Balancer ---
|
|
50
|
+
resource "aws_lb" "main" {
|
|
51
|
+
name = "${var.project_name}-alb"
|
|
52
|
+
internal = false
|
|
53
|
+
load_balancer_type = "application"
|
|
54
|
+
security_groups = [aws_security_group.alb_sg.id]
|
|
55
|
+
subnets = var.public_subnet_ids
|
|
56
|
+
|
|
57
|
+
tags = { Name = "${var.project_name}-alb" }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
resource "aws_lb_target_group" "app" {
|
|
61
|
+
name = "${var.project_name}-tg"
|
|
62
|
+
port = var.app_port
|
|
63
|
+
protocol = "HTTP"
|
|
64
|
+
vpc_id = var.vpc_id
|
|
65
|
+
|
|
66
|
+
health_check {
|
|
67
|
+
path = "/health"
|
|
68
|
+
healthy_threshold = 2
|
|
69
|
+
unhealthy_threshold = 10
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resource "aws_lb_listener" "http" {
|
|
74
|
+
load_balancer_arn = aws_lb.main.arn
|
|
75
|
+
port = "80"
|
|
76
|
+
protocol = "HTTP"
|
|
77
|
+
|
|
78
|
+
default_action {
|
|
79
|
+
type = "forward"
|
|
80
|
+
target_group_arn = aws_lb_target_group.app.arn
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# --- AWS WAF (Web Application Firewall) ---
|
|
85
|
+
# Simple WAF with Common Rules (SQLi, Linux, etc.)
|
|
86
|
+
resource "aws_wafv2_web_acl" "main" {
|
|
87
|
+
count = var.enable_waf ? 1 : 0
|
|
88
|
+
name = "${var.project_name}-waf"
|
|
89
|
+
description = "WAF for Node.js Application"
|
|
90
|
+
scope = "REGIONAL"
|
|
91
|
+
|
|
92
|
+
default_action {
|
|
93
|
+
allow {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
rule {
|
|
97
|
+
name = "AWSManagedRulesCommonRuleSet"
|
|
98
|
+
priority = 1
|
|
99
|
+
|
|
100
|
+
override_action {
|
|
101
|
+
none {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
statement {
|
|
105
|
+
managed_rule_group_statement {
|
|
106
|
+
name = "AWSManagedRulesCommonRuleSet"
|
|
107
|
+
vendor_name = "AWS"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
visibility_config {
|
|
112
|
+
cloudwatch_metrics_enabled = true
|
|
113
|
+
metric_name = "WAFCommonRule"
|
|
114
|
+
sampled_requests_enabled = true
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
visibility_config {
|
|
119
|
+
cloudwatch_metrics_enabled = true
|
|
120
|
+
metric_name = "WAFMain"
|
|
121
|
+
sampled_requests_enabled = true
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Associate WAF with ALB
|
|
126
|
+
resource "aws_wafv2_web_acl_association" "main" {
|
|
127
|
+
count = var.enable_waf ? 1 : 0
|
|
128
|
+
resource_arn = aws_lb.main.arn
|
|
129
|
+
web_acl_arn = aws_wafv2_web_acl.main[0].arn
|
|
130
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
output "alb_dns_name" {
|
|
2
|
+
value = aws_lb.main.dns_name
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
output "alb_target_group_arn" {
|
|
6
|
+
value = aws_lb_target_group.app.arn
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
output "app_sg_id" {
|
|
10
|
+
value = aws_security_group.app_sg.id
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
output "alb_sg_id" {
|
|
14
|
+
value = aws_security_group.alb_sg.id
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
variable "project_name" { type = string }
|
|
2
|
+
variable "environment" { type = string }
|
|
3
|
+
variable "vpc_id" { type = string }
|
|
4
|
+
variable "public_subnet_ids" { type = list(string) }
|
|
5
|
+
variable "enable_waf" {
|
|
6
|
+
type = bool
|
|
7
|
+
default = false
|
|
8
|
+
}
|
|
9
|
+
variable "app_port" {
|
|
10
|
+
type = number
|
|
11
|
+
default = 3000
|
|
12
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
resource "aws_vpc" "main" {
|
|
2
|
+
cidr_block = var.vpc_cidr
|
|
3
|
+
enable_dns_hostnames = true
|
|
4
|
+
enable_dns_support = true
|
|
5
|
+
|
|
6
|
+
tags = {
|
|
7
|
+
Name = "${var.project_name}-vpc"
|
|
8
|
+
Environment = var.environment
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# --- Internet Gateway ---
|
|
13
|
+
resource "aws_internet_gateway" "igw" {
|
|
14
|
+
vpc_id = aws_vpc.main.id
|
|
15
|
+
|
|
16
|
+
tags = {
|
|
17
|
+
Name = "${var.project_name}-igw"
|
|
18
|
+
Environment = var.environment
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# --- Subnets ---
|
|
23
|
+
|
|
24
|
+
# Public Subnets (For ALB and NAT GW)
|
|
25
|
+
resource "aws_subnet" "public" {
|
|
26
|
+
count = length(var.public_subnets)
|
|
27
|
+
vpc_id = aws_vpc.main.id
|
|
28
|
+
cidr_block = var.public_subnets[count.index]
|
|
29
|
+
availability_zone = var.availability_zones[count.index]
|
|
30
|
+
map_public_ip_on_launch = true
|
|
31
|
+
|
|
32
|
+
tags = {
|
|
33
|
+
Name = "${var.project_name}-public-${count.index + 1}"
|
|
34
|
+
Type = "Public"
|
|
35
|
+
Environment = var.environment
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Private Subnets (For Application)
|
|
40
|
+
resource "aws_subnet" "private" {
|
|
41
|
+
count = length(var.private_subnets)
|
|
42
|
+
vpc_id = aws_vpc.main.id
|
|
43
|
+
cidr_block = var.private_subnets[count.index]
|
|
44
|
+
availability_zone = var.availability_zones[count.index]
|
|
45
|
+
|
|
46
|
+
tags = {
|
|
47
|
+
Name = "${var.project_name}-private-${count.index + 1}"
|
|
48
|
+
Type = "Private"
|
|
49
|
+
Environment = var.environment
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Isolated Subnets (For Database)
|
|
54
|
+
resource "aws_subnet" "isolated" {
|
|
55
|
+
count = length(var.isolated_subnets)
|
|
56
|
+
vpc_id = aws_vpc.main.id
|
|
57
|
+
cidr_block = var.isolated_subnets[count.index]
|
|
58
|
+
availability_zone = var.availability_zones[count.index]
|
|
59
|
+
|
|
60
|
+
tags = {
|
|
61
|
+
Name = "${var.project_name}-isolated-${count.index + 1}"
|
|
62
|
+
Type = "Isolated"
|
|
63
|
+
Environment = var.environment
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# --- NAT Gateway (1 per AZ for Production, 1 total for Standard) ---
|
|
68
|
+
resource "aws_eip" "nat" {
|
|
69
|
+
count = var.is_production ? length(var.public_subnets) : 1
|
|
70
|
+
domain = "vpc"
|
|
71
|
+
tags = { Name = "${var.project_name}-nat-eip-${count.index + 1}" }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
resource "aws_nat_gateway" "main" {
|
|
75
|
+
count = var.is_production ? length(var.public_subnets) : 1
|
|
76
|
+
allocation_id = aws_eip.nat[count.index].id
|
|
77
|
+
subnet_id = aws_subnet.public[count.index].id
|
|
78
|
+
|
|
79
|
+
tags = {
|
|
80
|
+
Name = "${var.project_name}-nat-gw-${count.index + 1}"
|
|
81
|
+
Environment = var.environment
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# --- Routing Tables ---
|
|
86
|
+
|
|
87
|
+
# Public Route Table
|
|
88
|
+
resource "aws_route_table" "public" {
|
|
89
|
+
vpc_id = aws_vpc.main.id
|
|
90
|
+
|
|
91
|
+
route {
|
|
92
|
+
cidr_block = "0.0.0.0/0"
|
|
93
|
+
gateway_id = aws_internet_gateway.igw.id
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
tags = { Name = "${var.project_name}-public-rt" }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
resource "aws_route_table_association" "public" {
|
|
100
|
+
count = length(var.public_subnets)
|
|
101
|
+
subnet_id = aws_subnet.public[count.index].id
|
|
102
|
+
route_table_id = aws_route_table.public.id
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Private Route Tables (1 per AZ for Production, 1 total for Standard)
|
|
106
|
+
resource "aws_route_table" "private" {
|
|
107
|
+
count = var.is_production ? length(var.private_subnets) : 1
|
|
108
|
+
vpc_id = aws_vpc.main.id
|
|
109
|
+
|
|
110
|
+
route {
|
|
111
|
+
cidr_block = "0.0.0.0/0"
|
|
112
|
+
nat_gateway_id = aws_nat_gateway.main[count.index].id
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
tags = { Name = "${var.project_name}-private-rt-${count.index + 1}" }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
resource "aws_route_table_association" "private" {
|
|
119
|
+
count = length(var.private_subnets)
|
|
120
|
+
subnet_id = aws_subnet.private[count.index].id
|
|
121
|
+
route_table_id = var.is_production ? aws_route_table.private[count.index].id : aws_route_table.private[0].id
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Isolated Route Table (No internet route)
|
|
125
|
+
resource "aws_route_table" "isolated" {
|
|
126
|
+
vpc_id = aws_vpc.main.id
|
|
127
|
+
tags = { Name = "${var.project_name}-isolated-rt" }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
resource "aws_route_table_association" "isolated" {
|
|
131
|
+
count = length(var.isolated_subnets)
|
|
132
|
+
subnet_id = aws_subnet.isolated[count.index].id
|
|
133
|
+
route_table_id = aws_route_table.isolated.id
|
|
134
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
output "vpc_id" {
|
|
2
|
+
value = aws_vpc.main.id
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
output "public_subnet_ids" {
|
|
6
|
+
value = aws_subnet.public[*].id
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
output "private_subnet_ids" {
|
|
10
|
+
value = aws_subnet.private[*].id
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
output "isolated_subnet_ids" {
|
|
14
|
+
value = aws_subnet.isolated[*].id
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
output "nat_gateway_ip" {
|
|
18
|
+
value = aws_nat_gateway.main[*].public_ip
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
variable "vpc_cidr" {
|
|
2
|
+
description = "CIDR block for the VPC"
|
|
3
|
+
type = string
|
|
4
|
+
default = "10.0.0.0/16"
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
variable "project_name" {
|
|
8
|
+
description = "Project name for tagging"
|
|
9
|
+
type = string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
variable "environment" {
|
|
13
|
+
description = "Environment name (dev, staging, prod)"
|
|
14
|
+
type = string
|
|
15
|
+
default = "dev"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
variable "availability_zones" {
|
|
19
|
+
description = "List of availability zones"
|
|
20
|
+
type = list(string)
|
|
21
|
+
default = ["us-east-1a", "us-east-1b"]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
variable "public_subnets" {
|
|
25
|
+
description = "CIDR blocks for public subnets"
|
|
26
|
+
type = list(string)
|
|
27
|
+
default = ["10.0.1.0/24", "10.0.2.0/24"]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
variable "private_subnets" {
|
|
31
|
+
description = "CIDR blocks for private subnets"
|
|
32
|
+
type = list(string)
|
|
33
|
+
default = ["10.0.10.0/24", "10.0.11.0/24"]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
variable "isolated_subnets" {
|
|
37
|
+
description = "CIDR blocks for isolated subnets"
|
|
38
|
+
type = list(string)
|
|
39
|
+
default = ["10.0.20.0/24", "10.0.21.0/24"]
|
|
40
|
+
}
|
|
41
|
+
variable "is_production" {
|
|
42
|
+
description = "Enable Production features (Multi-NAT)"
|
|
43
|
+
type = bool
|
|
44
|
+
default = false
|
|
45
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
output "application_url" {
|
|
2
|
+
description = "URL of the application (Load Balancer)"
|
|
3
|
+
value = "http://${module.security.alb_dns_name}"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "database_endpoint" {
|
|
7
|
+
value = module.database.db_endpoint
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
output "database_name" {
|
|
11
|
+
value = module.database.db_name
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
output "database_user" {
|
|
15
|
+
value = module.database.db_user
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
output "database_password" {
|
|
19
|
+
value = module.database.db_password
|
|
20
|
+
sensitive = true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
output "nat_gateway_ip" {
|
|
24
|
+
value = module.vpc.nat_gateway_ip
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
output "redis_endpoint" {
|
|
28
|
+
value = module.cache.redis_endpoint
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_version = ">= 1.0.0"
|
|
3
|
+
required_providers {
|
|
4
|
+
aws = {
|
|
5
|
+
source = "hashicorp/aws"
|
|
6
|
+
version = "~> 5.0"
|
|
7
|
+
}
|
|
8
|
+
random = {
|
|
9
|
+
source = "hashicorp/random"
|
|
10
|
+
version = "~> 3.0"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
provider "aws" {
|
|
16
|
+
region = var.aws_region
|
|
17
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
variable "aws_region" {
|
|
2
|
+
description = "AWS region"
|
|
3
|
+
default = "us-east-1"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "project_name" {
|
|
7
|
+
description = "Name of the project"
|
|
8
|
+
default = "nodejs-quickstart"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "environment" {
|
|
12
|
+
description = "Environment (dev, staging, prod)"
|
|
13
|
+
default = "dev"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
variable "db_engine" {
|
|
17
|
+
description = "Database engine (mysql or postgres)"
|
|
18
|
+
default = "mysql"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
variable "db_name" {
|
|
22
|
+
default = "myappdb"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
variable "db_user" {
|
|
26
|
+
default = "admin"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
variable "is_production" {
|
|
30
|
+
description = "Enable production-grade features (Multi-AZ, WAF, Scaling)"
|
|
31
|
+
type = bool
|
|
32
|
+
default = false
|
|
33
|
+
}
|