deploy-stack 0.15.2 β†’ 0.16.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/README.md CHANGED
@@ -48,6 +48,7 @@ You retain complete ownership of your infrastructure code without relying on bla
48
48
  * **Zero Vendor Lock-In:** Generates standard, readable Terraform (`.tf`) files. You own the infrastructure.
49
49
  * **Native S3 State Locking:** Automatically creates an encrypted S3 state bucket utilizing modern Terraform concurrency locking.
50
50
  * **Safe Iteration:** Idempotent CLI safely backs up existing configurations to `.bak` files to guarantee zero data loss.
51
+ * **Ephemeral PR Previews (Opt-In):** Automatically spins up completely isolated AWS Fargate environments for every Pull Request and posts the live preview URL to GitHub, accelerating team code reviews.
51
52
 
52
53
  ---
53
54
 
@@ -57,6 +58,7 @@ Transitioning from PaaS to AWS involves a few architectural shifts. We've writte
57
58
  * [Managing Secrets & Environment Variables](./docs/guides/secrets-management.md)
58
59
  * [Zero-Trust Database Connections](./docs/guides/database-connections.md)
59
60
  * [Migrating Next.js from Vercel](./docs/migrations/nextjs-vercel-to-aws.md)
61
+ * [Ephemeral PR Previews & AWS Costs](./docs/guides/ephemeral-pr-previews.md)
60
62
 
61
63
  ---
62
64
 
@@ -154,11 +156,11 @@ npx deploy-stack --no-telemetry
154
156
 
155
157
  ## πŸ—ΊοΈ Roadmap
156
158
 
157
- ### Current Focus (Phase 6: Migration & Trust Engine)
158
- - [x] **Dry-Run Visualization:** Interactive pre-flight terminal UI with ASCII topology maps and precise, dynamic AWS cost estimation.
159
- - [x] **PaaS Importers:** Auto-parse `vercel.json` and Heroku `Procfile` configurations to map routing rules, web commands, and background workers automatically.
160
- - [x] **Docker Compose to ECS Translator:** Automatically converting a familiar local `docker-compose.yml` into production ECS task definitions.
161
- - [x] **AI Agent Rulesets:** Publishing `.cursorrules` and Copilot instructions that teach AI assistants exactly how to utilize the CLI on the user's behalf.
159
+ ### Current Focus (Phase 7: Team Workflows & Ecosystem Integrations)
160
+ - [x] **Ephemeral PR Previews:** Generate GitHub Actions workflows that spin up temporary ECS Fargate tasks and post live preview URLs directly in pull request comments to streamline team code reviews.
161
+ - [ ] **AI Context Synchronization:** Implement `deploy-stack sync-ai` to automatically generate `.cursorrules` and AI context files, ensuring coding assistants generate accurate deployment commands tailored to the project.
162
+ - [ ] **Native Ecosystem Integrations:** Publish seamless, push-button plugins across major frameworks. Targets include a `svelte-adapter-deploy-stack`, an official `create-next-app` AWS template, a `vite-plugin-deploy-stack`, a NestJS schematic, and a Django Cookiecutter template.
163
+ - [ ] **Automated Troubleshooting:** Build `deploy-stack diagnose` (alias: `wtf`) to automatically analyze and troubleshoot common day-2 AWS operational issues (e.g., Fargate OOM kills, ALB 502s) directly from the terminal.
162
164
 
163
165
  πŸ‘‰ **[See the full project history and future plans in ROADMAP.md](./ROADMAP.md)**
164
166
 
package/docs/ROADMAP.md CHANGED
@@ -16,8 +16,15 @@
16
16
  - [x] **Local Execution Wrapper:** Native `deploy-stack apply` command with terminal-optimized streaming to eliminate Terraform context switching.
17
17
  - [x] **Ecosystem Integrations:** Official plugins published to the Astro Integrations directory (`astro-deploy-stack`) and Nuxt module registry (`nuxt-deploy-stack`).
18
18
 
19
- ### Phase 6: Migration & Trust Engine (Current)
19
+ ### Phase 6: Migration & Trust Engine (Completed)
20
20
  - [x] **Dry-Run Visualization:** Interactive pre-flight terminal UI with ASCII topology maps and precise, dynamic AWS cost estimation.
21
21
  - [x] **PaaS Importers:** Auto-parse `vercel.json` and Heroku `Procfile` configurations to map routing rules, web commands, and background workers automatically.
22
22
  - [x] **Docker Compose to ECS Translator:** Automatically converting a familiar local `docker-compose.yml` into production ECS task definitions.
