Skip to content

Instantly share code, notes, and snippets.

@luckerby
Created December 14, 2020 00:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save luckerby/7a2320fb7e833627cf5ecb8d3eeefef6 to your computer and use it in GitHub Desktop.
Save luckerby/7a2320fb7e833627cf5ecb8d3eeefef6 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Amazon;
using Amazon.EC2;
using Amazon.EC2.Model;
using Amazon.Organizations;
using Amazon.Organizations.Model;
using Amazon.Runtime;
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;
namespace AWSRetrieveEC2Instances
{
class EC2InstanceLimitedData
{
public string InstanceId { get; set; }
public string InstanceType { get; set; }
public string PrivateIpAddresses { get; set; }
public string PublicIpAddresses { get; set; }
public string AWSAccountName { get; set; }
public string AWSAccountId { get; set; }
}
class Program
{
static async Task Main(string[] args)
{
List<Account> awsAccounts;
try
{
// Get the list of the accounts in the current organization
awsAccounts = await GetAWSAccounts();
}
catch (Exception e)
{
Console.WriteLine($"Error retrieving the list of accounts, most likely missing rights or invalid credentials provided. Exception follows:");
Console.WriteLine($"{e.Message}");
return;
}
// The list that will eventually contain all the EC2 instances retrieved from all the accounts
List<EC2InstanceLimitedData> ec2Instances = new List<EC2InstanceLimitedData>();
foreach (var awsAccount in awsAccounts)
{
Console.WriteLine($"In account {awsAccount.Id}");
// Get the temporary credentials for the target role from a regional STS endpoint
// that's guaranteed to always be enabled, so that they work in all the enabled
// regions in the target accounts
var credentialsForTargetRole = await GetEC2ReadAccessRoleCredentials(awsAccount.Id, RegionEndpoint.USEast1);
if (credentialsForTargetRole == null) {
Console.WriteLine($"Invalid credentials provided!");
return;
}
foreach (var enabledRegion in await GetEC2EnabledRegions(credentialsForTargetRole))
{
var targetRegion = RegionEndpoint.GetBySystemName(enabledRegion.RegionName);
// Build an EC2 client based on the temporary credentials handed back
// by STS when assuming the role in the current account,targeting the
// current region
using (var ec2Client = new AmazonEC2Client(credentialsForTargetRole, targetRegion))
{
// Add the EC2 instances retrieved for the current account and region to
// the overall list
ec2Instances.AddRange(await GetEC2InstancesForAccountAndRegion(ec2Client, awsAccount));
}
}
}
Console.WriteLine($"{ec2Instances.Count} EC2 instances retrieved overall");
// Serialize the list of EC2 instance data to json
var ec2InstancesAsJson = JsonSerializer.Serialize(ec2Instances,
new JsonSerializerOptions
{
WriteIndented = true
});
System.IO.File.WriteAllText("awsEC2Instances.json", ec2InstancesAsJson);
}
private static async Task<List<Account>> GetAWSAccounts()
{
List<Account> awsAccounts = new List<Account>();
// Retrieve the AWS accounts information from the organization itself;
// use us-east-1, as it's a region that will always be enabled
AmazonOrganizationsClient awsOrganizationsClient =
new AmazonOrganizationsClient(RegionEndpoint.USEast1);
string nextToken = null;
do
{
var listAccountsResponse = await awsOrganizationsClient.ListAccountsAsync(
new ListAccountsRequest { NextToken = nextToken });
awsAccounts.AddRange(listAccountsResponse.Accounts);
nextToken = listAccountsResponse.NextToken;
} while (nextToken != null);
return awsAccounts;
}
private static async Task<Credentials> GetEC2ReadAccessRoleCredentials(string awsAccountId,
RegionEndpoint regionEndpoint)
{
// Don't use the default 'legacy' mode, but use instead the 'regional' one, so that
// a regional STS (Security Token Service) endpoint is contacted, which will emit temporary credentials that
// are valid in all the enabled regions
using (var stsClient = new AmazonSecurityTokenServiceClient(new AmazonSecurityTokenServiceConfig
{ StsRegionalEndpoints = StsRegionalEndpointsValue.Regional, RegionEndpoint = regionEndpoint }))
{
Credentials credentialsForTargetRole = null;
try
{
var assumeRoleRequest = new AssumeRoleRequest
{
RoleArn = $"arn:aws:iam::{awsAccountId}:role/EC2ReadAccess",
RoleSessionName =
"EC2InventoryCode_IAM" // RoleSessionName is mandatory, otherwise an exception is thrown
};
var response = await stsClient.AssumeRoleAsync(assumeRoleRequest);
// There's no point in checking the underlying response code, since an exception
// would take care of this anyway. See https://forums.aws.amazon.com/thread.jspa?threadID=171415
credentialsForTargetRole = response.Credentials;
}
catch (AmazonSecurityTokenServiceException ex)
{
Console.WriteLine($"Exception thrown for region {regionEndpoint.DisplayName}: {ex.Message}");
}
return credentialsForTargetRole;
}
}
private static async Task<List<Region>> GetEC2EnabledRegions(Credentials credentialsForTargetRole)
{
// Build a throw-away AmazonSecurityTokenServiceClient and AmazonEC2Client objects
// just to get the enabled regions. We'll use the us-east-1 region, since that
// can't be disabled and it's enabled by default
using (var tempEc2Client = new AmazonEC2Client(credentialsForTargetRole,
RegionEndpoint.USEast1))
{
// Retrieve the regions that are enabled under the account where the provided
// credentials are to be used
var regionsResponse = await tempEc2Client.DescribeRegionsAsync();
return regionsResponse.Regions;
}
}
// Note that the AmazonEC2Client is already built against a specific AWS account and a specific region
private static async Task<List<EC2InstanceLimitedData>> GetEC2InstancesForAccountAndRegion(
AmazonEC2Client ec2Client, Account awsAccount)
{
// Don't get fooled by the fact that the AWS account gets passed
// through as parameter. It doesn't act as a "filter" for the EC2
// instance data retrieved, as that's done based off the AmazonEC2Client
// object, which is also passed as a parameter (and was previously
// built against a a specific account using a specific set of
// credentials, and also against a specific region). Its sole purpose
// is to be able to add information about the account with each
// EC2 instance element in our final list, as extracting the account
// info directly from the AmazonEC2Client doesn't look possible
// In the EC2 world there are reservations (do not confuse with Reserved Instances)
// that refer to a launch event. Within a reservation there can be one or more EC2
// instances (the actual VMs); if a launch fired up multiple instances, then all
// those instances will belong to the reservation corresponding to the launch.
// The list of EC2 instances we'll gradually build, for the AWS account and region
// the supplied AmazonEC2Client was built against
List<EC2InstanceLimitedData> ec2Instances = new List<EC2InstanceLimitedData>();
string nextToken = null;
do
{
var describeInstancesResult = await ec2Client.DescribeInstancesAsync(
new DescribeInstancesRequest() {NextToken = nextToken});
foreach (var reservation in describeInstancesResult.Reservations)
{
foreach (var instance in reservation.Instances)
{
Console.WriteLine($"name={instance.InstanceId} (reservation id= {reservation.ReservationId})");
// Add all the private IPs along with their corresponding public IPs;
// iterate through network adapters, then one level down through
// each private IP
List<string> currentPrivateIPsList = new List<string>();
List<string> currentPublicIPsList = new List<string>();
foreach (var eni in instance.NetworkInterfaces)
{
foreach (var privateIP in eni.PrivateIpAddresses)
{
currentPrivateIPsList.Add(privateIP.PrivateIpAddress);
if (privateIP.Association != null)
currentPublicIPsList.Add(privateIP.Association.PublicIp);
}
}
var currentPrivateIPsString = String.Join(",", currentPrivateIPsList.ToArray());
var currentPublicIPsString = String.Join(",", currentPublicIPsList.ToArray());
ec2Instances.Add(new EC2InstanceLimitedData
{
AWSAccountId = awsAccount.Id,
AWSAccountName = awsAccount.Name,
InstanceId = instance.InstanceId,
InstanceType = instance.InstanceType,
PrivateIpAddresses = currentPrivateIPsString,
PublicIpAddresses = currentPublicIPsString
});
}
}
nextToken = describeInstancesResult.NextToken;
} while (nextToken != null);
Console.WriteLine(
$"{ec2Instances.Count} instances found in region {ec2Client.Config.RegionEndpoint.DisplayName}");
return ec2Instances;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment