cloudmason2 1.0.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/.marketplace/AdditionalResources.json +1 -0
- package/.marketplace/ArchitectureDiagram.png +0 -0
- package/.marketplace/Logo.png +0 -0
- package/.marketplace/LongDescription.txt +0 -0
- package/.marketplace/ShortDescription.txt +0 -0
- package/.marketplace/SupportDescription.txt +0 -0
- package/.marketplace/UsageInstructions.txt +0 -0
- package/.marketplace/VideoUrls.txt +0 -0
- package/README.md +5 -0
- package/commands/app_details.js +108 -0
- package/commands/create_cdn.js +130 -0
- package/commands/delete.js +194 -0
- package/commands/helpers/apps.js +20 -0
- package/commands/helpers/org_config.js +20 -0
- package/commands/init_org.js +10 -0
- package/commands/launch_app.js +155 -0
- package/commands/list_apps.js +48 -0
- package/commands/new_app.js +227 -0
- package/commands/publish.js +254 -0
- package/commands/ssh_build.js +622 -0
- package/commands/update_app.js +279 -0
- package/commands/update_listing.js +124 -0
- package/main.js +202 -0
- package/package.json +38 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
const {
|
|
2
|
+
EC2Client,
|
|
3
|
+
RunInstancesCommand,
|
|
4
|
+
DescribeImagesCommand,
|
|
5
|
+
DescribeInstancesCommand,
|
|
6
|
+
DescribeVpcsCommand,
|
|
7
|
+
CreateSecurityGroupCommand,
|
|
8
|
+
AuthorizeSecurityGroupIngressCommand,
|
|
9
|
+
AuthorizeSecurityGroupEgressCommand,
|
|
10
|
+
RevokeSecurityGroupEgressCommand,
|
|
11
|
+
CreateKeyPairCommand,
|
|
12
|
+
StopInstancesCommand,
|
|
13
|
+
CreateImageCommand,
|
|
14
|
+
TerminateInstancesCommand,
|
|
15
|
+
DeleteSecurityGroupCommand,
|
|
16
|
+
DeleteKeyPairCommand,
|
|
17
|
+
waitUntilInstanceRunning,
|
|
18
|
+
waitUntilInstanceStopped,
|
|
19
|
+
waitUntilImageAvailable,
|
|
20
|
+
waitUntilInstanceTerminated
|
|
21
|
+
} = require('@aws-sdk/client-ec2');
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const { Client } = require('ssh2');
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
// All SSH setup commands - array of [description, command]
|
|
29
|
+
const SETUP_COMMANDS = [
|
|
30
|
+
['Upgrading to latest AL2023 release', 'sudo dnf upgrade --releasever=latest -y'],
|
|
31
|
+
['Setting up NodeSource for Node.js 24 LTS', 'curl -fsSL https://rpm.nodesource.com/setup_24.x | sudo bash -'],
|
|
32
|
+
['Installing nodejs', 'sudo dnf install -y nodejs'],
|
|
33
|
+
['Node version', 'node --version'],
|
|
34
|
+
['Installing cloudwatch agent', 'sudo dnf install -y amazon-cloudwatch-agent'],
|
|
35
|
+
['Installing python', 'sudo dnf -y install python3'],
|
|
36
|
+
['Installing unzip', 'sudo dnf -y install unzip'],
|
|
37
|
+
['Installing pm2', 'sudo npm install -g pm2'],
|
|
38
|
+
['Creating app directory', 'sudo mkdir -p /home/ec2-user/app'],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class EC2AMIBuilder {
|
|
43
|
+
constructor(amiName, instanceType = 'm6a.large', localZipPath, arch = 'x86_64') {
|
|
44
|
+
if (!amiName || !localZipPath) {
|
|
45
|
+
throw new Error('amiName and localZipPath are required parameters');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.amiName = amiName;
|
|
49
|
+
this.instanceType = instanceType;
|
|
50
|
+
this.localZipPath = localZipPath;
|
|
51
|
+
this.arch = arch;
|
|
52
|
+
|
|
53
|
+
// AWS clients
|
|
54
|
+
const region = process.env.orgRegion || process.env.AWS_REGION || 'us-east-1';
|
|
55
|
+
this.ec2Client = new EC2Client({ region });
|
|
56
|
+
|
|
57
|
+
// Generate unique names for temporary resources
|
|
58
|
+
this.timestamp = Date.now();
|
|
59
|
+
this.keyPairName = `ec2-builder-keypair-${this.timestamp}`;
|
|
60
|
+
this.securityGroupName = `ec2-builder-sg-${this.timestamp}`;
|
|
61
|
+
this.privateKeyPath = path.join(__dirname, `${this.keyPairName}.pem`);
|
|
62
|
+
|
|
63
|
+
// Resource tracking for cleanup
|
|
64
|
+
this.createdResources = {
|
|
65
|
+
instanceId: null,
|
|
66
|
+
keyPairName: null,
|
|
67
|
+
securityGroupId: null
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
this.sshConnection = null;
|
|
71
|
+
this.publicIp = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getLatestAmazonLinuxAMI() {
|
|
75
|
+
console.log('๐ Finding latest Amazon Linux AMI...');
|
|
76
|
+
|
|
77
|
+
const command = new DescribeImagesCommand({
|
|
78
|
+
Filters: [
|
|
79
|
+
{
|
|
80
|
+
Name: 'name',
|
|
81
|
+
// al2023-ami-2023* matches only the standard AL2023 images,
|
|
82
|
+
// excluding the ecs-hvm and minimal variants
|
|
83
|
+
Values: [this.arch === 'arm' ? 'al2023-ami-2023*-arm64' : 'al2023-ami-2023*-x86_64']
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
Name: 'owner-alias',
|
|
87
|
+
Values: ['amazon']
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
Name: 'state',
|
|
91
|
+
Values: ['available']
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
Owners: ['amazon']
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = await this.ec2Client.send(command);
|
|
98
|
+
|
|
99
|
+
const latestAMI = result.Images
|
|
100
|
+
.sort((a, b) => new Date(b.CreationDate) - new Date(a.CreationDate))[0];
|
|
101
|
+
// console.log('latestAMI:', latestAMI);
|
|
102
|
+
console.log(`โ
Found latest AMI: ${latestAMI.ImageId} ${latestAMI.Description} (${latestAMI.Name})`);
|
|
103
|
+
return latestAMI.ImageId;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async createKeyPair() {
|
|
107
|
+
console.log('๐ Creating temporary key pair...');
|
|
108
|
+
|
|
109
|
+
const command = new CreateKeyPairCommand({
|
|
110
|
+
KeyName: this.keyPairName,
|
|
111
|
+
KeyType: 'rsa',
|
|
112
|
+
KeyFormat: 'pem'
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const result = await this.ec2Client.send(command);
|
|
116
|
+
this.createdResources.keyPairName = this.keyPairName;
|
|
117
|
+
|
|
118
|
+
// Save private key to file
|
|
119
|
+
fs.writeFileSync(this.privateKeyPath, result.KeyMaterial, { mode: 0o600 });
|
|
120
|
+
|
|
121
|
+
console.log(`โ
Key pair created: ${this.keyPairName}`);
|
|
122
|
+
return this.keyPairName;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async createSecurityGroup() {
|
|
126
|
+
console.log('๐ก๏ธ Creating security group...');
|
|
127
|
+
|
|
128
|
+
// Get default VPC
|
|
129
|
+
const vpcCommand = new DescribeVpcsCommand({
|
|
130
|
+
Filters: [{ Name: 'isDefault', Values: ['true'] }]
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const vpcs = await this.ec2Client.send(vpcCommand);
|
|
134
|
+
const defaultVpcId = vpcs.Vpcs[0]?.VpcId;
|
|
135
|
+
|
|
136
|
+
if (!defaultVpcId) {
|
|
137
|
+
throw new Error('No default VPC found. Please ensure you have a default VPC in your region.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create security group
|
|
141
|
+
const sgCommand = new CreateSecurityGroupCommand({
|
|
142
|
+
GroupName: this.securityGroupName,
|
|
143
|
+
Description: 'Temporary security group for EC2 AMI builder',
|
|
144
|
+
VpcId: defaultVpcId
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const sgResult = await this.ec2Client.send(sgCommand);
|
|
148
|
+
const securityGroupId = sgResult.GroupId;
|
|
149
|
+
this.createdResources.securityGroupId = securityGroupId;
|
|
150
|
+
|
|
151
|
+
// Add inbound rules (SSH)
|
|
152
|
+
const ingressCommand = new AuthorizeSecurityGroupIngressCommand({
|
|
153
|
+
GroupId: securityGroupId,
|
|
154
|
+
IpPermissions: [
|
|
155
|
+
{
|
|
156
|
+
IpProtocol: 'tcp',
|
|
157
|
+
FromPort: 22,
|
|
158
|
+
ToPort: 22,
|
|
159
|
+
IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'SSH access' }]
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await this.ec2Client.send(ingressCommand);
|
|
165
|
+
|
|
166
|
+
// Remove default egress rule
|
|
167
|
+
const revokeEgressCommand = new RevokeSecurityGroupEgressCommand({
|
|
168
|
+
GroupId: securityGroupId,
|
|
169
|
+
IpPermissions: [
|
|
170
|
+
{
|
|
171
|
+
IpProtocol: '-1',
|
|
172
|
+
IpRanges: [{ CidrIp: '0.0.0.0/0' }]
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await this.ec2Client.send(revokeEgressCommand);
|
|
178
|
+
|
|
179
|
+
// Add specific outbound rules
|
|
180
|
+
const egressCommand = new AuthorizeSecurityGroupEgressCommand({
|
|
181
|
+
GroupId: securityGroupId,
|
|
182
|
+
IpPermissions: [
|
|
183
|
+
{
|
|
184
|
+
IpProtocol: 'tcp',
|
|
185
|
+
FromPort: 443,
|
|
186
|
+
ToPort: 443,
|
|
187
|
+
IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' }]
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
IpProtocol: 'tcp',
|
|
191
|
+
FromPort: 80,
|
|
192
|
+
ToPort: 80,
|
|
193
|
+
IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' }]
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
IpProtocol: 'udp',
|
|
197
|
+
FromPort: 53,
|
|
198
|
+
ToPort: 53,
|
|
199
|
+
IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'DNS outbound' }]
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
IpProtocol: 'udp',
|
|
203
|
+
FromPort: 123,
|
|
204
|
+
ToPort: 123,
|
|
205
|
+
IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'NTP outbound' }]
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await this.ec2Client.send(egressCommand);
|
|
211
|
+
|
|
212
|
+
console.log(`โ
Security group created: ${securityGroupId}`);
|
|
213
|
+
return securityGroupId;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async launchInstance() {
|
|
217
|
+
console.log('๐ Launching EC2 instance...');
|
|
218
|
+
|
|
219
|
+
const amiId = await this.getLatestAmazonLinuxAMI();
|
|
220
|
+
const keyPairName = await this.createKeyPair();
|
|
221
|
+
const securityGroupId = await this.createSecurityGroup();
|
|
222
|
+
|
|
223
|
+
const command = new RunInstancesCommand({
|
|
224
|
+
ImageId: amiId,
|
|
225
|
+
InstanceType: this.instanceType,
|
|
226
|
+
KeyName: keyPairName,
|
|
227
|
+
SecurityGroupIds: [securityGroupId],
|
|
228
|
+
MinCount: 1,
|
|
229
|
+
MaxCount: 1,
|
|
230
|
+
BlockDeviceMappings: [
|
|
231
|
+
{
|
|
232
|
+
DeviceName: '/dev/xvda', // Root device for Amazon Linux
|
|
233
|
+
Ebs: {
|
|
234
|
+
VolumeSize: 40, // Increase from default 8GB to 20GB
|
|
235
|
+
VolumeType: 'gp3',
|
|
236
|
+
DeleteOnTermination: true
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
TagSpecifications: [
|
|
241
|
+
{
|
|
242
|
+
ResourceType: 'instance',
|
|
243
|
+
Tags: [
|
|
244
|
+
{ Key: 'Name', Value: `AMI-Builder-${this.timestamp}` },
|
|
245
|
+
{ Key: 'Purpose', Value: 'Temporary AMI Builder' }
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const result = await this.ec2Client.send(command);
|
|
252
|
+
this.createdResources.instanceId = result.Instances[0].InstanceId;
|
|
253
|
+
|
|
254
|
+
console.log(`โ
Instance launched: ${this.createdResources.instanceId}`);
|
|
255
|
+
|
|
256
|
+
await this.waitForInstanceRunning();
|
|
257
|
+
await this.getInstancePublicIP();
|
|
258
|
+
|
|
259
|
+
console.log(`๐ Instance public IP: ${this.publicIp}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async waitForInstanceRunning() {
|
|
263
|
+
console.log('โณ Waiting for instance to be running...');
|
|
264
|
+
|
|
265
|
+
await waitUntilInstanceRunning(
|
|
266
|
+
{ client: this.ec2Client, maxWaitTime: 300 },
|
|
267
|
+
{ InstanceIds: [this.createdResources.instanceId] }
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
console.log('โ
Instance is running');
|
|
271
|
+
|
|
272
|
+
// Wait for SSH service to be ready
|
|
273
|
+
console.log('โณ Waiting for SSH service to be ready...');
|
|
274
|
+
await new Promise(resolve => setTimeout(resolve, 60000));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async getInstancePublicIP() {
|
|
278
|
+
const command = new DescribeInstancesCommand({
|
|
279
|
+
InstanceIds: [this.createdResources.instanceId]
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const result = await this.ec2Client.send(command);
|
|
283
|
+
this.publicIp = result.Reservations[0].Instances[0].PublicIpAddress;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async connectSSH() {
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
288
|
+
console.log('๐ Connecting to instance via SSH...');
|
|
289
|
+
|
|
290
|
+
this.sshConnection = new Client();
|
|
291
|
+
|
|
292
|
+
this.sshConnection.on('ready', () => {
|
|
293
|
+
console.log('โ
SSH connection established');
|
|
294
|
+
resolve();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
this.sshConnection.on('error', (err) => {
|
|
298
|
+
console.error('โ SSH connection error:', err.message);
|
|
299
|
+
reject(err);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
this.sshConnection.connect({
|
|
303
|
+
host: this.publicIp,
|
|
304
|
+
username: 'ec2-user',
|
|
305
|
+
privateKey: fs.readFileSync(this.privateKeyPath),
|
|
306
|
+
readyTimeout: 60000
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async executeCommand(command, description) {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
console.log(`\n๐ง ${description}...`);
|
|
314
|
+
console.log(`๐ Command: ${command}`);
|
|
315
|
+
console.log('๐ค Output:');
|
|
316
|
+
console.log('โ'.repeat(50));
|
|
317
|
+
|
|
318
|
+
this.sshConnection.exec(command, (err, stream) => {
|
|
319
|
+
if (err) {
|
|
320
|
+
console.error(`โ Error executing command: ${err.message}`);
|
|
321
|
+
reject(err);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let output = '';
|
|
326
|
+
let errorOutput = '';
|
|
327
|
+
|
|
328
|
+
stream.on('close', (code) => {
|
|
329
|
+
console.log('โ'.repeat(50));
|
|
330
|
+
if (code === 0) {
|
|
331
|
+
console.log(`โ
${description} completed successfully (exit code: ${code})\n`);
|
|
332
|
+
resolve(output);
|
|
333
|
+
} else {
|
|
334
|
+
console.log(`โ ${description} failed with exit code ${code}\n`);
|
|
335
|
+
if (errorOutput.trim()) {
|
|
336
|
+
console.error('๐จ Error details:');
|
|
337
|
+
console.error(errorOutput);
|
|
338
|
+
}
|
|
339
|
+
reject(new Error(`Command failed with exit code ${code}`));
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
stream.on('data', (data) => {
|
|
344
|
+
const text = data.toString();
|
|
345
|
+
output += text;
|
|
346
|
+
process.stdout.write(text);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
stream.stderr.on('data', (data) => {
|
|
350
|
+
const text = data.toString();
|
|
351
|
+
errorOutput += text;
|
|
352
|
+
// Print stderr in red color if possible
|
|
353
|
+
process.stderr.write(`\x1b[31m${text}\x1b[0m`);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async setupSystem() {
|
|
360
|
+
console.log('๐ง Setting up system packages...');
|
|
361
|
+
|
|
362
|
+
// Execute all setup commands from the array
|
|
363
|
+
for (const [description, command] of SETUP_COMMANDS) {
|
|
364
|
+
await this.executeCommand(command, description);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
console.log('โ
System setup completed');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async uploadAppViaSFTP() {
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
console.log('๐ค Uploading application via SFTP...');
|
|
373
|
+
console.log(` Local file: ${this.localZipPath}`);
|
|
374
|
+
|
|
375
|
+
this.sshConnection.sftp((err, sftp) => {
|
|
376
|
+
if (err) {
|
|
377
|
+
console.error('โ SFTP session error:', err.message);
|
|
378
|
+
return reject(err);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const readStream = fs.createReadStream(this.localZipPath);
|
|
382
|
+
const writeStream = sftp.createWriteStream('/tmp/app.zip');
|
|
383
|
+
|
|
384
|
+
const fileSize = fs.statSync(this.localZipPath).size;
|
|
385
|
+
let uploaded = 0;
|
|
386
|
+
|
|
387
|
+
readStream.on('data', (chunk) => {
|
|
388
|
+
uploaded += chunk.length;
|
|
389
|
+
const percent = Math.round((uploaded / fileSize) * 100);
|
|
390
|
+
process.stdout.write(`\r Progress: ${percent}% (${Math.round(uploaded / 1024)}KB / ${Math.round(fileSize / 1024)}KB)`);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
writeStream.on('close', () => {
|
|
394
|
+
console.log('\nโ
SFTP upload completed');
|
|
395
|
+
resolve();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
writeStream.on('error', (err) => {
|
|
399
|
+
console.error('\nโ SFTP write error:', err.message);
|
|
400
|
+
reject(err);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
readStream.on('error', (err) => {
|
|
404
|
+
console.error('\nโ File read error:', err.message);
|
|
405
|
+
reject(err);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
readStream.pipe(writeStream);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async uploadAndSetupApp() {
|
|
414
|
+
console.log('๐ฆ Setting up application...');
|
|
415
|
+
|
|
416
|
+
// Upload via SFTP
|
|
417
|
+
await this.uploadAppViaSFTP();
|
|
418
|
+
|
|
419
|
+
// Application setup commands
|
|
420
|
+
const appCommands = [
|
|
421
|
+
['Extracting application package', 'sudo unzip -o /tmp/app.zip -d /home/ec2-user/app'],
|
|
422
|
+
['Setting ownership to ec2-user', 'sudo chown -R ec2-user:ec2-user /home/ec2-user/app'],
|
|
423
|
+
['Cleaning up package archive', 'rm -f /tmp/app.zip'],
|
|
424
|
+
['Directory files', 'ls -la /home/ec2-user/app'],
|
|
425
|
+
['Showing application structure', 'find /home/ec2-user/app -maxdepth 2 -name "node_modules" -prune -o -print']
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
// Execute all app setup commands
|
|
429
|
+
for (const [description, command] of appCommands) {
|
|
430
|
+
await this.executeCommand(command, description);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.log('โ
Application setup completed');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async createAMI() {
|
|
437
|
+
console.log('๐ธ Creating AMI from instance...');
|
|
438
|
+
|
|
439
|
+
// Cleanup commands before AMI creation - remove all sensitive data
|
|
440
|
+
const cleanupCommands = [
|
|
441
|
+
// Remove SSH authorized keys (contains the temporary build key)
|
|
442
|
+
['Removing SSH authorized keys', 'rm -f ~/.ssh/authorized_keys && sudo rm -f /root/.ssh/authorized_keys'],
|
|
443
|
+
// Remove SSH host keys (new instances will regenerate their own)
|
|
444
|
+
['Removing SSH host keys', 'sudo rm -f /etc/ssh/ssh_host_*'],
|
|
445
|
+
// Clean cloud-init so it runs fresh on new instances
|
|
446
|
+
['Cleaning cloud-init data', 'sudo rm -rf /var/lib/cloud/*'],
|
|
447
|
+
// Reset machine-id for unique instance identification
|
|
448
|
+
['Resetting machine-id', 'sudo truncate -s 0 /etc/machine-id'],
|
|
449
|
+
// Clean bash history for all users
|
|
450
|
+
['Cleaning bash history', 'rm -f ~/.bash_history && sudo rm -f /root/.bash_history'],
|
|
451
|
+
// Clean logs and temp files
|
|
452
|
+
['Cleaning logs and temp files', 'sudo rm -rf /tmp/* /var/tmp/* /var/log/messages* /var/log/secure* /var/log/cloud-init*.log'],
|
|
453
|
+
// Clean DNF cache
|
|
454
|
+
['Cleaning DNF cache', 'sudo dnf clean all'],
|
|
455
|
+
// Verify cleanup and check disk usage
|
|
456
|
+
['Checking disk usage', 'df -h && du -sh /home/ec2-user/app']
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
// Execute cleanup commands
|
|
460
|
+
for (const [description, command] of cleanupCommands) {
|
|
461
|
+
await this.executeCommand(command, description);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Close SSH connection
|
|
465
|
+
if (this.sshConnection) {
|
|
466
|
+
this.sshConnection.end();
|
|
467
|
+
this.sshConnection = null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Stop the instance
|
|
471
|
+
console.log('๐ Stopping instance before AMI creation...');
|
|
472
|
+
const stopCommand = new StopInstancesCommand({
|
|
473
|
+
InstanceIds: [this.createdResources.instanceId]
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
await this.ec2Client.send(stopCommand);
|
|
477
|
+
|
|
478
|
+
await waitUntilInstanceStopped(
|
|
479
|
+
{ client: this.ec2Client, maxWaitTime: 300 },
|
|
480
|
+
{ InstanceIds: [this.createdResources.instanceId] }
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
console.log('โ
Instance stopped');
|
|
484
|
+
|
|
485
|
+
// Create AMI
|
|
486
|
+
const createImageCommand = new CreateImageCommand({
|
|
487
|
+
InstanceId: this.createdResources.instanceId,
|
|
488
|
+
Name: this.amiName,
|
|
489
|
+
Description: `AMI with Node.js application - Created ${new Date().toISOString()}`,
|
|
490
|
+
NoReboot: true
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const result = await this.ec2Client.send(createImageCommand);
|
|
494
|
+
const amiId = result.ImageId;
|
|
495
|
+
|
|
496
|
+
console.log(`โ
AMI creation started: ${amiId}`);
|
|
497
|
+
console.log('โณ Waiting for AMI to be available (this may take several minutes)...');
|
|
498
|
+
|
|
499
|
+
await waitUntilImageAvailable(
|
|
500
|
+
{ client: this.ec2Client, maxWaitTime: 3800 },
|
|
501
|
+
{ ImageIds: [amiId] }
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
console.log(`๐ AMI created successfully: ${amiId}`);
|
|
505
|
+
return amiId;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async cleanup() {
|
|
509
|
+
console.log('๐งน Cleaning up temporary resources...');
|
|
510
|
+
|
|
511
|
+
// Close SSH connection
|
|
512
|
+
if (this.sshConnection) {
|
|
513
|
+
this.sshConnection.end();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Delete private key file
|
|
517
|
+
if (fs.existsSync(this.privateKeyPath)) {
|
|
518
|
+
fs.unlinkSync(this.privateKeyPath);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
// Terminate instance
|
|
523
|
+
if (this.createdResources.instanceId) {
|
|
524
|
+
console.log('๐๏ธ Terminating instance...');
|
|
525
|
+
const terminateCommand = new TerminateInstancesCommand({
|
|
526
|
+
InstanceIds: [this.createdResources.instanceId]
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
await this.ec2Client.send(terminateCommand);
|
|
530
|
+
|
|
531
|
+
await waitUntilInstanceTerminated(
|
|
532
|
+
{ client: this.ec2Client, maxWaitTime: 300 },
|
|
533
|
+
{ InstanceIds: [this.createdResources.instanceId] }
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Delete security group
|
|
538
|
+
if (this.createdResources.securityGroupId) {
|
|
539
|
+
console.log('๐๏ธ Deleting security group...');
|
|
540
|
+
const deleteSecurityGroupCommand = new DeleteSecurityGroupCommand({
|
|
541
|
+
GroupId: this.createdResources.securityGroupId
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
await this.ec2Client.send(deleteSecurityGroupCommand);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Delete key pair
|
|
548
|
+
if (this.createdResources.keyPairName) {
|
|
549
|
+
console.log('๐๏ธ Deleting key pair...');
|
|
550
|
+
const deleteKeyPairCommand = new DeleteKeyPairCommand({
|
|
551
|
+
KeyName: this.createdResources.keyPairName
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
await this.ec2Client.send(deleteKeyPairCommand);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
console.log('โ
Cleanup completed');
|
|
558
|
+
|
|
559
|
+
} catch (error) {
|
|
560
|
+
console.warn('โ ๏ธ Some cleanup operations failed:', error.message);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async build() {
|
|
565
|
+
console.log('Starting SSH AMI Build Process...');
|
|
566
|
+
const start = Date.now();
|
|
567
|
+
try {
|
|
568
|
+
await this.launchInstance();
|
|
569
|
+
await this.connectSSH();
|
|
570
|
+
await this.setupSystem();
|
|
571
|
+
await this.uploadAndSetupApp();
|
|
572
|
+
console.log('Build complete after', Math.ceil((Date.now() - start)/1000/60), 'minutes');
|
|
573
|
+
const amiId = await this.createAMI();
|
|
574
|
+
console.log('AMI Created after', Math.ceil((Date.now() - start)/1000/60), 'minutes');
|
|
575
|
+
console.log(`๐ Summary:`);
|
|
576
|
+
console.log(` - AMI ID: ${amiId}`);
|
|
577
|
+
console.log(` - AMI Name: ${this.amiName}`);
|
|
578
|
+
console.log(` - Instance Type Used: ${this.instanceType}`);
|
|
579
|
+
console.log(` - Local Package: ${this.localZipPath}`);
|
|
580
|
+
|
|
581
|
+
return amiId;
|
|
582
|
+
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error('โ AMI Build failed:', error.message);
|
|
585
|
+
throw error;
|
|
586
|
+
} finally {
|
|
587
|
+
await this.cleanup();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function sshAMI(amiName, localZipPath, instanceType, arch){
|
|
593
|
+
const builder = new EC2AMIBuilder(amiName, instanceType, localZipPath, arch);
|
|
594
|
+
const result = await builder.build();
|
|
595
|
+
console.log('AMI ID:', result);
|
|
596
|
+
return result;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
// Convenience function for direct usage
|
|
601
|
+
module.exports.buildAMI = sshAMI;
|
|
602
|
+
|
|
603
|
+
// // CLI usage if called directly
|
|
604
|
+
// if (require.main === module) {
|
|
605
|
+
// const [,, amiName, instanceType, s3PackageUrl] = process.argv;
|
|
606
|
+
|
|
607
|
+
// if (!amiName || !s3PackageUrl) {
|
|
608
|
+
// console.error('Usage: node ec2-ami-builder.js <amiName> [instanceType] <s3PackageUrl>');
|
|
609
|
+
// console.error('Example: node ec2-ami-builder.js "my-app-ami" "t3.micro" "s3://mybucket/myapp.zip"');
|
|
610
|
+
// process.exit(1);
|
|
611
|
+
// }
|
|
612
|
+
|
|
613
|
+
// module.exports.buildAMI(amiName, instanceType || 't3.micro', s3PackageUrl)
|
|
614
|
+
// .then(amiId => {
|
|
615
|
+
// console.log(`\n๐ Your new AMI is ready: ${amiId}`);
|
|
616
|
+
// process.exit(0);
|
|
617
|
+
// })
|
|
618
|
+
// .catch(error => {
|
|
619
|
+
// console.error('\n๐ฅ Build failed:', error.message);
|
|
620
|
+
// process.exit(1);
|
|
621
|
+
// });
|
|
622
|
+
// }
|