Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,106 @@ gpuaudit diff scan-apr-08.json scan-apr-15.json

Matches instances by ID. Reports added, removed, and changed instances with per-field diffs (instance type, pricing model, cost, state, GPU allocation, waste severity).

## Multi-Account Scanning

Scan multiple AWS accounts in a single invocation using STS AssumeRole.

### Prerequisites

Deploy a read-only IAM role (`gpuaudit-reader`) to each target account. See [Cross-Account Role Setup](#cross-account-role-setup) below.

### Usage

```bash
# Scan specific accounts
gpuaudit scan --targets 111111111111,222222222222 --role gpuaudit-reader

# Scan entire AWS Organization
gpuaudit scan --org --role gpuaudit-reader

# Exclude management account
gpuaudit scan --org --role gpuaudit-reader --skip-self

# With external ID
gpuaudit scan --targets 111111111111 --role gpuaudit-reader --external-id my-secret
```

### Cross-Account Role Setup

#### Terraform

```hcl
variable "management_account_id" {
description = "AWS account ID where gpuaudit runs"
type = string
}

resource "aws_iam_role" "gpuaudit_reader" {
name = "gpuaudit-reader"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${var.management_account_id}:root" }
Action = "sts:AssumeRole"
}]
})
}

resource "aws_iam_role_policy" "gpuaudit_reader" {
name = "gpuaudit-policy"
role = aws_iam_role.gpuaudit_reader.id
policy = file("gpuaudit-policy.json") # from: gpuaudit iam-policy > gpuaudit-policy.json
}
```

Deploy to all accounts using Terraform workspaces or CloudFormation StackSets.

#### CloudFormation StackSet

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
ManagementAccountId:
Type: String
Resources:
GpuAuditRole:
Type: AWS::IAM::Role
Properties:
RoleName: gpuaudit-reader
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${ManagementAccountId}:root"
Action: sts:AssumeRole
Policies:
- PolicyName: gpuaudit-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ec2:DescribeInstances
- ec2:DescribeInstanceTypes
- ec2:DescribeRegions
- sagemaker:ListEndpoints
- sagemaker:DescribeEndpoint
- sagemaker:DescribeEndpointConfig
- eks:ListClusters
- eks:ListNodegroups
- eks:DescribeNodegroup
- cloudwatch:GetMetricData
- cloudwatch:GetMetricStatistics
- cloudwatch:ListMetrics
- ce:GetCostAndUsage
- ce:GetReservationUtilization
- ce:GetSavingsPlansUtilization
- pricing:GetProducts
Resource: "*"
```

## IAM permissions

gpuaudit is read-only. It never modifies your infrastructure. Generate the minimal IAM policy:
Expand Down
33 changes: 33 additions & 0 deletions cmd/gpuaudit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ var (
scanKubeContext string
scanExcludeTags []string
scanMinUptimeDays int
scanTargets []string
scanRole string
scanExternalID string
scanOrg bool
scanSkipSelf bool
)

// --- diff command ---
Expand Down Expand Up @@ -85,6 +90,12 @@ func init() {
scanCmd.Flags().StringVar(&scanKubeContext, "kube-context", "", "Kubernetes context to use (default: current context)")
scanCmd.Flags().StringSliceVar(&scanExcludeTags, "exclude-tag", nil, "Exclude instances matching tag (key=value, repeatable)")
scanCmd.Flags().IntVar(&scanMinUptimeDays, "min-uptime-days", 0, "Only flag instances running for at least this many days")
scanCmd.Flags().StringSliceVar(&scanTargets, "targets", nil, "Account IDs to scan (comma-separated)")
scanCmd.Flags().StringVar(&scanRole, "role", "", "IAM role name to assume in each target")
scanCmd.Flags().StringVar(&scanExternalID, "external-id", "", "STS external ID for cross-account role assumption")
scanCmd.Flags().BoolVar(&scanOrg, "org", false, "Auto-discover all accounts from AWS Organizations")
scanCmd.Flags().BoolVar(&scanSkipSelf, "skip-self", false, "Exclude the caller's own account from the scan")
scanCmd.MarkFlagsMutuallyExclusive("targets", "org")

diffCmd.Flags().StringVar(&diffFormat, "format", "table", "Output format: table, json")

Expand All @@ -98,6 +109,10 @@ func init() {
func runScan(cmd *cobra.Command, args []string) error {
ctx := context.Background()

if (len(scanTargets) > 0 || scanOrg) && scanRole == "" {
return fmt.Errorf("--role is required when using --targets or --org")
}

opts := awsprovider.DefaultScanOptions()
opts.Profile = scanProfile
opts.Regions = scanRegions
Expand All @@ -107,6 +122,11 @@ func runScan(cmd *cobra.Command, args []string) error {
opts.SkipCosts = scanSkipCosts
opts.ExcludeTags = parseExcludeTags(scanExcludeTags)
opts.MinUptimeDays = scanMinUptimeDays
opts.Targets = scanTargets
opts.Role = scanRole
opts.ExternalID = scanExternalID
opts.OrgScan = scanOrg
opts.SkipSelf = scanSkipSelf

result, err := awsprovider.Scan(ctx, opts)
if err != nil {
Expand Down Expand Up @@ -322,8 +342,21 @@ var iamPolicyCmd = &cobra.Command{
},
"Resource": "*",
},
{
"Sid": "GPUAuditCrossAccount",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::*:role/gpuaudit-reader",
},
{
"Sid": "GPUAuditOrganizations",
"Effect": "Allow",
"Action": "organizations:ListAccounts",
"Resource": "*",
},
},
}
fmt.Fprintln(os.Stdout, "// The last two statements (CrossAccount, Organizations) are only needed for --targets or --org scanning.")
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(policy)
Expand Down
Loading