deploy-stack 0.4.0 → 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 +8 -0
- package/bin/cli.js +4 -1
- package/package.json +1 -1
- package/src/commands/destroy.js +77 -0
- package/src/commands/init.js +1 -0
- package/src/utils/aws.js +43 -1
- package/templates/README.md +7 -9
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ You retain complete ownership of your infrastructure code without relying on bla
|
|
|
33
33
|
* **Remote State with Native S3 Locking:** Automatically creates an encrypted S3 state bucket utilizing modern native S3 concurrency locking.
|
|
34
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.
|
|
35
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.
|
|
36
38
|
|
|
37
39
|
---
|
|
38
40
|
|
|
@@ -81,6 +83,12 @@ npx deploy-stack secrets push .env.production
|
|
|
81
83
|
|
|
82
84
|
---
|
|
83
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
|
+
|
|
84
92
|
## 📁 Generated File Structure
|
|
85
93
|
|
|
86
94
|
Running the CLI generates a modular architecture tailored to your service:
|
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
|
@@ -232,6 +232,7 @@ export async function mainStack() {
|
|
|
232
232
|
trackEvent('project_provisioned', {
|
|
233
233
|
projectName: actualProjectName,
|
|
234
234
|
framework: finalFramework,
|
|
235
|
+
specific_framework: detectedFramework?.name || finalFramework,
|
|
235
236
|
region: project.region,
|
|
236
237
|
size: project.size,
|
|
237
238
|
setup_mode: setupType,
|
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/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.
|