deploy-stack 0.1.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.
@@ -0,0 +1,13 @@
1
+ import util from 'util';
2
+ import { exec } from 'child_process';
3
+
4
+ const execAsync = util.promisify(exec);
5
+
6
+ export async function checkDependency(command) {
7
+ try {
8
+ await execAsync(`${command} --version`);
9
+ return true;
10
+ } catch (error) {
11
+ return false;
12
+ }
13
+ }
@@ -0,0 +1,85 @@
1
+ # {{PROJECT_NAME}} - Cloud Infrastructure
2
+
3
+ This project was provisioned by `deploy-stack`. It contains a production-ready AWS ECS Fargate architecture and a GitHub Actions deployment pipeline.
4
+
5
+ ## 💰 Cost Estimate & Disclaimer
6
+
7
+ This infrastructure provisions a highly available Application Load Balancer (ALB) and an ECS Fargate container (Size: **{{COMPUTE_TIER}}**).
8
+
9
+ * **Estimated Monthly Cost:** {{ESTIMATED_COST}}
10
+ * *Note: AWS bills by the hour. If you destroy this stack after a few hours of testing, it will cost less than $0.20.*
11
+
12
+ > **⚠️ DISCLAIMER:** This cost is a rough estimate. AWS pricing changes and varies by region. **You are solely responsible for all AWS charges incurred by deploying this infrastructure.** The creators of `deploy-stack` are not liable for unexpected cloud costs, compromised credentials, or runaway billing. Always monitor your AWS Billing Dashboard and set up budget alerts.
13
+
14
+ ## 🚀 Deployment Guide
15
+
16
+ 1. **Initial Provisioning:**
17
+ ```bash
18
+ cd terraform
19
+ terraform init
20
+ terraform apply
21
+ ```
22
+
23
+ 2. **Push Secrets (Optional):**
24
+ If your application requires environment variables, create a local `.env` file and sync it directly to AWS Secrets Manager:
25
+ ```bash
26
+ npx deploy-stack secrets push .env
27
+ ```
28
+
29
+ 3. **Automated CI/CD (Keyless via OIDC):**
30
+ Push this repository to GitHub. Your deployment pipeline uses AWS IAM OpenID Connect (OIDC) to authenticate securely with temporary credentials—**no long-lived AWS secret keys are required in GitHub Secrets**. Every push to `main` will automatically build, package, and deploy your application.
31
+
32
+ ### ⚠️ Troubleshooting: OIDC Provider Already Exists
33
+ AWS only permits one GitHub Actions OIDC provider per AWS account. If `terraform apply` fails with an `EntityAlreadyExists` error regarding the OIDC provider, it indicates GitHub Actions was previously configured in this account.
34
+
35
+ **The Fix:**
36
+ Open `terraform/oidc.tf` and update the default value of `create_oidc_provider` to `false`:
37
+ ```hcl
38
+ variable "create_oidc_provider" {
39
+ type = bool
40
+ default = false # <--- Change this from true to false
41
+ }
42
+ ```
43
+ Re-run `terraform apply` to link directly to your existing provider.
44
+
45
+ ## 🛑 Safe Teardown (Destroying the Stack)
46
+
47
+ If you are done testing and want to stop all AWS billing, you must destroy the infrastructure.
48
+
49
+ Because our Terraform configuration is set to force-delete the ECR image repository (even if images are present), teardown is a single, clean command:
50
+
51
+ ```bash
52
+ cd terraform
53
+ terraform destroy
54
+ ```
55
+ *Type `yes` when prompted. This will permanently delete the Load Balancer, ECS cluster, log groups, and associated networking components.*
56
+
57
+ ## ⚠️ Critical Application Prerequisites
58
+
59
+ Before you push your code to GitHub, ensure your application is configured to run inside a Docker container and respond to AWS Load Balancer health checks.
60
+
61
+ ### 1. The Health Check Route (All Frameworks)
62
+
63
+ AWS constantly pings your container to ensure it is alive. If you configured a custom health check path (e.g., `/api/health`) during the CLI setup, **you must create that route in your application**. If AWS receives a `404 Not Found`, it will assume your app is broken and terminate the container.
64
+
65
+ Make sure your app returns a `200 OK` at your configured path:
66
+
67
+ * **Next.js (App Router):** Create `app/api/health/route.ts` returning a 200 response.
68
+ * **Express.js:** Add `app.get('/api/health', (req, res) => res.sendStatus(200));`
69
+ * **FastAPI/Python:** Add `@app.get("/api/health")` returning a 200 status.
70
+
71
+ ### 2. Enable Standalone Output (Next.js ONLY)
72
+
73
+ Next.js must be configured in "standalone" mode so it can bundle a minimal Node.js server. Without this, your GitHub Actions Docker build will crash.
74
+
75
+ Open `next.config.js` or `next.config.ts` in your root directory and add `output: 'standalone'`:
76
+
77
+ ```typescript
78
+ import type { NextConfig } from 'next';
79
+
80
+ const nextConfig: NextConfig = {
81
+ output: 'standalone', // <--- Add this exact line
82
+ };
83
+
84
+ export default nextConfig;
85
+ ```
@@ -0,0 +1,19 @@
1
+ # Terraform
2
+ terraform/.terraform/
3
+ terraform/*.tfstate
4
+ terraform/*.tfstate.backup
5
+ terraform/.terraform.lock.hcl
6
+ terraform/secret_keys.json
7
+ terraform/.terraform.*
8
+
9
+ # Node & Next.js
10
+ node_modules/
11
+ .next/
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+
16
+ # Python
17
+ __pycache__/
18
+ *.pyc
19
+ venv/
@@ -0,0 +1,32 @@
1
+ # Stage 1: Install dependencies and build the app
2
+ FROM node:20-alpine AS builder
3
+ WORKDIR /app
4
+
5
+ # Copy package files and install dependencies
6
+ COPY package.json package-lock.json* ./
7
+ RUN npm ci
8
+
9
+ # Copy the rest of the application code
10
+ COPY . .
11
+
12
+ # Build the Next.js application
13
+ # (This requires output: 'standalone' in next.config.ts)
14
+ RUN npm run build
15
+
16
+ # Stage 2: Production environment
17
+ FROM node:20-alpine AS runner
18
+ WORKDIR /app
19
+
20
+ ENV NODE_ENV=production
21
+ ENV PORT={{PORT}}
22
+ ENV HOSTNAME="0.0.0.0"
23
+
24
+ # Copy the standalone output from the builder stage
25
+ COPY --from=builder /app/.next/standalone ./
26
+ COPY --from=builder /app/.next/static ./.next/static
27
+ COPY --from=builder /app/public ./public
28
+
29
+ EXPOSE {{PORT}}
30
+
31
+ # Start the standalone Node.js server
32
+ CMD ["node", "server.js"]
@@ -0,0 +1,7 @@
1
+ FROM node:18-alpine
2
+ WORKDIR /app
3
+ COPY package*.json ./
4
+ RUN npm install
5
+ COPY . .
6
+ EXPOSE {{PORT}}
7
+ CMD ["npm", "start"]
@@ -0,0 +1,7 @@
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install -r requirements.txt
5
+ COPY . .
6
+ EXPOSE {{PORT}}
7
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "{{PORT}}"]
@@ -0,0 +1,13 @@
1
+ FROM nginx:alpine
2
+
3
+ # Remove default Nginx static assets
4
+ RUN rm -rf /usr/share/nginx/html/*
5
+
6
+ # Copy your static build output (e.g., from Gatsby, React, or pure HTML)
7
+ COPY . /usr/share/nginx/html
8
+
9
+ # Dynamically update the Nginx config to use the user's chosen port
10
+ RUN sed -i 's/listen *80;/listen {{PORT}};/g' /etc/nginx/conf.d/default.conf
11
+
12
+ EXPOSE {{PORT}}
13
+ CMD ["nginx", "-g", "daemon off;"]
@@ -0,0 +1,53 @@
1
+ name: Deploy to AWS ECS
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - {{DEPLOY_BRANCH}}
7
+
8
+ env:
9
+ AWS_REGION: {{REGION}}
10
+ ECR_REPOSITORY: {{PROJECT_NAME}}-repo
11
+ ECS_CLUSTER: {{PROJECT_NAME}}-cluster
12
+ ECS_SERVICE: {{PROJECT_NAME}}-service
13
+
14
+ permissions:
15
+ id-token: write
16
+ contents: read
17
+
18
+ jobs:
19
+ deploy:
20
+ name: Build & Deploy
21
+ runs-on: ubuntu-latest
22
+
23
+ steps:
24
+ - name: Checkout Code
25
+ uses: actions/checkout@v4
26
+
27
+ - name: Configure AWS Credentials
28
+ uses: aws-actions/configure-aws-credentials@v4
29
+ with:
30
+ role-to-assume: arn:aws:iam::{{AWS_ACCOUNT_ID}}:role/{{PROJECT_NAME}}-github-actions-role
31
+ aws-region: ${{ env.AWS_REGION }}
32
+
33
+ - name: Login to Amazon ECR
34
+ id: login-ecr
35
+ uses: aws-actions/amazon-ecr-login@v2
36
+
37
+ - name: Build, tag, and push image to Amazon ECR
38
+ id: build-image
39
+ env:
40
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
41
+ IMAGE_TAG: latest
42
+ run: |
43
+ docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
44
+ docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
45
+ echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
46
+
47
+ - name: Force ECS deployment
48
+ run: |
49
+ aws ecs update-service \
50
+ --cluster ${{ env.ECS_CLUSTER }} \
51
+ --service ${{ env.ECS_SERVICE }} \
52
+ --force-new-deployment \
53
+ --region ${{ env.AWS_REGION }}
@@ -0,0 +1,9 @@
1
+ terraform {
2
+ backend "s3" {
3
+ bucket = "{{STATE_BUCKET}}"
4
+ key = "state/terraform.tfstate"
5
+ region = "{{REGION}}"
6
+ encrypt = true
7
+ use_lockfile = true
8
+ }
9
+ }
@@ -0,0 +1,58 @@
1
+ # Fetch the AWS Managed Cache Policy (Optimized for standard web traffic)
2
+ data "aws_cloudfront_cache_policy" "optimized" {
3
+ name = "Managed-CachingOptimized"
4
+ }
5
+
6
+ # Fetch the AWS Managed Origin Request Policy (Passes query strings/cookies to your app)
7
+ data "aws_cloudfront_origin_request_policy" "all_viewer" {
8
+ name = "Managed-AllViewerExceptHostHeader"
9
+ }
10
+
11
+ resource "aws_cloudfront_distribution" "cdn" {
12
+ enabled = true
13
+ is_ipv6_enabled = true
14
+ wait_for_deployment = false # Prevents Terraform from hanging for 10+ minutes
15
+
16
+ origin {
17
+ # Point the CDN at the Load Balancer we provision in main.tf
18
+ domain_name = aws_lb.main.dns_name
19
+ origin_id = "ALBOrigin"
20
+
21
+ custom_origin_config {
22
+ http_port = 80
23
+ https_port = 443
24
+ # Because this is the free tier without a custom SSL domain, we route internally via HTTP
25
+ origin_protocol_policy = "http-only"
26
+ origin_ssl_protocols = ["TLSv1.2"]
27
+ }
28
+ }
29
+
30
+ default_cache_behavior {
31
+ # Allow all HTTP methods so API POST/PUT requests still work
32
+ allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
33
+ cached_methods = ["GET", "HEAD"]
34
+ target_origin_id = "ALBOrigin"
35
+
36
+ viewer_protocol_policy = "redirect-to-https"
37
+
38
+ # Attach the Managed Policies
39
+ cache_policy_id = data.aws_cloudfront_cache_policy.optimized.id
40
+ origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer.id
41
+ }
42
+
43
+ restrictions {
44
+ geo_restriction {
45
+ restriction_type = "none"
46
+ }
47
+ }
48
+
49
+ viewer_certificate {
50
+ # Gives the user a free HTTPS *.cloudfront.net domain out of the box
51
+ cloudfront_default_certificate = true
52
+ }
53
+ }
54
+
55
+ output "cloudfront_url" {
56
+ description = "Your globally cached, HTTPS-secured application URL"
57
+ value = "https://${aws_cloudfront_distribution.cdn.domain_name}"
58
+ }
@@ -0,0 +1,186 @@
1
+ # deploy-stack generated infrastructure
2
+ provider "aws" {
3
+ region = "{{REGION}}"
4
+ }
5
+
6
+ locals {
7
+ # Safely read the keys generated by the CLI, default to empty list if file doesn't exist
8
+ secret_keys = fileexists("${path.module}/secret_keys.json") ? jsondecode(file("${path.module}/secret_keys.json")) : []
9
+ }
10
+
11
+ # --- CloudWatch Logs ---
12
+ resource "aws_cloudwatch_log_group" "app_logs" {
13
+ name = "/ecs/{{PROJECT_NAME}}"
14
+ retention_in_days = 14
15
+ }
16
+
17
+ # --- ECR Repository ---
18
+ resource "aws_ecr_repository" "app" {
19
+ name = "{{PROJECT_NAME}}-repo"
20
+ image_tag_mutability = "MUTABLE"
21
+ force_delete = true
22
+ }
23
+
24
+ # --- IAM: Execution Role ---
25
+ # Allows the underlying AWS Fargate agent to pull images and push logs
26
+ data "aws_iam_policy_document" "ecs_trust" {
27
+ statement {
28
+ actions = ["sts:AssumeRole"]
29
+ principals {
30
+ type = "Service"
31
+ identifiers = ["ecs-tasks.amazonaws.com"]
32
+ }
33
+ }
34
+ }
35
+
36
+ resource "aws_iam_role" "execution_role" {
37
+ name = "{{PROJECT_NAME}}-execution-role"
38
+ assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
39
+ }
40
+
41
+ resource "aws_iam_role_policy_attachment" "execution_role_policy" {
42
+ role = aws_iam_role.execution_role.name
43
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
44
+ }
45
+
46
+ # --- IAM: Task Role ---
47
+ # Allows your application code running INSIDE the container to access AWS services
48
+ resource "aws_iam_role" "task_role" {
49
+ name = "{{PROJECT_NAME}}-task-role"
50
+ assume_role_policy = data.aws_iam_policy_document.ecs_trust.json
51
+ }
52
+
53
+ # --- ECS Cluster ---
54
+ resource "aws_ecs_cluster" "main" {
55
+ name = "{{PROJECT_NAME}}-cluster"
56
+ }
57
+
58
+ # --- ECS Task Definition ---
59
+ resource "aws_ecs_task_definition" "app" {
60
+ family = "{{PROJECT_NAME}}-task"
61
+ network_mode = "awsvpc"
62
+ requires_compatibilities = ["FARGATE"]
63
+ cpu = "{{CPU}}"
64
+ memory = "{{MEMORY}}"
65
+ execution_role_arn = aws_iam_role.execution_role.arn
66
+ task_role_arn = aws_iam_role.task_role.arn
67
+
68
+ container_definitions = jsonencode([
69
+ {
70
+ name = "{{PROJECT_NAME}}-container"
71
+ image = "${aws_ecr_repository.app.repository_url}:latest"
72
+ essential = true
73
+
74
+ # Dynamically map every secret key found in the local JSON file
75
+ secrets = [
76
+ for key in local.secret_keys : {
77
+ name = key
78
+ valueFrom = "${aws_secretsmanager_secret.app_secrets.arn}:${key}::"
79
+ }
80
+ ]
81
+
82
+ portMappings = [
83
+ {
84
+ containerPort = {{PORT}}
85
+ hostPort = {{PORT}}
86
+ protocol = "tcp"
87
+ }
88
+ ]
89
+
90
+ logConfiguration = {
91
+ logDriver = "awslogs"
92
+ options = {
93
+ "awslogs-group" = aws_cloudwatch_log_group.app_logs.name
94
+ "awslogs-region" = "{{REGION}}"
95
+ "awslogs-stream-prefix" = "ecs"
96
+ }
97
+ }
98
+ }
99
+ ])
100
+ }
101
+
102
+ # --- Application Load Balancer ---
103
+ resource "aws_lb" "main" {
104
+ name = "{{PROJECT_NAME}}-alb"
105
+ load_balancer_type = "application"
106
+ security_groups = [aws_security_group.alb.id]
107
+ subnets = aws_subnet.public[*].id
108
+ }
109
+
110
+ resource "aws_lb_target_group" "app" {
111
+ name = "{{PROJECT_NAME}}-tg"
112
+ port = {{PORT}}
113
+ protocol = "HTTP"
114
+ vpc_id = aws_vpc.main.id
115
+ target_type = "ip"
116
+
117
+ health_check {
118
+ path = "{{HEALTH_CHECK_PATH}}"
119
+ matcher = "200-399"
120
+ interval = 30
121
+ timeout = 5
122
+ healthy_threshold = 2
123
+ unhealthy_threshold = 3
124
+ }
125
+ }
126
+
127
+ resource "aws_lb_listener" "http" {
128
+ load_balancer_arn = aws_lb.main.arn
129
+ port = "80"
130
+ protocol = "HTTP"
131
+
132
+ default_action {
133
+ type = "forward"
134
+ target_group_arn = aws_lb_target_group.app.arn
135
+ }
136
+ }
137
+
138
+ # --- ECS Service ---
139
+ resource "aws_ecs_service" "app" {
140
+ name = "{{PROJECT_NAME}}-service"
141
+ cluster = aws_ecs_cluster.main.id
142
+ task_definition = aws_ecs_task_definition.app.arn
143
+ launch_type = "FARGATE"
144
+ desired_count = {{DESIRED_COUNT}}
145
+
146
+ network_configuration {
147
+ subnets = aws_subnet.public[*].id
148
+ security_groups = [aws_security_group.ecs_tasks.id]
149
+ assign_public_ip = true
150
+ }
151
+
152
+ load_balancer {
153
+ target_group_arn = aws_lb_target_group.app.arn
154
+ container_name = "{{PROJECT_NAME}}-container"
155
+ container_port = {{PORT}}
156
+ }
157
+
158
+ depends_on = [aws_lb_listener.http]
159
+ }
160
+
161
+ # Allow the ECS agent to read the specific secret from Secrets Manager
162
+ resource "aws_iam_role_policy" "secrets_policy" {
163
+ name = "{{PROJECT_NAME}}-secrets-policy"
164
+ role = aws_iam_role.execution_role.id
165
+ policy = jsonencode({
166
+ Version = "2012-10-17"
167
+ Statement = [
168
+ {
169
+ Effect = "Allow"
170
+ Action = ["secretsmanager:GetSecretValue"]
171
+ Resource = [aws_secretsmanager_secret.app_secrets.arn]
172
+ }
173
+ ]
174
+ })
175
+ }
176
+
177
+ # --- Outputs ---
178
+ output "website_url" {
179
+ description = "The public URL of your load balancer"
180
+ value = "http://${aws_lb.main.dns_name}"
181
+ }
182
+
183
+ output "ecr_repository_url" {
184
+ description = "The URL of the ECR repository"
185
+ value = aws_ecr_repository.app.repository_url
186
+ }
@@ -0,0 +1,90 @@
1
+ # --- VPC & Networking ---
2
+ resource "aws_vpc" "main" {
3
+ cidr_block = "10.0.0.0/16"
4
+ enable_dns_hostnames = true
5
+ enable_dns_support = true
6
+
7
+ tags = {
8
+ Name = "{{PROJECT_NAME}}-vpc"
9
+ }
10
+ }
11
+
12
+ # Fetch available zones in the current region
13
+ data "aws_availability_zones" "available" {
14
+ state = "available"
15
+ }
16
+
17
+ resource "aws_internet_gateway" "main" {
18
+ vpc_id = aws_vpc.main.id
19
+ }
20
+
21
+ # Create 2 Public Subnets
22
+ resource "aws_subnet" "public" {
23
+ count = 2
24
+ vpc_id = aws_vpc.main.id
25
+ cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
26
+ availability_zone = data.aws_availability_zones.available.names[count.index]
27
+ map_public_ip_on_launch = true
28
+
29
+ tags = {
30
+ Name = "{{PROJECT_NAME}}-public-subnet-${count.index + 1}"
31
+ }
32
+ }
33
+
34
+ # Route internet traffic through the Gateway
35
+ resource "aws_route_table" "public" {
36
+ vpc_id = aws_vpc.main.id
37
+ route {
38
+ cidr_block = "0.0.0.0/0"
39
+ gateway_id = aws_internet_gateway.main.id
40
+ }
41
+ }
42
+
43
+ resource "aws_route_table_association" "public" {
44
+ count = 2
45
+ subnet_id = aws_subnet.public[count.index].id
46
+ route_table_id = aws_route_table.public.id
47
+ }
48
+
49
+ # --- Security Groups ---
50
+ # ALB SG: Allow public internet access on HTTP
51
+ resource "aws_security_group" "alb" {
52
+ name = "{{PROJECT_NAME}}-alb-sg"
53
+ description = "Allow inbound HTTP to ALB"
54
+ vpc_id = aws_vpc.main.id
55
+
56
+ ingress {
57
+ from_port = 80
58
+ to_port = 80
59
+ protocol = "tcp"
60
+ cidr_blocks = ["0.0.0.0/0"]
61
+ }
62
+
63
+ egress {
64
+ from_port = 0
65
+ to_port = 0
66
+ protocol = "-1"
67
+ cidr_blocks = ["0.0.0.0/0"]
68
+ }
69
+ }
70
+
71
+ # ECS SG: Allow traffic ONLY from the ALB on the app's specific port
72
+ resource "aws_security_group" "ecs_tasks" {
73
+ name = "{{PROJECT_NAME}}-ecs-sg"
74
+ description = "Allow inbound access from the ALB only"
75
+ vpc_id = aws_vpc.main.id
76
+
77
+ ingress {
78
+ from_port = {{PORT}}
79
+ to_port = {{PORT}}
80
+ protocol = "tcp"
81
+ security_groups = [aws_security_group.alb.id]
82
+ }
83
+
84
+ egress {
85
+ from_port = 0
86
+ to_port = 0
87
+ protocol = "-1"
88
+ cidr_blocks = ["0.0.0.0/0"]
89
+ }
90
+ }
@@ -0,0 +1,62 @@
1
+ # Fetch GitHub's OIDC certificate thumbprint
2
+ data "tls_certificate" "github" {
3
+ url = "https://token.actions.githubusercontent.com"
4
+ }
5
+
6
+ variable "create_oidc_provider" {
7
+ description = "Set to false if you already have OIDC provider in your AWS account"
8
+ type = bool
9
+ default = true
10
+ }
11
+
12
+ # Register GitHub as an Identity Provider in AWS
13
+ resource "aws_iam_openid_connect_provider" "github" {
14
+ count = var.create_oidc_provider ? 1 : 0
15
+ url = "https://token.actions.githubusercontent.com"
16
+ client_id_list = ["sts.amazonaws.com"]
17
+ thumbprint_list = [data.tls_certificate.github.certificates[0].sha1_fingerprint]
18
+ }
19
+
20
+ # Reference the Provider (whether newly created or already existing)
21
+ data "aws_iam_openid_connect_provider" "github_existing" {
22
+ count = var.create_oidc_provider ? 0 : 1
23
+ url = "https://token.actions.githubusercontent.com"
24
+ }
25
+
26
+ # Dynamically pick the ARN of the provider based on the variable
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
29
+ }
30
+
31
+ # Create the IAM Role that GitHub Actions will assume
32
+ resource "aws_iam_role" "github_actions" {
33
+ name = "{{PROJECT_NAME}}-github-actions-role"
34
+
35
+ assume_role_policy = jsonencode({
36
+ Version = "2012-10-17"
37
+ Statement = [
38
+ {
39
+ Action = "sts:AssumeRoleWithWebIdentity"
40
+ Effect = "Allow"
41
+ Principal = {
42
+ Federated = local.github_provider_arn
43
+ }
44
+ Condition = {
45
+ StringLike = {
46
+ # ⚠️ SECURITY: Update "repo:*" to "repo:your-github-username/your-repo-name:*" in production
47
+ "token.actions.githubusercontent.com:sub" : "repo:*"
48
+ }
49
+ StringEquals = {
50
+ "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
51
+ }
52
+ }
53
+ }
54
+ ]
55
+ })
56
+ }
57
+
58
+ # Grant the CI/CD pipeline permissions to provision infrastructure
59
+ 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
+ }
@@ -0,0 +1,18 @@
1
+ # --- AWS Secrets Manager ---
2
+ resource "aws_secretsmanager_secret" "app_secrets" {
3
+ name = "{{PROJECT_NAME}}-secrets"
4
+ description = "Environment variables for {{PROJECT_NAME}}"
5
+ recovery_window_in_days = 0 # Allows instant deletion for dev/POC environments
6
+ }
7
+
8
+ # Initial placeholder secret so the ECS task doesn't fail on first boot
9
+ resource "aws_secretsmanager_secret_version" "app_secrets_initial" {
10
+ secret_id = aws_secretsmanager_secret.app_secrets.id
11
+ secret_string = jsonencode({
12
+ EXAMPLE_API_KEY = "replace_me_in_aws_console_or_cli"
13
+ })
14
+
15
+ lifecycle {
16
+ ignore_changes = [secret_string]
17
+ }
18
+ }