amazon-web-servicesrustaws-sdk-rust

How do I assume a role in a different account and list ec2 instances filtered by region in Rust using AWS SDK?


I've done this many times using python but in Rust I am having much difficulty with creating a client from the assumed role credentials and filtering ec2s based on region. Here is the working python code that I am attempting to rewrite in Rust:

    assumed_role_object = sts.assume_role(
        RoleArn=f"arn:aws:iam::{account}:role/role",
        RoleSessionName="lambdaSession"
    )

    # get regions using global assumed role credentials
    ec2_client = boto3.client('ec2',
        aws_access_key_id=assumed_role_object['Credentials']['AccessKeyId'],
        aws_secret_access_key=assumed_role_object['Credentials']['SecretAccessKey'],
        aws_session_token=assumed_role_object['Credentials']['SessionToken'])
    regions = [region['RegionName'] for region in ec2_client.describe_regions()['Regions']]

    # for each region get ec2 instances
    for region in regions:
        try:
            # try global sts with region filter
            filters = [{'Name':'region-name', 'Values':[region]}]
            ec2_response = ec2_client.describe_instances(Filters=filters)
        # Catch ClientError exception
        except botocore.exceptions.ClientError as _error:
            try: #try regional sts
                # create regional sts session
                sts_regional_client = boto3.client('sts', region_name=region)
                regional_assumed_role_object = sts_regional_client.assume_role(
                    RoleArn=f"arn:aws:iam::{account}:role/role",
                    RoleSessionName="regionallambdasession"
                    )
                ec2_client = boto3.client(
                    "ec2",
                    region_name=region,
                    aws_access_key_id=regional_assumed_role_object["Credentials"]["AccessKeyId"],
                    aws_secret_access_key=regional_assumed_role_object["Credentials"]["SecretAccessKey"],
                    aws_session_token=regional_assumed_role_object["Credentials"]["SessionToken"],
                    )
                ec2_response = ec2_client.describe_instances()
                
            except botocore.exceptions.ClientError as _error2:
                print(f"skipping {region} bc of error {_error2}\n")
                continue

In Rust, the sts assume role isn't working, nor is the filtering by region, I am able to list the regions and loop through them.


use aws_sdk_ec2 as ec2;
use ec2::{Client as Ec2Client, Error as Ec2Error, config::Region};
use aws_config::meta::Region;
use aws_config::meta::region::RegionProviderChain;
use aws_config::Region;

fn main(){
    let config = aws_config::load_from_env().await;

    // This might work - I need to get these credentials and list ec2 instances in this account
    let sts_client = StsClient::new(&config);
    let assume_role_response = sts_client.assume_role()
        .role_arn(format!("arn:aws:iam::{}:role/role", account_id))
        .role_session_name("inventorySession".to_string())
        .send()
        .await;
    
    // I want to use the assumed_role_response when creating this client
    let ec2_client = ec2::Client::new(&config);
    // get regions
    let regions = ec2_client.describe_regions().send().await;

    for region in regions?.regions(){
        let region_name = region.region_name().unwrap();
        println!("loop Region: {:?}", region_name);

        let region_provider = RegionProviderChain::first_try(Region::new(region_name.to_string()))
            .or_default_provider()
            .or_else(Region::new(region_name.to_string()));
        
        // This works - I'm able to create the region_provider
        println!(
            "Region:                    {}",
            region_provider.region().await.unwrap().as_ref()
        );
        
        // I want to use credentials from assumed_role_response and new region to create ec2_regional_config ???
        let new_config = config.region(Region::new(region_name.to_string())).unwrap();
        let ec2_regional_client = Ec2Client::new(&new_config); // This isn't using the region or the assumed_role credentials.
        
        let ec2_instances = ec2_regional_client
        .describe_instances()
        .send()
        .await;
        println!("{:?}", ec2_instances);
    }
}

When I initially started this project I was using the rusoto crate and was able to do all of this but was catching errors on apse4 which is a newer region the rusoto crate didn't include! rusoto isn't going to add that region because it is now no longer maintained :(. Someone please help if you know how to do this using the aws-sdk-rust! Thank you!


Solution

  • Here is the solution I got working for me. I needed to include the .region() in addition to the .credentials_provider(provider) for the regional_config. Also needed to turn region_name into a static string reference ('reg') that is a heap allocated pointer to the static string in the helper function so the region_name variable isn't getting recreated each time we loop through the regions loop.

    use aws_sdk_ec2 as ec2;
    use ec2::{Client as Ec2Client, Error as Ec2Error};
    use aws_config::sts::AssumeRoleProvider;
    use aws_config::Region;
    
    // receive ec2_client and role_arn from main function
    async fn list_ec2_instance_ids(ec2_client: &Ec2Client, role_arn: &String ) -> Result<(), Ec2Error> {
        let regions = ec2_client.describe_regions().send().await.unwrap();
    
        for region in regions.regions.unwrap_or_default() {
            // create regional config and assumed role provider
            println!("Region: {}", region.region_name().unwrap());
            let reg: &'static str = Box::leak(Box::from(region.region_name().unwrap()));
            let config = aws_config::from_env().region(Region::from_static(reg)).load().await;
            let provider = aws_config::sts::AssumeRoleProvider::builder(role_arn.clone())
            .configure(&config)
            .build()
            .await;
    
            // load other credentials from env variables
            let regional_config = aws_config::from_env()
                .region(Region::from_static(reg))
                .credentials_provider(provider)
                .load()
                .await;
    
            let regional_ec2_client = Ec2Client::new(&regional_config);
    
            // filter region
            let ec2_response = regional_ec2_client
            .describe_instances()
            .send()
            .await?;
    
            for reservation in ec2_response.reservations.unwrap_or_default() {
                for instance in reservation.instances.unwrap_or_default() {
                    println!("Instance ID: {}", instance.instance_id().unwrap_or_default());
                }            
            }
        }
        Ok(())
    }
    
    
    #[tokio::main]
    async fn main(){
        let account_id = "123456789012";
    
        // Get list of regions for this account
        let role_arn = format!("arn:aws:iam::{}:role/test-role", account_id);
        let assumed_role_provider = AssumeRoleProvider::builder(role_arn.clone())
            .region(Region::from_static("us-east-1"))
            .session_name("testAR")
            .build().await;
        let config = aws_config::from_env()
            .region("us-east-1")
            .credentials_provider(assumed_role_provider)
            .load()
            .await;
        // create ec2 client for account and pass to regions function
        let ec2_client = Ec2Client::new(&config);
        list_ec2_instance_ids(&ec2_client, &role_arn).await.unwrap();
    
    }