deploy-stack 0.3.3 → 0.5.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 +19 -5
- package/bin/cli.js +4 -1
- package/package.json +1 -1
- package/src/commands/destroy.js +77 -0
- package/src/commands/init.js +21 -5
- package/src/utils/aws.js +43 -1
- package/src/utils/detector.js +11 -1
- package/src/utils/generator.js +2 -0
- package/src/utils/warnings.js +3 -4
- package/templates/README.md +7 -9
- package/templates/docker/static.Dockerfile +2 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-

|
|
7
|
+
<!--  -->
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -25,13 +25,16 @@ You retain complete ownership of your infrastructure code without relying on bla
|
|
|
25
25
|
## ✨ Features
|
|
26
26
|
|
|
27
27
|
* **Zero Vendor Lock-In:** Generates standard, clean `.tf` files. Modify, expand, or decouple them at any time.
|
|
28
|
-
* **Framework Agnostic:** Tailored container presets for Next.js
|
|
28
|
+
* **Framework Agnostic:** Tailored container presets for Next.js, Express.js, FastAPI, and **Zero-Config Static Sites** (React, Vue, SvelteKit, Astro, Vite).
|
|
29
|
+
* **Smart Git Integration:** Automatically detects your working branch (`main`, `master`, `develop`) and binds it directly to the generated GitHub Actions pipeline.
|
|
29
30
|
* **Production-Grade Defaults:** Automatically provisions an Amazon ECS Fargate cluster fronted by an Application Load Balancer across multiple availability zones.
|
|
30
31
|
* **Global Edge Acceleration:** Includes an integrated AWS CloudFront CDN distribution with SSL termination and optimized caching.
|
|
31
32
|
* **Keyless, Zero-Secret CI/CD:** Uses AWS IAM OpenID Connect (OIDC) for automated GitHub Actions deployments—no long-lived AWS keys stored in GitHub Secrets.
|
|
32
33
|
* **Remote State with Native S3 Locking:** Automatically creates an encrypted S3 state bucket utilizing modern native S3 concurrency locking.
|
|
33
34
|
* **Built-in Secrets Sync:** Provides a dedicated CLI workflow to securely push local `.env` variables into AWS Secrets Manager and map them directly into containers at runtime.
|
|
34
35
|
* **Non-Destructive:** Safely analyzes existing directories and prompts for confirmation before updating any files.
|
|
36
|
+
* **Zero-Config Detection:** Automatically resolves output directories for Vite, Astro, SvelteKit, CRA, and more.
|
|
37
|
+
* **Safe Teardown:** Completely remove all generated AWS resources and empty S3 state buckets with a single `destroy` command.
|
|
35
38
|
|
|
36
39
|
---
|
|
37
40
|
|
|
@@ -46,7 +49,7 @@ npx deploy-stack
|
|
|
46
49
|
The interactive CLI will guide you through the setup:
|
|
47
50
|
1. **Target Directory / Name:** (Type `.` to bootstrap your current directory)
|
|
48
51
|
2. **Setup Mode:** (Choose **Quickstart** for sensible defaults, or **Advanced** for custom scaling and branch names)
|
|
49
|
-
3. **Zero-Config Detection:** The CLI automatically scans your
|
|
52
|
+
3. **Zero-Config Detection:** The CLI automatically scans your project to detect your framework. For static sites, it intelligently discovers your build output directory (`dist`, `build`, `.output`, etc.) and dynamically configures the hardened Nginx container.
|
|
50
53
|
4. **AWS Region & Compute Tier:** (Select your target region and Fargate size with live cost estimates)
|
|
51
54
|
|
|
52
55
|
---
|
|
@@ -80,6 +83,12 @@ npx deploy-stack secrets push .env.production
|
|
|
80
83
|
|
|
81
84
|
---
|
|
82
85
|
|
|
86
|
+
## 🗑️ Infrastructure Teardown
|
|
87
|
+
To safely completely remove your ECS cluster, load balancers, and empty the remote S3 state bucket, run:
|
|
88
|
+
`npx deploy-stack destroy`
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
83
92
|
## 📁 Generated File Structure
|
|
84
93
|
|
|
85
94
|
Running the CLI generates a modular architecture tailored to your service:
|
|
@@ -104,8 +113,13 @@ your-project/
|
|
|
104
113
|
|
|
105
114
|
## 📦 Reference Implementations
|
|
106
115
|
|
|
107
|
-
* **[Next.js Fullstack
|
|
108
|
-
|
|
116
|
+
* **[Next.js Fullstack App](https://github.com/anton-codes-iac/deploy-stack-nextjs-example):** A complete Next.js deployment showcasing the generated Terraform, CloudFront setup, and automated OIDC workflow.
|
|
117
|
+
* **[Express.js API](https://github.com/anton-codes-iac/deploy-stack-express-example):** A standard Node.js backend setup.
|
|
118
|
+
* **[Python FastAPI](https://github.com/anton-codes-iac/deploy-stack-fastapi-example):** A Python API demonstrating unprivileged port mapping.
|
|
119
|
+
* **[Vite / React SPA](https://github.com/anton-codes-iac/deploy-stack-vite-example):** Demonstrates SPA routing and `dist/` auto-detection.
|
|
120
|
+
* **[Create React App](https://github.com/anton-codes-iac/deploy-stack-cra-example):** Validates backward compatibility with legacy Webpack pipelines and `build/` auto-detection.
|
|
121
|
+
* **[Astro Static Site](https://github.com/anton-codes-iac/deploy-stack-astro-example):** Demonstrates modern static site generation (SSG).
|
|
122
|
+
* **[SvelteKit Application](https://github.com/anton-codes-iac/deploy-stack-svelte-example):** Demonstrates static adapter integration and custom output folder detection.
|
|
109
123
|
---
|
|
110
124
|
|
|
111
125
|
## 🛡️ Telemetry & Privacy
|
package/bin/cli.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { mainStack } from '../src/commands/init.js';
|
|
4
|
+
import { destroyStack } from '../src/commands/destroy.js';
|
|
3
5
|
import { runDoctor } from '../src/commands/doctor.js';
|
|
4
6
|
import { pushSecrets } from '../src/commands/secrets.js';
|
|
5
|
-
import { mainStack } from '../src/commands/init.js';
|
|
6
7
|
|
|
7
8
|
// 1. Extract the telemetry flag and set the environment variable
|
|
8
9
|
const rawArgs = process.argv.slice(2);
|
|
@@ -22,6 +23,8 @@ if (args[0] === 'secrets' && args[1] === 'push') {
|
|
|
22
23
|
pushSecrets(envFile, projectName).catch(console.error);
|
|
23
24
|
} else if (args[0] === 'doctor') {
|
|
24
25
|
runDoctor().catch(console.error);
|
|
26
|
+
} else if (args[0] === 'destroy') {
|
|
27
|
+
destroyStack().catch(console.error);
|
|
25
28
|
} else {
|
|
26
29
|
mainStack().catch(console.error);
|
|
27
30
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { intro, outro, confirm, spinner, cancel } from '@clack/prompts';
|
|
4
|
+
import color from 'picocolors';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { teardownStateBucket } from '../utils/aws.js';
|
|
7
|
+
import { checkDependency } from '../utils/system.js';
|
|
8
|
+
import { trackEvent, flushTelemetry } from '../core/telemetry.js';
|
|
9
|
+
|
|
10
|
+
export async function destroyStack() {
|
|
11
|
+
intro(color.bgRed(color.white(' deploy-stack destroy 🗑️ ')));
|
|
12
|
+
|
|
13
|
+
const tfDirPath = path.join(process.cwd(), 'terraform');
|
|
14
|
+
const backendFilePath = path.join(tfDirPath, 'backend.tf');
|
|
15
|
+
|
|
16
|
+
if (!fsSync.existsSync(backendFilePath)) {
|
|
17
|
+
console.error(color.red('✖ No terraform/backend.tf found in the current directory.'));
|
|
18
|
+
console.log(color.yellow('Are you in the root of a deploy-stack project?'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hasTerraform = await checkDependency('terraform');
|
|
23
|
+
if (!hasTerraform) {
|
|
24
|
+
console.error(color.red('✖ Terraform is not installed.'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const proceed = await confirm({
|
|
29
|
+
message: color.red('⚠️ WARNING: This will permanently destroy all AWS resources associated with this project. Are you absolutely sure?'),
|
|
30
|
+
initialValue: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!proceed) {
|
|
34
|
+
cancel('Destruction cancelled. Your infrastructure is safe.');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const s = spinner();
|
|
39
|
+
|
|
40
|
+
// 1. Extract Bucket and Region from backend.tf
|
|
41
|
+
const backendContent = fsSync.readFileSync(backendFilePath, 'utf-8');
|
|
42
|
+
const bucketMatch = backendContent.match(/bucket\s*=\s*"([^"]+)"/);
|
|
43
|
+
const regionMatch = backendContent.match(/region\s*=\s*"([^"]+)"/);
|
|
44
|
+
|
|
45
|
+
const bucketName = bucketMatch ? bucketMatch[1] : null;
|
|
46
|
+
const region = regionMatch ? regionMatch[1] : 'us-east-1';
|
|
47
|
+
|
|
48
|
+
// 2. Execute Terraform Destroy
|
|
49
|
+
console.log(color.cyan('\nInitiating Terraform destroy (this may take a few minutes)...\n'));
|
|
50
|
+
try {
|
|
51
|
+
execSync('terraform destroy -auto-approve', { cwd: tfDirPath, stdio: 'inherit' });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(color.red('\n✖ Terraform destroy failed. Please check the output above.'));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 3. Clean up the S3 State Bucket
|
|
58
|
+
if (bucketName) {
|
|
59
|
+
s.start(`Emptying and deleting S3 state bucket: ${bucketName}...`);
|
|
60
|
+
try {
|
|
61
|
+
await teardownStateBucket(region, bucketName);
|
|
62
|
+
s.stop(`S3 bucket ${bucketName} successfully deleted.`);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
s.stop(`❌ Failed to delete S3 bucket. You may need to delete it manually in the AWS Console.`);
|
|
65
|
+
console.error(color.red(`AWS Error: ${error.message}`));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
trackEvent('project_destroyed', {
|
|
70
|
+
region,
|
|
71
|
+
bucket: bucketName,
|
|
72
|
+
success: true
|
|
73
|
+
});
|
|
74
|
+
await flushTelemetry();
|
|
75
|
+
|
|
76
|
+
outro(color.green('✅ Infrastructure successfully destroyed. Your AWS bill is safe.'));
|
|
77
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { intro, outro, group, text, select, spinner, cancel, confirm, log } from '@clack/prompts';
|
|
5
5
|
import color from 'picocolors';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
6
7
|
|
|
7
8
|
import { checkDependency } from '../utils/system.js';
|
|
8
9
|
import { detectFramework } from '../utils/detector.js';
|
|
@@ -88,6 +89,14 @@ export async function mainStack() {
|
|
|
88
89
|
if (finalFramework === 'static') defaultPort = '8080';
|
|
89
90
|
if (finalFramework === 'python') defaultPort = '8000';
|
|
90
91
|
|
|
92
|
+
// 2.8 Check current Git branch
|
|
93
|
+
let currentGitBranch = 'main';
|
|
94
|
+
try {
|
|
95
|
+
currentGitBranch = execSync('git symbolic-ref --short HEAD', { cwd: targetDir, stdio: 'pipe' }).toString().trim();
|
|
96
|
+
} catch (e) {
|
|
97
|
+
// Not a git repo yet, fallback to 'main'
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
// 3. Prompt Configuration Group
|
|
92
101
|
const project = await group(
|
|
93
102
|
{
|
|
@@ -140,8 +149,8 @@ export async function mainStack() {
|
|
|
140
149
|
if (setupType === 'quick') return undefined;
|
|
141
150
|
return text({
|
|
142
151
|
message: 'Primary Git deployment branch for CI/CD:',
|
|
143
|
-
placeholder:
|
|
144
|
-
defaultValue:
|
|
152
|
+
placeholder: currentGitBranch,
|
|
153
|
+
defaultValue: currentGitBranch,
|
|
145
154
|
});
|
|
146
155
|
},
|
|
147
156
|
},
|
|
@@ -162,7 +171,9 @@ export async function mainStack() {
|
|
|
162
171
|
|
|
163
172
|
const healthCheckPath = project.healthCheckPath || '/';
|
|
164
173
|
const desiredCount = project.desiredCount || '1';
|
|
165
|
-
const deployBranch = project.branch ||
|
|
174
|
+
const deployBranch = project.branch || currentGitBranch;
|
|
175
|
+
|
|
176
|
+
const buildDir = detectedFramework?.buildDir || 'dist';
|
|
166
177
|
|
|
167
178
|
// 5. Check for existing files that might be overwritten
|
|
168
179
|
const dockerfilePath = path.join(targetDir, 'Dockerfile');
|
|
@@ -213,6 +224,7 @@ export async function mainStack() {
|
|
|
213
224
|
HEALTH_CHECK_PATH: healthCheckPath,
|
|
214
225
|
DESIRED_COUNT: desiredCount,
|
|
215
226
|
DEPLOY_BRANCH: deployBranch,
|
|
227
|
+
BUILD_DIR: buildDir,
|
|
216
228
|
finalFramework: finalFramework
|
|
217
229
|
});
|
|
218
230
|
|
|
@@ -220,6 +232,7 @@ export async function mainStack() {
|
|
|
220
232
|
trackEvent('project_provisioned', {
|
|
221
233
|
projectName: actualProjectName,
|
|
222
234
|
framework: finalFramework,
|
|
235
|
+
specific_framework: detectedFramework?.name || finalFramework,
|
|
223
236
|
region: project.region,
|
|
224
237
|
size: project.size,
|
|
225
238
|
setup_mode: setupType,
|
|
@@ -230,7 +243,10 @@ export async function mainStack() {
|
|
|
230
243
|
s.stop('Infrastructure provisioned successfully!');
|
|
231
244
|
|
|
232
245
|
// 9. Provide the Outro, Framework Warnings and Next Steps
|
|
233
|
-
|
|
246
|
+
let frameworkWarnings = '';
|
|
247
|
+
if (!(finalFramework === 'static' && detectedFramework?.buildDir)) {
|
|
248
|
+
frameworkWarnings = getFrameworkWarning(finalFramework);
|
|
249
|
+
}
|
|
234
250
|
|
|
235
251
|
const isGitInitialized = fsSync.existsSync(path.join(targetDir, '.git'));
|
|
236
252
|
|
|
@@ -240,7 +256,7 @@ export async function mainStack() {
|
|
|
240
256
|
|
|
241
257
|
const gitInstructions = isGitInitialized
|
|
242
258
|
? `git add .\n git commit -m "chore: add AWS infrastructure and CI/CD"\n git push`
|
|
243
|
-
: `git init\n git add .\n git commit -m "chore: add AWS infrastructure and CI/CD"\n git branch -M
|
|
259
|
+
: `git init\n git add .\n git commit -m "chore: add AWS infrastructure and CI/CD"\n git branch -M ${deployBranch}\n git remote add origin https://github.com/your-username/your-repo.git\n git push -u origin ${deployBranch}`;
|
|
244
260
|
|
|
245
261
|
outro(`
|
|
246
262
|
${color.green('✅ Project provisioned successfully!')}
|
package/src/utils/aws.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
|
2
|
-
import { S3Client, CreateBucketCommand, PutBucketVersioningCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { S3Client, CreateBucketCommand, PutBucketVersioningCommand, PutBucketTaggingCommand } from '@aws-sdk/client-s3';
|
|
3
|
+
import { DeleteBucketCommand, ListObjectVersionsCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
3
4
|
|
|
4
5
|
export async function provisionStateBucket(region, projectName) {
|
|
5
6
|
const stsClient = new STSClient({ region });
|
|
@@ -23,6 +24,15 @@ export async function provisionStateBucket(region, projectName) {
|
|
|
23
24
|
CreateBucketConfiguration: region === 'us-east-1' ? undefined : { LocationConstraint: region }
|
|
24
25
|
}));
|
|
25
26
|
|
|
27
|
+
await s3Client.send(new PutBucketTaggingCommand({
|
|
28
|
+
Bucket: stateBucketName,
|
|
29
|
+
Tagging: {
|
|
30
|
+
TagSet: [
|
|
31
|
+
{ Key: "ManagedBy", Value: "deploy-stack" }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
|
|
26
36
|
await s3Client.send(new PutBucketVersioningCommand({
|
|
27
37
|
Bucket: stateBucketName,
|
|
28
38
|
VersioningConfiguration: { Status: 'Enabled' }
|
|
@@ -35,4 +45,36 @@ export async function provisionStateBucket(region, projectName) {
|
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
return { awsAccountId, stateBucketName };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function teardownStateBucket(region, bucketName) {
|
|
51
|
+
const client = new S3Client({ region });
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// 1. Fetch all object versions and delete markers
|
|
55
|
+
const listCommand = new ListObjectVersionsCommand({ Bucket: bucketName });
|
|
56
|
+
const { Versions, DeleteMarkers } = await client.send(listCommand);
|
|
57
|
+
|
|
58
|
+
const objectsToDelete = [];
|
|
59
|
+
if (Versions) objectsToDelete.push(...Versions.map(v => ({ Key: v.Key, VersionId: v.VersionId })));
|
|
60
|
+
if (DeleteMarkers) objectsToDelete.push(...DeleteMarkers.map(v => ({ Key: v.Key, VersionId: v.VersionId })));
|
|
61
|
+
|
|
62
|
+
// 2. Delete all contents if any exist
|
|
63
|
+
if (objectsToDelete.length > 0) {
|
|
64
|
+
const deleteCommand = new DeleteObjectsCommand({
|
|
65
|
+
Bucket: bucketName,
|
|
66
|
+
Delete: { Objects: objectsToDelete }
|
|
67
|
+
});
|
|
68
|
+
await client.send(deleteCommand);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Delete the now-empty bucket
|
|
72
|
+
const deleteBucketCommand = new DeleteBucketCommand({ Bucket: bucketName });
|
|
73
|
+
await client.send(deleteBucketCommand);
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.name === 'NoSuchBucket') return true; // Already deleted
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
38
80
|
}
|
package/src/utils/detector.js
CHANGED
|
@@ -12,9 +12,19 @@ export function detectFramework(targetDir) {
|
|
|
12
12
|
// Merge dependencies and devDependencies to check both
|
|
13
13
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
14
14
|
|
|
15
|
+
// Next.js & Express
|
|
15
16
|
if (deps['next']) return { id: 'nextjs', name: 'Next.js' };
|
|
16
|
-
if (deps['vite'] || deps['astro'] || deps['@sveltejs/kit'] || deps['gatsby'] || deps['react-scripts'] || deps['@vue/cli-service']) return { id: 'static', name: 'Static Site (Vite, Astro, React)' };
|
|
17
17
|
if (deps['express']) return { id: 'node', name: 'Node.js / Express' };
|
|
18
|
+
|
|
19
|
+
// Static Site Generators & SPAs (with precise build directories)
|
|
20
|
+
if (deps['@sveltejs/kit']) return { id: 'static', name: 'SvelteKit', buildDir: 'build' };
|
|
21
|
+
if (deps['react-scripts']) return { id: 'static', name: 'Create React App', buildDir: 'build' };
|
|
22
|
+
if (deps['gatsby']) return { id: 'static', name: 'Gatsby', buildDir: 'public' };
|
|
23
|
+
if (deps['nuxt']) return { id: 'static', name: 'Nuxt', buildDir: '.output/public' };
|
|
24
|
+
if (deps['astro']) return { id: 'static', name: 'Astro', buildDir: 'dist' };
|
|
25
|
+
if (deps['vite']) return { id: 'static', name: 'Vite', buildDir: 'dist' };
|
|
26
|
+
if (deps['@vue/cli-service']) return { id: 'static', name: 'Vue.js', buildDir: 'dist' };
|
|
27
|
+
if (deps['@angular/cli']) return { id: 'static', name: 'Angular', buildDir: 'dist' };
|
|
18
28
|
} catch (e) {
|
|
19
29
|
// Silently fail if package.json is malformed
|
|
20
30
|
}
|
package/src/utils/generator.js
CHANGED
package/src/utils/warnings.js
CHANGED
|
@@ -25,10 +25,9 @@ export function getFrameworkWarning(frameworkId) {
|
|
|
25
25
|
return (
|
|
26
26
|
color.bgYellow(color.black(' ⚠️ IMPORTANT: STATIC SITE SETUP REQUIRED ')) +
|
|
27
27
|
color.yellow('\n 1. Open your generated Dockerfile.') +
|
|
28
|
-
color.yellow('\n 2.
|
|
29
|
-
color.yellow('\n
|
|
30
|
-
color.yellow('\n
|
|
31
|
-
color.yellow('\n 3. Ensure your package.json has a "build" script (e.g., "vite build").\n\n')
|
|
28
|
+
color.yellow('\n 2. We defaulted your output folder to /app/dist.') +
|
|
29
|
+
color.yellow('\n 3. If your framework uses a different folder (like build/ or out/), change it in the COPY command.') +
|
|
30
|
+
color.yellow('\n 4. Ensure your package.json has a "build" script (e.g., "vite build").\n\n')
|
|
32
31
|
);
|
|
33
32
|
default:
|
|
34
33
|
return '';
|
package/templates/README.md
CHANGED
|
@@ -29,7 +29,7 @@ This infrastructure provisions a highly available Application Load Balancer (ALB
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
3. **Automated CI/CD (Keyless via OIDC):**
|
|
32
|
-
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 `
|
|
32
|
+
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 `{{DEPLOY_BRANCH}}` will automatically build, package, and deploy your application.
|
|
33
33
|
|
|
34
34
|
### ⚠️ Troubleshooting: OIDC Provider Already Exists
|
|
35
35
|
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.
|
|
@@ -48,13 +48,11 @@ Re-run `terraform apply` to link directly to your existing provider.
|
|
|
48
48
|
|
|
49
49
|
If you are done testing and want to stop all AWS billing, you must destroy the infrastructure.
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
Run the automated teardown command from the root of your project:
|
|
53
52
|
```bash
|
|
54
|
-
|
|
55
|
-
terraform destroy
|
|
53
|
+
npx deploy-stack destroy
|
|
56
54
|
```
|
|
57
|
-
*Type `yes` when prompted. This will
|
|
55
|
+
*Type `yes` when prompted. This will execute a safe Terraform teardown of your Load Balancer, ECS cluster, and networking components, followed by automatically emptying and deleting your remote S3 state bucket.*
|
|
58
56
|
|
|
59
57
|
## ⚠️ Critical Application Prerequisites
|
|
60
58
|
|
|
@@ -94,8 +92,8 @@ Make sure your app is configured correctly:
|
|
|
94
92
|
* **Express.js:** `app.listen(port, '0.0.0.0', () => ...)`
|
|
95
93
|
* **FastAPI:** `uvicorn.run(app, host="0.0.0.0", port=8000)`
|
|
96
94
|
|
|
97
|
-
### 4. Static Sites (Vite, Astro, React, Vue)
|
|
95
|
+
### 4. Static Sites (Vite, Astro, React, Vue, SvelteKit)
|
|
98
96
|
|
|
99
97
|
If you are deploying a static site, your application is served via a highly optimized, unprivileged Nginx container.
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
* **Zero-Config Build:** The CLI automatically detected your framework's output folder (`dist`, `build`, etc.) and pre-configured your Dockerfile.
|
|
99
|
+
* **Health Checks:** You do not need to configure a custom `/health` route. Nginx will automatically return a `200 OK` when AWS pings the root `/` index page.
|
|
@@ -13,9 +13,8 @@ RUN npm run build
|
|
|
13
13
|
# STAGE 2: Serve with Hardened Nginx
|
|
14
14
|
FROM nginx:alpine
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
16
|
+
# Adjusts BUILD_DIR to match your framework's output folder
|
|
17
|
+
COPY --from=builder /app/{{BUILD_DIR}} /usr/share/nginx/html
|
|
19
18
|
|
|
20
19
|
# Inject custom Nginx configuration for unprivileged ports and SPA routing
|
|
21
20
|
RUN echo "server {" > /etc/nginx/conf.d/default.conf && \
|