23
- - [ ] **AI Agent Rulesets:** Publishing `.cursorrules` and Copilot instructions that teach AI assistants exactly how to utilize the CLI on the user's behalf.
23
+ - [x] **AI Agent Rulesets:** Publishing `.cursorrules` and Copilot instructions that teach AI assistants exactly how to utilize the CLI on the user's behalf.
24
+
25
+ ### Phase 7: Team Workflows & Ecosystem Integrations (Current)
26
+ *Focus: Enhance collaborative development and expand native support across major framework ecosystems.*
27
+ - [x] **Ephemeral PR Previews:** Generate GitHub Actions workflows that spin up temporary ECS Fargate tasks and post live URLs directly in PR comments to streamline team code reviews.
28
+ - [ ] **AI Context Synchronization:** Implement `deploy-stack sync-ai` to automatically generate `.cursorrules` and AI context files, ensuring IDE assistants understand the infrastructure.
29
+ - [ ] **Native Ecosystem Integrations:** Publish push-button plugins across major frameworks (e.g., `svelte-adapter-deploy-stack`, `create-next-app` AWS template, `vite-plugin-deploy-stack`, NestJS deployment schematic, Django Cookiecutter).
30
+ - [ ] **Automated Troubleshooting:** Build `deploy-stack diagnose` to auto-diagnose common day-2 AWS failures (e.g., Fargate OOM kills, ALB 502s) to establish immediate technical credibility and simplify maintenance.
@@ -0,0 +1,32 @@
1
+ # πŸš€ Ephemeral PR Previews
2
+
3
+ When enabled, `deploy-stack` automatically configures your GitHub Actions pipeline to spin up isolated, temporary AWS environments every time a developer opens a Pull Request.
4
+
5
+ A bot will comment on the PR with a live URL (e.g., `http://pr-123-your-app...`), allowing your team to test features, UI changes, and API updates before merging to `main`. When the PR is closed or merged, the environment is automatically destroyed.
6
+
7
+ ### πŸ—οΈ How it Works
8
+
9
+ Under the hood, `deploy-stack` utilizes **Terraform Workspaces**.
10
+
11
+ When a PR is opened, Terraform creates a new workspace (e.g., `pr-12`). It provisions a completely isolated Application Load Balancer and ECS Fargate Task using the exact same infrastructure definitions as your production environment, ensuring 100% parity.
12
+
13
+ To save time and simplify architecture, PR environments **share** your production AWS Secrets Manager vault and ECR Image Repository.
14
+
15
+ ### βš–οΈ The Rule of Thumb: Should I enable this?
16
+
17
+ **βœ… Enable PR Previews if:**
18
+ * You are working on a team of 2+ developers and require visual QA or UX sign-off before merging code.
19
+ * You are building a frontend application or full-stack monolith where seeing the live UI is critical.
20
+
21
+ **❌ Do NOT enable PR Previews if:**
22
+ * You are a solo developer (you can just test locally).
23
+ * You have a massive volume of PRs (e.g., automated Dependabot updates). Spinning up an AWS Load Balancer takes ~3 minutes, which will slow down rapid automated merges.
24
+
25
+ ### πŸ’° AWS Cost Implications
26
+
27
+ Because PR previews provision a real Application Load Balancer (ALB) and ECS Fargate compute tasks, **they are not free.**
28
+
29
+ * **Compute:** You are charged standard AWS Fargate rates per minute while the PR environment is running.
30
+ * **Load Balancing:** AWS charges ~$16/month per active Load Balancer. If a PR is open for 2 days, you pay the prorated ALB cost for those 48 hours (~$1.00).
31
+
32
+ To keep costs low, ensure your team closes or merges Pull Requests promptly so the `teardown.yml` workflow can destroy the resources and stop the billing clock!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deploy-stack",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "Provision production-ready AWS infrastructure and CI/CD pipelines in seconds.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -143,7 +143,8 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
143
143
  DISABLE_DEFAULT_CI: disableDefaultCI,
144
144
  PROCFILE: procfile,
145
145
  VERCEL_RULES: vercelRules,
146
- DOCKER_COMPOSE: dockerCompose
146
+ DOCKER_COMPOSE: dockerCompose,
147
+ ENABLE_PR_PREVIEWS: config.enablePrPreviews
147
148
  });
148
149
 
149
150
  // 8. Telemetry
@@ -169,6 +170,7 @@ export async function mainStack({ isHeadless = false, headlessOptions = {} } = {
169
170
  is_heroku_migration: !!procfile,
170
171
  is_vercel_migration: !!vercelRules,
171
172
  is_docker_compose: !!dockerCompose,
173
+ has_pr_previews: config.enablePrPreviews,
172
174
  });
