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