Amazon EC2 examples using SDK for Rust (original) (raw)
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:🥅: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:🥅: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 AWS SDK for Rust API reference.
- AllocateAddress
- AssociateAddress
- AuthorizeSecurityGroupIngress
- CreateKeyPair
- CreateSecurityGroup
- DeleteKeyPair
- DeleteSecurityGroup
- DescribeImages
- DescribeInstanceTypes
- DescribeInstances
- DescribeKeyPairs
- DescribeSecurityGroups
- DisassociateAddress
- ReleaseAddress
- RunInstances
- StartInstances
- StopInstances
- TerminateInstances
- UnmonitorInstances