173
175
 
174
176
  s.stop('Infrastructure provisioned successfully!');
@@ -26,6 +26,12 @@ export async function generateTemplates(targetDir, config) {
26
26
  { src: 'README.md', dest: 'README.md' }
27
27
  ];
28
28
 
29
+ // 2.1 Add Ephemeral PR workflows only if opted in
30
+ if (config.ENABLE_PR_PREVIEWS) {
31
+ filesToProcess.push({ src: 'github/preview.yml', dest: '.github/workflows/preview.yml' });
32
+ filesToProcess.push({ src: 'github/teardown.yml', dest: '.github/workflows/teardown.yml' });
33
+ }
34
+
29
35
  // 3. Configure Compute Commands & Environment Variables
30
36
  let secretsArray = [];
31
37
 
@@ -122,7 +128,7 @@ export async function generateTemplates(targetDir, config) {
122
128
 
123
129
  // 4.1. Inject Rails Master Key if applicable
124
130
  if (config.finalFramework === 'rails') {
125
- secretsArray.push(`{ "name": "RAILS_MASTER_KEY", "valueFrom": "\${aws_secretsmanager_secret.app_secrets.arn}:RAILS_MASTER_KEY::" }`);
131
+ secretsArray.push(`{ "name": "RAILS_MASTER_KEY", "valueFrom": "\${local.secret_arn}:RAILS_MASTER_KEY::" }`);
126
132
 
127
133
  initialSecretMap += `,\n RAILS_MASTER_KEY = var.rails_master_key`;
128
134
 
@@ -46,6 +46,7 @@ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, d
46
46
  desiredCount: headlessOptions.desiredCount || '1',
47
47
  branch: headlessOptions.branch || 'main',
48
48
  needsDatabase: false,
49
+ enablePrPreviews: headlessOptions.enablePrPreviews || false,
49
50
  setupType: 'headless'
50
51
  };
51
52
  }
@@ -99,6 +100,16 @@ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, d
99
100
  needsDatabase = dbChoice;
100
101
  }
101
102
 
