/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { fileURLToPath } from "url"; // snippet-start:[javascript.v3.ec2.scenarios.basic] import { mkdtempSync, writeFileSync, rmSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { get } from "http"; import { AllocateAddressCommand, AssociateAddressCommand, AuthorizeSecurityGroupIngressCommand, CreateKeyPairCommand, CreateSecurityGroupCommand, DeleteKeyPairCommand, DeleteSecurityGroupCommand, DescribeInstancesCommand, DescribeKeyPairsCommand, DescribeSecurityGroupsCommand, DisassociateAddressCommand, EC2Client, paginateDescribeImages, paginateDescribeInstanceTypes, ReleaseAddressCommand, RunInstancesCommand, StartInstancesCommand, StopInstancesCommand, TerminateInstancesCommand, waitUntilInstanceStatusOk, waitUntilInstanceStopped, waitUntilInstanceTerminated, } from "@aws-sdk/client-ec2"; import { paginateGetParametersByPath, SSMClient } from "@aws-sdk/client-ssm"; import { promptToSelect, promptToContinue, } from "@aws-sdk-examples/libs/utils/util-io.js"; import { wrapText } from "@aws-sdk-examples/libs/utils/util-string.js"; const ec2Client = new EC2Client(); const ssmClient = new SSMClient(); const tmpDirectory = mkdtempSync(join(tmpdir(), "ec2-scenario-tmp")); const createKeyPair = async (keyPairName) => { // Create a key pair in Amazon EC2. const { KeyMaterial, KeyPairId } = await ec2Client.send( // A unique name for the key pair. Up to 255 ASCII characters. new CreateKeyPairCommand({ KeyName: keyPairName }), ); // Save the private key in a temporary location. writeFileSync(`${tmpDirectory}/${keyPairName}.pem`, KeyMaterial, { mode: 0o400, }); return KeyPairId; }; const describeKeyPair = async (keyPairName) => { const command = new DescribeKeyPairsCommand({ KeyNames: [keyPairName], }); const { KeyPairs } = await ec2Client.send(command); return KeyPairs[0]; }; const createSecurityGroup = async (securityGroupName) => { const command = new CreateSecurityGroupCommand({ GroupName: securityGroupName, Description: "A security group for the Amazon EC2 example.", }); const { GroupId } = await ec2Client.send(command); return GroupId; }; const allocateIpAddress = async () => { const command = new AllocateAddressCommand({}); const { PublicIp, AllocationId } = await ec2Client.send(command); return { PublicIp, AllocationId }; }; const getLocalIpAddress = () => { return new Promise((res, rej) => { get("http://checkip.amazonaws.com", (response) => { let data = ""; response.on("data", (chunk) => (data += chunk)); response.on("end", () => res(data.trim())); }).on("error", (err) => { rej(err); }); }); }; const authorizeSecurityGroupIngress = async (securityGroupId) => { const ipAddress = await getLocalIpAddress(); const command = new AuthorizeSecurityGroupIngressCommand({ GroupId: securityGroupId, IpPermissions: [ { IpProtocol: "tcp", FromPort: 22, ToPort: 22, IpRanges: [{ CidrIp: `${ipAddress}/32` }], }, ], }); await ec2Client.send(command); return ipAddress; }; const describeSecurityGroup = async (securityGroupName) => { const command = new DescribeSecurityGroupsCommand({ GroupNames: [securityGroupName], }); const { SecurityGroups } = await ec2Client.send(command); return SecurityGroups[0]; }; const getAmznLinux2AMIs = async () => { const AMIs = []; for await (const page of paginateGetParametersByPath( { client: ssmClient, }, { Path: "/aws/service/ami-amazon-linux-latest" }, )) { page.Parameters.forEach((param) => { if (param.Name.includes("amzn2")) { AMIs.push(param.Value); } }); } const imageDetails = []; for await (const page of paginateDescribeImages( { client: ec2Client }, { ImageIds: AMIs }, )) { imageDetails.push(...(page.Images || [])); } const options = imageDetails.map( (image) => `${image.ImageId} - ${image.Description}`, ); /** * @type {number[]} */ const [selectedIndex] = await promptToSelect(options); return imageDetails[selectedIndex]; }; /** * @param {import('@aws-sdk/client-ec2').Image} imageDetails */ const getCompatibleInstanceTypes = async (imageDetails) => { const paginator = paginateDescribeInstanceTypes( { client: ec2Client, pageSize: 25 }, { Filters: [ { Name: "processor-info.supported-architecture", Values: [imageDetails.Architecture], }, { Name: "instance-type", Values: ["*.micro", "*.small"] }, ], }, ); const instanceTypes = []; for await (const page of paginator) { if (page.InstanceTypes.length) { instanceTypes.push(...(page.InstanceTypes || [])); } } const instanceTypeList = instanceTypes.map( (type) => `${type.InstanceType} - Memory:${type.MemoryInfo.SizeInMiB}`, ); /** * @type {number[]} */ const [selectedIndex] = await promptToSelect( instanceTypeList, "Select an instance type.", ); return instanceTypes[selectedIndex]; }; const runInstance = async ({ keyPairName, securityGroupId, imageId, instanceType, }) => { const command = new RunInstancesCommand({ KeyName: keyPairName, SecurityGroupIds: [securityGroupId], ImageId: imageId, InstanceType: instanceType, MinCount: 1, MaxCount: 1, }); const { Instances } = await ec2Client.send(command); await waitUntilInstanceStatusOk( { client: ec2Client }, { InstanceIds: [Instances[0].InstanceId] }, ); return Instances[0].InstanceId; }; const describeInstance = async (instanceId) => { const command = new DescribeInstancesCommand({ InstanceIds: [instanceId], }); const { Reservations } = await ec2Client.send(command); return Reservations[0].Instances[0]; }; const displaySSHConnectionInfo = ({ publicIp, keyPairName }) => { return `ssh -i ${tmpDirectory}/${keyPairName}.pem ec2-user@${publicIp}`; }; const stopInstance = async (instanceId) => { const command = new StopInstancesCommand({ InstanceIds: [instanceId] }); await ec2Client.send(command); await waitUntilInstanceStopped( { client: ec2Client }, { InstanceIds: [instanceId] }, ); }; const startInstance = async (instanceId) => { const startCommand = new StartInstancesCommand({ InstanceIds: [instanceId] }); await ec2Client.send(startCommand); await waitUntilInstanceStatusOk( { client: ec2Client }, { InstanceIds: [instanceId] }, ); return await describeInstance(instanceId); }; const associateAddress = async ({ allocationId, instanceId }) => { const command = new AssociateAddressCommand({ AllocationId: allocationId, InstanceId: instanceId, }); const { AssociationId } = await ec2Client.send(command); return AssociationId; }; const disassociateAddress = async (associationId) => { const command = new DisassociateAddressCommand({ AssociationId: associationId, }); await ec2Client.send(command); }; const releaseAddress = async (allocationId) => { const command = new ReleaseAddressCommand({ AllocationId: allocationId, }); try { await ec2Client.send(command); console.log(`🧹 Address with allocation ID ${allocationId} released.\n`); } catch (err) { console.log(err); } }; const restartInstance = async (instanceId) => { console.log("Stopping instance."); await stopInstance(instanceId); console.log("Instance stopped."); console.log("Starting instance."); const { PublicIpAddress } = await startInstance(instanceId); return PublicIpAddress; }; const terminateInstance = async (instanceId) => { const command = new TerminateInstancesCommand({ InstanceIds: [instanceId], }); try { await ec2Client.send(command); await waitUntilInstanceTerminated( { client: ec2Client }, { InstanceIds: [instanceId] }, ); console.log(`🧹 Instance with ID ${instanceId} terminated.\n`); } catch (err) { console.error(err); } }; const deleteSecurityGroup = async (securityGroupId) => { const command = new DeleteSecurityGroupCommand({ GroupId: securityGroupId, }); try { await ec2Client.send(command); console.log(`🧹 Security group ${securityGroupId} deleted.\n`); } catch (err) { console.error(err); } }; const deleteKeyPair = async (keyPairName) => { const command = new DeleteKeyPairCommand({ KeyName: keyPairName, }); try { await ec2Client.send(command); console.log(`🧹 Key pair ${keyPairName} deleted.\n`); } catch (err) { console.error(err); } }; const deleteTemporaryDirectory = () => { try { rmSync(tmpDirectory, { recursive: true }); console.log(`🧹 Temporary directory ${tmpDirectory} deleted.\n`); } catch (err) { console.error(err); } }; export const main = async () => { const keyPairName = "ec2-scenario-key-pair"; const securityGroupName = "ec2-scenario-security-group"; let securityGroupId, ipAllocationId, publicIp, instanceId, associationId; console.log(wrapText("Welcome to the Amazon EC2 basic usage scenario.")); try { // Prerequisites console.log( "Before you launch an instance, you'll need a few things:", "\n - A Key Pair", "\n - A Security Group", "\n - An IP Address", "\n - An AMI", "\n - A compatible instance type", "\n\n I'll go ahead and take care of the first three, but I'll need your help for the rest.", ); await promptToContinue(); await createKeyPair(keyPairName); securityGroupId = await createSecurityGroup(securityGroupName); const { PublicIp, AllocationId } = await allocateIpAddress(); ipAllocationId = AllocationId; publicIp = PublicIp; const ipAddress = await authorizeSecurityGroupIngress(securityGroupId); const { KeyName } = await describeKeyPair(keyPairName); const { GroupName } = await describeSecurityGroup(securityGroupName); console.log(`✅ created the key pair ${KeyName}.\n`); console.log( `✅ created the security group ${GroupName}`, `and allowed SSH access from ${ipAddress} (your IP).\n`, ); console.log(`✅ allocated ${publicIp} to be used for your EC2 instance.\n`); await promptToContinue(); // Creating the instance console.log(wrapText("Create the instance.")); console.log( "You get to choose which image you want. Select an amazon-linux-2 image from the following:", ); const imageDetails = await getAmznLinux2AMIs(); const instanceTypeDetails = await getCompatibleInstanceTypes(imageDetails); console.log("Creating your instance. This can take a few seconds."); instanceId = await runInstance({ keyPairName, securityGroupId, imageId: imageDetails.ImageId, instanceType: instanceTypeDetails.InstanceType, }); const instanceDetails = await describeInstance(instanceId); console.log(`✅ instance ${instanceId}.\n`); console.log(instanceDetails); console.log( `\nYou should now be able to SSH into your instance from another terminal:`, `\n${displaySSHConnectionInfo({ publicIp: instanceDetails.PublicIpAddress, keyPairName, })}`, ); await promptToContinue(); // Understanding the IP address. console.log(wrapText("Understanding the IP address.")); console.log( "When you stop and start an instance, the IP address will change. I'll restart your", "instance for you. Notice how the IP address changes.", ); const ipAddressAfterRestart = await restartInstance(instanceId); console.log( `\n Instance started. The IP address changed from ${instanceDetails.PublicIpAddress} to ${ipAddressAfterRestart}`, `\n${displaySSHConnectionInfo({ publicIp: ipAddressAfterRestart, keyPairName, })}`, ); await promptToContinue(); console.log( `If you want to the IP address to be static, you can associate an allocated`, `IP address to your instance. I allocated ${publicIp} for you earlier, and now I'll associate it to your instance.`, ); associationId = await associateAddress({ allocationId: ipAllocationId, instanceId, }); console.log( "Done. Now you should be able to SSH using the new IP.\n", `${displaySSHConnectionInfo({ publicIp, keyPairName })}`, ); await promptToContinue(); console.log( "I'll restart the server again so you can see the IP address remains the same.", ); const ipAddressAfterAssociated = await restartInstance(instanceId); console.log( `Done. Here's your SSH info. Notice the IP address hasn't changed.`, `\n${displaySSHConnectionInfo({ publicIp: ipAddressAfterAssociated, keyPairName, })}`, ); await promptToContinue(); } catch (err) { console.error(err); } finally { // Clean up. console.log(wrapText("Clean up.")); console.log("Now I'll clean up all of the stuff I created."); await promptToContinue(); console.log("Cleaning up. Some of these steps can take a bit of time."); await disassociateAddress(associationId); await terminateInstance(instanceId); await releaseAddress(ipAllocationId); await deleteSecurityGroup(securityGroupId); deleteTemporaryDirectory(); await deleteKeyPair(keyPairName); console.log( "Done cleaning up. Thanks for staying until the end!", "If you have any feedback please use the feedback button in the docs", "or create an issue on GitHub.", ); } }; // snippet-end:[javascript.v3.ec2.scenarios.basic] // Invoke main function if this file was run directly. if (process.argv[1] === fileURLToPath(import.meta.url)) { main(); }