Created
December 14, 2020 00:23
-
-
Save luckerby/7a2320fb7e833627cf5ecb8d3eeefef6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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