103
+ let enablePrPreviews = false;
104
+ if (setupType === 'advanced') {
105
+ const prChoice = await confirm({
106
+ message: `Enable Ephemeral PR Previews? (Spins up isolated, temporary AWS environments for PRs)\n ${color.gray('πŸ“– Learn more: https://github.com/anton-codes-iac/deploy-stack/blob/main/docs/guides/ephemeral-pr-previews.md')}`,
107
+ initialValue: false,
108
+ });
109
+ if (typeof prChoice === 'symbol') process.exit(0);
110
+ enablePrPreviews = prChoice;
111
+ }
112
+
102
113
  const project = await group({
103
114
  region: () => select({
104
115
  message: 'Which AWS region do you want to deploy to?',
@@ -151,6 +162,7 @@ export async function getProjectConfig(isHeadless, headlessOptions, targetDir, d
151
162
  desiredCount: project.desiredCount || '1',
152
163
  branch: project.branch || currentGitBranch,
153
164
  needsDatabase,
165
+ enablePrPreviews,
154
166
  setupType
155
167
  };
156
168
  }
@@ -0,0 +1,69 @@
1
+ name: Ephemeral PR Preview
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened]
6
+
7
+ env:
8
+ AWS_REGION: {{REGION}}
9
+ ECR_REPOSITORY: {{PROJECT_NAME}}-repo
10
+ WORKSPACE: pr-${{ github.event.pull_request.number }}
11
+
12
+ permissions:
13
+ id-token: write
14
+ contents: read
15
+ pull-requests: write # Required to post the comment
16
+
17
+ jobs:
18
+ deploy-preview:
19
+ name: Provision Preview Environment
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - name: Checkout Code
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Configure AWS credentials
27
+ uses: aws-actions/configure-aws-credentials@v4
28
+ with:
29
+ role-to-assume: arn:aws:iam::{{AWS_ACCOUNT_ID}}:role/{{PROJECT_NAME}}-github-actions-role
30
+ aws-region: ${{ env.AWS_REGION }}
31
+
32
+ - name: Login to Amazon ECR
33
+ id: login-ecr
34
+ uses: aws-actions/amazon-ecr-login@v2
35
+
36
+ - name: Build and Push Docker image
37
+ env:
38
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
39
+ run: |
40
+ # We tag the image specifically for this PR workspace
41
+ docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.WORKSPACE }} .
42
+ docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.WORKSPACE }}
43
+
44
+ - name: Setup Terraform
45
+ uses: hashicorp/setup-terraform@v3
46
+
47
+ - name: Terraform Init & Apply
48
+ id: tf-apply
49
+ working-directory: ./terraform
50
+ run: |
51
+ terraform init
52
+ # Create or select the workspace specific to this PR
53
+ terraform workspace select -or-create ${{ env.WORKSPACE }}
54
+ terraform apply -auto-approve
55
+
56
+ # Extract the ALB URL to post to GitHub
57
+ ALB_URL=$(terraform output -raw alb_direct_url)
58
+ echo "PREVIEW_URL=$ALB_URL" >> $GITHUB_ENV
59
+
60
+ - name: Comment PR
61
+ uses: actions/github-script@v7
62
+ with:
63
+ script: |
64
+ github.rest.issues.createComment({
65
+ issue_number: context.issue.number,
66
+ owner: context.repo.owner,
67
+ repo: context.repo.repo,
68
+ body: `πŸš€ **Preview Environment Deployed!**\n\nYour ephemeral AWS Fargate environment is live at:\n[${process.env.PREVIEW_URL}](${process.env.PREVIEW_URL})\n\n_This environment will be automatically destroyed when the PR is merged or closed._\n\n---\n<div align="right"><sup>☁️ Generated by <a href="https://github.com/anton-codes-iac/deploy-stack">deploy-stack</a></sup></div>`
69
+ })
@@ -0,0 +1,43 @@
1
+ name: Destroy PR Preview
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+
7
+ env:
8
+ AWS_REGION: {{REGION}}
9
+ WORKSPACE: pr-${{ github.event.pull_request.number }}
10
+
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+
15
+ jobs:
16
+ destroy-preview:
17
+ name: Teardown Preview Environment
18
+ runs-on: ubuntu-latest
19
+
20
+ steps:
21
+ - name: Checkout Code
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Configure AWS credentials
25
+ uses: aws-actions/configure-aws-credentials@v4
26
+ with:
27
+ role-to-assume: arn:aws:iam::{{AWS_ACCOUNT_ID}}:role/{{PROJECT_NAME}}-github-actions-role
28
+ aws-region: ${{ env.AWS_REGION }}
29
+
30
+ - name: Setup Terraform
31
+ uses: hashicorp/setup-terraform@v3
32
+
33
+ - name: Terraform Init & Destroy
34
+ working-directory: ./terraform
35
+ run: |
36
+ terraform init
37
+ # Select the PR workspace
38
+ terraform workspace select ${{ env.WORKSPACE }}
39
+ # Destroy all resources in this workspace safely
40
+ terraform destroy -auto-approve
41
+ # Switch back to default and delete the PR workspace
42
+ terraform workspace select default
43
+ terraform workspace delete ${{ env.WORKSPACE }}
@@ -6,19 +6,19 @@ resource "aws_subnet" "db_isolated" {
6
6
  availability_zone = data.aws_availability_zones.available.names[count.index]
7
7
 
8
8
  tags = {
9
- Name = "{{PROJECT_NAME}}-db-isolated-${count.index}"
9
+ Name = "${local.app_name}-db-isolated-${count.index}"
10
10
  }
11
11
  }
12
12
 
13
13
  resource "aws_db_subnet_group" "main" {
14
- name = "{{PROJECT_NAME}}-db-subnet-group"
14
+ name = "${local.app_name}-db-subnet-group"
15
15
  subnet_ids = aws_subnet.db_isolated[*].id
16
16
  }
17
17
 
18
18
  # 2. Database Security Group
