aws_throwaway/
lib.rs

1mod backend;
2mod ec2_instance;
3mod ec2_instance_definition;
4mod ssh;
5
6use std::net::IpAddr;
7
8pub use backend::{Aws, InstanceType, PlacementStrategy};
9pub use ec2_instance::{Ec2Instance, NetworkInterface};
10pub use ec2_instance_definition::{Ec2InstanceDefinition, InstanceOs};
11
12// include a magic number in the keyname to avoid collisions
13// This can never change or we may fail to cleanup resources.
14const USER_TAG_NAME: &str = "aws-throwaway-23c2d22c-d929-43fc-b2a4-c1c72f0b733f:user";
15const APP_TAG_NAME: &str = "aws-throwaway-23c2d22c-d929-43fc-b2a4-c1c72f0b733f:app";
16
17pub struct AwsBuilder {
18    cleanup: CleanupResources,
19    use_public_addresses: bool,
20    ingress_restriction: IngressRestriction,
21    vpc_id: Option<String>,
22    az_name: Option<String>,
23    subnet_id: Option<String>,
24    placement_strategy: PlacementStrategy,
25    security_group_id: Option<String>,
26    expose_ports_to_internet: Vec<u16>,
27}
28
29/// The default configuration will succeed for an AMI user with sufficient access and unmodified default vpcs/subnets
30/// Consider altering the configuration if:
31/// * you want to reduce the amount of access required by the user
32/// * you want to connect directly from within the VPC
33/// * you have already created a specific VPC, subnet or security group that you want aws-throwaway to make use of.
34///
35/// All resources will be created in us-east-1.
36/// This is hardcoded so that aws-throwaway only has to look into one region when cleaning up.
37/// All instances are created in a single spread placement group in a single AZ to ensure consistent latency between instances.
38// TODO: document minimum required access for default configuration.
39impl AwsBuilder {
40    fn new(cleanup: CleanupResources) -> Self {
41        AwsBuilder {
42            cleanup,
43            use_public_addresses: true,
44            ingress_restriction: IngressRestriction::NoRestrictions,
45            vpc_id: None,
46            az_name: None,
47            subnet_id: None,
48            // refer to: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html
49            // I believe Spread is the best default since it is the easiest for amazon to fulfill and gives the most realistic results in benchmarks.
50            placement_strategy: PlacementStrategy::Spread,
51            security_group_id: None,
52            expose_ports_to_internet: vec![],
53        }
54    }
55
56    /// When set to:
57    /// * true => aws-throwaway will connect to the public ip of the instances that it creates.
58    ///     + The subnet must have the property MapPublicIpOnLaunch set to true (the unmodified default subnet meets this requirement)
59    ///     + Elastic IPs will be created for instances with multiple network interfaces because AWS does not assign a public IP in that scenario
60    /// * false => aws-throwaway will connect to the private ip of the instances that it creates.
61    ///     + aws-throwaway must be running on a machine within the VPC used by aws-throwaaway or a VPN must be used to connect to the VPC or another similar setup.
62    ///
63    /// If the subnet used has MapPublicIpOnLaunch=true then all instances will be publically accessible regardless of this use_public_addresses field.
64    ///
65    /// The default is `true`.
66    pub fn use_public_addresses(mut self, use_public_addresses: bool) -> Self {
67        self.use_public_addresses = use_public_addresses;
68        self
69    }
70
71    pub fn use_ingress_restriction(mut self, ingress_restriction: IngressRestriction) -> Self {
72        self.ingress_restriction = ingress_restriction;
73        self
74    }
75
76    /// * Some(_) => All resources will go into the specified vpc
77    /// * None => All resources will go into the default vpc
78    ///
79    /// The default is `None`
80    pub fn use_vpc_id(mut self, vpc_id: Option<String>) -> Self {
81        self.vpc_id = vpc_id;
82        self
83    }
84
85    /// * Some(_) => All resources will go into the specified AZ
86    /// * None => All resources will go into the default AZ (us-east-1c)
87    ///
88    /// The default is `None`
89    pub fn use_az(mut self, az_name: Option<String>) -> Self {
90        self.az_name = az_name;
91        self
92    }
93
94    /// * Some(_) => All instances will go into the specified subnet
95    /// * None => All instances will go into the default subnet for the specified or default vpc
96    ///
97    /// The default is `None`
98    pub fn use_subnet_id(mut self, subnet_id: Option<String>) -> Self {
99        self.subnet_id = subnet_id;
100        self
101    }
102
103    /// All EC2 instances are created within a single placement group with the specified strategy.
104    ///
105    /// The default is `PlacementStrategy::Spread`
106    pub fn use_placement_strategy(mut self, placement_strategy: PlacementStrategy) -> Self {
107        self.placement_strategy = placement_strategy;
108        self
109    }
110
111    /// * Some(_) => All instances will use the specified security group
112    /// * None => A single security group will be created for all instances to use. It will allow:
113    ///      + ssh traffic in from the internet
114    ///      + all traffic out to the internet
115    ///      + all traffic in+out between instances in the security group, i.e. all ec2 instances created by this [`Aws`] instance
116    ///
117    /// The default is `None`
118    pub fn use_security_group_id(mut self, security_group_id: Option<String>) -> Self {
119        self.security_group_id = security_group_id;
120        self
121    }
122
123    /// Adds the provided ports as allowing traffic in+out to internet in the automatically generated security group.
124    /// By default ingress is allowed from port 22 and this cannot be disabled.
125    pub fn expose_ports_to_internet(mut self, ports: Vec<u16>) -> Self {
126        self.expose_ports_to_internet = ports;
127        self
128    }
129
130    /// Builds the Aws instance.
131    ///
132    /// Will panic if both `expose_ports_to_internet` and `use_security_group_id` are enabled.
133    pub async fn build(self) -> Aws {
134        if !self.expose_ports_to_internet.is_empty() && self.security_group_id.is_some() {
135            panic!(
136                "Both `use_security_group_id` and `expose_ports_to_internet` are set. Ensure only one of these options is set."
137            )
138        }
139        Aws::new(self).await
140    }
141}
142
143/// Specify the cleanup process to use.
144pub enum CleanupResources {
145    /// Cleanup resources created by all [`Aws`] instances that use [`CleanupResources::WithAppTag`] of the same tag.
146    /// It is highly reccomended that this tag is hardcoded, generating this tag could easily lead to forgotten resources.
147    WithAppTag(String),
148    /// Cleanup resources created by all [`Aws`] instances regardless of whether it was created via [`CleanupResources::AllResources`] or [`CleanupResources::WithAppTag`]
149    AllResources,
150}
151
152/// Defines how to derive the ingress rules of the generated security group for external access.
153///
154/// Internal network traffic between instances created through aws-throwaway is always allowed,
155/// regardless of the `IngressRestriction` value used.
156///
157/// These rules apply to the always enabled port 22 and any extra ports enabled by `AwsBuilder::expose_ports_to_internet`.
158#[non_exhaustive]
159pub enum IngressRestriction {
160    /// Allow ingress from any machine on the internet.
161    /// Many corporate environments will disallow this.
162    NoRestrictions,
163    /// Allow ingress only from the public IP address of the machine aws-throwaway is running on.
164    /// Possibly slightly slower to startup, the public IP will be fetched from https://api.ipify.org in parallel to other work.
165    LocalPublicAddress,
166    // In the future we might add:
167    //UseSpecificAddress(IpAddr)
168}
169
170impl IngressRestriction {
171    async fn cidr_ip(&self) -> String {
172        match self {
173            IngressRestriction::NoRestrictions => "0.0.0.0/0".to_owned(),
174            IngressRestriction::LocalPublicAddress => {
175                let api = "https://api.ipify.org";
176                let ip = reqwest::get(api).await.unwrap().text().await.unwrap();
177                // roundtrip through IpAddr to ensure that we did in fact receive an IP.
178                let ip: IpAddr = ip.parse().unwrap();
179                format!("{ip}/32")
180            }
181        }
182    }
183}