Learn the basics of Amazon EC2 with an Amazon SDK
The following code examples show how to:
Create a key pair and security group.
Select an Amazon Machine Image (AMI) and compatible instance type, then create an instance.
Stop and restart the instance.
Associate an Elastic IP address with your instance.
Connect to your instance with SSH, then clean up resources.
- .NET
-
- Amazon SDK for .NET
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the Amazon Code Examples Repository
. Run a scenario at a command prompt.
/// <summary> /// Show Amazon Elastic Compute Cloud (Amazon EC2) Basics actions. /// </summary> public class EC2Basics { public static ILogger<EC2Basics> _logger = null!; public static EC2Wrapper _ec2Wrapper = null!; public static SsmWrapper _ssmWrapper = null!; public static UiMethods _uiMethods = null!; public static string associationId = null!; public static string allocationId = null!; public static string instanceId = null!; public static string keyPairName = null!; public static string groupName = null!; public static string tempFileName = null!; public static string secGroupId = null!; public static bool isInteractive = true; /// <summary> /// Perform the actions defined for the Amazon EC2 Basics scenario. /// </summary> /// <param name="args">Command line arguments.</param> /// <returns>A Task object.</returns> public static async Task Main(string[] args) { // Set up dependency injection for Amazon EC2 and Amazon Simple Systems // Management (Amazon SSM) Service. using var host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => services.AddAWSService<IAmazonEC2>() .AddAWSService<IAmazonSimpleSystemsManagement>() .AddTransient<EC2Wrapper>() .AddTransient<SsmWrapper>() ) .Build(); SetUpServices(host); var uniqueName = Guid.NewGuid().ToString(); keyPairName = "mvp-example-key-pair" + uniqueName; groupName = "ec2-scenario-group" + uniqueName; var groupDescription = "A security group created for the EC2 Basics scenario."; try { // Start the scenario. _uiMethods.DisplayOverview(); _uiMethods.PressEnter(isInteractive); // Create the key pair. _uiMethods.DisplayTitle("Create RSA key pair"); Console.Write("Let's create an RSA key pair that you can be use to "); Console.WriteLine("securely connect to your EC2 instance."); var keyPair = await _ec2Wrapper.CreateKeyPair(keyPairName); // Save key pair information to a temporary file. tempFileName = _ec2Wrapper.SaveKeyPair(keyPair); Console.WriteLine( $"Created the key pair: {keyPair.KeyName} and saved it to: {tempFileName}"); string? answer = ""; if (isInteractive) { do { Console.Write("Would you like to list your existing key pairs? "); answer = Console.ReadLine(); } while (answer!.ToLower() != "y" && answer.ToLower() != "n"); } if (!isInteractive || answer == "y") { // List existing key pairs. _uiMethods.DisplayTitle("Existing key pairs"); // Passing an empty string to the DescribeKeyPairs method will return // a list of all existing key pairs. var keyPairs = await _ec2Wrapper.DescribeKeyPairs(""); keyPairs.ForEach(kp => { Console.WriteLine( $"{kp.KeyName} created at: {kp.CreateTime} Fingerprint: {kp.KeyFingerprint}"); }); } _uiMethods.PressEnter(isInteractive); // Create the security group. Console.WriteLine( "Let's create a security group to manage access to your instance."); secGroupId = await _ec2Wrapper.CreateSecurityGroup(groupName, groupDescription); Console.WriteLine( "Let's add rules to allow all HTTP and HTTPS inbound traffic and to allow SSH only from your current IP address."); _uiMethods.DisplayTitle("Security group information"); var secGroups = await _ec2Wrapper.DescribeSecurityGroups(secGroupId); Console.WriteLine($"Created security group {groupName} in your default VPC."); secGroups.ForEach(group => { _ec2Wrapper.DisplaySecurityGroupInfoAsync(group); }); _uiMethods.PressEnter(isInteractive); Console.WriteLine( "Now we'll authorize the security group we just created so that it can"); Console.WriteLine("access the EC2 instances you create."); await _ec2Wrapper.AuthorizeSecurityGroupIngress(groupName); secGroups = await _ec2Wrapper.DescribeSecurityGroups(secGroupId); Console.WriteLine($"Now let's look at the permissions again."); secGroups.ForEach(group => { _ec2Wrapper.DisplaySecurityGroupInfoAsync(group); }); _uiMethods.PressEnter(isInteractive); // Get list of available Amazon Linux 2 Amazon Machine Images (AMIs). var parameters = await _ssmWrapper.GetParametersByPath( "/aws/service/ami-amazon-linux-latest"); List<string> imageIds = parameters.Select(param => param.Value).ToList(); var images = await _ec2Wrapper.DescribeImages(imageIds); var i = 1; images.ForEach(image => { Console.WriteLine($"\t{i++}\t{image.Description}"); }); int choice = 1; bool validNumber = false; if (isInteractive) { do { Console.Write("Please select an image: "); var selImage = Console.ReadLine(); validNumber = int.TryParse(selImage, out choice); } while (!validNumber); } var selectedImage = images[choice - 1]; // Display available instance types. _uiMethods.DisplayTitle("Instance Types"); var instanceTypes = await _ec2Wrapper.DescribeInstanceTypes(selectedImage.Architecture); i = 1; instanceTypes.ForEach(instanceType => { Console.WriteLine($"\t{i++}\t{instanceType.InstanceType}"); }); if (isInteractive) { do { Console.Write("Please select an instance type: "); var selImage = Console.ReadLine(); validNumber = int.TryParse(selImage, out choice); } while (!validNumber); } var selectedInstanceType = instanceTypes[choice - 1].InstanceType; // Create an EC2 instance. _uiMethods.DisplayTitle("Creating an EC2 Instance"); instanceId = await _ec2Wrapper.RunInstances(selectedImage.ImageId, selectedInstanceType, keyPairName, secGroupId); _uiMethods.PressEnter(isInteractive); var instance = await _ec2Wrapper.DescribeInstance(instanceId); _uiMethods.DisplayTitle("New Instance Information"); _ec2Wrapper.DisplayInstanceInformation(instance); Console.WriteLine( "\nYou can use SSH to connect to your instance. For example:"); Console.WriteLine( $"\tssh -i {tempFileName} ec2-user@{instance.PublicIpAddress}"); _uiMethods.PressEnter(isInteractive); Console.WriteLine( "Now we'll stop the instance and then start it again to see what's changed."); await _ec2Wrapper.StopInstances(instanceId); Console.WriteLine("Now let's start it up again."); await _ec2Wrapper.StartInstances(instanceId); Console.WriteLine("\nLet's see what changed."); instance = await _ec2Wrapper.DescribeInstance(instanceId); _uiMethods.DisplayTitle("New Instance Information"); _ec2Wrapper.DisplayInstanceInformation(instance); Console.WriteLine("\nNotice the change in the SSH information:"); Console.WriteLine( $"\tssh -i {tempFileName} ec2-user@{instance.PublicIpAddress}"); _uiMethods.PressEnter(isInteractive); Console.WriteLine( "Now we will stop the instance again. Then we will create and associate an"); Console.WriteLine("Elastic IP address to use with our instance."); await _ec2Wrapper.StopInstances(instanceId); _uiMethods.PressEnter(isInteractive); _uiMethods.DisplayTitle("Allocate Elastic IP address"); Console.WriteLine( "You can allocate an Elastic IP address and associate it with your instance\nto keep a consistent IP address even when your instance restarts."); var allocationResponse = await _ec2Wrapper.AllocateAddress(); allocationId = allocationResponse.AllocationId; Console.WriteLine( "Now we will associate the Elastic IP address with our instance."); associationId = await _ec2Wrapper.AssociateAddress(allocationId, instanceId); // Start the instance again. Console.WriteLine("Now let's start the instance again."); await _ec2Wrapper.StartInstances(instanceId); Console.WriteLine("\nLet's see what changed."); instance = await _ec2Wrapper.DescribeInstance(instanceId); _uiMethods.DisplayTitle("Instance information"); _ec2Wrapper.DisplayInstanceInformation(instance); Console.WriteLine("\nHere is the SSH information:"); Console.WriteLine( $"\tssh -i {tempFileName} ec2-user@{instance.PublicIpAddress}"); Console.WriteLine("Let's stop and start the instance again."); _uiMethods.PressEnter(isInteractive); await _ec2Wrapper.StopInstances(instanceId); Console.WriteLine("\nThe instance has stopped."); Console.WriteLine("Now let's start it up again."); await _ec2Wrapper.StartInstances(instanceId); instance = await _ec2Wrapper.DescribeInstance(instanceId); _uiMethods.DisplayTitle("New Instance Information"); _ec2Wrapper.DisplayInstanceInformation(instance); Console.WriteLine("Note that the IP address did not change this time."); _uiMethods.PressEnter(isInteractive); await Cleanup(); } catch (Exception ex) { _logger.LogError(ex, "There was a problem with the scenario, starting cleanup."); await Cleanup(); } _uiMethods.DisplayTitle("EC2 Basics Scenario completed."); _uiMethods.PressEnter(isInteractive); } /// <summary> /// Set up the services and logging. /// </summary> /// <param name="host"></param> public static void SetUpServices(IHost host) { var loggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); }); _logger = new Logger<EC2Basics>(loggerFactory); // Now the client is available for injection. _ec2Wrapper = host.Services.GetRequiredService<EC2Wrapper>(); _ssmWrapper = host.Services.GetRequiredService<SsmWrapper>(); _uiMethods = new UiMethods(); } /// <summary> /// Clean up any resources from the scenario. /// </summary> /// <returns></returns> public static async Task Cleanup() { _uiMethods.DisplayTitle("Clean up resources"); Console.WriteLine("Now let's clean up the resources we created."); Console.WriteLine("Disassociate the Elastic IP address and release it."); // Disassociate the Elastic IP address. await _ec2Wrapper.DisassociateIp(associationId); // Delete the Elastic IP address. await _ec2Wrapper.ReleaseAddress(allocationId); // Terminate the instance. Console.WriteLine("Terminating the instance we created."); await _ec2Wrapper.TerminateInstances(instanceId); // Delete the security group. Console.WriteLine($"Deleting the Security Group: {groupName}."); await _ec2Wrapper.DeleteSecurityGroup(secGroupId); // Delete the RSA key pair. Console.WriteLine($"Deleting the key pair: {keyPairName}"); await _ec2Wrapper.DeleteKeyPair(keyPairName); Console.WriteLine("Deleting the temporary file with the key information."); _ec2Wrapper.DeleteTempFile(tempFileName); _uiMethods.PressEnter(isInteractive); } }
Define a class that wraps EC2 actions.
/// <summary> /// Methods of this class perform Amazon Elastic Compute Cloud (Amazon EC2). /// </summary> public class EC2Wrapper { private readonly IAmazonEC2 _amazonEC2; private readonly ILogger<EC2Wrapper> _logger; /// <summary> /// Constructor for the EC2Wrapper class. /// </summary> /// <param name="amazonScheduler">The injected EC2 client.</param> /// <param name="logger">The injected logger.</param> public EC2Wrapper(IAmazonEC2 amazonService, ILogger<EC2Wrapper> logger) { _amazonEC2 = amazonService; _logger = logger; } /// <summary> /// Allocates an Elastic IP address that can be associated with an Amazon EC2 // instance. By using an Elastic IP address, you can keep the public IP address // constant even when you restart the associated instance. /// </summary> /// <returns>The response object for the allocated address.</returns> public async Task<AllocateAddressResponse> AllocateAddress() { var request = new AllocateAddressRequest(); try { var response = await _amazonEC2.AllocateAddressAsync(request); Console.WriteLine($"Allocated IP: {response.PublicIp} with allocation ID {response.AllocationId}."); return response; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "AddressLimitExceeded") { // For more information on Elastic IP address quotas, see: // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html#using-instance-addressing-limit _logger.LogError($"Unable to allocate Elastic IP, address limit exceeded. {ec2Exception.Message}"); } throw; } catch (Exception ex) { _logger.LogError($"An error occurred while allocating Elastic IP.: {ex.Message}"); throw; } } /// <summary> /// Associates an Elastic IP address with an instance. When this association is /// created, the Elastic IP's public IP address is immediately used as the public /// IP address of the associated instance. /// </summary> /// <param name="allocationId">The allocation Id of an Elastic IP address.</param> /// <param name="instanceId">The instance Id of the EC2 instance to /// associate the address with.</param> /// <returns>The association Id that represents /// the association of the Elastic IP address with an instance.</returns> public async Task<string> AssociateAddress(string allocationId, string instanceId) { try { var request = new AssociateAddressRequest { AllocationId = allocationId, InstanceId = instanceId }; var response = await _amazonEC2.AssociateAddressAsync(request); return response.AssociationId; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidInstanceId") { _logger.LogError( $"InstanceId is invalid, unable to associate address. {ec2Exception.Message}"); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while associating the Elastic IP.: {ex.Message}"); throw; } } /// <summary> /// Authorize the local computer ingress to EC2 instances associated /// with the virtual private cloud (VPC) security group. /// </summary> /// <param name="groupName">The name of the security group.</param> /// <returns>A Boolean value indicating the success of the action.</returns> public async Task<bool> AuthorizeSecurityGroupIngress(string groupName) { try { // Get the IP address for the local computer. var ipAddress = await GetIpAddress(); Console.WriteLine($"Your IP address is: {ipAddress}"); var ipRanges = new List<IpRange> { new IpRange { CidrIp = $"{ipAddress}/32" } }; var permission = new IpPermission { Ipv4Ranges = ipRanges, IpProtocol = "tcp", FromPort = 22, ToPort = 22 }; var permissions = new List<IpPermission> { permission }; var response = await _amazonEC2.AuthorizeSecurityGroupIngressAsync( new AuthorizeSecurityGroupIngressRequest(groupName, permissions)); return response.HttpStatusCode == HttpStatusCode.OK; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidPermission.Duplicate") { _logger.LogError( $"The ingress rule already exists. {ec2Exception.Message}"); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while authorizing ingress.: {ex.Message}"); throw; } } /// <summary> /// Authorize the local computer for ingress to /// the Amazon EC2 SecurityGroup. /// </summary> /// <returns>The IPv4 address of the computer running the scenario.</returns> private static async Task<string> GetIpAddress() { var httpClient = new HttpClient(); var ipString = await httpClient.GetStringAsync("https://checkip.amazonaws.com"); // The IP address is returned with a new line // character on the end. Trim off the whitespace and // return the value to the caller. return ipString.Trim(); } /// <summary> /// Create an Amazon EC2 key pair with a specified name. /// </summary> /// <param name="keyPairName">The name for the new key pair.</param> /// <returns>The Amazon EC2 key pair created.</returns> public async Task<KeyPair?> CreateKeyPair(string keyPairName) { try { var request = new CreateKeyPairRequest { KeyName = keyPairName, }; var response = await _amazonEC2.CreateKeyPairAsync(request); var kp = response.KeyPair; // Return the key pair so it can be saved if needed. // Wait until the key pair exists. int retries = 5; while (retries-- > 0) { Console.WriteLine($"Checking for new KeyPair {keyPairName}..."); var keyPairs = await DescribeKeyPairs(keyPairName); if (keyPairs.Any()) { return kp; } Thread.Sleep(5000); retries--; } _logger.LogError($"Unable to find newly created KeyPair {keyPairName}."); throw new DoesNotExistException("KeyPair not found"); } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidKeyPair.Duplicate") { _logger.LogError( $"A key pair called {keyPairName} already exists."); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while creating the key pair.: {ex.Message}"); throw; } } /// <summary> /// Save KeyPair information to a temporary file. /// </summary> /// <param name="keyPair">The name of the key pair.</param> /// <returns>The full path to the temporary file.</returns> public string SaveKeyPair(KeyPair keyPair) { var tempPath = Path.GetTempPath(); var tempFileName = $"{tempPath}\\{Path.GetRandomFileName()}"; var pemFileName = Path.ChangeExtension(tempFileName, "pem"); // Save the key pair to a file in a temporary folder. using var stream = new FileStream(pemFileName, FileMode.Create); using var writer = new StreamWriter(stream); writer.WriteLine(keyPair.KeyMaterial); return pemFileName; } /// <summary> /// Create an Amazon EC2 security group with a specified name and description. /// </summary> /// <param name="groupName">The name for the new security group.</param> /// <param name="groupDescription">A description of the new security group.</param> /// <returns>The group Id of the new security group.</returns> public async Task<string> CreateSecurityGroup(string groupName, string groupDescription) { try { var response = await _amazonEC2.CreateSecurityGroupAsync( new CreateSecurityGroupRequest(groupName, groupDescription)); // Wait until the security group exists. int retries = 5; while (retries-- > 0) { var groups = await DescribeSecurityGroups(response.GroupId); if (groups.Any()) { return response.GroupId; } Thread.Sleep(5000); retries--; } _logger.LogError($"Unable to find newly created group {groupName}."); throw new DoesNotExistException("security group not found"); } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "ResourceAlreadyExists") { _logger.LogError( $"A security group with the name {groupName} already exists. {ec2Exception.Message}"); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while creating the security group.: {ex.Message}"); throw; } } /// <summary> /// Create a new Amazon EC2 VPC. /// </summary> /// <param name="cidrBlock">The CIDR block for the new security group.</param> /// <returns>The VPC Id of the new VPC.</returns> public async Task<string?> CreateVPC(string cidrBlock) { try { var response = await _amazonEC2.CreateVpcAsync(new CreateVpcRequest { CidrBlock = cidrBlock, }); Vpc vpc = response.Vpc; Console.WriteLine($"Created VPC with ID: {vpc.VpcId}."); return vpc.VpcId; } catch (AmazonEC2Exception ex) { Console.WriteLine($"Couldn't create VPC because: {ex.Message}"); return null; } } /// <summary> /// Delete an Amazon EC2 key pair. /// </summary> /// <param name="keyPairName">The name of the key pair to delete.</param> /// <returns>A Boolean value indicating the success of the action.</returns> public async Task<bool> DeleteKeyPair(string keyPairName) { try { await _amazonEC2.DeleteKeyPairAsync(new DeleteKeyPairRequest(keyPairName)).ConfigureAwait(false); return true; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidKeyPair.NotFound") { _logger.LogError($"KeyPair {keyPairName} does not exist and cannot be deleted. Please verify the key pair name and try again."); } return false; } catch (Exception ex) { Console.WriteLine($"Couldn't delete the key pair because: {ex.Message}"); return false; } } /// <summary> /// Delete the temporary file where the key pair information was saved. /// </summary> /// <param name="tempFileName">The path to the temporary file.</param> public void DeleteTempFile(string tempFileName) { if (File.Exists(tempFileName)) { File.Delete(tempFileName); } } /// <summary> /// Delete an Amazon EC2 security group. /// </summary> /// <param name="groupName">The name of the group to delete.</param> /// <returns>A Boolean value indicating the success of the action.</returns> public async Task<bool> DeleteSecurityGroup(string groupId) { try { var response = await _amazonEC2.DeleteSecurityGroupAsync( new DeleteSecurityGroupRequest { GroupId = groupId }); return response.HttpStatusCode == HttpStatusCode.OK; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidGroup.NotFound") { _logger.LogError( $"Security Group {groupId} does not exist and cannot be deleted. Please verify the ID and try again."); } return false; } catch (Exception ex) { Console.WriteLine($"Couldn't delete the security group because: {ex.Message}"); return false; } } /// <summary> /// Delete an Amazon EC2 VPC. /// </summary> /// <returns>A Boolean value indicating the success of the action.</returns> public async Task<bool> DeleteVpc(string vpcId) { var request = new DeleteVpcRequest { VpcId = vpcId, }; var response = await _amazonEC2.DeleteVpcAsync(request); return response.HttpStatusCode == System.Net.HttpStatusCode.OK; } /// <summary> /// Get information about existing Amazon EC2 images. /// </summary> /// <returns>A list of image information.</returns> public async Task<List<Image>> DescribeImages(List<string>? imageIds) { var request = new DescribeImagesRequest(); if (imageIds is not null) { // If the imageIds list is not null, add the list // to the request object. request.ImageIds = imageIds; } var response = await _amazonEC2.DescribeImagesAsync(request); return response.Images; } /// <summary> /// Display the information returned by DescribeImages. /// </summary> /// <param name="images">The list of image information to display.</param> public void DisplayImageInfo(List<Image> images) { images.ForEach(image => { Console.WriteLine($"{image.Name} Created on: {image.CreationDate}"); }); } /// <summary> /// Get information about an Amazon EC2 instance. /// </summary> /// <param name="instanceId">The instance Id of the EC2 instance.</param> /// <returns>An EC2 instance.</returns> public async Task<Instance> DescribeInstance(string instanceId) { var response = await _amazonEC2.DescribeInstancesAsync( new DescribeInstancesRequest { InstanceIds = new List<string> { instanceId } }); return response.Reservations[0].Instances[0]; } /// <summary> /// Display EC2 instance information. /// </summary> /// <param name="instance">The instance Id of the EC2 instance.</param> public void DisplayInstanceInformation(Instance instance) { Console.WriteLine($"ID: {instance.InstanceId}"); Console.WriteLine($"Image ID: {instance.ImageId}"); Console.WriteLine($"{instance.InstanceType}"); Console.WriteLine($"Key Name: {instance.KeyName}"); Console.WriteLine($"VPC ID: {instance.VpcId}"); Console.WriteLine($"Public IP: {instance.PublicIpAddress}"); Console.WriteLine($"State: {instance.State.Name}"); } /// <summary> /// Get information about EC2 instances with a particular state. /// </summary> /// <param name="tagName">The name of the tag to filter on.</param> /// <param name="tagValue">The value of the tag to look for.</param> /// <returns>True if successful.</returns> public async Task<bool> GetInstancesWithState(string state) { try { // Filters the results of the instance list. var filters = new List<Filter> { new Filter { Name = $"instance-state-name", Values = new List<string> { state, }, }, }; var request = new DescribeInstancesRequest { Filters = filters, }; Console.WriteLine($"\nShowing instances with state {state}"); var paginator = _amazonEC2.Paginators.DescribeInstances(request); await foreach (var response in paginator.Responses) { foreach (var reservation in response.Reservations) { foreach (var instance in reservation.Instances) { Console.Write($"Instance ID: {instance.InstanceId} "); Console.WriteLine($"\tCurrent State: {instance.State.Name}"); } } } return true; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidParameterValue") { _logger.LogError( $"Invalid parameter value for filtering instances."); } return false; } catch (Exception ex) { Console.WriteLine($"Couldn't list instances because: {ex.Message}"); return false; } } /// <summary> /// Describe the instance types available. /// </summary> /// <returns>A list of instance type information.</returns> public async Task<List<InstanceTypeInfo>> DescribeInstanceTypes(ArchitectureValues architecture) { try { var request = new DescribeInstanceTypesRequest(); var filters = new List<Filter> { new Filter("processor-info.supported-architecture", new List<string> { architecture.ToString() }) }; filters.Add(new Filter("instance-type", new() { "*.micro", "*.small" })); request.Filters = filters; var instanceTypes = new List<InstanceTypeInfo>(); var paginator = _amazonEC2.Paginators.DescribeInstanceTypes(request); await foreach (var instanceType in paginator.InstanceTypes) { instanceTypes.Add(instanceType); } return instanceTypes; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidParameterValue") { _logger.LogError( $"Parameters are invalid. Ensure architecture and size strings conform to DescribeInstanceTypes API reference."); } throw; } catch (Exception ex) { Console.WriteLine($"Couldn't delete the security group because: {ex.Message}"); throw; } } /// <summary> /// Get information about an Amazon EC2 key pair. /// </summary> /// <param name="keyPairName">The name of the key pair.</param> /// <returns>A list of key pair information.</returns> public async Task<List<KeyPairInfo>> DescribeKeyPairs(string keyPairName) { try { var request = new DescribeKeyPairsRequest(); if (!string.IsNullOrEmpty(keyPairName)) { request = new DescribeKeyPairsRequest { KeyNames = new List<string> { keyPairName } }; } var response = await _amazonEC2.DescribeKeyPairsAsync(request); return response.KeyPairs.ToList(); } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidKeyPair.NotFound") { _logger.LogError( $"A key pair called {keyPairName} does not exist."); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while describing the key pair.: {ex.Message}"); throw; } } /// <summary> /// Retrieve information for one or all Amazon EC2 security group. /// </summary> /// <param name="groupId">The optional Id of a specific Amazon EC2 security group.</param> /// <returns>A list of security group information.</returns> public async Task<List<SecurityGroup>> DescribeSecurityGroups(string groupId) { try { var securityGroups = new List<SecurityGroup>(); var request = new DescribeSecurityGroupsRequest(); if (!string.IsNullOrEmpty(groupId)) { var groupIds = new List<string> { groupId }; request.GroupIds = groupIds; } var paginatorForSecurityGroups = _amazonEC2.Paginators.DescribeSecurityGroups(request); await foreach (var securityGroup in paginatorForSecurityGroups.SecurityGroups) { securityGroups.Add(securityGroup); } return securityGroups; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidGroup.NotFound") { _logger.LogError( $"A security group {groupId} does not exist."); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while listing security groups. {ex.Message}"); throw; } } /// <summary> /// Display the information returned by the call to /// DescribeSecurityGroupsAsync. /// </summary> /// <param name="securityGroup">A list of security group information.</param> public void DisplaySecurityGroupInfoAsync(SecurityGroup securityGroup) { Console.WriteLine($"{securityGroup.GroupName}"); Console.WriteLine("Ingress permissions:"); securityGroup.IpPermissions.ForEach(permission => { Console.WriteLine($"\tFromPort: {permission.FromPort}"); Console.WriteLine($"\tIpProtocol: {permission.IpProtocol}"); Console.Write($"\tIpv4Ranges: "); permission.Ipv4Ranges.ForEach(range => { Console.Write($"{range.CidrIp} "); }); Console.WriteLine($"\n\tIpv6Ranges:"); permission.Ipv6Ranges.ForEach(range => { Console.Write($"{range.CidrIpv6} "); }); Console.Write($"\n\tPrefixListIds: "); permission.PrefixListIds.ForEach(id => Console.Write($"{id.Id} ")); Console.WriteLine($"\n\tTo Port: {permission.ToPort}"); }); Console.WriteLine("Egress permissions:"); securityGroup.IpPermissionsEgress.ForEach(permission => { Console.WriteLine($"\tFromPort: {permission.FromPort}"); Console.WriteLine($"\tIpProtocol: {permission.IpProtocol}"); Console.Write($"\tIpv4Ranges: "); permission.Ipv4Ranges.ForEach(range => { Console.Write($"{range.CidrIp} "); }); Console.WriteLine($"\n\tIpv6Ranges:"); permission.Ipv6Ranges.ForEach(range => { Console.Write($"{range.CidrIpv6} "); }); Console.Write($"\n\tPrefixListIds: "); permission.PrefixListIds.ForEach(id => Console.Write($"{id.Id} ")); Console.WriteLine($"\n\tTo Port: {permission.ToPort}"); }); } /// <summary> /// Disassociate an Elastic IP address from an EC2 instance. /// </summary> /// <param name="associationId">The association Id.</param> /// <returns>A Boolean value indicating the success of the action.</returns> public async Task<bool> DisassociateIp(string associationId) { try { var response = await _amazonEC2.DisassociateAddressAsync( new DisassociateAddressRequest { AssociationId = associationId }); return response.HttpStatusCode == HttpStatusCode.OK; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidAssociationID.NotFound") { _logger.LogError( $"AssociationId is invalid, unable to disassociate address. {ec2Exception.Message}"); } return false; } catch (Exception ex) { _logger.LogError( $"An error occurred while disassociating the Elastic IP.: {ex.Message}"); return false; } } /// <summary> /// Retrieve a list of available Amazon Linux images. /// </summary> /// <returns>A list of image information.</returns> public async Task<List<Image>> GetEC2AmiList() { var filter = new Filter { Name = "architecture", Values = new List<string> { "x86_64" } }; var filters = new List<Filter> { filter }; var response = await _amazonEC2.DescribeImagesAsync(new DescribeImagesRequest { Filters = filters }); return response.Images; } /// <summary> /// Reboot a specific EC2 instance. /// </summary> /// <param name="ec2InstanceId">The instance Id of the instance that will be rebooted.</param> /// <returns>Async Task.</returns> public async Task<bool> RebootInstances(string ec2InstanceId) { try { var request = new RebootInstancesRequest { InstanceIds = new List<string> { ec2InstanceId }, }; await _amazonEC2.RebootInstancesAsync(request); // Wait for the instance to be running. Console.Write("Waiting for the instance to start."); await WaitForInstanceState(ec2InstanceId, InstanceStateName.Running); return true; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidInstanceId") { _logger.LogError( $"InstanceId {ec2InstanceId} is invalid, unable to reboot. {ec2Exception.Message}"); } return false; } catch (Exception ex) { _logger.LogError( $"An error occurred while rebooting the instance {ec2InstanceId}.: {ex.Message}"); return false; } } /// <summary> /// Release an Elastic IP address. After the Elastic IP address is released, /// it can no longer be used. /// </summary> /// <param name="allocationId">The allocation Id of the Elastic IP address.</param> /// <returns>True if successful.</returns> public async Task<bool> ReleaseAddress(string allocationId) { try { var request = new ReleaseAddressRequest { AllocationId = allocationId }; var response = await _amazonEC2.ReleaseAddressAsync(request); return response.HttpStatusCode == HttpStatusCode.OK; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidAllocationID.NotFound") { _logger.LogError( $"AllocationId {allocationId} was not found. {ec2Exception.Message}"); } return false; } catch (Exception ex) { _logger.LogError( $"An error occurred while releasing the AllocationId {allocationId}.: {ex.Message}"); return false; } } /// <summary> /// Create and run an EC2 instance. /// </summary> /// <param name="ImageId">The image Id of the image used as a basis for the /// EC2 instance.</param> /// <param name="instanceType">The instance type of the EC2 instance to create.</param> /// <param name="keyName">The name of the key pair to associate with the /// instance.</param> /// <param name="groupId">The Id of the Amazon EC2 security group that will be /// allowed to interact with the new EC2 instance.</param> /// <returns>The instance Id of the new EC2 instance.</returns> public async Task<string> RunInstances(string imageId, string instanceType, string keyName, string groupId) { try { var request = new RunInstancesRequest { ImageId = imageId, InstanceType = instanceType, KeyName = keyName, MinCount = 1, MaxCount = 1, SecurityGroupIds = new List<string> { groupId } }; var response = await _amazonEC2.RunInstancesAsync(request); var instanceId = response.Reservation.Instances[0].InstanceId; Console.Write("Waiting for the instance to start."); await WaitForInstanceState(instanceId, InstanceStateName.Running); return instanceId; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidGroupId.NotFound") { _logger.LogError( $"GroupId {groupId} was not found. {ec2Exception.Message}"); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while running the instance.: {ex.Message}"); throw; } } /// <summary> /// Start an EC2 instance. /// </summary> /// <param name="ec2InstanceId">The instance Id of the Amazon EC2 instance /// to start.</param> /// <returns>Async task.</returns> public async Task StartInstances(string ec2InstanceId) { try { var request = new StartInstancesRequest { InstanceIds = new List<string> { ec2InstanceId }, }; await _amazonEC2.StartInstancesAsync(request); Console.Write("Waiting for instance to start. "); await WaitForInstanceState(ec2InstanceId, InstanceStateName.Running); } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidInstanceId") { _logger.LogError( $"InstanceId is invalid, unable to start. {ec2Exception.Message}"); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while starting the instance.: {ex.Message}"); throw; } } /// <summary> /// Stop an EC2 instance. /// </summary> /// <param name="ec2InstanceId">The instance Id of the EC2 instance to /// stop.</param> /// <returns>Async task.</returns> public async Task StopInstances(string ec2InstanceId) { try { var request = new StopInstancesRequest { InstanceIds = new List<string> { ec2InstanceId }, }; await _amazonEC2.StopInstancesAsync(request); Console.Write("Waiting for the instance to stop."); await WaitForInstanceState(ec2InstanceId, InstanceStateName.Stopped); Console.WriteLine("\nThe instance has stopped."); } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidInstanceId") { _logger.LogError( $"InstanceId is invalid, unable to stop. {ec2Exception.Message}"); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while stopping the instance.: {ex.Message}"); throw; } } /// <summary> /// Terminate an EC2 instance. /// </summary> /// <param name="ec2InstanceId">The instance Id of the EC2 instance /// to terminate.</param> /// <returns>Async task.</returns> public async Task<List<InstanceStateChange>> TerminateInstances(string ec2InstanceId) { try { var request = new TerminateInstancesRequest { InstanceIds = new List<string> { ec2InstanceId } }; var response = await _amazonEC2.TerminateInstancesAsync(request); Console.Write("Waiting for the instance to terminate."); await WaitForInstanceState(ec2InstanceId, InstanceStateName.Terminated); Console.WriteLine($"\nThe instance {ec2InstanceId} has been terminated."); return response.TerminatingInstances; } catch (AmazonEC2Exception ec2Exception) { if (ec2Exception.ErrorCode == "InvalidInstanceId") { _logger.LogError( $"InstanceId is invalid, unable to terminate. {ec2Exception.Message}"); } throw; } catch (Exception ex) { _logger.LogError( $"An error occurred while terminating the instance.: {ex.Message}"); throw; } } /// <summary> /// Wait until an EC2 instance is in a specified state. /// </summary> /// <param name="instanceId">The instance Id.</param> /// <param name="stateName">The state to wait for.</param> /// <returns>A Boolean value indicating the success of the action.</returns> public async Task<bool> WaitForInstanceState(string instanceId, InstanceStateName stateName) { var request = new DescribeInstancesRequest { InstanceIds = new List<string> { instanceId } }; // Wait until the instance is in the specified state. var hasState = false; do { // Wait 5 seconds. Thread.Sleep(5000); // Check for the desired state. var response = await _amazonEC2.DescribeInstancesAsync(request); var instance = response.Reservations[0].Instances[0]; hasState = instance.State.Name == stateName; Console.Write(". "); } while (!hasState); return hasState; } }
-
For API details, see the following topics in Amazon SDK for .NET API Reference.
-
- Bash
-
- Amazon CLI with Bash script
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the Amazon Code Examples Repository
. Run an interactive scenario at a command prompt.
############################################################################### # function get_started_with_ec2_instances # # Runs an interactive scenario that shows how to get started using EC2 instances. # # "EC2 access" permissions are needed to run this code. # # Returns: # 0 - If successful. # 1 - If an error occurred. ############################################################################### function get_started_with_ec2_instances() { # Requires version 4 for mapfile. local required_version=4.0 # Get the current Bash version # Check if BASH_VERSION is set local current_version if [[ -n "$BASH_VERSION" ]]; then # Convert BASH_VERSION to a number for comparison current_version=$BASH_VERSION else # Get the current Bash version using the bash command current_version=$(bash --version | head -n 1 | awk '{ print $4 }') fi # Convert version strings to numbers for comparison local required_version_num current_version_num required_version_num=$(echo "$required_version" | awk -F. '{ print ($1 * 10000) + ($2 * 100) + $3 }') current_version_num=$(echo "$current_version" | awk -F. '{ print ($1 * 10000) + ($2 * 100) + $3 }') # Compare versions if ((current_version_num < required_version_num)); then echo "Error: This script requires Bash version $required_version or higher." echo "Your current Bash version is number is $current_version." exit 1 fi { if [ "$EC2_OPERATIONS_SOURCED" != "True" ]; then source ./ec2_operations.sh fi } echo_repeat "*" 88 echo "Welcome to the Amazon Elastic Compute Cloud (Amazon EC2) get started with instances demo." echo_repeat "*" 88 echo echo "Let's create an RSA key pair that you can be use to securely connect to " echo "your EC2 instance." echo -n "Enter a unique name for your key: " get_input local key_name key_name=$get_input_result local temp_dir temp_dir=$(mktemp -d) local key_file_name="$temp_dir/${key_name}.pem" if ec2_create_keypair -n "${key_name}" -f "${key_file_name}"; then echo "Created a key pair $key_name and saved the private key to $key_file_name" echo else errecho "The key pair failed to create. This demo will exit." return 1 fi chmod 400 "${key_file_name}" if yes_no_input "Do you want to list some of your key pairs? (y/n) "; then local keys_and_fingerprints keys_and_fingerprints="$(ec2_describe_key_pairs)" && { local image_name_and_id while IFS=$'\n' read -r image_name_and_id; do local entries IFS=$'\t' read -ra entries <<<"$image_name_and_id" echo "Found rsa key ${entries[0]} with fingerprint:" echo " ${entries[1]}" done <<<"$keys_and_fingerprints" } fi echo_repeat "*" 88 echo_repeat "*" 88 echo "Let's create a security group to manage access to your instance." echo -n "Enter a unique name for your security group: " get_input local security_group_name security_group_name=$get_input_result local security_group_id security_group_id=$(ec2_create_security_group -n "$security_group_name" \ -d "Security group for EC2 instance") || { errecho "The security failed to create. This demo will exit." clean_up "$key_name" "$key_file_name" return 1 } echo "Security group created with ID $security_group_id" echo local public_ip public_ip=$(curl -s http://checkip.amazonaws.com) echo "Let's add a rule to allow SSH only from your current IP address." echo "Your public IP address is $public_ip" echo -n "press return to add this rule to your security group." get_input if ! ec2_authorize_security_group_ingress -g "$security_group_id" -i "$public_ip" -p tcp -f 22 -t 22; then errecho "The security group rules failed to update. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 fi echo "Security group rules updated" local security_group_description security_group_description="$(ec2_describe_security_groups -g "${security_group_id}")" || { errecho "Failed to describe security groups. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } mapfile -t parameters <<<"$security_group_description" IFS=$'\t' read -ra entries <<<"${parameters[0]}" echo "Security group: ${entries[0]}" echo " ID: ${entries[1]}" echo " VPC: ${entries[2]}" echo "Inbound permissions:" IFS=$'\t' read -ra entries <<<"${parameters[1]}" echo " IpProtocol: ${entries[0]}" echo " FromPort: ${entries[1]}" echo " ToPort: ${entries[2]}" echo " CidrIp: ${parameters[2]}" local parameters parameters="$(ssm_get_parameters_by_path -p "/aws/service/ami-amazon-linux-latest")" || { errecho "Failed to get parameters. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } local image_ids="" mapfile -t parameters <<<"$parameters" for image_name_and_id in "${parameters[@]}"; do IFS=$'\t' read -ra values <<<"$image_name_and_id" if [[ "${values[0]}" == *"amzn2"* ]]; then image_ids+="${values[1]} " fi done local images images="$(ec2_describe_images -i "$image_ids")" || { errecho "Failed to describe images. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } new_line_and_tab_to_list "$images" local images=("${list_result[@]}") # Get the size of the array local images_count=${#images[@]} if ((images_count == 0)); then errecho "No images found. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 fi echo_repeat "*" 88 echo_repeat "*" 88 echo "Let's create an instance from an Amazon Linux 2 AMI. Here are some options:" for ((i = 0; i < images_count; i += 3)); do echo "$(((i / 3) + 1)) - ${images[$i]}" done integer_input "Please enter the number of the AMI you want to use: " 1 "$((images_count / 3))" local choice=$get_input_result choice=$(((choice - 1) * 3)) echo "Great choice." echo local architecture=${images[$((choice + 1))]} local image_id=${images[$((choice + 2))]} echo "Here are some instance types that support the ${architecture} architecture of the image:" response="$(ec2_describe_instance_types -a "${architecture}" -t "*.micro,*.small")" || { errecho "Failed to describe instance types. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } local instance_types mapfile -t instance_types <<<"$response" # Get the size of the array local instance_types_count=${#instance_types[@]} echo "Here are some options:" for ((i = 0; i < instance_types_count; i++)); do echo "$((i + 1)) - ${instance_types[$i]}" done integer_input "Which one do you want to use? " 1 "${#instance_types[@]} " choice=$get_input_result local instance_type=${instance_types[$((choice - 1))]} echo "Another great choice." echo echo "Creating your instance and waiting for it to start..." local instance_id instance_id=$(ec2_run_instances -i "$image_id" -t "$instance_type" -k "$key_name" -s "$security_group_id") || { errecho "Failed to run instance. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } ec2_wait_for_instance_running -i "$instance_id" echo "Your instance is ready:" echo local instance_details instance_details="$(ec2_describe_instances -i "${instance_id}")" echo print_instance_details "${instance_details}" local public_ip public_ip=$(echo "${instance_details}" | awk '{print $6}') echo echo "You can use SSH to connect to your instance" echo "If the connection attempt times out, you might have to manually update the SSH ingress rule" echo "for your IP address in the AWS Management Console." connect_to_instance "$key_file_name" "$public_ip" echo -n "Press Enter when you're ready to continue the demo: " get_input echo_repeat "*" 88 echo_repeat "*" 88 echo "Let's stop and start your instance to see what changes." echo "Stopping your instance and waiting until it's stopped..." ec2_stop_instances -i "$instance_id" ec2_wait_for_instance_stopped -i "$instance_id" echo "Your instance is stopped. Restarting..." ec2_start_instances -i "$instance_id" ec2_wait_for_instance_running -i "$instance_id" echo "Your instance is running again." local instance_details instance_details="$(ec2_describe_instances -i "${instance_id}")" print_instance_details "${instance_details}" public_ip=$(echo "${instance_details}" | awk '{print $6}') echo "Every time your instance is restarted, its public IP address changes" connect_to_instance "$key_file_name" "$public_ip" echo -n "Press Enter when you're ready to continue the demo: " get_input echo_repeat "*" 88 echo_repeat "*" 88 echo "You can allocate an Elastic IP address and associate it with your instance" echo "to keep a consistent IP address even when your instance restarts." local result result=$(ec2_allocate_address -d vpc) || { errecho "Failed to allocate an address. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" "$instance_id" return 1 } local elastic_ip allocation_id elastic_ip=$(echo "$result" | awk '{print $1}') allocation_id=$(echo "$result" | awk '{print $2}') echo "Allocated static Elastic IP address: $elastic_ip" local association_id association_id=$(ec2_associate_address -i "$instance_id" -a "$allocation_id") || { errecho "Failed to associate an address. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" "$instance_id" "$allocation_id" return 1 } echo "Associated your Elastic IP with your instance." echo "You can now use SSH to connect to your instance by using the Elastic IP." connect_to_instance "$key_file_name" "$elastic_ip" echo -n "Press Enter when you're ready to continue the demo: " get_input echo_repeat "*" 88 echo_repeat "*" 88 echo "Let's stop and start your instance to see what changes." echo "Stopping your instance and waiting until it's stopped..." ec2_stop_instances -i "$instance_id" ec2_wait_for_instance_stopped -i "$instance_id" echo "Your instance is stopped. Restarting..." ec2_start_instances -i "$instance_id" ec2_wait_for_instance_running -i "$instance_id" echo "Your instance is running again." local instance_details instance_details="$(ec2_describe_instances -i "${instance_id}")" print_instance_details "${instance_details}" echo "Because you have associated an Elastic IP with your instance, you can" echo "connect by using a consistent IP address after the instance restarts." connect_to_instance "$key_file_name" "$elastic_ip" echo -n "Press Enter when you're ready to continue the demo: " get_input echo_repeat "*" 88 echo_repeat "*" 88 if yes_no_input "Do you want to delete the resources created in this demo: (y/n) "; then clean_up "$key_name" "$key_file_name" "$security_group_id" "$instance_id" \ "$allocation_id" "$association_id" else echo "The following resources were not deleted." echo "Key pair: $key_name" echo "Key file: $key_file_name" echo "Security group: $security_group_id" echo "Instance: $instance_id" echo "Elastic IP address: $elastic_ip" fi } ############################################################################### # function clean_up # # This function cleans up the created resources. # $1 - The name of the ec2 key pair to delete. # $2 - The name of the key file to delete. # $3 - The ID of the security group to delete. # $4 - The ID of the instance to terminate. # $5 - The ID of the elastic IP address to release. # $6 - The ID of the elastic IP address to disassociate. # # Returns: # 0 - If successful. # 1 - If an error occurred. ############################################################################### function clean_up() { local result=0 local key_pair_name=$1 local key_file_name=$2 local security_group_id=$3 local instance_id=$4 local allocation_id=$5 local association_id=$6 if [ -n "$association_id" ]; then # bashsupport disable=BP2002 if (ec2_disassociate_address -a "$association_id"); then echo "Disassociated elastic IP address with ID $association_id" else errecho "The elastic IP address disassociation failed." result=1 fi fi if [ -n "$allocation_id" ]; then # bashsupport disable=BP2002 if (ec2_release_address -a "$allocation_id"); then echo "Released elastic IP address with ID $allocation_id" else errecho "The elastic IP address release failed." result=1 fi fi if [ -n "$instance_id" ]; then # bashsupport disable=BP2002 if (ec2_terminate_instances -i "$instance_id"); then echo "Started terminating instance with ID $instance_id" ec2_wait_for_instance_terminated -i "$instance_id" else errecho "The instance terminate failed." result=1 fi fi if [ -n "$security_group_id" ]; then # bashsupport disable=BP2002 if (ec2_delete_security_group -i "$security_group_id"); then echo "Deleted security group with ID $security_group_id" else errecho "The security group delete failed." result=1 fi fi if [ -n "$key_pair_name" ]; then # bashsupport disable=BP2002 if (ec2_delete_keypair -n "$key_pair_name"); then echo "Deleted key pair named $key_pair_name" else errecho "The key pair delete failed." result=1 fi fi if [ -n "$key_file_name" ]; then rm -f "$key_file_name" fi return $result } ############################################################################### # function ssm_get_parameters_by_path # # This function retrieves one or more parameters from the AWS Systems Manager Parameter Store # by specifying a parameter path. # # Parameters: # -p parameter_path - The path of the parameter(s) to retrieve. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ssm_get_parameters_by_path() { local parameter_path response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ssm_get_parameters_by_path" echo "Retrieves one or more parameters from the AWS Systems Manager Parameter Store by specifying a parameter path." echo " -p parameter_path - The path of the parameter(s) to retrieve." echo "" } # Retrieve the calling parameters. while getopts "p:h" option; do case "${option}" in p) parameter_path="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$parameter_path" ]]; then errecho "ERROR: You must provide a parameter path with the -p parameter." usage return 1 fi response=$(aws ssm get-parameters-by-path \ --path "$parameter_path" \ --query "Parameters[*].[Name, Value]" \ --output text) || { aws_cli_error_log $? errecho "ERROR: AWS reports get-parameters-by-path operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # function print_instance_details # # This function prints the details of an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # instance_details - The instance details in the format "InstanceId ImageId InstanceType KeyName VpcId PublicIpAddress State.Name". # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function print_instance_details() { local instance_details="$1" if [[ -z "${instance_details}" ]]; then echo "Error: Missing required instance details argument." return 1 fi local instance_id image_id instance_type key_name vpc_id public_ip state instance_id=$(echo "${instance_details}" | awk '{print $1}') image_id=$(echo "${instance_details}" | awk '{print $2}') instance_type=$(echo "${instance_details}" | awk '{print $3}') key_name=$(echo "${instance_details}" | awk '{print $4}') vpc_id=$(echo "${instance_details}" | awk '{print $5}') public_ip=$(echo "${instance_details}" | awk '{print $6}') state=$(echo "${instance_details}" | awk '{print $7}') echo " ID: ${instance_id}" echo " Image ID: ${image_id}" echo " Instance type: ${instance_type}" echo " Key name: ${key_name}" echo " VPC ID: ${vpc_id}" echo " Public IP: ${public_ip}" echo " State: ${state}" return 0 } ############################################################################### # function connect_to_instance # # This function displays the public IP address of an Amazon Elastic Compute Cloud (Amazon EC2) instance and prompts the user to connect to the instance via SSH. # # Parameters: # $1 - The name of the key file used to connect to the instance. # $2 - The public IP address of the instance. # # Returns: # None ############################################################################### function connect_to_instance() { local key_file_name="$1" local public_ip="$2" # Validate the input parameters if [[ -z "$key_file_name" ]]; then echo "ERROR: You must provide a key file name as the first argument." >&2 return 1 fi if [[ -z "$public_ip" ]]; then echo "ERROR: You must provide a public IP address as the second argument." >&2 return 1 fi # Display the public IP address and connection command echo "To connect, run the following command:" echo " ssh -i ${key_file_name} ec2-user@${public_ip}" # Prompt the user to connect to the instance if yes_no_input "Do you want to connect now? (y/n) "; then echo "After you have connected, you can return to this example by typing 'exit'" ssh -i "${key_file_name}" ec2-user@"${public_ip}" fi } ############################################################################### # function get_input # # This function gets user input from the command line. # # Outputs: # User input to stdout. # # Returns: # 0 ############################################################################### function get_input() { if [ -z "${mock_input+x}" ]; then read -r get_input_result else if [ "$mock_input_array_index" -lt ${#mock_input_array[@]} ]; then get_input_result="${mock_input_array[$mock_input_array_index]}" # bashsupport disable=BP2001 # shellcheck disable=SC2206 ((mock_input_array_index++)) echo -n "$get_input_result" else echo "MOCK_INPUT_ARRAY has no more elements" 1>&2 return 1 fi fi return 0 } ############################################################################### # function yes_no_input # # This function requests a yes/no answer from the user, following to a prompt. # # Parameters: # $1 - The prompt. # # Returns: # 0 - If yes. # 1 - If no. ############################################################################### function yes_no_input() { if [ -z "$1" ]; then echo "Internal error yes_no_input" return 1 fi local index=0 local response="N" while [[ $index -lt 10 ]]; do index=$((index + 1)) echo -n "$1" if ! get_input; then return 1 fi response=$(echo "$get_input_result" | tr '[:upper:]' '[:lower:]') if [ "$response" = "y" ] || [ "$response" = "n" ]; then break else echo -e "\nPlease enter or 'y' or 'n'." fi done echo if [ "$response" = "y" ]; then return 0 else return 1 fi } ############################################################################### # function integer_input # # This function prompts the user to enter an integer within a specified range # and validates the input. # # Parameters: # $1 - The prompt message to display to the user. # $2 - The minimum value of the accepted range. # $3 - The maximum value of the accepted range. # # Returns: # The valid integer input from the user. # If the input is invalid or out of range, the function will continue # prompting the user until a valid input is provided. ############################################################################### function integer_input() { local prompt="$1" local min_value="$2" local max_value="$3" local input="" while true; do # Display the prompt message and wait for user input echo -n "$prompt" if ! get_input; then return 1 fi input="$get_input_result" # Check if the input is a valid integer if [[ "$input" =~ ^-?[0-9]+$ ]]; then # Check if the input is within the specified range if ((input >= min_value && input <= max_value)); then return 0 else echo "Error: Input, $input, must be between $min_value and $max_value." fi else echo "Error: Invalid input- $input. Please enter an integer." fi done } ############################################################################### # function new_line_and_tab_to_list # # This function takes a string input containing newlines and tabs, and # converts it into a list (array) of elements. # # Parameters: # $1 - The input string containing newlines and tabs. # # Returns: # The resulting list (array) is stored in the global variable # 'list_result'. ############################################################################### function new_line_and_tab_to_list() { local input=$1 export list_result list_result=() mapfile -t lines <<<"$input" local line for line in "${lines[@]}"; do IFS=$'\t' read -ra parameters <<<"$line" list_result+=("${parameters[@]}") done } ############################################################################### # function echo_repeat # # This function prints a string 'n' times to stdout. # # Parameters: # $1 - The string. # $2 - Number of times to print the string. # # Outputs: # String 'n' times to stdout. # # Returns: # 0 ############################################################################### function echo_repeat() { local end=$2 for ((i = 0; i < end; i++)); do echo -n "$1" done echo }
The DynamoDB functions used in this scenario.
############################################################################### # function ec2_create_keypair # # This function creates an Amazon Elastic Compute Cloud (Amazon EC2) ED25519 or 2048-bit RSA key pair # and writes it to a file. # # Parameters: # -n key_pair_name - A key pair name. # -f file_path - File to store the key pair. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_create_keypair() { local key_pair_name file_path response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_create_keypair" echo "Creates an Amazon Elastic Compute Cloud (Amazon EC2) ED25519 or 2048-bit RSA key pair" echo " and writes it to a file." echo " -n key_pair_name - A key pair name." echo " -f file_path - File to store the key pair." echo "" } # Retrieve the calling parameters. while getopts "n:f:h" option; do case "${option}" in n) key_pair_name="${OPTARG}" ;; f) file_path="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$key_pair_name" ]]; then errecho "ERROR: You must provide a key name with the -n parameter." usage return 1 fi if [[ -z "$file_path" ]]; then errecho "ERROR: You must provide a file path with the -f parameter." usage return 1 fi response=$(aws ec2 create-key-pair \ --key-name "$key_pair_name" \ --query 'KeyMaterial' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports create-access-key operation failed.$response" return 1 } if [[ -n "$file_path" ]]; then echo "$response" >"$file_path" fi return 0 } ############################################################################### # function ec2_describe_key_pairs # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) key pairs. # # Parameters: # -h - Display help. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_key_pairs() { local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_key_pairs" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) key pairs." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "h" option; do case "${option}" in h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local response response=$(aws ec2 describe-key-pairs \ --query 'KeyPairs[*].[KeyName, KeyFingerprint]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports describe-key-pairs operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_create_security_group # # This function creates an Amazon Elastic Compute Cloud (Amazon EC2) security group. # # Parameters: # -n security_group_name - The name of the security group. # -d security_group_description - The description of the security group. # # Returns: # The ID of the created security group, or an error message if the operation fails. # And: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_create_security_group() { local security_group_name security_group_description response # Function to display usage information function usage() { echo "function ec2_create_security_group" echo "Creates an Amazon Elastic Compute Cloud (Amazon EC2) security group." echo " -n security_group_name - The name of the security group." echo " -d security_group_description - The description of the security group." echo "" } # Parse the command-line arguments while getopts "n:d:h" option; do case "${option}" in n) security_group_name="${OPTARG}" ;; d) security_group_description="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$security_group_name" ]]; then errecho "ERROR: You must provide a security group name with the -n parameter." return 1 fi if [[ -z "$security_group_description" ]]; then errecho "ERROR: You must provide a security group description with the -d parameter." return 1 fi # Create the security group response=$(aws ec2 create-security-group \ --group-name "$security_group_name" \ --description "$security_group_description" \ --query "GroupId" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports create-security-group operation failed." errecho "$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_describe_security_groups # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) security groups. # # Parameters: # -g security_group_id - The ID of the security group to describe (optional). # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_security_groups() { local security_group_id response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_security_groups" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) security groups." echo " -g security_group_id - The ID of the security group to describe (optional)." echo "" } # Retrieve the calling parameters. while getopts "g:h" option; do case "${option}" in g) security_group_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local query="SecurityGroups[*].[GroupName, GroupId, VpcId, IpPermissions[*].[IpProtocol, FromPort, ToPort, IpRanges[*].CidrIp]]" if [[ -n "$security_group_id" ]]; then response=$(aws ec2 describe-security-groups --group-ids "$security_group_id" --query "${query}" --output text) else response=$(aws ec2 describe-security-groups --query "${query}" --output text) fi local error_code=${?} if [[ $error_code -ne 0 ]]; then aws_cli_error_log $error_code errecho "ERROR: AWS reports describe-security-groups operation failed.$response" return 1 fi echo "$response" return 0 } ############################################################################### # function ec2_authorize_security_group_ingress # # This function authorizes an ingress rule for an Amazon Elastic Compute Cloud (Amazon EC2) security group. # # Parameters: # -g security_group_id - The ID of the security group. # -i ip_address - The IP address or CIDR block to authorize. # -p protocol - The protocol to authorize (e.g., tcp, udp, icmp). # -f from_port - The start of the port range to authorize. # -t to_port - The end of the port range to authorize. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_authorize_security_group_ingress() { local security_group_id ip_address protocol from_port to_port response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_authorize_security_group_ingress" echo "Authorizes an ingress rule for an Amazon Elastic Compute Cloud (Amazon EC2) security group." echo " -g security_group_id - The ID of the security group." echo " -i ip_address - The IP address or CIDR block to authorize." echo " -p protocol - The protocol to authorize (e.g., tcp, udp, icmp)." echo " -f from_port - The start of the port range to authorize." echo " -t to_port - The end of the port range to authorize." echo "" } # Retrieve the calling parameters. while getopts "g:i:p:f:t:h" option; do case "${option}" in g) security_group_id="${OPTARG}" ;; i) ip_address="${OPTARG}" ;; p) protocol="${OPTARG}" ;; f) from_port="${OPTARG}" ;; t) to_port="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$security_group_id" ]]; then errecho "ERROR: You must provide a security group ID with the -g parameter." usage return 1 fi if [[ -z "$ip_address" ]]; then errecho "ERROR: You must provide an IP address or CIDR block with the -i parameter." usage return 1 fi if [[ -z "$protocol" ]]; then errecho "ERROR: You must provide a protocol with the -p parameter." usage return 1 fi if [[ -z "$from_port" ]]; then errecho "ERROR: You must provide a start port with the -f parameter." usage return 1 fi if [[ -z "$to_port" ]]; then errecho "ERROR: You must provide an end port with the -t parameter." usage return 1 fi response=$(aws ec2 authorize-security-group-ingress \ --group-id "$security_group_id" \ --cidr "${ip_address}/32" \ --protocol "$protocol" \ --port "$from_port-$to_port" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports authorize-security-group-ingress operation failed.$response" return 1 } return 0 } ############################################################################### # function ec2_describe_images # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) images. # # Parameters: # -i image_ids - A space-separated list of image IDs (optional). # -h - Display help. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_images() { local image_ids response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_images" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) images." echo " -i image_ids - A space-separated list of image IDs (optional)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) image_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local aws_cli_args=() if [[ -n "$image_ids" ]]; then # shellcheck disable=SC2206 aws_cli_args+=("--image-ids" $image_ids) fi response=$(aws ec2 describe-images \ "${aws_cli_args[@]}" \ --query 'Images[*].[Description,Architecture,ImageId]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports describe-images operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # ec2_describe_instance_types # # This function describes EC2 instance types filtered by processor architecture # and optionally by instance type. It takes the following arguments: # # -a, --architecture ARCHITECTURE Specify the processor architecture (e.g., x86_64) # -t, --type INSTANCE_TYPE Comma-separated list of instance types (e.g., t2.micro) # -h, --help Show the usage help # # The function prints the instance type and supported architecture for each # matching instance type. ############################################################################### function ec2_describe_instance_types() { local architecture="" local instance_types="" # bashsupport disable=BP5008 function usage() { echo "Usage: ec2_describe_instance_types [-a|--architecture ARCHITECTURE] [-t|--type INSTANCE_TYPE] [-h|--help]" echo " -a, --architecture ARCHITECTURE Specify the processor architecture (e.g., x86_64)" echo " -t, --type INSTANCE_TYPE Comma-separated list of instance types (e.g., t2.micro)" echo " -h, --help Show this help message" } while [[ $# -gt 0 ]]; do case "$1" in -a | --architecture) architecture="$2" shift 2 ;; -t | --type) instance_types="$2" shift 2 ;; -h | --help) usage return 0 ;; *) echo "Unknown argument: $1" return 1 ;; esac done if [[ -z "$architecture" ]]; then errecho "Error: Architecture not specified." usage return 1 fi if [[ -z "$instance_types" ]]; then errecho "Error: Instance type not specified." usage return 1 fi local tmp_json_file="temp_ec2.json" echo -n '[ { "Name": "processor-info.supported-architecture", "Values": [' >"$tmp_json_file" local items IFS=',' read -ra items <<<"$architecture" local array_size array_size=${#items[@]} for i in $(seq 0 $((array_size - 1))); do echo -n '"'"${items[$i]}"'"' >>"$tmp_json_file" if [[ $i -lt $((array_size - 1)) ]]; then echo -n ',' >>"$tmp_json_file" fi done echo -n ']}, { "Name": "instance-type", "Values": [' >>"$tmp_json_file" IFS=',' read -ra items <<<"$instance_types" local array_size array_size=${#items[@]} for i in $(seq 0 $((array_size - 1))); do echo -n '"'"${items[$i]}"'"' >>"$tmp_json_file" if [[ $i -lt $((array_size - 1)) ]]; then echo -n ',' >>"$tmp_json_file" fi done echo -n ']}]' >>"$tmp_json_file" local response response=$(aws ec2 describe-instance-types --filters file://"$tmp_json_file" \ --query 'InstanceTypes[*].[InstanceType]' --output text) local error_code=$? rm "$tmp_json_file" if [[ $error_code -ne 0 ]]; then aws_cli_error_log $error_code echo "ERROR: AWS reports describe-instance-types operation failed." return 1 fi echo "$response" return 0 } ############################################################################### # function ec2_run_instances # # This function launches one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i image_id - The ID of the Amazon Machine Image (AMI) to use. # -t instance_type - The instance type to use (e.g., t2.micro). # -k key_pair_name - The name of the key pair to use. # -s security_group_id - The ID of the security group to use. # -c count - The number of instances to launch (default: 1). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_run_instances() { local image_id instance_type key_pair_name security_group_id count response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_run_instances" echo "Launches one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i image_id - The ID of the Amazon Machine Image (AMI) to use." echo " -t instance_type - The instance type to use (e.g., t2.micro)." echo " -k key_pair_name - The name of the key pair to use." echo " -s security_group_id - The ID of the security group to use." echo " -c count - The number of instances to launch (default: 1)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:t:k:s:c:h" option; do case "${option}" in i) image_id="${OPTARG}" ;; t) instance_type="${OPTARG}" ;; k) key_pair_name="${OPTARG}" ;; s) security_group_id="${OPTARG}" ;; c) count="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$image_id" ]]; then errecho "ERROR: You must provide an Amazon Machine Image (AMI) ID with the -i parameter." usage return 1 fi if [[ -z "$instance_type" ]]; then errecho "ERROR: You must provide an instance type with the -t parameter." usage return 1 fi if [[ -z "$key_pair_name" ]]; then errecho "ERROR: You must provide a key pair name with the -k parameter." usage return 1 fi if [[ -z "$security_group_id" ]]; then errecho "ERROR: You must provide a security group ID with the -s parameter." usage return 1 fi if [[ -z "$count" ]]; then count=1 fi response=$(aws ec2 run-instances \ --image-id "$image_id" \ --instance-type "$instance_type" \ --key-name "$key_pair_name" \ --security-group-ids "$security_group_id" \ --count "$count" \ --query 'Instances[*].[InstanceId]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports run-instances operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_describe_instances # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i instance_id - The ID of the instance to describe (optional). # -q query - The query to filter the response (optional). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_instances() { local instance_id query response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_instances" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_id - The ID of the instance to describe (optional)." echo " -q query - The query to filter the response (optional)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:q:h" option; do case "${option}" in i) instance_id="${OPTARG}" ;; q) query="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local aws_cli_args=() if [[ -n "$instance_id" ]]; then # shellcheck disable=SC2206 aws_cli_args+=("--instance-ids" $instance_id) fi local query_arg="" if [[ -n "$query" ]]; then query_arg="--query '$query'" else query_arg="--query Reservations[*].Instances[*].[InstanceId,ImageId,InstanceType,KeyName,VpcId,PublicIpAddress,State.Name]" fi # shellcheck disable=SC2086 response=$(aws ec2 describe-instances \ "${aws_cli_args[@]}" \ $query_arg \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports describe-instances operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_stop_instances # # This function stops one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i instance_id - The ID(s) of the instance(s) to stop (comma-separated). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_stop_instances() { local instance_ids local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_stop_instances" echo "Stops one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_id - The ID(s) of the instance(s) to stop (comma-separated)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) instance_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$instance_ids" ]]; then errecho "ERROR: You must provide one or more instance IDs with the -i parameter." usage return 1 fi response=$(aws ec2 stop-instances \ --instance-ids "${instance_ids}") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports stop-instances operation failed with $response." return 1 } return 0 } ############################################################################### # function ec2_start_instances # # This function starts one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i instance_id - The ID(s) of the instance(s) to start (comma-separated). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_start_instances() { local instance_ids local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_start_instances" echo "Starts one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_id - The ID(s) of the instance(s) to start (comma-separated)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) instance_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$instance_ids" ]]; then errecho "ERROR: You must provide one or more instance IDs with the -i parameter." usage return 1 fi response=$(aws ec2 start-instances \ --instance-ids "${instance_ids}") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports start-instances operation failed with $response." return 1 } return 0 } ############################################################################### # function ec2_allocate_address # # This function allocates an Elastic IP address for use with Amazon Elastic Compute Cloud (Amazon EC2) instances in a specific AWS Region. # # Parameters: # -d domain - The domain for the Elastic IP address (either 'vpc' or 'standard'). # # Returns: # The allocated Elastic IP address, or an error message if the operation fails. # And: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_allocate_address() { local domain response # Function to display usage information function usage() { echo "function ec2_allocate_address" echo "Allocates an Elastic IP address for use with Amazon Elastic Compute Cloud (Amazon EC2) instances in a specific AWS Region." echo " -d domain - The domain for the Elastic IP address (either 'vpc' or 'standard')." echo "" } # Parse the command-line arguments while getopts "d:h" option; do case "${option}" in d) domain="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$domain" ]]; then errecho "ERROR: You must provide a domain with the -d parameter (either 'vpc' or 'standard')." return 1 fi if [[ "$domain" != "vpc" && "$domain" != "standard" ]]; then errecho "ERROR: Invalid domain value. Must be either 'vpc' or 'standard'." return 1 fi # Allocate the Elastic IP address response=$(aws ec2 allocate-address \ --domain "$domain" \ --query "[PublicIp,AllocationId]" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports allocate-address operation failed." errecho "$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_associate_address # # This function associates an Elastic IP address with an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # -a allocation_id - The allocation ID of the Elastic IP address to associate. # -i instance_id - The ID of the EC2 instance to associate the Elastic IP address with. # # Returns: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_associate_address() { local allocation_id instance_id response # Function to display usage information function usage() { echo "function ec2_associate_address" echo "Associates an Elastic IP address with an Amazon Elastic Compute Cloud (Amazon EC2) instance." echo " -a allocation_id - The allocation ID of the Elastic IP address to associate." echo " -i instance_id - The ID of the EC2 instance to associate the Elastic IP address with." echo "" } # Parse the command-line arguments while getopts "a:i:h" option; do case "${option}" in a) allocation_id="${OPTARG}" ;; i) instance_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$allocation_id" ]]; then errecho "ERROR: You must provide an allocation ID with the -a parameter." return 1 fi if [[ -z "$instance_id" ]]; then errecho "ERROR: You must provide an instance ID with the -i parameter." return 1 fi # Associate the Elastic IP address response=$(aws ec2 associate-address \ --allocation-id "$allocation_id" \ --instance-id "$instance_id" \ --query "AssociationId" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports associate-address operation failed." errecho "$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_disassociate_address # # This function disassociates an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # -a association_id - The association ID that represents the association of the Elastic IP address with an instance. # # And: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_disassociate_address() { local association_id response # Function to display usage information function usage() { echo "function ec2_disassociate_address" echo "Disassociates an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance." echo " -a association_id - The association ID that represents the association of the Elastic IP address with an instance." echo "" } # Parse the command-line arguments while getopts "a:h" option; do case "${option}" in a) association_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$association_id" ]]; then errecho "ERROR: You must provide an association ID with the -a parameter." return 1 fi response=$(aws ec2 disassociate-address \ --association-id "$association_id") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports disassociate-address operation failed." errecho "$response" return 1 } return 0 } ############################################################################### # function ec2_release_address # # This function releases an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # -a allocation_id - The allocation ID of the Elastic IP address to release. # # Returns: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_release_address() { local allocation_id response # Function to display usage information function usage() { echo "function ec2_release_address" echo "Releases an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance." echo " -a allocation_id - The allocation ID of the Elastic IP address to release." echo "" } # Parse the command-line arguments while getopts "a:h" option; do case "${option}" in a) allocation_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$allocation_id" ]]; then errecho "ERROR: You must provide an allocation ID with the -a parameter." return 1 fi response=$(aws ec2 release-address \ --allocation-id "$allocation_id") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports release-address operation failed." errecho "$response" return 1 } return 0 } ############################################################################### # function ec2_terminate_instances # # This function terminates one or more Amazon Elastic Compute Cloud (Amazon EC2) # instances using the AWS CLI. # # Parameters: # -i instance_ids - A space-separated list of instance IDs. # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_terminate_instances() { local instance_ids response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_terminate_instances" echo "Terminates one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_ids - A space-separated list of instance IDs." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) instance_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Check if instance ID is provided if [[ -z "${instance_ids}" ]]; then echo "Error: Missing required instance IDs parameter." usage return 1 fi # shellcheck disable=SC2086 response=$(aws ec2 terminate-instances \ "--instance-ids" $instance_ids \ --query 'TerminatingInstances[*].[InstanceId,CurrentState.Name]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports terminate-instances operation failed.$response" return 1 } return 0 } ############################################################################### # function ec2_delete_security_group # # This function deletes an Amazon Elastic Compute Cloud (Amazon EC2) security group. # # Parameters: # -i security_group_id - The ID of the security group to delete. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_delete_security_group() { local security_group_id response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_delete_security_group" echo "Deletes an Amazon Elastic Compute Cloud (Amazon EC2) security group." echo " -i security_group_id - The ID of the security group to delete." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) security_group_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$security_group_id" ]]; then errecho "ERROR: You must provide a security group ID with the -i parameter." usage return 1 fi response=$(aws ec2 delete-security-group --group-id "$security_group_id" --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports delete-security-group operation failed.$response" return 1 } return 0 } ############################################################################### # function ec2_delete_keypair # # This function deletes an Amazon EC2 ED25519 or 2048-bit RSA key pair. # # Parameters: # -n key_pair_name - A key pair name. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_delete_keypair() { local key_pair_name response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_delete_keypair" echo "Deletes an Amazon EC2 ED25519 or 2048-bit RSA key pair." echo " -n key_pair_name - A key pair name." echo "" } # Retrieve the calling parameters. while getopts "n:h" option; do case "${option}" in n) key_pair_name="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$key_pair_name" ]]; then errecho "ERROR: You must provide a key pair name with the -n parameter." usage return 1 fi response=$(aws ec2 delete-key-pair \ --key-name "$key_pair_name") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports delete-key-pair operation failed.$response" return 1 } return 0 }
The utility functions used in this scenario.
############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }
-
For API details, see the following topics in Amazon CLI Command Reference.
-
- Java
-
- SDK for Java 2.x
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the Amazon Code Examples Repository
. Run a scenario at a command prompt.
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.ec2.model.CreateKeyPairResponse; import software.amazon.awssdk.services.ec2.model.DeleteKeyPairResponse; import software.amazon.awssdk.services.ec2.model.DescribeKeyPairsResponse; import software.amazon.awssdk.services.ec2.model.DisassociateAddressResponse; import software.amazon.awssdk.services.ec2.model.Ec2Exception; import software.amazon.awssdk.services.ec2.model.ReleaseAddressResponse; import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse; import software.amazon.awssdk.services.ssm.model.Parameter; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.List; import java.util.Scanner; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; /** * Before running this Java (v2) code example, set up your development * environment, including your credentials. * * For more information, see the following documentation topic: * * https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/get-started.html * * This Java example performs the following tasks: * * 1. Creates an RSA key pair and saves the private key data as a .pem file. * 2. Lists key pairs. * 3. Creates a security group for the default VPC. * 4. Displays security group information. * 5. Gets a list of Amazon Linux 2 AMIs and selects one. * 6. Gets additional information about the image. * 7. Gets a list of instance types that are compatible with the selected AMI’s * architecture. * 8. Creates an instance with the key pair, security group, AMI, and an * instance type. * 9. Displays information about the instance. * 10. Stops the instance and waits for it to stop. * 11. Starts the instance and waits for it to start. * 12. Allocates an Elastic IP address and associates it with the instance. * 13. Displays SSH connection info for the instance. * 14. Disassociates and deletes the Elastic IP address. * 15. Terminates the instance and waits for it to terminate. * 16. Deletes the security group. * 17. Deletes the key pair. */ public class EC2Scenario { public static final String DASHES = new String(new char[80]).replace("\0", "-"); private static final Logger logger = LoggerFactory.getLogger(EC2Scenario.class); public static void main(String[] args) throws InterruptedException, UnknownHostException { logger.info(""" Usage: <keyName> <fileName> <groupName> <groupDesc> Where: keyName - A key pair name (for example, TestKeyPair).\s fileName - A file name where the key information is written to.\s groupName - The name of the security group.\s groupDesc - The description of the security group.\s """); Scanner scanner = new Scanner(System.in); EC2Actions ec2Actions = new EC2Actions(); String keyName = "TestKeyPair7" ; String fileName = "ec2Key.pem"; String groupName = "TestSecGroup7" ; String groupDesc = "Test Group" ; String vpcId = ec2Actions.describeFirstEC2VpcAsync().join().vpcId(); InetAddress localAddress = InetAddress.getLocalHost(); String myIpAddress = localAddress.getHostAddress(); logger.info(""" Amazon Elastic Compute Cloud (EC2) is a web service that provides secure, resizable compute capacity in the cloud. It allows developers and organizations to easily launch and manage virtual server instances, known as EC2 instances, to run their applications. EC2 provides a wide range of instance types, each with different compute, memory, and storage capabilities, to meet the diverse needs of various workloads. Developers can choose the appropriate instance type based on their application's requirements, such as high-performance computing, memory-intensive tasks, or GPU-accelerated workloads. The `Ec2AsyncClient` interface in the AWS SDK for Java 2.x provides a set of methods to programmatically interact with the Amazon EC2 service. This allows developers to automate the provisioning, management, and monitoring of EC2 instances as part of their application deployment pipelines. With EC2, teams can focus on building and deploying their applications without having to worry about the underlying infrastructure required to host and manage physical servers. This scenario walks you through how to perform key operations for this service. Let's get started... """); waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("1. Create an RSA key pair and save the private key material as a .pem file."); logger.info(""" An RSA key pair for Amazon EC2 is a security mechanism used to authenticate and secure access to your EC2 instances. It consists of a public key and a private key, which are generated as a pair. """); waitForInputToContinue(scanner); try { CompletableFuture<CreateKeyPairResponse> future = ec2Actions.createKeyPairAsync(keyName, fileName); CreateKeyPairResponse response = future.join(); logger.info("Key Pair successfully created. Key Fingerprint: " + response.keyFingerprint()); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { if (ec2Ex.getMessage().contains("already exists")) { // Key pair already exists. logger.info("The key pair '" + keyName + "' already exists. Moving on..."); } else { logger.info("EC2 error occurred: Error message: {}, Error code {}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } } else { logger.info("An unexpected error occurred: " + (rt.getMessage())); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("2. List key pairs."); waitForInputToContinue(scanner); try { CompletableFuture<DescribeKeyPairsResponse> future = ec2Actions.describeKeysAsync(); DescribeKeyPairsResponse keyPairsResponse = future.join(); keyPairsResponse.keyPairs().forEach(keyPair -> logger.info( "Found key pair with name {} and fingerprint {}", keyPair.keyName(), keyPair.keyFingerprint())); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Error message: {}, Error code {}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", (cause != null ? cause.getMessage() : rt.getMessage())); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("3. Create a security group."); logger.info(""" An AWS EC2 Security Group is a virtual firewall that controls the inbound and outbound traffic to an EC2 instance. It acts as a first line of defense for your EC2 instances, allowing you to specify the rules that govern the network traffic entering and leaving your instances. """); waitForInputToContinue(scanner); String groupId = ""; try { CompletableFuture<String> future = ec2Actions.createSecurityGroupAsync(groupName, groupDesc, vpcId, myIpAddress); future.join(); logger.info("Created security group") ; } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { if (ec2Ex.awsErrorDetails().errorMessage().contains("already exists")) { logger.info("The Security Group already exists. Moving on..."); } else { logger.error("An unexpected error occurred: {}", ec2Ex.awsErrorDetails().errorMessage()); return; } } else { logger.error("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("4. Display security group information for the new security group."); waitForInputToContinue(scanner); try { CompletableFuture<String> future = ec2Actions.describeSecurityGroupArnByNameAsync(groupName); groupId = future.join(); logger.info("The security group Id is "+groupId); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { String errorCode = ec2Ex.awsErrorDetails().errorCode(); if ("InvalidGroup.NotFound".equals(errorCode)) { logger.info("Security group '{}' does not exist. Error Code: {}", groupName, errorCode); } else { logger.info("EC2 error occurred: Message {}, Error Code: {}", ec2Ex.getMessage(), errorCode); } } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("5. Get a list of Amazon Linux 2 AMIs and select one with amzn2 in the name."); logger.info(""" An Amazon EC2 AMI (Amazon Machine Image) is a pre-configured virtual machine image that serves as a template for launching EC2 instances. It contains all the necessary software and configurations required to run an application or operating system on an EC2 instance. """); waitForInputToContinue(scanner); String instanceAMI=""; try { CompletableFuture<GetParametersByPathResponse> future = ec2Actions.getParaValuesAsync(); GetParametersByPathResponse pathResponse = future.join(); List<Parameter> parameterList = pathResponse.parameters(); for (Parameter para : parameterList) { if (filterName(para.name())) { instanceAMI = para.value(); break; } } } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } logger.info("The AMI value with amzn2 is: {}", instanceAMI); waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("6. Get the (Amazon Machine Image) AMI value from the amzn2 image."); logger.info(""" An AMI value represents a specific version of a virtual machine (VM) or server image. It uniquely identifies a particular version of an EC2 instance, including its operating system, pre-installed software, and any custom configurations. This allows you to consistently deploy the same VM image across your infrastructure. """); waitForInputToContinue(scanner); String amiValue; try { CompletableFuture<String> future = ec2Actions.describeImageAsync(instanceAMI); amiValue = future.join(); } catch (CompletionException ce) { Throwable cause = ce.getCause(); if (cause instanceof Ec2Exception) { Ec2Exception ec2Ex = (Ec2Exception) cause; logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("7. Retrieves an instance type available in the current AWS region."); waitForInputToContinue(scanner); String instanceType; try { CompletableFuture<String> future = ec2Actions.getInstanceTypesAsync(); instanceType = future.join(); if (!instanceType.isEmpty()) { logger.info("Found instance type: " + instanceType); } else { logger.info("Desired instance type not found."); } } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("8. Create an Amazon EC2 instance using the key pair, the instance type, the security group, and the EC2 AMI value."); logger.info("Once the EC2 instance is created, it is placed into a running state."); waitForInputToContinue(scanner); String newInstanceId; try { CompletableFuture<String> future = ec2Actions.runInstanceAsync(instanceType, keyName, groupName, amiValue); newInstanceId = future.join(); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception) { Ec2Exception ec2Ex = (Ec2Exception) cause; switch (ec2Ex.awsErrorDetails().errorCode()) { case "InvalidParameterValue": logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); break; case "InsufficientInstanceCapacity": // Handle insufficient instance capacity. logger.info("Insufficient instance capacity: {}, {}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); break; case "InvalidGroup.NotFound": // Handle security group not found. logger.info("Security group not found: {},{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); break; default: logger.info("EC2 error occurred: {} (Code: {}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); break; } return; } else { logger.info("An unexpected error occurred: {}", (cause != null ? cause.getMessage() : rt.getMessage())); return; } } logger.info("The instance Id is " + newInstanceId); waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("9. Display information about the running instance. "); waitForInputToContinue(scanner); String publicIp; try { CompletableFuture<String> future = ec2Actions.describeEC2InstancesAsync(newInstanceId); publicIp = future.join(); logger.info("EC2 instance public IP {}", publicIp); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } logger.info("You can SSH to the instance using this command:"); logger.info("ssh -i " + fileName + " ec2-user@" + publicIp); waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("10. Stop the instance using a waiter (this may take a few mins)."); // Remove the 2nd one waitForInputToContinue(scanner); try { CompletableFuture<Void> future = ec2Actions.stopInstanceAsync(newInstanceId); future.join(); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("11. Start the instance using a waiter (this may take a few mins)."); try { CompletableFuture<Void> future = ec2Actions.startInstanceAsync(newInstanceId); future.join(); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { // Handle EC2 exceptions. logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("12. Allocate an Elastic IP address and associate it with the instance."); logger.info(""" An Elastic IP address is a static public IP address that you can associate with your EC2 instance. This allows you to have a fixed, predictable IP address that remains the same even if your instance is stopped, terminated, or replaced. This is particularly useful for applications or services that need to be accessed consistently from a known IP address. An EC2 Allocation ID (also known as a Reserved Instance Allocation ID) is a unique identifier associated with a Reserved Instance (RI) that you have purchased in AWS. When you purchase a Reserved Instance, AWS assigns a unique Allocation ID to it. This Allocation ID is used to track and identify the specific RI you have purchased, and it is important for managing and monitoring your Reserved Instances. """); waitForInputToContinue(scanner); String allocationId; try { CompletableFuture<String> future = ec2Actions.allocateAddressAsync(); allocationId = future.join(); logger.info("Successfully allocated address with ID: " +allocationId); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } logger.info("The allocation Id value is " + allocationId); waitForInputToContinue(scanner); String associationId; try { CompletableFuture<String> future = ec2Actions.associateAddressAsync(newInstanceId, allocationId); associationId = future.join(); logger.info("Successfully associated address with ID: " +associationId); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("13. Describe the instance again. Note that the public IP address has changed"); waitForInputToContinue(scanner); try { CompletableFuture<String> future = ec2Actions.describeEC2InstancesAsync(newInstanceId); publicIp = future.join(); logger.info("EC2 instance public IP: " + publicIp); logger.info("You can SSH to the instance using this command:"); logger.info("ssh -i " + fileName + " ec2-user@" + publicIp); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("14. Disassociate and release the Elastic IP address."); waitForInputToContinue(scanner); try { CompletableFuture<DisassociateAddressResponse> future = ec2Actions.disassociateAddressAsync(associationId); future.join(); logger.info("Address successfully disassociated."); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { // Handle EC2 exceptions. logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); try { CompletableFuture<ReleaseAddressResponse> future = ec2Actions.releaseEC2AddressAsync(allocationId); future.join(); // Wait for the operation to complete logger.info("Elastic IP address successfully released."); } catch (RuntimeException rte) { logger.info("An unexpected error occurred: {}", rte.getMessage()); return; } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("15. Terminate the instance and use a waiter (this may take a few mins)."); waitForInputToContinue(scanner); try { CompletableFuture<Object> future = ec2Actions.terminateEC2Async(newInstanceId); future.join(); logger.info("EC2 instance successfully terminated."); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { // Handle EC2 exceptions. logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } logger.info(DASHES); logger.info(DASHES); logger.info("16. Delete the security group."); waitForInputToContinue(scanner); try { CompletableFuture<Void> future = ec2Actions.deleteEC2SecGroupAsync(groupId); future.join(); logger.info("Security group successfully deleted."); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("17. Delete the key."); waitForInputToContinue(scanner); try { CompletableFuture<DeleteKeyPairResponse> future = ec2Actions.deleteKeysAsync(keyName); future.join(); logger.info("Successfully deleted key pair named " + keyName); } catch (RuntimeException rt) { Throwable cause = rt.getCause(); if (cause instanceof Ec2Exception ec2Ex) { logger.info("EC2 error occurred: Message {}, Error Code:{}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode()); return; } else { logger.info("An unexpected error occurred: {}", cause.getMessage()); return; } } waitForInputToContinue(scanner); logger.info(DASHES); logger.info(DASHES); logger.info("You successfully completed the Amazon EC2 scenario."); logger.info(DASHES); } public static boolean filterName(String name) { String[] parts = name.split("/"); String myValue = parts[4]; return myValue.contains("amzn2"); } private static void waitForInputToContinue(Scanner scanner) { while (true) { logger.info(""); logger.info("Enter 'c' followed by <ENTER> to continue:"); String input = scanner.nextLine(); if (input.trim().equalsIgnoreCase("c")) { logger.info("Continuing with the program..."); logger.info(""); break; } else { // Handle invalid input. logger.info("Invalid input. Please try again."); } } } }
Define a class that wraps EC2 actions.
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.ec2.Ec2AsyncClient; import software.amazon.awssdk.services.ec2.model.AllocateAddressRequest; import software.amazon.awssdk.services.ec2.model.AllocateAddressResponse; import software.amazon.awssdk.services.ec2.model.AssociateAddressRequest; import software.amazon.awssdk.services.ec2.model.AssociateAddressResponse; import software.amazon.awssdk.services.ec2.model.AuthorizeSecurityGroupIngressRequest; import software.amazon.awssdk.services.ec2.model.CreateKeyPairRequest; import software.amazon.awssdk.services.ec2.model.CreateKeyPairResponse; import software.amazon.awssdk.services.ec2.model.CreateSecurityGroupRequest; import software.amazon.awssdk.services.ec2.model.DeleteKeyPairRequest; import software.amazon.awssdk.services.ec2.model.DeleteKeyPairResponse; import software.amazon.awssdk.services.ec2.model.DeleteSecurityGroupRequest; import software.amazon.awssdk.services.ec2.model.DeleteSecurityGroupResponse; import software.amazon.awssdk.services.ec2.model.DescribeImagesRequest; import software.amazon.awssdk.services.ec2.model.DescribeInstanceTypesRequest; import software.amazon.awssdk.services.ec2.model.DescribeInstanceTypesResponse; import software.amazon.awssdk.services.ec2.model.DescribeInstancesRequest; import software.amazon.awssdk.services.ec2.model.DescribeKeyPairsResponse; import software.amazon.awssdk.services.ec2.model.DescribeSecurityGroupsRequest; import software.amazon.awssdk.services.ec2.model.DescribeSecurityGroupsResponse; import software.amazon.awssdk.services.ec2.model.DescribeVpcsRequest; import software.amazon.awssdk.services.ec2.model.DisassociateAddressRequest; import software.amazon.awssdk.services.ec2.model.DisassociateAddressResponse; import software.amazon.awssdk.services.ec2.model.DomainType; import software.amazon.awssdk.services.ec2.model.Ec2Exception; import software.amazon.awssdk.services.ec2.model.Filter; import software.amazon.awssdk.services.ec2.model.InstanceTypeInfo; import software.amazon.awssdk.services.ec2.model.IpPermission; import software.amazon.awssdk.services.ec2.model.IpRange; import software.amazon.awssdk.services.ec2.model.ReleaseAddressRequest; import software.amazon.awssdk.services.ec2.model.ReleaseAddressResponse; import software.amazon.awssdk.services.ec2.model.RunInstancesRequest; import software.amazon.awssdk.services.ec2.model.RunInstancesResponse; import software.amazon.awssdk.services.ec2.model.StopInstancesRequest; import software.amazon.awssdk.services.ec2.model.StartInstancesRequest; import software.amazon.awssdk.services.ec2.model.TerminateInstancesRequest; import software.amazon.awssdk.services.ec2.model.Vpc; import software.amazon.awssdk.services.ec2.paginators.DescribeImagesPublisher; import software.amazon.awssdk.services.ec2.paginators.DescribeInstancesPublisher; import software.amazon.awssdk.services.ec2.paginators.DescribeSecurityGroupsPublisher; import software.amazon.awssdk.services.ec2.paginators.DescribeVpcsPublisher; import software.amazon.awssdk.services.ec2.waiters.Ec2AsyncWaiter; import software.amazon.awssdk.services.ssm.SsmAsyncClient; import software.amazon.awssdk.services.ssm.model.GetParametersByPathRequest; import software.amazon.awssdk.services.ssm.model.GetParametersByPathResponse; import software.amazon.awssdk.services.ec2.model.TerminateInstancesResponse; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicReference; public class EC2Actions { private static final Logger logger = LoggerFactory.getLogger(EC2Actions.class); private static Ec2AsyncClient ec2AsyncClient; /** * Retrieves an asynchronous Amazon Elastic Container Registry (ECR) client. * * @return the configured ECR asynchronous client. */ private static Ec2AsyncClient getAsyncClient() { if (ec2AsyncClient == null) { /* The `NettyNioAsyncHttpClient` class is part of the AWS SDK for Java, version 2, and it is designed to provide a high-performance, asynchronous HTTP client for interacting with AWS services. It uses the Netty framework to handle the underlying network communication and the Java NIO API to provide a non-blocking, event-driven approach to HTTP requests and responses. */ SdkAsyncHttpClient httpClient = NettyNioAsyncHttpClient.builder() .maxConcurrency(50) // Adjust as needed. .connectionTimeout(Duration.ofSeconds(60)) // Set the connection timeout. .readTimeout(Duration.ofSeconds(60)) // Set the read timeout. .writeTimeout(Duration.ofSeconds(60)) // Set the write timeout. .build(); ClientOverrideConfiguration overrideConfig = ClientOverrideConfiguration.builder() .apiCallTimeout(Duration.ofMinutes(2)) // Set the overall API call timeout. .apiCallAttemptTimeout(Duration.ofSeconds(90)) // Set the individual call attempt timeout. .build(); ec2AsyncClient = Ec2AsyncClient.builder() .region(Region.US_EAST_1) .httpClient(httpClient) .overrideConfiguration(overrideConfig) .build(); } return ec2AsyncClient; } /** * Deletes a key pair asynchronously. * * @param keyPair the name of the key pair to delete * @return a {@link CompletableFuture} that represents the result of the asynchronous operation. * The {@link CompletableFuture} will complete with a {@link DeleteKeyPairResponse} object * that provides the result of the key pair deletion operation. */ public CompletableFuture<DeleteKeyPairResponse> deleteKeysAsync(String keyPair) { DeleteKeyPairRequest request = DeleteKeyPairRequest.builder() .keyName(keyPair) .build(); // Initiate the asynchronous request to delete the key pair. CompletableFuture<DeleteKeyPairResponse> response = getAsyncClient().deleteKeyPair(request); return response.whenComplete((resp, ex) -> { if (ex != null) { throw new RuntimeException("Failed to delete key pair: " + keyPair, ex); } else if (resp == null) { throw new RuntimeException("No response received for deleting key pair: " + keyPair); } }); } /** * Deletes an EC2 security group asynchronously. * * @param groupId the ID of the security group to delete * @return a CompletableFuture that completes when the security group is deleted */ public CompletableFuture<Void> deleteEC2SecGroupAsync(String groupId) { DeleteSecurityGroupRequest request = DeleteSecurityGroupRequest.builder() .groupId(groupId) .build(); CompletableFuture<DeleteSecurityGroupResponse> response = getAsyncClient().deleteSecurityGroup(request); return response.whenComplete((resp, ex) -> { if (ex != null) { throw new RuntimeException("Failed to delete security group with Id " + groupId, ex); } else if (resp == null) { throw new RuntimeException("No response received for deleting security group with Id " + groupId); } }).thenApply(resp -> null); } /** * Terminates an EC2 instance asynchronously and waits for it to reach the terminated state. * * @param instanceId the ID of the EC2 instance to terminate * @return a {@link CompletableFuture} that completes when the instance has been terminated * @throws RuntimeException if there is no response from the AWS SDK or if there is a failure during the termination process */ public CompletableFuture<Object> terminateEC2Async(String instanceId) { TerminateInstancesRequest terminateRequest = TerminateInstancesRequest.builder() .instanceIds(instanceId) .build(); CompletableFuture<TerminateInstancesResponse> responseFuture = getAsyncClient().terminateInstances(terminateRequest); return responseFuture.thenCompose(terminateResponse -> { if (terminateResponse == null) { throw new RuntimeException("No response received for terminating instance " + instanceId); } System.out.println("Going to terminate an EC2 instance and use a waiter to wait for it to be in terminated state"); return getAsyncClient().waiter() .waitUntilInstanceTerminated(r -> r.instanceIds(instanceId)) .thenApply(waiterResponse -> null); }).exceptionally(throwable -> { // Handle any exceptions that occurred during the async call throw new RuntimeException("Failed to terminate EC2 instance: " + throwable.getMessage(), throwable); }); } /** * Releases an Elastic IP address asynchronously. * * @param allocId the allocation ID of the Elastic IP address to be released * @return a {@link CompletableFuture} representing the asynchronous operation of releasing the Elastic IP address */ public CompletableFuture<ReleaseAddressResponse> releaseEC2AddressAsync(String allocId) { ReleaseAddressRequest request = ReleaseAddressRequest.builder() .allocationId(allocId) .build(); CompletableFuture<ReleaseAddressResponse> response = getAsyncClient().releaseAddress(request); response.whenComplete((resp, ex) -> { if (ex != null) { throw new RuntimeException("Failed to release Elastic IP address", ex); } }); return response; } /** * Disassociates an Elastic IP address from an instance asynchronously. * * @param associationId The ID of the association you want to disassociate. * @return a {@link CompletableFuture} representing the asynchronous operation of disassociating the address. The * {@link CompletableFuture} will complete with a {@link DisassociateAddressResponse} when the operation is * finished. * @throws RuntimeException if the disassociation of the address fails. */ public CompletableFuture<DisassociateAddressResponse> disassociateAddressAsync(String associationId) { Ec2AsyncClient ec2 = getAsyncClient(); DisassociateAddressRequest addressRequest = DisassociateAddressRequest.builder() .associationId(associationId) .build(); // Disassociate the address asynchronously. CompletableFuture<DisassociateAddressResponse> response = ec2.disassociateAddress(addressRequest); response.whenComplete((resp, ex) -> { if (ex != null) { throw new RuntimeException("Failed to disassociate address", ex); } }); return response; } /** * Associates an Elastic IP address with an EC2 instance asynchronously. * * @param instanceId the ID of the EC2 instance to associate the Elastic IP address with * @param allocationId the allocation ID of the Elastic IP address to associate * @return a {@link CompletableFuture} that completes with the association ID when the operation is successful, * or throws a {@link RuntimeException} if the operation fails */ public CompletableFuture<String> associateAddressAsync(String instanceId, String allocationId) { AssociateAddressRequest associateRequest = AssociateAddressRequest.builder() .instanceId(instanceId) .allocationId(allocationId) .build(); CompletableFuture<AssociateAddressResponse> responseFuture = getAsyncClient().associateAddress(associateRequest); return responseFuture.thenApply(response -> { if (response.associationId() != null) { return response.associationId(); } else { throw new RuntimeException("Association ID is null after associating address."); } }).whenComplete((result, ex) -> { if (ex != null) { throw new RuntimeException("Failed to associate address", ex); } }); } /** * Allocates an Elastic IP address asynchronously in the VPC domain. * * @return a {@link CompletableFuture} containing the allocation ID of the allocated Elastic IP address */ public CompletableFuture<String> allocateAddressAsync() { AllocateAddressRequest allocateRequest = AllocateAddressRequest.builder() .domain(DomainType.VPC) .build(); CompletableFuture<AllocateAddressResponse> responseFuture = getAsyncClient().allocateAddress(allocateRequest); return responseFuture.thenApply(AllocateAddressResponse::allocationId).whenComplete((result, ex) -> { if (ex != null) { throw new RuntimeException("Failed to allocate address", ex); } }); } /** * Asynchronously describes the state of an EC2 instance. * The paginator helps you iterate over multiple pages of results. * * @param newInstanceId the ID of the EC2 instance to describe * @return a {@link CompletableFuture} that, when completed, contains a string describing the state of the EC2 instance */ public CompletableFuture<String> describeEC2InstancesAsync(String newInstanceId) { DescribeInstancesRequest request = DescribeInstancesRequest.builder() .instanceIds(newInstanceId) .build(); DescribeInstancesPublisher paginator = getAsyncClient().describeInstancesPaginator(request); AtomicReference<String> publicIpAddressRef = new AtomicReference<>(); return paginator.subscribe(response -> { response.reservations().stream() .flatMap(reservation -> reservation.instances().stream()) .filter(instance -> instance.instanceId().equals(newInstanceId)) .findFirst() .ifPresent(instance -> publicIpAddressRef.set(instance.publicIpAddress())); }).thenApply(v -> { String publicIpAddress = publicIpAddressRef.get(); if (publicIpAddress == null) { throw new RuntimeException("Instance with ID " + newInstanceId + " not found."); } return publicIpAddress; }).exceptionally(ex -> { logger.info("Failed to describe instances: " + ex.getMessage()); throw new RuntimeException("Failed to describe instances", ex); }); } /** * Runs an EC2 instance asynchronously. * * @param instanceType The instance type to use for the EC2 instance. * @param keyName The name of the key pair to associate with the EC2 instance. * @param groupName The name of the security group to associate with the EC2 instance. * @param amiId The ID of the Amazon Machine Image (AMI) to use for the EC2 instance. * @return A {@link CompletableFuture} that completes with the ID of the started EC2 instance. * @throws RuntimeException If there is an error running the EC2 instance. */ public CompletableFuture<String> runInstanceAsync(String instanceType, String keyName, String groupName, String amiId) { RunInstancesRequest runRequest = RunInstancesRequest.builder() .instanceType(instanceType) .keyName(keyName) .securityGroups(groupName) .maxCount(1) .minCount(1) .imageId(amiId) .build(); CompletableFuture<RunInstancesResponse> responseFuture = getAsyncClient().runInstances(runRequest); return responseFuture.thenCompose(response -> { String instanceIdVal = response.instances().get(0).instanceId(); System.out.println("Going to start an EC2 instance and use a waiter to wait for it to be in running state"); return getAsyncClient().waiter() .waitUntilInstanceExists(r -> r.instanceIds(instanceIdVal)) .thenCompose(waitResponse -> getAsyncClient().waiter() .waitUntilInstanceRunning(r -> r.instanceIds(instanceIdVal)) .thenApply(runningResponse -> instanceIdVal)); }).exceptionally(throwable -> { // Handle any exceptions that occurred during the async call throw new RuntimeException("Failed to run EC2 instance: " + throwable.getMessage(), throwable); }); } /** * Asynchronously retrieves the instance types available in the current AWS region. * <p> * This method uses the AWS SDK's asynchronous API to fetch the available instance types * and then processes the response. It logs the memory information, network information, * and instance type for each instance type returned. Additionally, it returns a * {@link CompletableFuture} that resolves to the instance type string for the "t2.2xlarge" * instance type, if it is found in the response. If the "t2.2xlarge" instance type is not * found, an empty string is returned. * </p> * * @return a {@link CompletableFuture} that resolves to the instance type string for the * "t2.2xlarge" instance type, or an empty string if the instance type is not found */ public CompletableFuture<String> getInstanceTypesAsync() { DescribeInstanceTypesRequest typesRequest = DescribeInstanceTypesRequest.builder() .maxResults(10) .build(); CompletableFuture<DescribeInstanceTypesResponse> response = getAsyncClient().describeInstanceTypes(typesRequest); response.whenComplete((resp, ex) -> { if (resp != null) { List<InstanceTypeInfo> instanceTypes = resp.instanceTypes(); for (InstanceTypeInfo type : instanceTypes) { logger.info("The memory information of this type is " + type.memoryInfo().sizeInMiB()); logger.info("Network information is " + type.networkInfo().toString()); logger.info("Instance type is " + type.instanceType().toString()); } } else { throw (RuntimeException) ex; } }); return response.thenApply(resp -> { for (InstanceTypeInfo type : resp.instanceTypes()) { String instanceType = type.instanceType().toString(); if (instanceType.equals("t2.2xlarge")) { return instanceType; } } return ""; }); } /** * Asynchronously describes an AWS EC2 image with the specified image ID. * * @param imageId the ID of the image to be described * @return a {@link CompletableFuture} that, when completed, contains the ID of the described image * @throws RuntimeException if no images are found with the provided image ID, or if an error occurs during the AWS API call */ public CompletableFuture<String> describeImageAsync(String imageId) { DescribeImagesRequest imagesRequest = DescribeImagesRequest.builder() .imageIds(imageId) .build(); AtomicReference<String> imageIdRef = new AtomicReference<>(); DescribeImagesPublisher paginator = getAsyncClient().describeImagesPaginator(imagesRequest); return paginator.subscribe(response -> { response.images().stream() .filter(image -> image.imageId().equals(imageId)) .findFirst() .ifPresent(image -> { logger.info("The description of the image is " + image.description()); logger.info("The name of the image is " + image.name()); imageIdRef.set(image.imageId()); }); }).thenApply(v -> { String id = imageIdRef.get(); if (id == null) { throw new RuntimeException("No images found with the provided image ID."); } return id; }).exceptionally(ex -> { logger.info("Failed to describe image: " + ex.getMessage()); throw new RuntimeException("Failed to describe image", ex); }); } /** * Retrieves the parameter values asynchronously using the AWS Systems Manager (SSM) API. * * @return a {@link CompletableFuture} that holds the response from the SSM API call to get parameters by path */ public CompletableFuture<GetParametersByPathResponse> getParaValuesAsync() { SsmAsyncClient ssmClient = SsmAsyncClient.builder() .region(Region.US_EAST_1) .build(); GetParametersByPathRequest parameterRequest = GetParametersByPathRequest.builder() .path("/aws/service/ami-amazon-linux-latest") .build(); // Create a CompletableFuture to hold the final result. CompletableFuture<GetParametersByPathResponse> responseFuture = new CompletableFuture<>(); ssmClient.getParametersByPath(parameterRequest) .whenComplete((response, exception) -> { if (exception != null) { responseFuture.completeExceptionally(new RuntimeException("Failed to get parameters by path", exception)); } else { responseFuture.complete(response); } }); return responseFuture; } /** * Asynchronously describes the security groups for the specified group ID. * * @param groupName the name of the security group to describe * @return a {@link CompletableFuture} that represents the asynchronous operation * of describing the security groups. The future will complete with a * {@link DescribeSecurityGroupsResponse} object that contains the * security group information. */ public CompletableFuture<String> describeSecurityGroupArnByNameAsync(String groupName) { DescribeSecurityGroupsRequest request = DescribeSecurityGroupsRequest.builder() .groupNames(groupName) .build(); DescribeSecurityGroupsPublisher paginator = getAsyncClient().describeSecurityGroupsPaginator(request); AtomicReference<String> groupIdRef = new AtomicReference<>(); return paginator.subscribe(response -> { response.securityGroups().stream() .filter(securityGroup -> securityGroup.groupName().equals(groupName)) .findFirst() .ifPresent(securityGroup -> groupIdRef.set(securityGroup.groupId())); }).thenApply(v -> { String groupId = groupIdRef.get(); if (groupId == null) { throw new RuntimeException("No security group found with the name: " + groupName); } return groupId; }).exceptionally(ex -> { logger.info("Failed to describe security group: " + ex.getMessage()); throw new RuntimeException("Failed to describe security group", ex); }); } /** * Creates a new security group asynchronously with the specified group name, description, and VPC ID. It also * authorizes inbound traffic on ports 80 and 22 from the specified IP address. * * @param groupName the name of the security group to create * @param groupDesc the description of the security group * @param vpcId the ID of the VPC in which to create the security group * @param myIpAddress the IP address from which to allow inbound traffic (e.g., "192.168.1.1/0" to allow traffic from * any IP address in the 192.168.1.0/24 subnet) * @return a CompletableFuture that, when completed, returns the ID of the created security group * @throws RuntimeException if there was a failure creating the security group or authorizing the inbound traffic */ public CompletableFuture<String> createSecurityGroupAsync(String groupName, String groupDesc, String vpcId, String myIpAddress) { CreateSecurityGroupRequest createRequest = CreateSecurityGroupRequest.builder() .groupName(groupName) .description(groupDesc) .vpcId(vpcId) .build(); return getAsyncClient().createSecurityGroup(createRequest) .thenCompose(createResponse -> { String groupId = createResponse.groupId(); IpRange ipRange = IpRange.builder() .cidrIp(myIpAddress + "/32") .build(); IpPermission ipPerm = IpPermission.builder() .ipProtocol("tcp") .toPort(80) .fromPort(80) .ipRanges(ipRange) .build(); IpPermission ipPerm2 = IpPermission.builder() .ipProtocol("tcp") .toPort(22) .fromPort(22) .ipRanges(ipRange) .build(); AuthorizeSecurityGroupIngressRequest authRequest = AuthorizeSecurityGroupIngressRequest.builder() .groupName(groupName) .ipPermissions(ipPerm, ipPerm2) .build(); return getAsyncClient().authorizeSecurityGroupIngress(authRequest) .thenApply(authResponse -> groupId); }) .whenComplete((result, exception) -> { if (exception != null) { if (exception instanceof CompletionException && exception.getCause() instanceof Ec2Exception) { throw (Ec2Exception) exception.getCause(); } else { throw new RuntimeException("Failed to create security group: " + exception.getMessage(), exception); } } }); } /** * Asynchronously describes the key pairs associated with the current AWS account. * * @return a {@link CompletableFuture} containing the {@link DescribeKeyPairsResponse} object, which provides * information about the key pairs. */ public CompletableFuture<DescribeKeyPairsResponse> describeKeysAsync() { CompletableFuture<DescribeKeyPairsResponse> responseFuture = getAsyncClient().describeKeyPairs(); responseFuture.whenComplete((response, exception) -> { if (exception != null) { throw new RuntimeException("Failed to describe key pairs: " + exception.getMessage(), exception); } }); return responseFuture; } /** * Creates a new key pair asynchronously. * * @param keyName the name of the key pair to create * @param fileName the name of the file to write the key material to * @return a {@link CompletableFuture} that represents the asynchronous operation * of creating the key pair and writing the key material to a file */ public CompletableFuture<CreateKeyPairResponse> createKeyPairAsync(String keyName, String fileName) { CreateKeyPairRequest request = CreateKeyPairRequest.builder() .keyName(keyName) .build(); CompletableFuture<CreateKeyPairResponse> responseFuture = getAsyncClient().createKeyPair(request); responseFuture.whenComplete((response, exception) -> { if (response != null) { try { BufferedWriter writer = new BufferedWriter(new FileWriter(fileName)); writer.write(response.keyMaterial()); writer.close(); } catch (IOException e) { throw new RuntimeException("Failed to write key material to file: " + e.getMessage(), e); } } else { throw new RuntimeException("Failed to create key pair: " + exception.getMessage(), exception); } }); return responseFuture; } /** * Describes the first default VPC asynchronously and using a paginator. * * @return a {@link CompletableFuture} that, when completed, contains the first default VPC found.\ */ public CompletableFuture<Vpc> describeFirstEC2VpcAsync() { Filter myFilter = Filter.builder() .name("is-default") .values("true") .build(); DescribeVpcsRequest request = DescribeVpcsRequest.builder() .filters(myFilter) .build(); DescribeVpcsPublisher paginator = getAsyncClient().describeVpcsPaginator(request); AtomicReference<Vpc> vpcRef = new AtomicReference<>(); return paginator.subscribe(response -> { response.vpcs().stream() .findFirst() .ifPresent(vpcRef::set); }).thenApply(v -> { Vpc vpc = vpcRef.get(); if (vpc == null) { throw new RuntimeException("Default VPC not found"); } return vpc; }).exceptionally(ex -> { logger.info("Failed to describe VPCs: " + ex.getMessage()); throw new RuntimeException("Failed to describe VPCs", ex); }); } /** * Stops the EC2 instance with the specified ID asynchronously and waits for the instance to stop. * * @param instanceId the ID of the EC2 instance to stop * @return a {@link CompletableFuture} that completes when the instance has been stopped, or exceptionally if an error occurs */ public CompletableFuture<Void> stopInstanceAsync(String instanceId) { StopInstancesRequest stopRequest = StopInstancesRequest.builder() .instanceIds(instanceId) .build(); DescribeInstancesRequest describeRequest = DescribeInstancesRequest.builder() .instanceIds(instanceId) .build(); Ec2AsyncWaiter ec2Waiter = Ec2AsyncWaiter.builder() .client(getAsyncClient()) .build(); CompletableFuture<Void> resultFuture = new CompletableFuture<>(); logger.info("Stopping instance " + instanceId + " and waiting for it to stop."); getAsyncClient().stopInstances(stopRequest) .thenCompose(response -> { if (response.stoppingInstances().isEmpty()) { return CompletableFuture.failedFuture(new RuntimeException("No instances were stopped. Please check the instance ID: " + instanceId)); } return ec2Waiter.waitUntilInstanceStopped(describeRequest); }) .thenAccept(waiterResponse -> { logger.info("Successfully stopped instance " + instanceId); resultFuture.complete(null); }) .exceptionally(throwable -> { logger.error("Failed to stop instance " + instanceId + ": " + throwable.getMessage(), throwable); resultFuture.completeExceptionally(new RuntimeException("Failed to stop instance: " + throwable.getMessage(), throwable)); return null; }); return resultFuture; } /** * Starts an Amazon EC2 instance asynchronously and waits until it is in the "running" state. * * @param instanceId the ID of the instance to start * @return a {@link CompletableFuture} that completes when the instance has been started and is in the "running" state, or exceptionally if an error occurs */ public CompletableFuture<Void> startInstanceAsync(String instanceId) { StartInstancesRequest startRequest = StartInstancesRequest.builder() .instanceIds(instanceId) .build(); Ec2AsyncWaiter ec2Waiter = Ec2AsyncWaiter.builder() .client(getAsyncClient()) .build(); DescribeInstancesRequest describeRequest = DescribeInstancesRequest.builder() .instanceIds(instanceId) .build(); logger.info("Starting instance " + instanceId + " and waiting for it to run."); CompletableFuture<Void> resultFuture = new CompletableFuture<>(); return getAsyncClient().startInstances(startRequest) .thenCompose(response -> ec2Waiter.waitUntilInstanceRunning(describeRequest) ) .thenAccept(waiterResponse -> { logger.info("Successfully started instance " + instanceId); resultFuture.complete(null); }) .exceptionally(throwable -> { resultFuture.completeExceptionally(new RuntimeException("Failed to start instance: " + throwable.getMessage(), throwable)); return null; }); } }
-
For API details, see the following topics in Amazon SDK for Java 2.x API Reference.
-
- JavaScript
-
- SDK for JavaScript (v3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the Amazon Code Examples Repository
. This file contains a list of common actions used with EC2. The steps are constructed with a Scenario framework that simplifies running an interactive example. For the full context, visit the GitHub repository.
import { tmpdir } from "node:os"; import { writeFile, mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { get } from "node:http"; import { AllocateAddressCommand, AssociateAddressCommand, AuthorizeSecurityGroupIngressCommand, CreateKeyPairCommand, CreateSecurityGroupCommand, DeleteKeyPairCommand, DeleteSecurityGroupCommand, DisassociateAddressCommand, paginateDescribeImages, paginateDescribeInstances, paginateDescribeInstanceTypes, ReleaseAddressCommand, RunInstancesCommand, StartInstancesCommand, StopInstancesCommand, TerminateInstancesCommand, waitUntilInstanceStatusOk, waitUntilInstanceStopped, waitUntilInstanceTerminated, } from "@aws-sdk/client-ec2"; import { ScenarioAction, ScenarioInput, ScenarioOutput, } from "@aws-doc-sdk-examples/lib/scenario/index.js"; import { paginateGetParametersByPath, SSMClient } from "@aws-sdk/client-ssm"; /** * @typedef {{ * ec2Client: import('@aws-sdk/client-ec2').EC2Client, * errors: Error[], * keyPairId?: string, * tmpDirectory?: string, * securityGroupId?: string, * ipAddress?: string, * images?: import('@aws-sdk/client-ec2').Image[], * image?: import('@aws-sdk/client-ec2').Image, * instanceTypes?: import('@aws-sdk/client-ec2').InstanceTypeInfo[], * instanceId?: string, * instanceIpAddress?: string, * allocationId?: string, * allocatedIpAddress?: string, * associationId?: string, * }} State */ /** * A skip function provided to the `skipWhen` of a Step when you want * to ignore that step if any errors have occurred. * @param {State} state */ const skipWhenErrors = (state) => state.errors.length > 0; const MAX_WAITER_TIME_IN_SECONDS = 60 * 8; export const confirm = new ScenarioInput("confirmContinue", "Continue?", { type: "confirm", skipWhen: skipWhenErrors, }); export const exitOnNoConfirm = new ScenarioAction( "exitOnConfirmContinueFalse", (/** @type { { earlyExit: boolean } & Record<string, any>} */ state) => { if (!state[confirm.name]) { state.earlyExit = true; } }, { skipWhen: skipWhenErrors, }, ); export const greeting = new ScenarioOutput( "greeting", ` Welcome to the Amazon EC2 basic usage scenario. Before you launch an instances, you'll need to provide a few things: - A key pair - This is for SSH access to your EC2 instance. You only need to provide the name. - A security group - This is used for configuring access to your instance. Again, only the name is needed. - An IP address - Your public IP address will be fetched. - An Amazon Machine Image (AMI) - A compatible instance type`, { header: true, preformatted: true, skipWhen: skipWhenErrors }, ); export const provideKeyPairName = new ScenarioInput( "keyPairName", "Provide a name for a new key pair.", { type: "input", default: "ec2-example-key-pair", skipWhen: skipWhenErrors }, ); export const createKeyPair = new ScenarioAction( "createKeyPair", async (/** @type {State} */ state) => { try { // Create a key pair in Amazon EC2. const { KeyMaterial, KeyPairId } = await state.ec2Client.send( // A unique name for the key pair. Up to 255 ASCII characters. new CreateKeyPairCommand({ KeyName: state[provideKeyPairName.name] }), ); state.keyPairId = KeyPairId; // Save the private key in a temporary location. state.tmpDirectory = await mkdtemp(join(tmpdir(), "ec2-scenario-tmp")); await writeFile( `${state.tmpDirectory}/${state[provideKeyPairName.name]}.pem`, KeyMaterial, { mode: 0o400, }, ); } catch (caught) { if ( caught instanceof Error && caught.name === "InvalidKeyPair.Duplicate" ) { caught.message = `${caught.message}. Try another key name.`; } state.errors.push(caught); } }, { skipWhen: skipWhenErrors }, ); export const logKeyPair = new ScenarioOutput( "logKeyPair", (/** @type {State} */ state) => `Created the key pair ${state[provideKeyPairName.name]}.`, { skipWhen: skipWhenErrors }, ); export const confirmDeleteKeyPair = new ScenarioInput( "confirmDeleteKeyPair", "Do you want to delete the key pair?", { type: "confirm", // Don't do anything when a key pair was never created. skipWhen: (/** @type {State} */ state) => !state.keyPairId, }, ); export const maybeDeleteKeyPair = new ScenarioAction( "deleteKeyPair", async (/** @type {State} */ state) => { try { // Delete a key pair by name from EC2 await state.ec2Client.send( new DeleteKeyPairCommand({ KeyName: state[provideKeyPairName.name] }), ); } catch (caught) { if ( caught instanceof Error && // Occurs when a required parameter (e.g. KeyName) is undefined. caught.name === "MissingParameter" ) { caught.message = `${caught.message}. Did you provide the required value?`; } state.errors.push(caught); } }, { // Don't do anything when there's no key pair to delete or the user chooses // to keep it. skipWhen: (/** @type {State} */ state) => !state.keyPairId || !state[confirmDeleteKeyPair.name], }, ); export const provideSecurityGroupName = new ScenarioInput( "securityGroupName", "Provide a name for a new security group.", { type: "input", default: "ec2-scenario-sg", skipWhen: skipWhenErrors }, ); export const createSecurityGroup = new ScenarioAction( "createSecurityGroup", async (/** @type {State} */ state) => { try { // Create a new security group that will be used to configure ingress/egress for // an EC2 instance. const { GroupId } = await state.ec2Client.send( new CreateSecurityGroupCommand({ GroupName: state[provideSecurityGroupName.name], Description: "A security group for the Amazon EC2 example.", }), ); state.securityGroupId = GroupId; } catch (caught) { if (caught instanceof Error && caught.name === "InvalidGroup.Duplicate") { caught.message = `${caught.message}. Please provide a different name for your security group.`; } state.errors.push(caught); } }, { skipWhen: skipWhenErrors }, ); export const logSecurityGroup = new ScenarioOutput( "logSecurityGroup", (/** @type {State} */ state) => `Created the security group ${state.securityGroupId}.`, { skipWhen: skipWhenErrors }, ); export const confirmDeleteSecurityGroup = new ScenarioInput( "confirmDeleteSecurityGroup", "Do you want to delete the security group?", { type: "confirm", // Don't do anything when a security group was never created. skipWhen: (/** @type {State} */ state) => !state.securityGroupId, }, ); export const maybeDeleteSecurityGroup = new ScenarioAction( "deleteSecurityGroup", async (/** @type {State} */ state) => { try { // Delete the security group if the 'skipWhen' condition below is not met. await state.ec2Client.send( new DeleteSecurityGroupCommand({ GroupId: state.securityGroupId, }), ); } catch (caught) { if ( caught instanceof Error && caught.name === "InvalidGroupId.Malformed" ) { caught.message = `${caught.message}. Please provide a valid GroupId.`; } state.errors.push(caught); } }, { // Don't do anything when there's no security group to delete // or the user chooses to keep it. skipWhen: (/** @type {State} */ state) => !state.securityGroupId || !state[confirmDeleteSecurityGroup.name], }, ); export const authorizeSecurityGroupIngress = new ScenarioAction( "authorizeSecurity", async (/** @type {State} */ state) => { try { // Get the public IP address of the machine running this example. const ipAddress = await 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); }); }); state.ipAddress = ipAddress; // Allow ingress from the IP address above to the security group. // This will allow you to SSH into the EC2 instance. const command = new AuthorizeSecurityGroupIngressCommand({ GroupId: state.securityGroupId, IpPermissions: [ { IpProtocol: "tcp", FromPort: 22, ToPort: 22, IpRanges: [{ CidrIp: `${ipAddress}/32` }], }, ], }); await state.ec2Client.send(command); } catch (caught) { if ( caught instanceof Error && caught.name === "InvalidGroupId.Malformed" ) { caught.message = `${caught.message}. Please provide a valid GroupId.`; } state.errors.push(caught); } }, { skipWhen: skipWhenErrors }, ); export const logSecurityGroupIngress = new ScenarioOutput( "logSecurityGroupIngress", (/** @type {State} */ state) => `Allowed SSH access from your public IP: ${state.ipAddress}.`, { skipWhen: skipWhenErrors }, ); export const getImages = new ScenarioAction( "images", async (/** @type {State} */ state) => { const AMIs = []; // Some AWS services publish information about common artifacts as AWS Systems Manager (SSM) // public parameters. For example, the Amazon Elastic Compute Cloud (Amazon EC2) // service publishes information about Amazon Machine Images (AMIs) as public parameters. // Create the paginator for getting images. Actions that return multiple pages of // results have paginators to simplify those calls. const getParametersByPathPaginator = paginateGetParametersByPath( { // Not storing this client in state since it's only used once. client: new SSMClient({}), }, { // The path to the public list of the latest amazon-linux instances. Path: "/aws/service/ami-amazon-linux-latest", }, ); try { for await (const page of getParametersByPathPaginator) { for (const param of page.Parameters) { // Filter by Amazon Linux 2 if (param.Name.includes("amzn2")) { AMIs.push(param.Value); } } } } catch (caught) { if (caught instanceof Error && caught.name === "InvalidFilterValue") { caught.message = `${caught.message} Please provide a valid filter value for paginateGetParametersByPath.`; } state.errors.push(caught); return; } const imageDetails = []; const describeImagesPaginator = paginateDescribeImages( { client: state.ec2Client }, // The images found from the call to SSM. { ImageIds: AMIs }, ); try { // Get more details for the images found above. for await (const page of describeImagesPaginator) { imageDetails.push(...(page.Images || [])); } // Store the image details for later use. state.images = imageDetails; } catch (caught) { if (caught instanceof Error && caught.name === "InvalidAMIID.NotFound") { caught.message = `${caught.message}. Please provide a valid image id.`; } state.errors.push(caught); } }, { skipWhen: skipWhenErrors }, ); export const provideImage = new ScenarioInput( "image", "Select one of the following images.", { type: "select", choices: (/** @type { State } */ state) => state.images.map((image) => ({ name: `${image.Description}`, value: image, })), default: (/** @type { State } */ state) => state.images[0], skipWhen: skipWhenErrors, }, ); export const getCompatibleInstanceTypes = new ScenarioAction( "getCompatibleInstanceTypes", async (/** @type {State} */ state) => { // Get more details about instance types that match the architecture of // the provided image. const paginator = paginateDescribeInstanceTypes( { client: state.ec2Client, pageSize: 25 }, { Filters: [ { Name: "processor-info.supported-architecture", // The value selected from provideImage() Values: [state.image.Architecture], }, // Filter for smaller, less expensive, types. { Name: "instance-type", Values: ["*.micro", "*.small"] }, ], }, ); const instanceTypes = []; try { for await (const page of paginator) { if (page.InstanceTypes.length) { instanceTypes.push(...(page.InstanceTypes || [])); } } if (!instanceTypes.length) { state.errors.push( "No instance types matched the instance type filters.", ); } } catch (caught) { if (caught instanceof Error && caught.name === "InvalidParameterValue") { caught.message = `${caught.message}. Please check the provided values and try again.`; } state.errors.push(caught); } state.instanceTypes = instanceTypes; }, { skipWhen: skipWhenErrors }, ); export const provideInstanceType = new ScenarioInput( "instanceType", "Select an instance type.", { choices: (/** @type {State} */ state) => state.instanceTypes.map((instanceType) => ({ name: `${instanceType.InstanceType} - Memory:${instanceType.MemoryInfo.SizeInMiB}`, value: instanceType.InstanceType, })), type: "select", default: (/** @type {State} */ state) => state.instanceTypes[0].InstanceType, skipWhen: skipWhenErrors, }, ); export const runInstance = new ScenarioAction( "runInstance", async (/** @type { State } */ state) => { const { Instances } = await state.ec2Client.send( new RunInstancesCommand({ KeyName: state[provideKeyPairName.name], SecurityGroupIds: [state.securityGroupId], ImageId: state.image.ImageId, InstanceType: state[provideInstanceType.name], // Availability Zones have capacity limitations that may impact your ability to launch instances. // The `RunInstances` operation will only succeed if it can allocate at least the `MinCount` of instances. // However, EC2 will attempt to launch up to the `MaxCount` of instances, even if the full request cannot be satisfied. // If you need a specific number of instances, use `MinCount` and `MaxCount` set to the same value. // If you want to launch up to a certain number of instances, use `MaxCount` and let EC2 provision as many as possible. // If you require a minimum number of instances, but do not want to exceed a maximum, use both `MinCount` and `MaxCount`. MinCount: 1, MaxCount: 1, }), ); state.instanceId = Instances[0].InstanceId; try { // Poll `DescribeInstanceStatus` until status is "ok". await waitUntilInstanceStatusOk( { client: state.ec2Client, maxWaitTime: MAX_WAITER_TIME_IN_SECONDS, }, { InstanceIds: [Instances[0].InstanceId] }, ); } catch (caught) { if (caught instanceof Error && caught.name === "TimeoutError") { caught.message = `${caught.message}. Try increasing the maxWaitTime in the waiter.`; } state.errors.push(caught); } }, { skipWhen: skipWhenErrors }, ); export const logRunInstance = new ScenarioOutput( "logRunInstance", "The next step is to run your EC2 instance for the first time. This can take a few minutes.", { header: true, skipWhen: skipWhenErrors }, ); export const describeInstance = new ScenarioAction( "describeInstance", async (/** @type { State } */ state) => { /** @type { import("@aws-sdk/client-ec2").Instance[] } */ const instances = []; try { const paginator = paginateDescribeInstances( { client: state.ec2Client, }, { // Only get our created instance. InstanceIds: [state.instanceId], }, ); for await (const page of paginator) { for (const reservation of page.Reservations) { instances.push(...reservation.Instances); } } if (instances.length !== 1) { throw new Error(`Instance ${state.instanceId} not found.`); } // The only info we need is the IP address for SSH purposes. state.instanceIpAddress = instances[0].PublicIpAddress; } catch (caught) { if (caught instanceof Error && caught.name === "InvalidParameterValue") { caught.message = `${caught.message}. Please check provided values and try again.`; } state.errors.push(caught); } }, { skipWhen: skipWhenErrors }, ); export const logSSHConnectionInfo = new ScenarioOutput( "logSSHConnectionInfo", (/** @type { State } */ state) => `You can now SSH into your instance using the following command: ssh -i ${state.tmpDirectory}/${state[provideKeyPairName.name]}.pem ec2-user@${state.instanceIpAddress}`, { preformatted: true, skipWhen: skipWhenErrors }, ); export const logStopInstance = new ScenarioOutput( "logStopInstance", "Stopping your EC2 instance.", { skipWhen: skipWhenErrors }, ); export const stopInstance = new ScenarioAction( "stopInstance", async (/** @type { State } */ state) => { try { await state.ec2Client.send( new StopInstancesCommand({ InstanceIds: [state.instanceId], }), ); await waitUntilInstanceStopped( { client: state.ec2Client, maxWaitTime: MAX_WAITER_TIME_IN_SECONDS, }, { InstanceIds: [state.instanceId] }, ); } catch (caught) { if (caught instanceof Error && caught.name === "TimeoutError") { caught.message = `${caught.message}. Try increasing the maxWaitTime in the waiter.`; } state.errors.push(caught); } }, // Don't try to stop an instance that doesn't exist. { skipWhen: (/** @type { State } */ state) => !state.instanceId }, ); export const logIpAddressBehavior = new ScenarioOutput( "logIpAddressBehavior", [ "When you run an instance, by default it's assigned an IP address.", "That IP address is not static. It will change every time the instance is restarted.", "The next step is to stop and restart your instance to demonstrate this behavior.", ].join(" "), { header: true, skipWhen: skipWhenErrors }, ); export const logStartInstance = new ScenarioOutput( "logStartInstance", (/** @type { State } */ state) => `Starting instance ${state.instanceId}`, { skipWhen: skipWhenErrors }, ); export const startInstance = new ScenarioAction( "startInstance", async (/** @type { State } */ state) => { try { await state.ec2Client.send( new StartInstancesCommand({ InstanceIds: [state.instanceId], }), ); await waitUntilInstanceStatusOk( { client: state.ec2Client, maxWaitTime: MAX_WAITER_TIME_IN_SECONDS, }, { InstanceIds: [state.instanceId] }, ); } catch (caught) { if (caught instanceof Error && caught.name === "TimeoutError") { caught.message = `${caught.message}. Try increasing the maxWaitTime in the waiter.`; } state.errors.push(caught); } }, { skipWhen: skipWhenErrors }, ); export const logIpAllocation = new ScenarioOutput( "logIpAllocation", [ "It is possible to have a static IP address.", "To demonstrate this, an IP will be allocated and associated to your EC2 instance.", ].join(" "), { header: true, skipWhen: skipWhenErrors }, ); export const allocateIp = new ScenarioAction( "allocateIp", async (/** @type { State } */ state) => { try { // An Elastic IP address is allocated to your AWS account, and is yours until you release it. const { AllocationId, PublicIp } = await state.ec2Client.send( new AllocateAddressCommand({}), ); state.allocationId = AllocationId; state.allocatedIpAddress = PublicIp; } catch (caught) { if (caught instanceof Error && caught.name === "MissingParameter") { caught.message = `${caught.message}. Did you provide these values?`; } state.errors.push(caught); } }, { skipWhen: skipWhenErrors }, ); export const associateIp = new ScenarioAction( "associateIp", async (/** @type { State } */ state) => { try { // Associate an allocated IP address to an EC2 instance. An IP address can be allocated // with the AllocateAddress action. const { AssociationId } = await state.ec2Client.send( new AssociateAddressCommand({ AllocationId: state.allocationId, InstanceId: state.instanceId, }), ); state.associationId = AssociationId; // Update the IP address that is being tracked to match // the one just associated. state.instanceIpAddress = state.allocatedIpAddress; } catch (caught) { if ( caught instanceof Error && caught.name === "InvalidAllocationID.NotFound" ) { caught.message = `${caught.message}. Did you provide the ID of a valid Elastic IP address AllocationId?`; } state.errors.push(caught); } }, { skipWhen: skipWhenErrors }, ); export const logStaticIpProof = new ScenarioOutput( "logStaticIpProof", "The IP address should remain the same even after stopping and starting the instance.", { header: true, skipWhen: skipWhenErrors }, ); export const logCleanUp = new ScenarioOutput( "logCleanUp", "That's it! You can choose to clean up the resources now, or clean them up on your own later.", { header: true, skipWhen: skipWhenErrors }, ); export const confirmDisassociateAddress = new ScenarioInput( "confirmDisassociateAddress", "Do you want to disassociate and release the static IP address created earlier?", { type: "confirm", skipWhen: (/** @type { State } */ state) => !state.associationId, }, ); export const maybeDisassociateAddress = new ScenarioAction( "maybeDisassociateAddress", async (/** @type { State } */ state) => { try { await state.ec2Client.send( new DisassociateAddressCommand({ AssociationId: state.associationId, }), ); } catch (caught) { if ( caught instanceof Error && caught.name === "InvalidAssociationID.NotFound" ) { caught.message = `${caught.message}. Please provide a valid association ID.`; } state.errors.push(caught); } }, { skipWhen: (/** @type { State } */ state) => !state[confirmDisassociateAddress.name] || !state.associationId, }, ); export const maybeReleaseAddress = new ScenarioAction( "maybeReleaseAddress", async (/** @type { State } */ state) => { try { await state.ec2Client.send( new ReleaseAddressCommand({ AllocationId: state.allocationId, }), ); } catch (caught) { if ( caught instanceof Error && caught.name === "InvalidAllocationID.NotFound" ) { caught.message = `${caught.message}. Please provide a valid AllocationID.`; } state.errors.push(caught); } }, { skipWhen: (/** @type { State } */ state) => !state[confirmDisassociateAddress.name] || !state.allocationId, }, ); export const confirmTerminateInstance = new ScenarioInput( "confirmTerminateInstance", "Do you want to terminate the instance?", // Don't do anything when an instance was never run. { skipWhen: (/** @type { State } */ state) => !state.instanceId, type: "confirm", }, ); export const maybeTerminateInstance = new ScenarioAction( "terminateInstance", async (/** @type { State } */ state) => { try { await state.ec2Client.send( new TerminateInstancesCommand({ InstanceIds: [state.instanceId], }), ); await waitUntilInstanceTerminated( { client: state.ec2Client }, { InstanceIds: [state.instanceId] }, ); } catch (caught) { if (caught instanceof Error && caught.name === "TimeoutError") { caught.message = `${caught.message}. Try increasing the maxWaitTime in the waiter.`; } state.errors.push(caught); } }, { // Don't do anything when there's no instance to terminate or the // use chooses not to terminate. skipWhen: (/** @type { State } */ state) => !state.instanceId || !state[confirmTerminateInstance.name], }, ); export const deleteTemporaryDirectory = new ScenarioAction( "deleteTemporaryDirectory", async (/** @type { State } */ state) => { try { await rm(state.tmpDirectory, { recursive: true }); } catch (caught) { state.errors.push(caught); } }, ); export const logErrors = new ScenarioOutput( "logErrors", (/** @type {State}*/ state) => { const errorList = state.errors .map((err) => ` - ${err.name}: ${err.message}`) .join("\n"); return `Scenario errors found:\n${errorList}`; }, { preformatted: true, header: true, // Don't log errors when there aren't any! skipWhen: (/** @type {State} */ state) => state.errors.length === 0, }, );
-
For API details, see the following topics in Amazon SDK for JavaScript API Reference.
-
- Kotlin
-
- SDK for Kotlin
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the Amazon Code Examples Repository
. /** Before running this Kotlin code example, set up your development environment, including your credentials. For more information, see the following documentation topic: https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/setup.html This Kotlin example performs the following tasks: 1. Creates an RSA key pair and saves the private key data as a .pem file. 2. Lists key pairs. 3. Creates a security group for the default VPC. 4. Displays security group information. 5. Gets a list of Amazon Linux 2 AMIs and selects one. 6. Gets more information about the image. 7. Gets a list of instance types that are compatible with the selected AMI’s architecture. 8. Creates an instance with the key pair, security group, AMI, and an instance type. 9. Displays information about the instance. 10. Stops the instance and waits for it to stop. 11. Starts the instance and waits for it to start. 12. Allocates an Elastic IP address and associates it with the instance. 13. Displays SSH connection info for the instance. 14. Disassociates and deletes the Elastic IP address. 15. Terminates the instance. 16. Deletes the security group. 17. Deletes the key pair. */ val DASHES = String(CharArray(80)).replace("\u0000", "-") suspend fun main(args: Array<String>) { val usage = """ Usage: <keyName> <fileName> <groupName> <groupDesc> <vpcId> <myIpAddress> Where: keyName - A key pair name (for example, TestKeyPair). fileName - A file name where the key information is written to. groupName - The name of the security group. groupDesc - The description of the security group. vpcId - A VPC ID. You can get this value from the AWS Management Console. myIpAddress - The IP address of your development machine. """ if (args.size != 6) { println(usage) exitProcess(0) } val keyName = args[0] val fileName = args[1] val groupName = args[2] val groupDesc = args[3] val vpcId = args[4] val myIpAddress = args[5] var newInstanceId: String? = "" println(DASHES) println("Welcome to the Amazon EC2 example scenario.") println(DASHES) println(DASHES) println("1. Create an RSA key pair and save the private key material as a .pem file.") createKeyPairSc(keyName, fileName) println(DASHES) println(DASHES) println("2. List key pairs.") describeEC2KeysSc() println(DASHES) println(DASHES) println("3. Create a security group.") val groupId = createEC2SecurityGroupSc(groupName, groupDesc, vpcId, myIpAddress) println(DASHES) println(DASHES) println("4. Display security group info for the newly created security group.") describeSecurityGroupsSc(groupId.toString()) println(DASHES) println(DASHES) println("5. Get a list of Amazon Linux 2 AMIs and select one with amzn2 in the name.") val instanceId = getParaValuesSc() if (instanceId == "") { println("The instance Id value isn't valid.") exitProcess(0) } println("The instance Id is $instanceId.") println(DASHES) println(DASHES) println("6. Get more information about an amzn2 image and return the AMI value.") val amiValue = instanceId?.let { describeImageSc(it) } if (instanceId == "") { println("The instance Id value is invalid.") exitProcess(0) } println("The AMI value is $amiValue.") println(DASHES) println(DASHES) println("7. Get a list of instance types.") val instanceType = getInstanceTypesSc() println(DASHES) println(DASHES) println("8. Create an instance.") if (amiValue != null) { newInstanceId = runInstanceSc(instanceType, keyName, groupName, amiValue) println("The instance Id is $newInstanceId") } println(DASHES) println(DASHES) println("9. Display information about the running instance. ") var ipAddress = describeEC2InstancesSc(newInstanceId) println("You can SSH to the instance using this command:") println("ssh -i " + fileName + "ec2-user@" + ipAddress) println(DASHES) println(DASHES) println("10. Stop the instance.") if (newInstanceId != null) { stopInstanceSc(newInstanceId) } println(DASHES) println(DASHES) println("11. Start the instance.") if (newInstanceId != null) { startInstanceSc(newInstanceId) } ipAddress = describeEC2InstancesSc(newInstanceId) println("You can SSH to the instance using this command:") println("ssh -i " + fileName + "ec2-user@" + ipAddress) println(DASHES) println(DASHES) println("12. Allocate an Elastic IP address and associate it with the instance.") val allocationId = allocateAddressSc() println("The allocation Id value is $allocationId") val associationId = associateAddressSc(newInstanceId, allocationId) println("The associate Id value is $associationId") println(DASHES) println(DASHES) println("13. Describe the instance again.") ipAddress = describeEC2InstancesSc(newInstanceId) println("You can SSH to the instance using this command:") println("ssh -i " + fileName + "ec2-user@" + ipAddress) println(DASHES) println(DASHES) println("14. Disassociate and release the Elastic IP address.") disassociateAddressSc(associationId) releaseEC2AddressSc(allocationId) println(DASHES) println(DASHES) println("15. Terminate the instance and use a waiter.") if (newInstanceId != null) { terminateEC2Sc(newInstanceId) } println(DASHES) println(DASHES) println("16. Delete the security group.") if (groupId != null) { deleteEC2SecGroupSc(groupId) } println(DASHES) println(DASHES) println("17. Delete the key pair.") deleteKeysSc(keyName) println(DASHES) println(DASHES) println("You successfully completed the Amazon EC2 scenario.") println(DASHES) } suspend fun deleteKeysSc(keyPair: String) { val request = DeleteKeyPairRequest { keyName = keyPair } Ec2Client { region = "us-west-2" }.use { ec2 -> ec2.deleteKeyPair(request) println("Successfully deleted key pair named $keyPair") } } suspend fun deleteEC2SecGroupSc(groupIdVal: String) { val request = DeleteSecurityGroupRequest { groupId = groupIdVal } Ec2Client { region = "us-west-2" }.use { ec2 -> ec2.deleteSecurityGroup(request) println("Successfully deleted security group with Id $groupIdVal") } } suspend fun terminateEC2Sc(instanceIdVal: String) { val ti = TerminateInstancesRequest { instanceIds = listOf(instanceIdVal) } println("Wait for the instance to terminate. This will take a few minutes.") Ec2Client { region = "us-west-2" }.use { ec2 -> ec2.terminateInstances(ti) ec2.waitUntilInstanceTerminated { // suspend call instanceIds = listOf(instanceIdVal) } println("$instanceIdVal is terminated!") } } suspend fun releaseEC2AddressSc(allocId: String?) { val request = ReleaseAddressRequest { allocationId = allocId } Ec2Client { region = "us-west-2" }.use { ec2 -> ec2.releaseAddress(request) println("Successfully released Elastic IP address $allocId") } } suspend fun disassociateAddressSc(associationIdVal: String?) { val addressRequest = DisassociateAddressRequest { associationId = associationIdVal } Ec2Client { region = "us-west-2" }.use { ec2 -> ec2.disassociateAddress(addressRequest) println("You successfully disassociated the address!") } } suspend fun associateAddressSc( instanceIdVal: String?, allocationIdVal: String?, ): String? { val associateRequest = AssociateAddressRequest { instanceId = instanceIdVal allocationId = allocationIdVal } Ec2Client { region = "us-west-2" }.use { ec2 -> val associateResponse = ec2.associateAddress(associateRequest) return associateResponse.associationId } } suspend fun allocateAddressSc(): String? { val allocateRequest = AllocateAddressRequest { domain = DomainType.Vpc } Ec2Client { region = "us-west-2" }.use { ec2 -> val allocateResponse = ec2.allocateAddress(allocateRequest) return allocateResponse.allocationId } } suspend fun startInstanceSc(instanceId: String) { val request = StartInstancesRequest { instanceIds = listOf(instanceId) } Ec2Client { region = "us-west-2" }.use { ec2 -> ec2.startInstances(request) println("Waiting until instance $instanceId starts. This will take a few minutes.") ec2.waitUntilInstanceRunning { // suspend call instanceIds = listOf(instanceId) } println("Successfully started instance $instanceId") } } suspend fun stopInstanceSc(instanceId: String) { val request = StopInstancesRequest { instanceIds = listOf(instanceId) } Ec2Client { region = "us-west-2" }.use { ec2 -> ec2.stopInstances(request) println("Waiting until instance $instanceId stops. This will take a few minutes.") ec2.waitUntilInstanceStopped { // suspend call instanceIds = listOf(instanceId) } println("Successfully stopped instance $instanceId") } } suspend fun describeEC2InstancesSc(newInstanceId: String?): String { var pubAddress = "" var isRunning = false val request = DescribeInstancesRequest { instanceIds = listOf(newInstanceId.toString()) } while (!isRunning) { Ec2Client { region = "us-west-2" }.use { ec2 -> val response = ec2.describeInstances(request) val state = response.reservations ?.get(0) ?.instances ?.get(0) ?.state ?.name ?. value if (state != null) { if (state.compareTo("running") == 0) { println("Image id is ${response.reservations!!.get(0).instances?.get(0)?.imageId}") println("Instance type is ${response.reservations!!.get(0).instances?.get(0)?.instanceType}") println("Instance state is ${response.reservations!!.get(0).instances?.get(0)?.state}") pubAddress = response.reservations!! .get(0) .instances ?.get(0) ?.publicIpAddress .toString() println("Instance address is $pubAddress") isRunning = true } } } } return pubAddress } suspend fun runInstanceSc( instanceTypeVal: String, keyNameVal: String, groupNameVal: String, amiIdVal: String, ): String { val runRequest = RunInstancesRequest { instanceType = InstanceType.fromValue(instanceTypeVal) keyName = keyNameVal securityGroups = listOf(groupNameVal) maxCount = 1 minCount = 1 imageId = amiIdVal } Ec2Client { region = "us-west-2" }.use { ec2 -> val response = ec2.runInstances(runRequest) val instanceId = response.instances?.get(0)?.instanceId println("Successfully started EC2 Instance $instanceId based on AMI $amiIdVal") return instanceId.toString() } } // Get a list of instance types. suspend fun getInstanceTypesSc(): String { var instanceType = "" val filterObs = ArrayList<Filter>() val filter = Filter { name = "processor-info.supported-architecture" values = listOf("arm64") } filterObs.add(filter) val typesRequest = DescribeInstanceTypesRequest { filters = filterObs maxResults = 10 } Ec2Client { region = "us-west-2" }.use { ec2 -> val response = ec2.describeInstanceTypes(typesRequest) response.instanceTypes?.forEach { type -> println("The memory information of this type is ${type.memoryInfo?.sizeInMib}") println("Maximum number of network cards is ${type.networkInfo?.maximumNetworkCards}") instanceType = type.instanceType.toString() } return instanceType } } // Display the Description field that corresponds to the instance Id value. suspend fun describeImageSc(instanceId: String): String? { val imagesRequest = DescribeImagesRequest { imageIds = listOf(instanceId) } Ec2Client { region = "us-west-2" }.use { ec2 -> val response = ec2.describeImages(imagesRequest) println("The description of the first image is ${response.images?.get(0)?.description}") println("The name of the first image is ${response.images?.get(0)?.name}") // Return the image Id value. return response.images?.get(0)?.imageId } } // Get the Id value of an instance with amzn2 in the name. suspend fun getParaValuesSc(): String? { val parameterRequest = GetParametersByPathRequest { path = "/aws/service/ami-amazon-linux-latest" } SsmClient { region = "us-west-2" }.use { ssmClient -> val response = ssmClient.getParametersByPath(parameterRequest) response.parameters?.forEach { para -> println("The name of the para is: ${para.name}") println("The type of the para is: ${para.type}") println("") if (para.name?.let { filterName(it) } == true) { return para.value } } } return "" } fun filterName(name: String): Boolean { val parts = name.split("/").toTypedArray() val myValue = parts[4] return myValue.contains("amzn2") } suspend fun describeSecurityGroupsSc(groupId: String) { val request = DescribeSecurityGroupsRequest { groupIds = listOf(groupId) } Ec2Client { region = "us-west-2" }.use { ec2 -> val response = ec2.describeSecurityGroups(request) for (group in response.securityGroups!!) { println("Found Security Group with id " + group.groupId.toString() + " and group VPC " + group.vpcId) } } } suspend fun createEC2SecurityGroupSc( groupNameVal: String?, groupDescVal: String?, vpcIdVal: String?, myIpAddress: String?, ): String? { val request = CreateSecurityGroupRequest { groupName = groupNameVal description = groupDescVal vpcId = vpcIdVal } Ec2Client { region = "us-west-2" }.use { ec2 -> val resp = ec2.createSecurityGroup(request) val ipRange = IpRange { cidrIp = "$myIpAddress/0" } val ipPerm = IpPermission { ipProtocol = "tcp" toPort = 80 fromPort = 80 ipRanges = listOf(ipRange) } val ipPerm2 = IpPermission { ipProtocol = "tcp" toPort = 22 fromPort = 22 ipRanges = listOf(ipRange) } val authRequest = AuthorizeSecurityGroupIngressRequest { groupName = groupNameVal ipPermissions = listOf(ipPerm, ipPerm2) } ec2.authorizeSecurityGroupIngress(authRequest) println("Successfully added ingress policy to Security Group $groupNameVal") return resp.groupId } } suspend fun describeEC2KeysSc() { Ec2Client { region = "us-west-2" }.use { ec2 -> val response = ec2.describeKeyPairs(DescribeKeyPairsRequest {}) response.keyPairs?.forEach { keyPair -> println("Found key pair with name ${keyPair.keyName} and fingerprint ${ keyPair.keyFingerprint}") } } } suspend fun createKeyPairSc( keyNameVal: String, fileNameVal: String, ) { val request = CreateKeyPairRequest { keyName = keyNameVal } Ec2Client { region = "us-west-2" }.use { ec2 -> val response = ec2.createKeyPair(request) val content = response.keyMaterial if (content != null) { File(fileNameVal).writeText(content) } println("Successfully created key pair named $keyNameVal") } }
-
For API details, see the following topics in Amazon SDK for Kotlin API reference.
-
- Python
-
- SDK for Python (Boto3)
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the Amazon Code Examples Repository
. Run an interactive scenario at a command prompt.
class EC2InstanceScenario: """ A scenario that demonstrates how to use Boto3 to manage Amazon EC2 resources. Covers creating a key pair, security group, launching an instance, associating an Elastic IP, and cleaning up resources. """ def __init__( self, inst_wrapper: EC2InstanceWrapper, key_wrapper: KeyPairWrapper, sg_wrapper: SecurityGroupWrapper, eip_wrapper: ElasticIpWrapper, ssm_client: boto3.client, remote_exec: bool = False, ): """ Initializes the EC2InstanceScenario with the necessary AWS service wrappers. :param inst_wrapper: Wrapper for EC2 instance operations. :param key_wrapper: Wrapper for key pair operations. :param sg_wrapper: Wrapper for security group operations. :param eip_wrapper: Wrapper for Elastic IP operations. :param ssm_client: Boto3 client for accessing SSM to retrieve AMIs. :param remote_exec: Flag to indicate if the scenario is running in a remote execution environment. Defaults to False. If True, the script won't prompt for user interaction. """ self.inst_wrapper = inst_wrapper self.key_wrapper = key_wrapper self.sg_wrapper = sg_wrapper self.eip_wrapper = eip_wrapper self.ssm_client = ssm_client self.remote_exec = remote_exec def create_and_list_key_pairs(self) -> None: """ Creates an RSA key pair for SSH access to the EC2 instance and lists available key pairs. """ console.print("**Step 1: Create a Secure Key Pair**", style="bold cyan") console.print( "Let's create a secure RSA key pair for connecting to your EC2 instance." ) key_name = f"MyUniqueKeyPair-{uuid.uuid4().hex[:8]}" console.print(f"- **Key Pair Name**: {key_name}") # Create the key pair and simulate the process with a progress bar. with alive_bar(1, title="Creating Key Pair") as bar: self.key_wrapper.create(key_name) time.sleep(0.4) # Simulate the delay in key creation bar() console.print(f"- **Private Key Saved to**: {self.key_wrapper.key_file_path}\n") # List key pairs (simulated) and show a progress bar. list_keys = True if list_keys: console.print("- Listing your key pairs...") start_time = time.time() with alive_bar(100, title="Listing Key Pairs") as bar: while time.time() - start_time < 2: time.sleep(0.2) bar(10) self.key_wrapper.list(5) if time.time() - start_time > 2: console.print( "Taking longer than expected! Please wait...", style="bold yellow", ) def create_security_group(self) -> None: """ Creates a security group that controls access to the EC2 instance and adds a rule to allow SSH access from the user's current public IP address. """ console.print("**Step 2: Create a Security Group**", style="bold cyan") console.print( "Security groups manage access to your instance. Let's create one." ) sg_name = f"MySecurityGroup-{uuid.uuid4().hex[:8]}" console.print(f"- **Security Group Name**: {sg_name}") # Create the security group and simulate the process with a progress bar. with alive_bar(1, title="Creating Security Group") as bar: self.sg_wrapper.create( sg_name, "Security group for example: get started with instances." ) time.sleep(0.5) bar() console.print(f"- **Security Group ID**: {self.sg_wrapper.security_group}\n") # Get the current public IP to set up SSH access. ip_response = urllib.request.urlopen("http://checkip.amazonaws.com") current_ip_address = ip_response.read().decode("utf-8").strip() console.print( "Let's add a rule to allow SSH only from your current IP address." ) console.print(f"- **Your Public IP Address**: {current_ip_address}") console.print("- Automatically adding SSH rule...") # Update security group rules to allow SSH and simulate with a progress bar. with alive_bar(1, title="Updating Security Group Rules") as bar: response = self.sg_wrapper.authorize_ingress(current_ip_address) time.sleep(0.4) if response and response.get("Return"): console.print("- **Security Group Rules Updated**.") else: console.print( "- **Error**: Couldn't update security group rules.", style="bold red", ) bar() self.sg_wrapper.describe(self.sg_wrapper.security_group) def create_instance(self) -> None: """ Launches an EC2 instance using an Amazon Linux 2 AMI and the created key pair and security group. Displays instance details and SSH connection information. """ # Retrieve Amazon Linux 2 AMIs from SSM. ami_paginator = self.ssm_client.get_paginator("get_parameters_by_path") ami_options = [] for page in ami_paginator.paginate(Path="/aws/service/ami-amazon-linux-latest"): ami_options += page["Parameters"] amzn2_images = self.inst_wrapper.get_images( [opt["Value"] for opt in ami_options if "amzn2" in opt["Name"]] ) console.print("\n**Step 3: Launch Your Instance**", style="bold cyan") console.print( "Let's create an instance from an Amazon Linux 2 AMI. Here are some options:" ) image_choice = 0 console.print(f"- Selected AMI: {amzn2_images[image_choice]['ImageId']}\n") # Display instance types compatible with the selected AMI inst_types = self.inst_wrapper.get_instance_types( amzn2_images[image_choice]["Architecture"] ) inst_type_choice = 0 console.print( f"- Selected instance type: {inst_types[inst_type_choice]['InstanceType']}\n" ) console.print("Creating your instance and waiting for it to start...") with alive_bar(1, title="Creating Instance") as bar: self.inst_wrapper.create( amzn2_images[image_choice]["ImageId"], inst_types[inst_type_choice]["InstanceType"], self.key_wrapper.key_pair["KeyName"], [self.sg_wrapper.security_group], ) time.sleep(21) bar() console.print(f"**Success! Your instance is ready:**\n", style="bold green") self.inst_wrapper.display() console.print( "You can use SSH to connect to your instance. " "If the connection attempt times out, you might have to manually update " "the SSH ingress rule for your IP address in the AWS Management Console." ) self._display_ssh_info() def _display_ssh_info(self) -> None: """ Displays SSH connection information for the user to connect to the EC2 instance. Handles the case where the instance does or does not have an associated public IP address. """ if ( not self.eip_wrapper.elastic_ips or not self.eip_wrapper.elastic_ips[0].allocation_id ): if self.inst_wrapper.instances: instance = self.inst_wrapper.instances[0] instance_id = instance["InstanceId"] waiter = self.inst_wrapper.ec2_client.get_waiter("instance_running") console.print( "Waiting for the instance to be in a running state with a public IP...", style="bold cyan", ) with alive_bar(1, title="Waiting for Instance to Start") as bar: waiter.wait(InstanceIds=[instance_id]) time.sleep(20) bar() instance = self.inst_wrapper.ec2_client.describe_instances( InstanceIds=[instance_id] )["Reservations"][0]["Instances"][0] public_ip = instance.get("PublicIpAddress") if public_ip: console.print( "\nTo connect via SSH, open another command prompt and run the following command:", style="bold cyan", ) console.print( f"\tssh -i {self.key_wrapper.key_file_path} ec2-user@{public_ip}" ) else: console.print( "Instance does not have a public IP address assigned.", style="bold red", ) else: console.print( "No instance available to retrieve public IP address.", style="bold red", ) else: elastic_ip = self.eip_wrapper.elastic_ips[0] elastic_ip_address = elastic_ip.public_ip console.print( f"\tssh -i {self.key_wrapper.key_file_path} ec2-user@{elastic_ip_address}" ) if not self.remote_exec: console.print("\nOpen a new terminal tab to try the above SSH command.") input("Press Enter to continue...") def associate_elastic_ip(self) -> None: """ Allocates an Elastic IP address and associates it with the EC2 instance. Displays the Elastic IP address and SSH connection information. """ console.print("\n**Step 4: Allocate an Elastic IP Address**", style="bold cyan") console.print( "You can allocate an Elastic IP address and associate it with your instance\n" "to keep a consistent IP address even when your instance restarts." ) with alive_bar(1, title="Allocating Elastic IP") as bar: elastic_ip = self.eip_wrapper.allocate() time.sleep(0.5) bar() console.print( f"- **Allocated Static Elastic IP Address**: {elastic_ip.public_ip}." ) with alive_bar(1, title="Associating Elastic IP") as bar: self.eip_wrapper.associate( elastic_ip.allocation_id, self.inst_wrapper.instances[0]["InstanceId"] ) time.sleep(2) bar() console.print(f"- **Associated Elastic IP with Your Instance**.") console.print( "You can now use SSH to connect to your instance by using the Elastic IP." ) self._display_ssh_info() def stop_and_start_instance(self) -> None: """ Stops and restarts the EC2 instance. Displays instance state and explains changes that occur when the instance is restarted, such as the potential change in the public IP address unless an Elastic IP is associated. """ console.print("\n**Step 5: Stop and Start Your Instance**", style="bold cyan") console.print("Let's stop and start your instance to see what changes.") console.print("- **Stopping your instance and waiting until it's stopped...**") with alive_bar(1, title="Stopping Instance") as bar: self.inst_wrapper.stop() time.sleep(360) bar() console.print("- **Your instance is stopped. Restarting...**") with alive_bar(1, title="Starting Instance") as bar: self.inst_wrapper.start() time.sleep(20) bar() console.print("**Your instance is running.**", style="bold green") self.inst_wrapper.display() elastic_ip = ( self.eip_wrapper.elastic_ips[0] if self.eip_wrapper.elastic_ips else None ) if elastic_ip is None or elastic_ip.allocation_id is None: console.print( "- **Note**: Every time your instance is restarted, its public IP address changes." ) else: console.print( f"Because you have associated an Elastic IP with your instance, you can \n" f"connect by using a consistent IP address after the instance restarts: {elastic_ip.public_ip}" ) self._display_ssh_info() def cleanup(self) -> None: """ Cleans up all the resources created during the scenario, including disassociating and releasing the Elastic IP, terminating the instance, deleting the security group, and deleting the key pair. """ console.print("\n**Step 6: Clean Up Resources**", style="bold cyan") console.print("Cleaning up resources:") for elastic_ip in self.eip_wrapper.elastic_ips: console.print(f"- **Elastic IP**: {elastic_ip.public_ip}") with alive_bar(1, title="Disassociating Elastic IP") as bar: self.eip_wrapper.disassociate(elastic_ip.allocation_id) time.sleep(2) bar() console.print("\t- **Disassociated Elastic IP from the Instance**") with alive_bar(1, title="Releasing Elastic IP") as bar: self.eip_wrapper.release(elastic_ip.allocation_id) time.sleep(1) bar() console.print("\t- **Released Elastic IP**") console.print(f"- **Instance**: {self.inst_wrapper.instances[0]['InstanceId']}") with alive_bar(1, title="Terminating Instance") as bar: self.inst_wrapper.terminate() time.sleep(380) bar() console.print("\t- **Terminated Instance**") console.print(f"- **Security Group**: {self.sg_wrapper.security_group}") with alive_bar(1, title="Deleting Security Group") as bar: self.sg_wrapper.delete(self.sg_wrapper.security_group) time.sleep(1) bar() console.print("\t- **Deleted Security Group**") console.print(f"- **Key Pair**: {self.key_wrapper.key_pair['KeyName']}") with alive_bar(1, title="Deleting Key Pair") as bar: self.key_wrapper.delete(self.key_wrapper.key_pair["KeyName"]) time.sleep(0.4) bar() console.print("\t- **Deleted Key Pair**") def run_scenario(self) -> None: """ Executes the entire EC2 instance scenario: creates key pairs, security groups, launches an instance, associates an Elastic IP, and cleans up all resources. """ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") console.print("-" * 88) console.print( "Welcome to the Amazon Elastic Compute Cloud (Amazon EC2) get started with instances demo.", style="bold magenta", ) console.print("-" * 88) self.create_and_list_key_pairs() self.create_security_group() self.create_instance() self.stop_and_start_instance() self.associate_elastic_ip() self.stop_and_start_instance() self.cleanup() console.print("\nThanks for watching!", style="bold green") console.print("-" * 88) if __name__ == "__main__": try: scenario = EC2InstanceScenario( EC2InstanceWrapper.from_client(), KeyPairWrapper.from_client(), SecurityGroupWrapper.from_client(), ElasticIpWrapper.from_client(), boto3.client("ssm"), ) scenario.run_scenario() except Exception: logging.exception("Something went wrong with the demo.")
Define a class that wraps key pair actions.
class KeyPairWrapper: """ Encapsulates Amazon Elastic Compute Cloud (Amazon EC2) key pair actions. This class provides methods to create, list, and delete EC2 key pairs. """ def __init__( self, ec2_client: boto3.client, key_file_dir: Union[tempfile.TemporaryDirectory, str], key_pair: Optional[dict] = None, ): """ Initializes the KeyPairWrapper with the specified EC2 client, key file directory, and an optional key pair. :param ec2_client: A Boto3 Amazon EC2 client. This client provides low-level access to AWS EC2 services. :param key_file_dir: The folder where the private key information is stored. This should be a secure folder. :param key_pair: A dictionary representing the Boto3 KeyPair object. This is a high-level object that wraps key pair actions. Optional. """ self.ec2_client = ec2_client self.key_pair = key_pair self.key_file_path: Optional[str] = None self.key_file_dir = key_file_dir @classmethod def from_client(cls) -> "KeyPairWrapper": """ Class method to create an instance of KeyPairWrapper using a new EC2 client and a temporary directory for storing key files. :return: An instance of KeyPairWrapper. """ ec2_client = boto3.client("ec2") return cls(ec2_client, tempfile.TemporaryDirectory()) def create(self, key_name: str) -> dict: """ Creates a key pair that can be used to securely connect to an EC2 instance. The returned key pair contains private key information that cannot be retrieved again. The private key data is stored as a .pem file. :param key_name: The name of the key pair to create. :return: A dictionary representing the Boto3 KeyPair object that represents the newly created key pair. :raises ClientError: If there is an error in creating the key pair, for example, if a key pair with the same name already exists. """ try: response = self.ec2_client.create_key_pair(KeyName=key_name) self.key_pair = response self.key_file_path = os.path.join( self.key_file_dir.name, f"{self.key_pair['KeyName']}.pem" ) with open(self.key_file_path, "w") as key_file: key_file.write(self.key_pair["KeyMaterial"]) except ClientError as err: if err.response["Error"]["Code"] == "InvalidKeyPair.Duplicate": logger.error( f"A key pair called {key_name} already exists. " "Please choose a different name for your key pair " "or delete the existing key pair before creating." ) raise else: return self.key_pair def list(self, limit: Optional[int] = None) -> None: """ Displays a list of key pairs for the current account. WARNING: Results are not paginated. :param limit: The maximum number of key pairs to list. If not specified, all key pairs will be listed. :raises ClientError: If there is an error in listing the key pairs. """ try: response = self.ec2_client.describe_key_pairs() key_pairs = response.get("KeyPairs", []) if limit: key_pairs = key_pairs[:limit] for key_pair in key_pairs: logger.info( f"Found {key_pair['KeyType']} key '{key_pair['KeyName']}' with fingerprint:" ) logger.info(f"\t{key_pair['KeyFingerprint']}") except ClientError as err: logger.error(f"Failed to list key pairs: {str(err)}") raise def delete(self, key_name: str) -> bool: """ Deletes a key pair by its name. :param key_name: The name of the key pair to delete. :return: A boolean indicating whether the deletion was successful. :raises ClientError: If there is an error in deleting the key pair, for example, if the key pair does not exist. """ try: self.ec2_client.delete_key_pair(KeyName=key_name) logger.info(f"Successfully deleted key pair: {key_name}") self.key_pair = None return True except self.ec2_client.exceptions.ClientError as err: logger.error(f"Deletion failed for key pair: {key_name}") error_code = err.response["Error"]["Code"] if error_code == "InvalidKeyPair.NotFound": logger.error( f"The key pair '{key_name}' does not exist and cannot be deleted. " "Please verify the key pair name and try again." ) raise
Define a class that wraps security group actions.
class SecurityGroupWrapper: """Encapsulates Amazon Elastic Compute Cloud (Amazon EC2) security group actions.""" def __init__(self, ec2_client: boto3.client, security_group: Optional[str] = None): """ Initializes the SecurityGroupWrapper with an EC2 client and an optional security group ID. :param ec2_client: A Boto3 Amazon EC2 client. This client provides low-level access to AWS EC2 services. :param security_group: The ID of a security group to manage. This is a high-level identifier that represents the security group. """ self.ec2_client = ec2_client self.security_group = security_group @classmethod def from_client(cls) -> "SecurityGroupWrapper": """ Creates a SecurityGroupWrapper instance with a default EC2 client. :return: An instance of SecurityGroupWrapper initialized with the default EC2 client. """ ec2_client = boto3.client("ec2") return cls(ec2_client) def create(self, group_name: str, group_description: str) -> str: """ Creates a security group in the default virtual private cloud (VPC) of the current account. :param group_name: The name of the security group to create. :param group_description: The description of the security group to create. :return: The ID of the newly created security group. :raise Handles AWS SDK service-level ClientError, with special handling for ResourceAlreadyExists """ try: response = self.ec2_client.create_security_group( GroupName=group_name, Description=group_description ) self.security_group = response["GroupId"] except ClientError as err: if err.response["Error"]["Code"] == "ResourceAlreadyExists": logger.error( f"Security group '{group_name}' already exists. Please choose a different name." ) raise else: return self.security_group def authorize_ingress(self, ssh_ingress_ip: str) -> Optional[Dict[str, Any]]: """ Adds a rule to the security group to allow access to SSH. :param ssh_ingress_ip: The IP address that is granted inbound access to connect to port 22 over TCP, used for SSH. :return: The response to the authorization request. The 'Return' field of the response indicates whether the request succeeded or failed, or None if no security group is set. :raise Handles AWS SDK service-level ClientError, with special handling for ResourceAlreadyExists """ if self.security_group is None: logger.info("No security group to update.") return None try: ip_permissions = [ { # SSH ingress open to only the specified IP address. "IpProtocol": "tcp", "FromPort": 22, "ToPort": 22, "IpRanges": [{"CidrIp": f"{ssh_ingress_ip}/32"}], } ] response = self.ec2_client.authorize_security_group_ingress( GroupId=self.security_group, IpPermissions=ip_permissions ) except ClientError as err: if err.response["Error"]["Code"] == "InvalidPermission.Duplicate": logger.error( f"The SSH ingress rule for IP {ssh_ingress_ip} already exists" f"in security group '{self.security_group}'." ) raise else: return response def describe(self, security_group_id: Optional[str] = None) -> bool: """ Displays information about the specified security group or all security groups if no ID is provided. :param security_group_id: The ID of the security group to describe. If None, an open search is performed to describe all security groups. :returns: True if the description is successful. :raises ClientError: If there is an error describing the security group(s), such as an invalid security group ID. """ try: paginator = self.ec2_client.get_paginator("describe_security_groups") if security_group_id is None: # If no ID is provided, return all security groups. page_iterator = paginator.paginate() else: page_iterator = paginator.paginate(GroupIds=[security_group_id]) for page in page_iterator: for security_group in page["SecurityGroups"]: print(f"Security group: {security_group['GroupName']}") print(f"\tID: {security_group['GroupId']}") print(f"\tVPC: {security_group['VpcId']}") if security_group["IpPermissions"]: print("Inbound permissions:") pp(security_group["IpPermissions"]) return True except ClientError as err: logger.error("Failed to describe security group(s).") if err.response["Error"]["Code"] == "InvalidGroup.NotFound": logger.error( f"Security group {security_group_id} does not exist " f"because the specified security group ID was not found." ) raise def delete(self, security_group_id: str) -> bool: """ Deletes the specified security group. :param security_group_id: The ID of the security group to delete. Required. :returns: True if the deletion is successful. :raises ClientError: If the security group cannot be deleted due to an AWS service error. """ try: self.ec2_client.delete_security_group(GroupId=security_group_id) logger.info(f"Successfully deleted security group '{security_group_id}'") return True except ClientError as err: logger.error(f"Deletion failed for security group '{security_group_id}'") error_code = err.response["Error"]["Code"] if error_code == "InvalidGroup.NotFound": logger.error( f"Security group '{security_group_id}' cannot be deleted because it does not exist." ) elif error_code == "DependencyViolation": logger.error( f"Security group '{security_group_id}' cannot be deleted because it is still in use." " Verify that it is:" "\n\t- Detached from resources" "\n\t- Removed from references in other groups" "\n\t- Removed from VPC's as a default group" ) raise
Define a class that wraps instance actions.
class EC2InstanceWrapper: """Encapsulates Amazon Elastic Compute Cloud (Amazon EC2) instance actions using the client interface.""" def __init__( self, ec2_client: Any, instances: Optional[List[Dict[str, Any]]] = None ) -> None: """ Initializes the EC2InstanceWrapper with an EC2 client and optional instances. :param ec2_client: A Boto3 Amazon EC2 client. This client provides low-level access to AWS EC2 services. :param instances: A list of dictionaries representing Boto3 Instance objects. These are high-level objects that wrap instance actions. """ self.ec2_client = ec2_client self.instances = instances or [] @classmethod def from_client(cls) -> "EC2InstanceWrapper": """ Creates an EC2InstanceWrapper instance with a default EC2 client. :return: An instance of EC2InstanceWrapper initialized with the default EC2 client. """ ec2_client = boto3.client("ec2") return cls(ec2_client) def create( self, image_id: str, instance_type: str, key_pair_name: str, security_group_ids: Optional[List[str]] = None, ) -> List[Dict[str, Any]]: """ Creates a new EC2 instance in the default VPC of the current account. The instance starts immediately after it is created. :param image_id: The ID of the Amazon Machine Image (AMI) to use for the instance. :param instance_type: The type of instance to create, such as 't2.micro'. :param key_pair_name: The name of the key pair to use for SSH access. :param security_group_ids: A list of security group IDs to associate with the instance. If not specified, the default security group of the VPC is used. :return: A list of dictionaries representing Boto3 Instance objects representing the newly created instances. """ try: instance_params = { "ImageId": image_id, "InstanceType": instance_type, "KeyName": key_pair_name, } if security_group_ids is not None: instance_params["SecurityGroupIds"] = security_group_ids response = self.ec2_client.run_instances( **instance_params, MinCount=1, MaxCount=1 ) instance = response["Instances"][0] self.instances.append(instance) waiter = self.ec2_client.get_waiter("instance_running") waiter.wait(InstanceIds=[instance["InstanceId"]]) except ClientError as err: params_str = "\n\t".join( f"{key}: {value}" for key, value in instance_params.items() ) logger.error( f"Failed to complete instance creation request.\nRequest details:{params_str}" ) error_code = err.response["Error"]["Code"] if error_code == "InstanceLimitExceeded": logger.error( ( f"Insufficient capacity for instance type '{instance_type}'. " "Terminate unused instances or contact AWS Support for a limit increase." ) ) if error_code == "InsufficientInstanceCapacity": logger.error( ( f"Insufficient capacity for instance type '{instance_type}'. " "Select a different instance type or launch in a different availability zone." ) ) raise return self.instances def display(self, state_filter: Optional[str] = "running") -> None: """ Displays information about instances, filtering by the specified state. :param state_filter: The instance state to include in the output. Only instances in this state will be displayed. Default is 'running'. Example states: 'running', 'stopped'. """ if not self.instances: logger.info("No instances to display.") return instance_ids = [instance["InstanceId"] for instance in self.instances] paginator = self.ec2_client.get_paginator("describe_instances") page_iterator = paginator.paginate(InstanceIds=instance_ids) try: for page in page_iterator: for reservation in page["Reservations"]: for instance in reservation["Instances"]: instance_state = instance["State"]["Name"] # Apply the state filter (default is 'running') if state_filter and instance_state != state_filter: continue # Skip this instance if it doesn't match the filter # Create a formatted string with instance details instance_info = ( f"• ID: {instance['InstanceId']}\n" f"• Image ID: {instance['ImageId']}\n" f"• Instance type: {instance['InstanceType']}\n" f"• Key name: {instance['KeyName']}\n" f"• VPC ID: {instance['VpcId']}\n" f"• Public IP: {instance.get('PublicIpAddress', 'N/A')}\n" f"• State: {instance_state}" ) print(instance_info) except ClientError as err: logger.error( f"Failed to display instance(s). : {' '.join(map(str, instance_ids))}" ) error_code = err.response["Error"]["Code"] if error_code == "InvalidInstanceID.NotFound": logger.error( "One or more instance IDs do not exist. " "Please verify the instance IDs and try again." ) raise def terminate(self) -> None: """ Terminates instances and waits for them to reach the terminated state. """ if not self.instances: logger.info("No instances to terminate.") return instance_ids = [instance["InstanceId"] for instance in self.instances] try: self.ec2_client.terminate_instances(InstanceIds=instance_ids) waiter = self.ec2_client.get_waiter("instance_terminated") waiter.wait(InstanceIds=instance_ids) self.instances.clear() for instance_id in instance_ids: print(f"• Instance ID: {instance_id}\n" f"• Action: Terminated") except ClientError as err: logger.error( f"Failed instance termination details:\n\t{str(self.instances)}" ) error_code = err.response["Error"]["Code"] if error_code == "InvalidInstanceID.NotFound": logger.error( "One or more instance IDs do not exist. " "Please verify the instance IDs and try again." ) raise def start(self) -> Optional[Dict[str, Any]]: """ Starts instances and waits for them to be in a running state. :return: The response to the start request. """ if not self.instances: logger.info("No instances to start.") return None instance_ids = [instance["InstanceId"] for instance in self.instances] try: start_response = self.ec2_client.start_instances(InstanceIds=instance_ids) waiter = self.ec2_client.get_waiter("instance_running") waiter.wait(InstanceIds=instance_ids) return start_response except ClientError as err: logger.error( f"Failed to start instance(s): {','.join(map(str, instance_ids))}" ) error_code = err.response["Error"]["Code"] if error_code == "IncorrectInstanceState": logger.error( "Couldn't start instance(s) because they are in an incorrect state. " "Ensure the instances are in a stopped state before starting them." ) raise def stop(self) -> Optional[Dict[str, Any]]: """ Stops instances and waits for them to be in a stopped state. :return: The response to the stop request, or None if there are no instances to stop. """ if not self.instances: logger.info("No instances to stop.") return None instance_ids = [instance["InstanceId"] for instance in self.instances] try: # Attempt to stop the instances stop_response = self.ec2_client.stop_instances(InstanceIds=instance_ids) waiter = self.ec2_client.get_waiter("instance_stopped") waiter.wait(InstanceIds=instance_ids) except ClientError as err: logger.error( f"Failed to stop instance(s): {','.join(map(str, instance_ids))}" ) error_code = err.response["Error"]["Code"] if error_code == "IncorrectInstanceState": logger.error( "Couldn't stop instance(s) because they are in an incorrect state. " "Ensure the instances are in a running state before stopping them." ) raise return stop_response def get_images(self, image_ids: List[str]) -> List[Dict[str, Any]]: """ Gets information about Amazon Machine Images (AMIs) from a list of AMI IDs. :param image_ids: The list of AMI IDs to look up. :return: A list of dictionaries representing the requested AMIs. """ try: response = self.ec2_client.describe_images(ImageIds=image_ids) images = response["Images"] except ClientError as err: logger.error(f"Failed to stop AMI(s): {','.join(map(str, image_ids))}") error_code = err.response["Error"]["Code"] if error_code == "InvalidAMIID.NotFound": logger.error("One or more of the AMI IDs does not exist.") raise return images def get_instance_types( self, architecture: str = "x86_64", sizes: List[str] = ["*.micro", "*.small"] ) -> List[Dict[str, Any]]: """ Gets instance types that support the specified architecture and size. See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstanceTypes.html for a list of allowable parameters. :param architecture: The architecture supported by instance types. Default: 'x86_64'. :param sizes: The size of instance types. Default: '*.micro', '*.small', :return: A list of dictionaries representing instance types that support the specified architecture and size. """ try: inst_types = [] paginator = self.ec2_client.get_paginator("describe_instance_types") for page in paginator.paginate( Filters=[ { "Name": "processor-info.supported-architecture", "Values": [architecture], }, {"Name": "instance-type", "Values": sizes}, ] ): inst_types += page["InstanceTypes"] except ClientError as err: logger.error( f"Failed to get instance types: {architecture}, {','.join(map(str, sizes))}" ) error_code = err.response["Error"]["Code"] if error_code == "InvalidParameterValue": logger.error( "Parameters are invalid. " "Ensure architecture and size strings conform to DescribeInstanceTypes API reference." ) raise else: return inst_types
Define a class that wraps Elastic IP actions.
class ElasticIpWrapper: """Encapsulates Amazon Elastic Compute Cloud (Amazon EC2) Elastic IP address actions using the client interface.""" class ElasticIp: """Represents an Elastic IP and its associated instance.""" def __init__( self, allocation_id: str, public_ip: str, instance_id: Optional[str] = None ) -> None: """ Initializes the ElasticIp object. :param allocation_id: The allocation ID of the Elastic IP. :param public_ip: The public IP address of the Elastic IP. :param instance_id: The ID of the associated EC2 instance, if any. """ self.allocation_id = allocation_id self.public_ip = public_ip self.instance_id = instance_id def __init__(self, ec2_client: Any) -> None: """ Initializes the ElasticIpWrapper with an EC2 client. :param ec2_client: A Boto3 Amazon EC2 client. This client provides low-level access to AWS EC2 services. """ self.ec2_client = ec2_client self.elastic_ips: List[ElasticIpWrapper.ElasticIp] = [] @classmethod def from_client(cls) -> "ElasticIpWrapper": """ Creates an ElasticIpWrapper instance with a default EC2 client. :return: An instance of ElasticIpWrapper initialized with the default EC2 client. """ ec2_client = boto3.client("ec2") return cls(ec2_client) def allocate(self) -> "ElasticIpWrapper.ElasticIp": """ Allocates an Elastic IP address that can be associated with an Amazon EC2 instance. By using an Elastic IP address, you can keep the public IP address constant even when you restart the associated instance. :return: The ElasticIp object for the newly created Elastic IP address. :raises ClientError: If the allocation fails, such as reaching the maximum limit of Elastic IPs. """ try: response = self.ec2_client.allocate_address(Domain="vpc") elastic_ip = self.ElasticIp( allocation_id=response["AllocationId"], public_ip=response["PublicIp"] ) self.elastic_ips.append(elastic_ip) except ClientError as err: if err.response["Error"]["Code"] == "AddressLimitExceeded": logger.error( "Max IP's reached. Release unused addresses or contact AWS Support for an increase." ) raise err return elastic_ip def associate( self, allocation_id: str, instance_id: str ) -> Union[Dict[str, Any], None]: """ Associates an Elastic IP address with an instance. When this association is created, the Elastic IP's public IP address is immediately used as the public IP address of the associated instance. :param allocation_id: The allocation ID of the Elastic IP. :param instance_id: The ID of the Amazon EC2 instance. :return: A response that contains the ID of the association, or None if no Elastic IP is found. :raises ClientError: If the association fails, such as when the instance ID is not found. """ elastic_ip = self.get_elastic_ip_by_allocation(self.elastic_ips, allocation_id) if elastic_ip is None: logger.info(f"No Elastic IP found with allocation ID {allocation_id}.") return None try: response = self.ec2_client.associate_address( AllocationId=allocation_id, InstanceId=instance_id ) elastic_ip.instance_id = ( instance_id # Track the instance associated with this Elastic IP. ) except ClientError as err: if err.response["Error"]["Code"] == "InvalidInstanceID.NotFound": logger.error( f"Failed to associate Elastic IP {allocation_id} with {instance_id} " "because the specified instance ID does not exist or has not propagated fully. " "Verify the instance ID and try again, or wait a few moments before attempting to " "associate the Elastic IP address." ) raise return response def disassociate(self, allocation_id: str) -> None: """ Removes an association between an Elastic IP address and an instance. When the association is removed, the instance is assigned a new public IP address. :param allocation_id: The allocation ID of the Elastic IP to disassociate. :raises ClientError: If the disassociation fails, such as when the association ID is not found. """ elastic_ip = self.get_elastic_ip_by_allocation(self.elastic_ips, allocation_id) if elastic_ip is None or elastic_ip.instance_id is None: logger.info( f"No association found for Elastic IP with allocation ID {allocation_id}." ) return try: # Retrieve the association ID before disassociating response = self.ec2_client.describe_addresses(AllocationIds=[allocation_id]) association_id = response["Addresses"][0].get("AssociationId") if association_id: self.ec2_client.disassociate_address(AssociationId=association_id) elastic_ip.instance_id = None # Remove the instance association else: logger.info( f"No Association ID found for Elastic IP with allocation ID {allocation_id}." ) except ClientError as err: if err.response["Error"]["Code"] == "InvalidAssociationID.NotFound": logger.error( f"Failed to disassociate Elastic IP {allocation_id} " "because the specified association ID for the Elastic IP address was not found. " "Verify the association ID and ensure the Elastic IP is currently associated with a " "resource before attempting to disassociate it." ) raise def release(self, allocation_id: str) -> None: """ Releases an Elastic IP address. After the Elastic IP address is released, it can no longer be used. :param allocation_id: The allocation ID of the Elastic IP to release. :raises ClientError: If the release fails, such as when the Elastic IP address is not found. """ elastic_ip = self.get_elastic_ip_by_allocation(self.elastic_ips, allocation_id) if elastic_ip is None: logger.info(f"No Elastic IP found with allocation ID {allocation_id}.") return try: self.ec2_client.release_address(AllocationId=allocation_id) self.elastic_ips.remove(elastic_ip) # Remove the Elastic IP from the list except ClientError as err: if err.response["Error"]["Code"] == "InvalidAddress.NotFound": logger.error( f"Failed to release Elastic IP address {allocation_id} " "because it could not be found. Verify the Elastic IP address " "and ensure it is allocated to your account in the correct region " "before attempting to release it." ) raise @staticmethod def get_elastic_ip_by_allocation( elastic_ips: List["ElasticIpWrapper.ElasticIp"], allocation_id: str ) -> Optional["ElasticIpWrapper.ElasticIp"]: """ Retrieves an Elastic IP object by its allocation ID from a given list of Elastic IPs. :param elastic_ips: A list of ElasticIp objects. :param allocation_id: The allocation ID of the Elastic IP to retrieve. :return: The ElasticIp object associated with the allocation ID, or None if not found. """ return next( (ip for ip in elastic_ips if ip.allocation_id == allocation_id), None )
-
For API details, see the following topics in Amazon SDK for Python (Boto3) API Reference.
-
- Rust
-
- SDK for Rust
-
Note
There's more on GitHub. Find the complete example and learn how to set up and run in the Amazon Code Examples Repository
. The EC2InstanceScenario implementation contains logic to run the example as a whole.
//! Scenario that uses the AWS SDK for Rust (the SDK) with Amazon Elastic Compute Cloud //! (Amazon EC2) to do the following: //! //! * Create a key pair that is used to secure SSH communication between your computer and //! an EC2 instance. //! * Create a security group that acts as a virtual firewall for your EC2 instances to //! control incoming and outgoing traffic. //! * Find an Amazon Machine Image (AMI) and a compatible instance type. //! * Create an instance that is created from the instance type and AMI you select, and //! is configured to use the security group and key pair created in this example. //! * Stop and restart the instance. //! * Create an Elastic IP address and associate it as a consistent IP address for your instance. //! * Connect to your instance with SSH, using both its public IP address and your Elastic IP //! address. //! * Clean up all of the resources created by this example. use std::net::Ipv4Addr; use crate::{ ec2::{EC2Error, EC2}, getting_started::{key_pair::KeyPairManager, util::Util}, ssm::SSM, }; use aws_sdk_ssm::types::Parameter; use super::{ elastic_ip::ElasticIpManager, instance::InstanceManager, security_group::SecurityGroupManager, util::ScenarioImage, }; pub struct Ec2InstanceScenario { ec2: EC2, ssm: SSM, util: Util, key_pair_manager: KeyPairManager, security_group_manager: SecurityGroupManager, instance_manager: InstanceManager, elastic_ip_manager: ElasticIpManager, } impl Ec2InstanceScenario { pub fn new(ec2: EC2, ssm: SSM, util: Util) -> Self { Ec2InstanceScenario { ec2, ssm, util, key_pair_manager: Default::default(), security_group_manager: Default::default(), instance_manager: Default::default(), elastic_ip_manager: Default::default(), } } pub async fn run(&mut self) -> Result<(), EC2Error> { self.create_and_list_key_pairs().await?; self.create_security_group().await?; self.create_instance().await?; self.stop_and_start_instance().await?; self.associate_elastic_ip().await?; self.stop_and_start_instance().await?; Ok(()) } /// 1. Creates an RSA key pair and saves its private key data as a .pem file in secure /// temporary storage. The private key data is deleted after the example completes. /// 2. Optionally, lists the first five key pairs for the current account. pub async fn create_and_list_key_pairs(&mut self) -> Result<(), EC2Error> { println!( "Let's create an RSA key pair that you can be use to securely connect to your EC2 instance."); let key_name = self.util.prompt_key_name()?; self.key_pair_manager .create(&self.ec2, &self.util, key_name) .await?; println!( "Created a key pair {} and saved the private key to {:?}.", self.key_pair_manager .key_pair() .key_name() .ok_or_else(|| EC2Error::new("No key name after creating key"))?, self.key_pair_manager .key_file_path() .ok_or_else(|| EC2Error::new("No key file after creating key"))? ); if self.util.should_list_key_pairs()? { for pair in self.key_pair_manager.list(&self.ec2).await? { println!( "Found {:?} key {} with fingerprint:\t{:?}", pair.key_type(), pair.key_name().unwrap_or("Unknown"), pair.key_fingerprint() ); } } Ok(()) } /// 1. Creates a security group for the default VPC. /// 2. Adds an inbound rule to allow SSH. The SSH rule allows only /// inbound traffic from the current computer’s public IPv4 address. /// 3. Displays information about the security group. /// /// This function uses <http://checkip.amazonaws.com> to get the current public IP /// address of the computer that is running the example. This method works in most /// cases. However, depending on how your computer connects to the internet, you /// might have to manually add your public IP address to the security group by using /// the AWS Management Console. pub async fn create_security_group(&mut self) -> Result<(), EC2Error> { println!("Let's create a security group to manage access to your instance."); let group_name = self.util.prompt_security_group_name()?; self.security_group_manager .create( &self.ec2, &group_name, "Security group for example: get started with instances.", ) .await?; println!( "Created security group {} in your default VPC {}.", self.security_group_manager.group_name(), self.security_group_manager .vpc_id() .unwrap_or("(unknown vpc)") ); let check_ip = self.util.do_get("https://checkip.amazonaws.com").await?; let current_ip_address: Ipv4Addr = check_ip.trim().parse().map_err(|e| { EC2Error::new(format!( "Failed to convert response {} to IP Address: {e:?}", check_ip )) })?; println!("Your public IP address seems to be {current_ip_address}"); if self.util.should_add_to_security_group() { match self .security_group_manager .authorize_ingress(&self.ec2, current_ip_address) .await { Ok(_) => println!("Security group rules updated"), Err(err) => eprintln!("Couldn't update security group rules: {err:?}"), } } println!("{}", self.security_group_manager); Ok(()) } /// 1. Gets a list of Amazon Linux 2 AMIs from AWS Systems Manager. Specifying the /// '/aws/service/ami-amazon-linux-latest' path returns only the latest AMIs. /// 2. Gets and displays information about the available AMIs and lets you select one. /// 3. Gets a list of instance types that are compatible with the selected AMI and /// lets you select one. /// 4. Creates an instance with the previously created key pair and security group, /// and the selected AMI and instance type. /// 5. Waits for the instance to be running and then displays its information. pub async fn create_instance(&mut self) -> Result<(), EC2Error> { let ami = self.find_image().await?; let instance_types = self .ec2 .list_instance_types(&ami.0) .await .map_err(|e| e.add_message("Could not find instance types"))?; println!( "There are several instance types that support the {} architecture of the image.", ami.0 .architecture .as_ref() .ok_or_else(|| EC2Error::new(format!("Missing architecture in {:?}", ami.0)))? ); let instance_type = self.util.select_instance_type(instance_types)?; println!("Creating your instance and waiting for it to start..."); self.instance_manager .create( &self.ec2, ami.0 .image_id() .ok_or_else(|| EC2Error::new("Could not find image ID"))?, instance_type, self.key_pair_manager.key_pair(), self.security_group_manager .security_group() .map(|sg| vec![sg]) .ok_or_else(|| EC2Error::new("Could not find security group"))?, ) .await .map_err(|e| e.add_message("Scenario failed to create instance"))?; while let Err(err) = self .ec2 .wait_for_instance_ready(self.instance_manager.instance_id(), None) .await { println!("{err}"); if !self.util.should_continue_waiting() { return Err(err); } } println!("Your instance is ready:\n{}", self.instance_manager); self.display_ssh_info(); Ok(()) } async fn find_image(&mut self) -> Result<ScenarioImage, EC2Error> { let params: Vec<Parameter> = self .ssm .list_path("/aws/service/ami-amazon-linux-latest") .await .map_err(|e| e.add_message("Could not find parameters for available images"))? .into_iter() .filter(|param| param.name().is_some_and(|name| name.contains("amzn2"))) .collect(); let amzn2_images: Vec<ScenarioImage> = self .ec2 .list_images(params) .await .map_err(|e| e.add_message("Could not find images"))? .into_iter() .map(ScenarioImage::from) .collect(); println!("We will now create an instance from an Amazon Linux 2 AMI"); let ami = self.util.select_scenario_image(amzn2_images)?; Ok(ami) } // 1. Stops the instance and waits for it to stop. // 2. Starts the instance and waits for it to start. // 3. Displays information about the instance. // 4. Displays an SSH connection string. When an Elastic IP address is associated // with the instance, the IP address stays consistent when the instance stops // and starts. pub async fn stop_and_start_instance(&self) -> Result<(), EC2Error> { println!("Let's stop and start your instance to see what changes."); println!("Stopping your instance and waiting until it's stopped..."); self.instance_manager.stop(&self.ec2).await?; println!("Your instance is stopped. Restarting..."); self.instance_manager.start(&self.ec2).await?; println!("Your instance is running."); println!("{}", self.instance_manager); if self.elastic_ip_manager.public_ip() == "0.0.0.0" { println!("Every time your instance is restarted, its public IP address changes."); } else { println!( "Because you have associated an Elastic IP with your instance, you can connect by using a consistent IP address after the instance restarts." ); } self.display_ssh_info(); Ok(()) } /// 1. Allocates an Elastic IP address and associates it with the instance. /// 2. Displays an SSH connection string that uses the Elastic IP address. async fn associate_elastic_ip(&mut self) -> Result<(), EC2Error> { self.elastic_ip_manager.allocate(&self.ec2).await?; println!( "Allocated static Elastic IP address: {}", self.elastic_ip_manager.public_ip() ); self.elastic_ip_manager .associate(&self.ec2, self.instance_manager.instance_id()) .await?; println!("Associated your Elastic IP with your instance."); println!("You can now use SSH to connect to your instance by using the Elastic IP."); self.display_ssh_info(); Ok(()) } /// Displays an SSH connection string that can be used to connect to a running /// instance. fn display_ssh_info(&self) { let ip_addr = if self.elastic_ip_manager.has_allocation() { self.elastic_ip_manager.public_ip() } else { self.instance_manager.instance_ip() }; let key_file_path = self.key_pair_manager.key_file_path().unwrap(); println!("To connect, open another command prompt and run the following command:"); println!("\nssh -i {} ec2-user@{ip_addr}\n", key_file_path.display()); let _ = self.util.enter_to_continue(); } /// 1. Disassociate and delete the previously created Elastic IP. /// 2. Terminate the previously created instance. /// 3. Delete the previously created security group. /// 4. Delete the previously created key pair. pub async fn clean_up(self) { println!("Let's clean everything up. This example created these resources:"); println!( "\tKey pair: {}", self.key_pair_manager .key_pair() .key_name() .unwrap_or("(unknown key pair)") ); println!( "\tSecurity group: {}", self.security_group_manager.group_name() ); println!( "\tInstance: {}", self.instance_manager.instance_display_name() ); if self.util.should_clean_resources() { if let Err(err) = self.elastic_ip_manager.remove(&self.ec2).await { eprintln!("{err}") } if let Err(err) = self.instance_manager.delete(&self.ec2).await { eprintln!("{err}") } if let Err(err) = self.security_group_manager.delete(&self.ec2).await { eprintln!("{err}"); } if let Err(err) = self.key_pair_manager.delete(&self.ec2, &self.util).await { eprintln!("{err}"); } } else { println!("Ok, not cleaning up any resources!"); } } } pub async fn run(mut scenario: Ec2InstanceScenario) { println!("--------------------------------------------------------------------------------"); println!( "Welcome to the Amazon Elastic Compute Cloud (Amazon EC2) get started with instances demo." ); println!("--------------------------------------------------------------------------------"); if let Err(err) = scenario.run().await { eprintln!("There was an error running the scenario: {err}") } println!("--------------------------------------------------------------------------------"); scenario.clean_up().await; println!("Thanks for running!"); println!("--------------------------------------------------------------------------------"); }
The EC2Impl struct serves as a an automock point for testing, and its functions wrap the EC2 SDK calls.
use std::{net::Ipv4Addr, time::Duration}; use aws_sdk_ec2::{ client::Waiters, error::ProvideErrorMetadata, operation::{ allocate_address::AllocateAddressOutput, associate_address::AssociateAddressOutput, }, types::{ DomainType, Filter, Image, Instance, InstanceType, IpPermission, IpRange, KeyPairInfo, SecurityGroup, Tag, }, Client as EC2Client, }; use aws_sdk_ssm::types::Parameter; use aws_smithy_runtime_api::client::waiters::error::WaiterError; #[cfg(test)] use mockall::automock; #[cfg(not(test))] pub use EC2Impl as EC2; #[cfg(test)] pub use MockEC2Impl as EC2; #[derive(Clone)] pub struct EC2Impl { pub client: EC2Client, } #[cfg_attr(test, automock)] impl EC2Impl { pub fn new(client: EC2Client) -> Self { EC2Impl { client } } pub async fn create_key_pair(&self, name: String) -> Result<(KeyPairInfo, String), EC2Error> { tracing::info!("Creating key pair {name}"); let output = self.client.create_key_pair().key_name(name).send().await?; let info = KeyPairInfo::builder() .set_key_name(output.key_name) .set_key_fingerprint(output.key_fingerprint) .set_key_pair_id(output.key_pair_id) .build(); let material = output .key_material .ok_or_else(|| EC2Error::new("Create Key Pair has no key material"))?; Ok((info, material)) } pub async fn list_key_pair(&self) -> Result<Vec<KeyPairInfo>, EC2Error> { let output = self.client.describe_key_pairs().send().await?; Ok(output.key_pairs.unwrap_or_default()) } pub async fn delete_key_pair(&self, key_name: &str) -> Result<(), EC2Error> { let key_name: String = key_name.into(); tracing::info!("Deleting key pair {key_name}"); self.client .delete_key_pair() .key_name(key_name) .send() .await?; Ok(()) } pub async fn create_security_group( &self, name: &str, description: &str, ) -> Result<SecurityGroup, EC2Error> { tracing::info!("Creating security group {name}"); let create_output = self .client .create_security_group() .group_name(name) .description(description) .send() .await .map_err(EC2Error::from)?; let group_id = create_output .group_id .ok_or_else(|| EC2Error::new("Missing security group id after creation"))?; let group = self .describe_security_group(&group_id) .await? .ok_or_else(|| { EC2Error::new(format!("Could not find security group with id {group_id}")) })?; tracing::info!("Created security group {name} as {group_id}"); Ok(group) } /// Find a single security group, by ID. Returns Err if multiple groups are found. pub async fn describe_security_group( &self, group_id: &str, ) -> Result<Option<SecurityGroup>, EC2Error> { let group_id: String = group_id.into(); let describe_output = self .client .describe_security_groups() .group_ids(&group_id) .send() .await?; let mut groups = describe_output.security_groups.unwrap_or_default(); match groups.len() { 0 => Ok(None), 1 => Ok(Some(groups.remove(0))), _ => Err(EC2Error::new(format!( "Expected single group for {group_id}" ))), } } /// Add an ingress rule to a security group explicitly allowing IPv4 address /// as {ip}/32 over TCP port 22. pub async fn authorize_security_group_ssh_ingress( &self, group_id: &str, ingress_ips: Vec<Ipv4Addr>, ) -> Result<(), EC2Error> { tracing::info!("Authorizing ingress for security group {group_id}"); self.client .authorize_security_group_ingress() .group_id(group_id) .set_ip_permissions(Some( ingress_ips .into_iter() .map(|ip| { IpPermission::builder() .ip_protocol("tcp") .from_port(22) .to_port(22) .ip_ranges(IpRange::builder().cidr_ip(format!("{ip}/32")).build()) .build() }) .collect(), )) .send() .await?; Ok(()) } pub async fn delete_security_group(&self, group_id: &str) -> Result<(), EC2Error> { tracing::info!("Deleting security group {group_id}"); self.client .delete_security_group() .group_id(group_id) .send() .await?; Ok(()) } pub async fn list_images(&self, ids: Vec<Parameter>) -> Result<Vec<Image>, EC2Error> { let image_ids = ids.into_iter().filter_map(|p| p.value).collect(); let output = self .client .describe_images() .set_image_ids(Some(image_ids)) .send() .await?; let images = output.images.unwrap_or_default(); if images.is_empty() { Err(EC2Error::new("No images for selected AMIs")) } else { Ok(images) } } /// List instance types that match an image's architecture and are free tier eligible. pub async fn list_instance_types(&self, image: &Image) -> Result<Vec<InstanceType>, EC2Error> { let architecture = format!( "{}", image.architecture().ok_or_else(|| EC2Error::new(format!( "Image {:?} does not have a listed architecture", image.image_id() )))? ); let free_tier_eligible_filter = Filter::builder() .name("free-tier-eligible") .values("false") .build(); let supported_architecture_filter = Filter::builder() .name("processor-info.supported-architecture") .values(architecture) .build(); let response = self .client .describe_instance_types() .filters(free_tier_eligible_filter) .filters(supported_architecture_filter) .send() .await?; Ok(response .instance_types .unwrap_or_default() .into_iter() .filter_map(|iti| iti.instance_type) .collect()) } pub async fn create_instance<'a>( &self, image_id: &'a str, instance_type: InstanceType, key_pair: &'a KeyPairInfo, security_groups: Vec<&'a SecurityGroup>, ) -> Result<String, EC2Error> { let run_instances = self .client .run_instances() .image_id(image_id) .instance_type(instance_type) .key_name( key_pair .key_name() .ok_or_else(|| EC2Error::new("Missing key name when launching instance"))?, ) .set_security_group_ids(Some( security_groups .iter() .filter_map(|sg| sg.group_id.clone()) .collect(), )) .min_count(1) .max_count(1) .send() .await?; if run_instances.instances().is_empty() { return Err(EC2Error::new("Failed to create instance")); } let instance_id = run_instances.instances()[0].instance_id().unwrap(); let response = self .client .create_tags() .resources(instance_id) .tags( Tag::builder() .key("Name") .value("From SDK Examples") .build(), ) .send() .await; match response { Ok(_) => tracing::info!("Created {instance_id} and applied tags."), Err(err) => { tracing::info!("Error applying tags to {instance_id}: {err:?}"); return Err(err.into()); } } tracing::info!("Instance is created."); Ok(instance_id.to_string()) } /// Wait for an instance to be ready and status ok (default wait 60 seconds) pub async fn wait_for_instance_ready( &self, instance_id: &str, duration: Option<Duration>, ) -> Result<(), EC2Error> { self.client .wait_until_instance_status_ok() .instance_ids(instance_id) .wait(duration.unwrap_or(Duration::from_secs(60))) .await .map_err(|err| match err { WaiterError::ExceededMaxWait(exceeded) => EC2Error(format!( "Exceeded max time ({}s) waiting for instance to start.", exceeded.max_wait().as_secs() )), _ => EC2Error::from(err), })?; Ok(()) } pub async fn describe_instance(&self, instance_id: &str) -> Result<Instance, EC2Error> { let response = self .client .describe_instances() .instance_ids(instance_id) .send() .await?; let instance = response .reservations() .first() .ok_or_else(|| EC2Error::new(format!("No instance reservations for {instance_id}")))? .instances() .first() .ok_or_else(|| { EC2Error::new(format!("No instances in reservation for {instance_id}")) })?; Ok(instance.clone()) } pub async fn start_instance(&self, instance_id: &str) -> Result<(), EC2Error> { tracing::info!("Starting instance {instance_id}"); self.client .start_instances() .instance_ids(instance_id) .send() .await?; tracing::info!("Started instance."); Ok(()) } pub async fn stop_instance(&self, instance_id: &str) -> Result<(), EC2Error> { tracing::info!("Stopping instance {instance_id}"); self.client .stop_instances() .instance_ids(instance_id) .send() .await?; self.wait_for_instance_stopped(instance_id, None).await?; tracing::info!("Stopped instance."); Ok(()) } pub async fn reboot_instance(&self, instance_id: &str) -> Result<(), EC2Error> { tracing::info!("Rebooting instance {instance_id}"); self.client .reboot_instances() .instance_ids(instance_id) .send() .await?; Ok(()) } pub async fn wait_for_instance_stopped( &self, instance_id: &str, duration: Option<Duration>, ) -> Result<(), EC2Error> { self.client .wait_until_instance_stopped() .instance_ids(instance_id) .wait(duration.unwrap_or(Duration::from_secs(60))) .await .map_err(|err| match err { WaiterError::ExceededMaxWait(exceeded) => EC2Error(format!( "Exceeded max time ({}s) waiting for instance to stop.", exceeded.max_wait().as_secs(), )), _ => EC2Error::from(err), })?; Ok(()) } pub async fn delete_instance(&self, instance_id: &str) -> Result<(), EC2Error> { tracing::info!("Deleting instance with id {instance_id}"); self.stop_instance(instance_id).await?; self.client .terminate_instances() .instance_ids(instance_id) .send() .await?; self.wait_for_instance_terminated(instance_id).await?; tracing::info!("Terminated instance with id {instance_id}"); Ok(()) } async fn wait_for_instance_terminated(&self, instance_id: &str) -> Result<(), EC2Error> { self.client .wait_until_instance_terminated() .instance_ids(instance_id) .wait(Duration::from_secs(60)) .await .map_err(|err| match err { WaiterError::ExceededMaxWait(exceeded) => EC2Error(format!( "Exceeded max time ({}s) waiting for instance to terminate.", exceeded.max_wait().as_secs(), )), _ => EC2Error::from(err), })?; Ok(()) } pub async fn allocate_ip_address(&self) -> Result<AllocateAddressOutput, EC2Error> { self.client .allocate_address() .domain(DomainType::Vpc) .send() .await .map_err(EC2Error::from) } pub async fn deallocate_ip_address(&self, allocation_id: &str) -> Result<(), EC2Error> { self.client .release_address() .allocation_id(allocation_id) .send() .await?; Ok(()) } pub async fn associate_ip_address( &self, allocation_id: &str, instance_id: &str, ) -> Result<AssociateAddressOutput, EC2Error> { let response = self .client .associate_address() .allocation_id(allocation_id) .instance_id(instance_id) .send() .await?; Ok(response) } pub async fn disassociate_ip_address(&self, association_id: &str) -> Result<(), EC2Error> { self.client .disassociate_address() .association_id(association_id) .send() .await?; Ok(()) } } #[derive(Debug)] pub struct EC2Error(String); impl EC2Error { pub fn new(value: impl Into<String>) -> Self { EC2Error(value.into()) } pub fn add_message(self, message: impl Into<String>) -> Self { EC2Error(format!("{}: {}", message.into(), self.0)) } } impl<T: ProvideErrorMetadata> From<T> for EC2Error { fn from(value: T) -> Self { EC2Error(format!( "{}: {}", value .code() .map(String::from) .unwrap_or("unknown code".into()), value .message() .map(String::from) .unwrap_or("missing reason".into()), )) } } impl std::error::Error for EC2Error {} impl std::fmt::Display for EC2Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } }
The SSM struct serves as a an automock point for testing, and its functions wraps SSM SDK calls.
use aws_sdk_ssm::{types::Parameter, Client}; use aws_smithy_async::future::pagination_stream::TryFlatMap; use crate::ec2::EC2Error; #[cfg(test)] use mockall::automock; #[cfg(not(test))] pub use SSMImpl as SSM; #[cfg(test)] pub use MockSSMImpl as SSM; pub struct SSMImpl { inner: Client, } #[cfg_attr(test, automock)] impl SSMImpl { pub fn new(inner: Client) -> Self { SSMImpl { inner } } pub async fn list_path(&self, path: &str) -> Result<Vec<Parameter>, EC2Error> { let maybe_params: Vec<Result<Parameter, _>> = TryFlatMap::new( self.inner .get_parameters_by_path() .path(path) .into_paginator() .send(), ) .flat_map(|item| item.parameters.unwrap_or_default()) .collect() .await; // Fail on the first error let params = maybe_params .into_iter() .collect::<Result<Vec<Parameter>, _>>()?; Ok(params) } }
The scenario uses several "Manager"-style structs to handle access to resources that are created and deleted throughout the scenario.
use aws_sdk_ec2::operation::{ allocate_address::AllocateAddressOutput, associate_address::AssociateAddressOutput, }; use crate::ec2::{EC2Error, EC2}; /// ElasticIpManager tracks the lifecycle of a public IP address, including its /// allocation from the global pool and association with a specific instance. #[derive(Debug, Default)] pub struct ElasticIpManager { elastic_ip: Option<AllocateAddressOutput>, association: Option<AssociateAddressOutput>, } impl ElasticIpManager { pub fn has_allocation(&self) -> bool { self.elastic_ip.is_some() } pub fn public_ip(&self) -> &str { if let Some(allocation) = &self.elastic_ip { if let Some(addr) = allocation.public_ip() { return addr; } } "0.0.0.0" } pub async fn allocate(&mut self, ec2: &EC2) -> Result<(), EC2Error> { let allocation = ec2.allocate_ip_address().await?; self.elastic_ip = Some(allocation); Ok(()) } pub async fn associate(&mut self, ec2: &EC2, instance_id: &str) -> Result<(), EC2Error> { if let Some(allocation) = &self.elastic_ip { if let Some(allocation_id) = allocation.allocation_id() { let association = ec2.associate_ip_address(allocation_id, instance_id).await?; self.association = Some(association); return Ok(()); } } Err(EC2Error::new("No ip address allocation to associate")) } pub async fn remove(mut self, ec2: &EC2) -> Result<(), EC2Error> { if let Some(association) = &self.association { if let Some(association_id) = association.association_id() { ec2.disassociate_ip_address(association_id).await?; } } self.association = None; if let Some(allocation) = &self.elastic_ip { if let Some(allocation_id) = allocation.allocation_id() { ec2.deallocate_ip_address(allocation_id).await?; } } self.elastic_ip = None; Ok(()) } } use std::fmt::Display; use aws_sdk_ec2::types::{Instance, InstanceType, KeyPairInfo, SecurityGroup}; use crate::ec2::{EC2Error, EC2}; /// InstanceManager wraps the lifecycle of an EC2 Instance. #[derive(Debug, Default)] pub struct InstanceManager { instance: Option<Instance>, } impl InstanceManager { pub fn instance_id(&self) -> &str { if let Some(instance) = &self.instance { if let Some(id) = instance.instance_id() { return id; } } "Unknown" } pub fn instance_name(&self) -> &str { if let Some(instance) = &self.instance { if let Some(tag) = instance.tags().iter().find(|e| e.key() == Some("Name")) { if let Some(value) = tag.value() { return value; } } } "Unknown" } pub fn instance_ip(&self) -> &str { if let Some(instance) = &self.instance { if let Some(public_ip_address) = instance.public_ip_address() { return public_ip_address; } } "0.0.0.0" } pub fn instance_display_name(&self) -> String { format!("{} ({})", self.instance_name(), self.instance_id()) } /// Create an EC2 instance with the given ID on a given type, using a /// generated KeyPair and applying a list of security groups. pub async fn create( &mut self, ec2: &EC2, image_id: &str, instance_type: InstanceType, key_pair: &KeyPairInfo, security_groups: Vec<&SecurityGroup>, ) -> Result<(), EC2Error> { let instance_id = ec2 .create_instance(image_id, instance_type, key_pair, security_groups) .await?; let instance = ec2.describe_instance(&instance_id).await?; self.instance = Some(instance); Ok(()) } /// Start the managed EC2 instance, if present. pub async fn start(&self, ec2: &EC2) -> Result<(), EC2Error> { if self.instance.is_some() { ec2.start_instance(self.instance_id()).await?; } Ok(()) } /// Stop the managed EC2 instance, if present. pub async fn stop(&self, ec2: &EC2) -> Result<(), EC2Error> { if self.instance.is_some() { ec2.stop_instance(self.instance_id()).await?; } Ok(()) } pub async fn reboot(&self, ec2: &EC2) -> Result<(), EC2Error> { if self.instance.is_some() { ec2.reboot_instance(self.instance_id()).await?; ec2.wait_for_instance_stopped(self.instance_id(), None) .await?; ec2.wait_for_instance_ready(self.instance_id(), None) .await?; } Ok(()) } /// Terminate and delete the managed EC2 instance, if present. pub async fn delete(self, ec2: &EC2) -> Result<(), EC2Error> { if self.instance.is_some() { ec2.delete_instance(self.instance_id()).await?; } Ok(()) } } impl Display for InstanceManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(instance) = &self.instance { writeln!(f, "\tID: {}", instance.instance_id().unwrap_or("(Unknown)"))?; writeln!( f, "\tImage ID: {}", instance.image_id().unwrap_or("(Unknown)") )?; writeln!( f, "\tInstance type: {}", instance .instance_type() .map(|it| format!("{it}")) .unwrap_or("(Unknown)".to_string()) )?; writeln!( f, "\tKey name: {}", instance.key_name().unwrap_or("(Unknown)") )?; writeln!(f, "\tVPC ID: {}", instance.vpc_id().unwrap_or("(Unknown)"))?; writeln!( f, "\tPublic IP: {}", instance.public_ip_address().unwrap_or("(Unknown)") )?; let instance_state = instance .state .as_ref() .map(|is| { is.name() .map(|isn| format!("{isn}")) .unwrap_or("(Unknown)".to_string()) }) .unwrap_or("(Unknown)".to_string()); writeln!(f, "\tState: {instance_state}")?; } else { writeln!(f, "\tNo loaded instance")?; } Ok(()) } } use std::{env, path::PathBuf}; use aws_sdk_ec2::types::KeyPairInfo; use crate::ec2::{EC2Error, EC2}; use super::util::Util; /// KeyPairManager tracks a KeyPairInfo and the path the private key has been /// written to, if it's been created. #[derive(Debug)] pub struct KeyPairManager { key_pair: KeyPairInfo, key_file_path: Option<PathBuf>, key_file_dir: PathBuf, } impl KeyPairManager { pub fn new() -> Self { Self::default() } pub fn key_pair(&self) -> &KeyPairInfo { &self.key_pair } pub fn key_file_path(&self) -> Option<&PathBuf> { self.key_file_path.as_ref() } pub fn key_file_dir(&self) -> &PathBuf { &self.key_file_dir } /// Creates a key pair that can be used to securely connect to an EC2 instance. /// The returned key pair contains private key information that cannot be retrieved /// again. The private key data is stored as a .pem file. /// /// :param key_name: The name of the key pair to create. pub async fn create( &mut self, ec2: &EC2, util: &Util, key_name: String, ) -> Result<KeyPairInfo, EC2Error> { let (key_pair, material) = ec2.create_key_pair(key_name.clone()).await.map_err(|e| { self.key_pair = KeyPairInfo::builder().key_name(key_name.clone()).build(); e.add_message(format!("Couldn't create key {key_name}")) })?; let path = self.key_file_dir.join(format!("{key_name}.pem")); // Save the key_pair information immediately, so it can get cleaned up if write_secure fails. self.key_file_path = Some(path.clone()); self.key_pair = key_pair.clone(); util.write_secure(&key_name, &path, material)?; Ok(key_pair) } pub async fn delete(self, ec2: &EC2, util: &Util) -> Result<(), EC2Error> { if let Some(key_name) = self.key_pair.key_name() { ec2.delete_key_pair(key_name).await?; if let Some(key_path) = self.key_file_path() { if let Err(err) = util.remove(key_path) { eprintln!("Failed to remove {key_path:?} ({err:?})"); } } } Ok(()) } pub async fn list(&self, ec2: &EC2) -> Result<Vec<KeyPairInfo>, EC2Error> { ec2.list_key_pair().await } } impl Default for KeyPairManager { fn default() -> Self { KeyPairManager { key_pair: KeyPairInfo::builder().build(), key_file_path: Default::default(), key_file_dir: env::temp_dir(), } } } use std::net::Ipv4Addr; use aws_sdk_ec2::types::SecurityGroup; use crate::ec2::{EC2Error, EC2}; /// SecurityGroupManager tracks the lifecycle of a SecurityGroup for an instance, /// including adding a rule to allow SSH from a public IP address. #[derive(Debug, Default)] pub struct SecurityGroupManager { group_name: String, group_description: String, security_group: Option<SecurityGroup>, } impl SecurityGroupManager { pub async fn create( &mut self, ec2: &EC2, group_name: &str, group_description: &str, ) -> Result<(), EC2Error> { self.group_name = group_name.into(); self.group_description = group_description.into(); self.security_group = Some( ec2.create_security_group(group_name, group_description) .await .map_err(|e| e.add_message("Couldn't create security group"))?, ); Ok(()) } pub async fn authorize_ingress(&self, ec2: &EC2, ip_address: Ipv4Addr) -> Result<(), EC2Error> { if let Some(sg) = &self.security_group { ec2.authorize_security_group_ssh_ingress( sg.group_id() .ok_or_else(|| EC2Error::new("Missing security group ID"))?, vec![ip_address], ) .await?; }; Ok(()) } pub async fn delete(self, ec2: &EC2) -> Result<(), EC2Error> { if let Some(sg) = &self.security_group { ec2.delete_security_group( sg.group_id() .ok_or_else(|| EC2Error::new("Missing security group ID"))?, ) .await?; }; Ok(()) } pub fn group_name(&self) -> &str { &self.group_name } pub fn vpc_id(&self) -> Option<&str> { self.security_group.as_ref().and_then(|sg| sg.vpc_id()) } pub fn security_group(&self) -> Option<&SecurityGroup> { self.security_group.as_ref() } } impl std::fmt::Display for SecurityGroupManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.security_group { Some(sg) => { writeln!( f, "Security group: {}", sg.group_name().unwrap_or("(unknown group)") )?; writeln!(f, "\tID: {}", sg.group_id().unwrap_or("(unknown group id)"))?; writeln!(f, "\tVPC: {}", sg.vpc_id().unwrap_or("(unknown group vpc)"))?; if !sg.ip_permissions().is_empty() { writeln!(f, "\tInbound Permissions:")?; for permission in sg.ip_permissions() { writeln!(f, "\t\t{permission:?}")?; } } Ok(()) } None => writeln!(f, "No security group loaded."), } } }
The main entry point for the scenario.
use ec2_code_examples::{ ec2::EC2, getting_started::{ scenario::{run, Ec2InstanceScenario}, util::UtilImpl, }, ssm::SSM, }; #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let sdk_config = aws_config::load_from_env().await; let ec2 = EC2::new(aws_sdk_ec2::Client::new(&sdk_config)); let ssm = SSM::new(aws_sdk_ssm::Client::new(&sdk_config)); let util = UtilImpl {}; let scenario = Ec2InstanceScenario::new(ec2, ssm, util); run(scenario).await; }
-
For API details, see the following topics in Amazon SDK for Rust API reference.
-
For a complete list of Amazon SDK developer guides and code examples, see Create Amazon EC2 resources using an Amazon SDK. This topic also includes information about getting started and details about previous SDK versions.