19
19
  resource "aws_security_group" "rds" {
20
- name = "{{PROJECT_NAME}}-rds-sg"
21
- vpc_id = aws_vpc.main.id
20
+ name = "${local.app_name}-rds-sg"
21
+ vpc_id = aws_vpc.main.id
22
22
 
23
23
  # ONLY allow inbound traffic from the ECS Fargate tasks
24
24
  ingress {
@@ -31,31 +31,31 @@ resource "aws_security_group" "rds" {
31
31
 
32
32
  # 3. The PostgreSQL Instance
33
33
  resource "aws_db_instance" "postgres" {
34
- identifier = "{{PROJECT_NAME}}-db"
35
- engine = "postgres"
36
- engine_version = "16"
37
- instance_class = "db.t4g.micro"
38
- allocated_storage = 20
39
- storage_encrypted = true
40
-
34
+ identifier = "${local.app_name}-db"
35
+ engine = "postgres"
36
+ engine_version = "16"
37
+ instance_class = "db.t4g.micro"
38
+ allocated_storage = 20
39
+ storage_encrypted = true
40
+
41
41
  # Clean up dashes for the database name (e.g. my-project -> my_project)
42
- db_name = replace("{{PROJECT_NAME}}", "-", "_")
43
- username = "dbadmin"
44
-
42
+ db_name = replace("${local.app_name}", "-", "_")
43
+ username = "dbadmin"
44
+
45
45
  # AWS automatically creates and manages the secret in Secrets Manager!
46
- manage_master_user_password = true
46
+ manage_master_user_password = true
47
47
 
48
- db_subnet_group_name = aws_db_subnet_group.main.name
49
- vpc_security_group_ids = [aws_security_group.rds.id]
50
-
51
- skip_final_snapshot = true
52
- publicly_accessible = false
48
+ db_subnet_group_name = aws_db_subnet_group.main.name
49
+ vpc_security_group_ids = [aws_security_group.rds.id]
50
+
51
+ skip_final_snapshot = true
52
+ publicly_accessible = false
53
53
  }
54
54
 
55
55
  # 4. IAM Permission for RDS Master Password Secret
56
56
  resource "aws_iam_role_policy" "rds_secret_access" {
57
- name = "{{PROJECT_NAME}}-rds-secret-policy"
58
- role = aws_iam_role.execution_role.id
57
+ name = "${local.app_name}-rds-secret-policy"
58
+ role = aws_iam_role.execution_role.id
59
59
  policy = jsonencode({
60
60
  Version = "2012-10-17"
61
61
  Statement = [
@@ -68,4 +68,4 @@ resource "aws_iam_role_policy" "rds_secret_access" {
68
68
  }
69
69
  ]
70
70
  })
71
- }
71
+ }
@@ -12,16 +12,23 @@ provider "aws" {
12
12
  locals {
13
13
  # Safely read the keys generated by the CLI, default to empty list if file doesn't exist
14
14
  secret_keys = fileexists("${path.module}/secret_keys.json") ? jsondecode(file("${path.module}/secret_keys.json")) : []
15
+
16
+ # Workspace suffix for PR preview environments (e.g., "-pr-123"). Empty for production.
17
+ env_suffix = terraform.workspace == "default" ? "" : "-${terraform.workspace}"
18
+
19
+ # Dynamic naming variable to prevent collisions
20
+ app_name = "{{PROJECT_NAME}}${local.env_suffix}"
15
21
  }
16
22
 
17
23
  # --- CloudWatch Logs ---
18
24
  resource "aws_cloudwatch_log_group" "app_logs" {
19
- name = "/ecs/{{PROJECT_NAME}}"
25
+ name = "/ecs/${local.app_name}"
20
26
  retention_in_days = 14
21
27
  }
22
28
 
23
- # --- ECR Repository ---
29
+ # --- ECR Repository (Shared across workspaces) ---
24
30
  resource "aws_ecr_repository" "app" {
31
+ count = terraform.workspace == "default" ? 1 : 0
25
32
  name = "{{PROJECT_NAME}}-repo"
26
33
  # trivy:ignore:AVD-AWS-0031 - Mutable tags allow the CI/CD pipeline to reuse the 'latest' tag for simplified deployments
27
34
  image_tag_mutability = "MUTABLE"
@@ -32,6 +39,16 @@ resource "aws_ecr_repository" "app" {
32
39
  }
33
40
  }
34
41
 
42
+ # In PR workspaces, fetch the existing production repository
43
+ data "aws_ecr_repository" "existing_app" {
44
+ count = terraform.workspace != "default" ? 1 : 0
45
+ name = "{{PROJECT_NAME}}-repo"
46
+ }
47
+
48
+ locals {
49
+ ecr_url = terraform.workspace == "default" ? aws_ecr_repository.app[0].repository_url : data.aws_ecr_repository.existing_app[0].repository_url
50
+ }
51
+
35
52
  # --- IAM: Execution Role ---
36
53
  # Allows the underlying AWS Fargate agent to pull images and push logs
37
54
  data "aws_iam_policy_document" "ecs_trust" {
@@ -45,7 +62,7 @@ data "aws_iam_policy_document" "ecs_trust" {
45
62
  }
46
63
 
47
64
  resource "aws_iam_role" "execution_role" {
48
- name = "{{PROJECT_NAME}}-execution-role"
65
+ name = "${local.app_name}-execution-role"
49
66
  assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
50
67
  }
51
68
 
@@ -57,18 +74,18 @@ resource "aws_iam_role_policy_attachment" "execution_role_policy" {
57
74
  # --- IAM: Task Role ---
58
75
  # Allows your application code running INSIDE the container to access AWS services
59
76
  resource "aws_iam_role" "task_role" {
60
- name = "{{PROJECT_NAME}}-task-role"
77
+ name = "${local.app_name}-task-role"
61
78
  assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
62
79
  }
63
80
 
64
81
  # --- ECS Cluster ---
65
82
  resource "aws_ecs_cluster" "main" {
66
- name = "{{PROJECT_NAME}}-cluster"
83
+ name = "${local.app_name}-cluster"
67
84
  }
68
85
 
69
86
  # --- ECS Task Definition ---
70
87
  resource "aws_ecs_task_definition" "app" {
71
- family = "{{PROJECT_NAME}}-task"
88
+ family = "${local.app_name}-task"
72
89
  network_mode = "awsvpc"
73
90
  requires_compatibilities = ["FARGATE"]
74
91
  cpu = "{{CPU}}"
@@ -78,8 +95,8 @@ resource "aws_ecs_task_definition" "app" {
78
95
 
79
96
  container_definitions = jsonencode([
80
97
  {
81
- name = "{{PROJECT_NAME}}-container"
82
- image = "${aws_ecr_repository.app.repository_url}:latest"
98
+ name = "${local.app_name}-container"
99
+ image = "${local.ecr_url}:${terraform.workspace == "default" ? "latest" : terraform.workspace}"
83
100
  essential = true
84
101
 
85
102
  environment = [
@@ -92,7 +109,7 @@ resource "aws_ecs_task_definition" "app" {
92
109
  [
93
110
  for key in local.secret_keys : {
94
111
  name = key
95
- valueFrom = "${aws_secretsmanager_secret.app_secrets.arn}:${key}::"
112
+ valueFrom = "${local.secret_arn}:${key}::"
96
113
  }
97
114
  ],
98
115
  [
@@ -125,7 +142,7 @@ resource "aws_ecs_task_definition" "app" {
125
142
  # --- Application Load Balancer ---
126
143
  # trivy:ignore:AVD-AWS-0053 - This ALB is intended to be publicly facing behind CloudFront
127
144
  resource "aws_lb" "main" {
128
- name = "{{SAFE_ALB_NAME}}-alb"
145
+ name = substr("${local.app_name}-alb", 0, 32)
129
146
  load_balancer_type = "application"
130
147
  security_groups = [aws_security_group.alb.id]
131
148
  subnets = aws_subnet.public[*].id
@@ -133,7 +150,7 @@ resource "aws_lb" "main" {
133
150
  }
134
151
 
135
152
  resource "aws_lb_target_group" "app" {
136
- name = "{{SAFE_ALB_NAME}}-tg"
153
+ name = substr("${local.app_name}-tg", 0, 32)
137
154
  port = {{PORT}}
138
155
  protocol = "HTTP"
139
156
  vpc_id = aws_vpc.main.id
@@ -163,7 +180,7 @@ resource "aws_lb_listener" "http" {
163
180
 
164
181
  # --- ECS Service ---
165
182
  resource "aws_ecs_service" "app" {
166
- name = "{{PROJECT_NAME}}-service"
183
+ name = "${local.app_name}-service"
167
184
  cluster = aws_ecs_cluster.main.id
168
185
  task_definition = aws_ecs_task_definition.app.arn
169
186
  launch_type = "FARGATE"
@@ -177,7 +194,7 @@ resource "aws_ecs_service" "app" {
177
194
 
178
195
  load_balancer {
179
196
  target_group_arn = aws_lb_target_group.app.arn
180
- container_name = "{{PROJECT_NAME}}-container"
197
+ container_name = "${local.app_name}-container"
181
198
  container_port = {{PORT}}
182
199
  }
183
200
 
@@ -194,7 +211,7 @@ resource "aws_iam_role_policy" "secrets_policy" {
194
211
  {
195
212
  Effect = "Allow"
196
213
  Action = ["secretsmanager:GetSecretValue"]
197
- Resource = [aws_secretsmanager_secret.app_secrets.arn]
214
+ Resource = [local.secret_arn]
198
215
  }
199
216
  ]
200
217
  })
@@ -208,12 +225,12 @@ output "alb_direct_url" {
208
225
 
209
226
  output "ecr_repository_url" {
210
227
  description = "The URL of the ECR repository"
211
- value = aws_ecr_repository.app.repository_url
228
+ value = local.ecr_url
212
229
  }
213
230
 
214
231
  # --- CloudWatch Alarms ---
215
232
  resource "aws_cloudwatch_metric_alarm" "alb_5xx_errors" {
216
- alarm_name = "{{PROJECT_NAME}}-high-5xx-errors"
233
+ alarm_name = "${local.app_name}-high-5xx-errors"
217
234
  comparison_operator = "GreaterThanThreshold"
218
235
  evaluation_periods = "2"
219
236
  metric_name = "HTTPCode_Target_5XX_Count"
@@ -5,7 +5,7 @@ resource "aws_vpc" "main" {
5
5
  enable_dns_support = true
6
6
 
7
7
  tags = {
8
- Name = "{{PROJECT_NAME}}-vpc"
8
+ Name = "${local.app_name}-vpc"
9
9
  }
10
10
  }
11
11
 
@@ -28,7 +28,7 @@ resource "aws_subnet" "public" {
28
28
  map_public_ip_on_launch = true
29
29
 
30
30
  tags = {
31
- Name = "{{PROJECT_NAME}}-public-subnet-${count.index + 1}"
31
+ Name = "${local.app_name}-public-subnet-${count.index + 1}"
32
32
  }
33
33
  }
34
34
 
@@ -50,7 +50,7 @@ resource "aws_route_table_association" "public" {
50
50
  # --- Security Groups ---
51
51
  # ALB SG: Allow public internet access on HTTP
52
52
  resource "aws_security_group" "alb" {
53
- name = "{{PROJECT_NAME}}-alb-sg"
53
+ name = "${local.app_name}-alb-sg"
54
54
  description = "Allow inbound HTTP to ALB"
55
55
  vpc_id = aws_vpc.main.id
56
56
 
@@ -72,7 +72,7 @@ resource "aws_security_group" "alb" {
72
72
 
73
73
  # ECS SG: Allow traffic ONLY from the ALB on the app's specific port
74
74
  resource "aws_security_group" "ecs_tasks" {
75
- name = "{{PROJECT_NAME}}-ecs-sg"
75
+ name = "${local.app_name}-ecs-sg"
76
76
  description = "Allow inbound access from the ALB only"
77
77
  vpc_id = aws_vpc.main.id
78
78
 
@@ -9,9 +9,9 @@ variable "create_oidc_provider" {
9
9
  default = true
10
10
  }
11
11
 
12
- # Register GitHub as an Identity Provider in AWS
12
+ # Register GitHub as an Identity Provider in AWS (Production Only)
13
13
  resource "aws_iam_openid_connect_provider" "github" {
14
- count = var.create_oidc_provider ? 1 : 0
14
+ count = (var.create_oidc_provider && terraform.workspace == "default") ? 1 : 0
15
15
  url = "https://token.actions.githubusercontent.com"
16
16
  client_id_list = ["sts.amazonaws.com"]
17
17
  thumbprint_list = [data.tls_certificate.github.certificates[0].sha1_fingerprint]
@@ -19,18 +19,19 @@ resource "aws_iam_openid_connect_provider" "github" {
19
19
 
20
20
  # Reference the Provider (whether newly created or already existing)
21
21
  data "aws_iam_openid_connect_provider" "github_existing" {
22
- count = var.create_oidc_provider ? 0 : 1
23
- url = "https://token.actions.githubusercontent.com"
22
+ count = (var.create_oidc_provider && terraform.workspace == "default") ? 0 : 1
23
+ url = "https://token.actions.githubusercontent.com"
24
24
  }
25
25
 
26
26
  # Dynamically pick the ARN of the provider based on the variable
27
27
  locals {
28
- github_provider_arn = var.create_oidc_provider ? aws_iam_openid_connect_provider.github[0].arn : data.aws_iam_openid_connect_provider.github_existing[0].arn
28
+ github_provider_arn = (var.create_oidc_provider && terraform.workspace == "default") ? aws_iam_openid_connect_provider.github[0].arn : data.aws_iam_openid_connect_provider.github_existing[0].arn
29
29
  }
30
30
 
31
31
  # Create the IAM Role that GitHub Actions will assume
32
32
  resource "aws_iam_role" "github_actions" {
33
- name = "{{PROJECT_NAME}}-github-actions-role"
33
+ count = terraform.workspace == "default" ? 1 : 0
34
+ name = "{{PROJECT_NAME}}-github-actions-role"
34
35
 
35
36
  assume_role_policy = jsonencode({
36
37
  Version = "2012-10-17"
@@ -44,10 +45,10 @@ resource "aws_iam_role" "github_actions" {
44
45
  Condition = {
45
46
  StringLike = {
46
47
  # ⚠️ SECURITY: Update "repo:*" to "repo:your-github-username/your-repo-name:*" in production
47
- "token.actions.githubusercontent.com:sub" : "repo:*"
48
+ "token.actions.githubusercontent.com:sub" : "repo:*"
48
49
  }
49
50
  StringEquals = {
50
- "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
51
+ "token.actions.githubusercontent.com:aud" : "sts.amazonaws.com"
51
52
  }
52
53
  }
53
54
  }
@@ -57,6 +58,7 @@ resource "aws_iam_role" "github_actions" {
57
58
 
58
59
  # Grant the CI/CD pipeline permissions to provision infrastructure
59
60
  resource "aws_iam_role_policy_attachment" "github_actions_admin" {
60
- role = aws_iam_role.github_actions.name
61
- policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
62
- }
61
+ count = terraform.workspace == "default" ? 1 : 0
62
+ role = aws_iam_role.github_actions[0].name
63
+ policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
64
+ }
@@ -1,10 +1,24 @@
1
- # --- AWS Secrets Manager ---
1
+ # --- Shared AWS Secrets Manager ---
2
+
3
+ # 1. Only create the Secret Vault in the default (production) workspace
2
4
  resource "aws_secretsmanager_secret" "app_secrets" {
5
+ count = terraform.workspace == "default" ? 1 : 0
3
6
  name = "{{PROJECT_NAME}}-secrets"
4
7
  description = "Environment variables for {{PROJECT_NAME}}"
5
8
  recovery_window_in_days = 0 # Allows instant deletion for dev/POC environments
6
9
  }
7
10
 
11
+ # 2. Fetch the existing Secret Vault when running in a PR workspace
12
+ data "aws_secretsmanager_secret" "existing_secrets" {
13
+ count = terraform.workspace != "default" ? 1 : 0
14
+ name = "{{PROJECT_NAME}}-secrets"
15
+ }
16
+
17
+ # 3. Export a single local variable that works in both environments
18
+ locals {
19
+ secret_arn = terraform.workspace == "default" ? aws_secretsmanager_secret.app_secrets[0].arn : data.aws_secretsmanager_secret.existing_secrets[0].arn
20
+ }
21
+
8
22
  # Fallback dummy key for CI/CD environments where the real key isn't present
9
23
  variable "rails_master_key" {
10
24
  type = string
@@ -13,7 +27,8 @@ variable "rails_master_key" {
13
27
 
14
28
  # Initial placeholder secret so the ECS task doesn't fail on first boot
15
29
  resource "aws_secretsmanager_secret_version" "app_secrets_initial" {
16
- secret_id = aws_secretsmanager_secret.app_secrets.id
30
+ count = terraform.workspace == "default" ? 1 : 0
31
+ secret_id = aws_secretsmanager_secret.app_secrets[0].id
17
32
  secret_string = jsonencode({{INITIAL_SECRET_MAP}})
18
33
 
19
34
  lifecycle {
@@ -1,6 +1,6 @@
1
1
  # --- Worker ECS Task Definition ---
2
2
  resource "aws_ecs_task_definition" "worker" {
3
- family = "{{PROJECT_NAME}}-worker-task"
3
+ family = "${local.app_name}-worker-task"
4
4
  network_mode = "awsvpc"
5
5
  requires_compatibilities = ["FARGATE"]
6
6
  cpu = "{{CPU}}"
@@ -10,8 +10,8 @@ resource "aws_ecs_task_definition" "worker" {
10
10
 
11
11
  container_definitions = jsonencode([
12
12
  {
13
- name = "{{PROJECT_NAME}}-worker-container"
14
- image = "${aws_ecr_repository.app.repository_url}:latest"
13
+ name = "${local.app_name}-worker-container"
14
+ image = "${aws_ecr_repository.app.repository_url}:${terraform.workspace == "default" ? "latest" : terraform.workspace}"
15
15
  essential = true
16
16
 
17
17
  environment = [
@@ -23,7 +23,7 @@ resource "aws_ecs_task_definition" "worker" {
23
23
  [
24
24
  for key in local.secret_keys : {
25
25
  name = key
26
- valueFrom = "${aws_secretsmanager_secret.app_secrets.arn}:${key}::"
26
+ valueFrom = "${local.secret_arn}:${key}::"
27
27
  }
28
28
  ],
29
29
  [
@@ -48,7 +48,7 @@ resource "aws_ecs_task_definition" "worker" {
48
48
  # --- Worker ECS Service ---
49
49
  # Notice there is NO load_balancer block. This service is strictly private.
50
50
  resource "aws_ecs_service" "worker" {
51
- name = "{{PROJECT_NAME}}-worker-service"
51
+ name = "${local.app_name}-worker-service"
52
52
  cluster = aws_ecs_cluster.main.id
53
53
  task_definition = aws_ecs_task_definition.worker.arn
54
54
  launch_type = "FARGATE"