Skip to content

Commit 3b3824b

Browse files
authored
GROW-662: Capture K8 related sub-counts for AWS (#9)
1 parent 48557cb commit 3b3824b

File tree

5 files changed

+219
-10
lines changed

5 files changed

+219
-10
lines changed

README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ Cloud Resource Counter (v0.7.0) running with:
133133
Activity
134134
* Retrieving Account ID...OK (240520192079)
135135
* Retrieving EC2 counts...................OK (5)
136+
* Retrieving EC2 K8 related VMs Sub-instance counts...................OK (1)
136137
* Retrieving Spot instance counts...................OK (4)
137138
* Retrieving EBS volume counts...................OK (9)
138139
* Retrieving Unique container counts...................OK (3)
@@ -151,7 +152,7 @@ As you can see above, no command line arguments were necessary: it used my defau
151152
Here is what the CSV file looks like. It is important to mention that this tool was run TWICE to collect the results of two different accounts/profiles.
152153

153154
```csv
154-
Account ID,Timestamp,Region,# of EC2 Instances,# of Spot Instances,# of EBS Volumes,# of Unique Containers,# of Lambda Functions,# of RDS Instances,# of Lightsail Instances,# of S3 Buckets,# of EKS Nodes
155+
Account ID,Timestamp,Region,# of EC2 Instances,# of EC2 K8 related VMs Sub-instances,# of Spot Instances,# of EBS Volumes,# of Unique Containers,# of Lambda Functions,# of RDS Instances,# of Lightsail Instances,# of S3 Buckets,# of EKS Nodes
155156
896149672290,2020-10-20T16:29:39-04:00,ALL_REGIONS,2,3,7,3,2,3,2,2,2
156157
240520192079,2020-10-21T16:24:06-04:00,ALL_REGIONS,5,4,9,3,12,7,0,13,0
157158
```
@@ -267,9 +268,10 @@ The `aws-resource-counter` examines the following resources:
267268
1. **EC2**. We count the number of EC2 **running** instances (both "normal" and Spot instances) across all regions.
268269
269270
* For EC2 instances, we only count those _without_ an Instance Lifecycle tag (which is either `spot` or `scheduled`).
271+
* For EC2 K8 related VMs sub-instances, we only count those with a tag of `aws:eks:cluster-name`.
270272
* For Spot instance, we only count those with an Instance Lifecycle tag of `spot`.
271273
272-
* This is stored in the generated CSV file under the "# of EC2 Instances" and "# of Spot Instances" columns.
274+
* This is stored in the generated CSV file under the "# of EC2 Instances", "# of EC2 K8 related VMs Sub-instances", and "# of Spot Instances" columns.
273275
274276
1. **EBS Volumes.** We count the number of "attached" EBS volumes across all regions.
275277
@@ -348,7 +350,7 @@ To collect the total number of EC2 instances across all regions, we will need to
348350
```bash
349351
$ aws ec2 describe-regions $aws_p \
350352
--filters Name=opt-in-status,Values=opt-in-not-required,opted-in \
351-
--region us-east-1 --output text --query Regions[].RegionName
353+
--region us-east-1 --output text --query 'Regions[].RegionName'
352354
eu-north-1 ap-south-1 eu-west-3 ...
353355
```
354356
@@ -364,7 +366,7 @@ We will be using the results of this command to "iterate" over all regions. To m
364366
```bash
365367
$ ec2_r=$(aws ec2 describe-regions $aws_p \
366368
--filters Name=opt-in-status,Values=opt-in-not-required,opted-in \
367-
--region us-east-1 --output text --query Regions[].RegionName )
369+
--region us-east-1 --output text --query 'Regions[].RegionName' )
368370
```
369371
370372
You can show the list of regions for your account by using the `echo` command:
@@ -417,6 +419,32 @@ The second and third lines are our call to `describe-instances` (as shown above)
417419
418420
In the fourth line, we paste all of the values into a long addition and use `bc` to sum the values.
419421
422+
#### EC2 K8 Related VMs Subcount Instances
423+
424+
Here is the command to count the number of _EC K8 related VMs subcount_ instances for a given region:
425+
426+
```bash
427+
$ aws ec2 describe-instances $aws_p --no-paginate --region us-east-1 \
428+
--filters Name=instance-state-name,Values=running \
429+
--filters Name=tag-key,Values='aws:eks:cluster-name' \
430+
--query 'length(Reservations[].Instances[?!not_null(InstanceLifecycle)].InstanceId[])'
431+
1
432+
```
433+
434+
This command is similar to the normal EC2 query, but now explicitly checks for EC2 instances whose `Tags` have that key `aws:eks:cluster-name`.
435+
436+
We will need to run this command over all regions. Here is what it looks like:
437+
438+
```bash
439+
$ for reg in $ec2_r; do \
440+
aws ec2 describe-instances $aws_p --no-paginate --region $reg \
441+
--filters Name=instance-state-name,Values=running \
442+
--filters Name=tag-key,Values='aws:eks:cluster-name' \
443+
--query 'length(Reservations[].Instances[?!not_null(InstanceLifecycle)].InstanceId[])' ; \
444+
done | paste -s -d+ - | bc
445+
5
446+
```
447+
420448
#### Spot Instances
421449
422450
Here is the command to count the number of _Spot_ instances for a given region:

ec2_k8_subcount.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/******************************************************************************
2+
Cloud Resource Counter
3+
File: spot.go
4+
5+
Summary: Provides a count of all Spot EC2 instances.
6+
******************************************************************************/
7+
8+
package main
9+
10+
import (
11+
"github.com/aws/aws-sdk-go/aws"
12+
"github.com/aws/aws-sdk-go/service/ec2"
13+
color "github.com/logrusorgru/aurora"
14+
)
15+
16+
// SpotInstances retrieves the count of all EC2 spot instances
17+
// either for all regions (allRegions is true) or the region
18+
// associated with the session.
19+
// This method gives status back to the user via the supplied
20+
// ActivityMonitor instance.
21+
func EC2K8SubInstances(sf ServiceFactory, am ActivityMonitor, allRegions bool) int {
22+
// Indicate activity
23+
am.StartAction("Retrieving EC2 K8 related VMs Sub-instance counts")
24+
25+
// Should we get the counts for all regions?
26+
instanceCount := 0
27+
if allRegions {
28+
// Get the list of all enabled regions for this account
29+
regionsSlice := GetEC2Regions(sf.GetEC2InstanceService(""), am)
30+
31+
// Loop through all of the regions
32+
for _, regionName := range regionsSlice {
33+
// Get the EC2 counts for a specific region
34+
instanceCount += ec2K8SubInstancesForSingleRegion(sf.GetEC2InstanceService(regionName), am)
35+
}
36+
} else {
37+
// Get the EC2 counts for the region selected by this session
38+
instanceCount = ec2K8SubInstancesForSingleRegion(sf.GetEC2InstanceService(""), am)
39+
}
40+
41+
// Indicate end of activity
42+
am.EndAction("OK (%d)", color.Bold(instanceCount))
43+
44+
return instanceCount
45+
}
46+
47+
func ec2K8SubInstancesForSingleRegion(ec2is *EC2InstanceService, am ActivityMonitor) int {
48+
// Indicate activity
49+
am.Message(".")
50+
51+
// Construct our input to find ONLY RUNNING EC2 instances that also have the "aws:eks:cluster-name" tag
52+
input := &ec2.DescribeInstancesInput{
53+
Filters: []*ec2.Filter{
54+
{
55+
Name: aws.String("tag-key"),
56+
Values: []*string{
57+
aws.String("aws:eks:cluster-name"),
58+
},
59+
},
60+
{
61+
Name: aws.String("instance-state-name"),
62+
Values: []*string{
63+
aws.String("running"),
64+
},
65+
},
66+
},
67+
}
68+
69+
// Invoke our service
70+
instanceCount := 0
71+
err := ec2is.InspectInstances(input, func(dio *ec2.DescribeInstancesOutput, lastPage bool) bool {
72+
// Loop through each reservation
73+
for _, reservation := range dio.Reservations {
74+
// We assume that the AWS Service has properly filtered the list of returned instances
75+
instanceCount += len(reservation.Instances)
76+
}
77+
78+
return true
79+
})
80+
81+
// Check for error
82+
am.CheckError(err)
83+
84+
return instanceCount
85+
}

ec2_k8_subcount_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/******************************************************************************
2+
Cloud Resource Counter
3+
File: ec2_k8_subcount_test.go
4+
5+
Summary: The Unit Test for EC2 K8 sub-count.
6+
******************************************************************************/
7+
8+
package main
9+
10+
import (
11+
"testing"
12+
13+
"github.com/expel-io/aws-resource-counter/mock"
14+
)
15+
16+
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
17+
// Unit Test for EC2 K8 sub-count.
18+
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
19+
20+
func TestEC2K8SubInstances(t *testing.T) {
21+
// Describe all of our test cases: 1 failure and 4 success cases
22+
cases := []struct {
23+
RegionName string
24+
AllRegions bool
25+
ExpectedCount int
26+
ExpectError bool
27+
}{
28+
{
29+
RegionName: "us-east-1",
30+
ExpectedCount: 1,
31+
}, {
32+
RegionName: "us-east-2",
33+
ExpectedCount: 0,
34+
}, {
35+
RegionName: "af-south-1",
36+
ExpectedCount: 0,
37+
}, {
38+
RegionName: "undefined-region",
39+
ExpectError: true,
40+
}, {
41+
AllRegions: true,
42+
ExpectedCount: 1,
43+
},
44+
}
45+
46+
// Loop through each test case
47+
for _, c := range cases {
48+
// Create our fake service factory
49+
sf := fakeEC2ServiceFactory{
50+
RegionName: c.RegionName,
51+
DRResponse: ec2Regions,
52+
}
53+
54+
// Create a mock activity monitor
55+
mon := &mock.ActivityMonitorImpl{}
56+
57+
// Invoke our EC K8 Subcount Instances function
58+
actualCount := EC2K8SubInstances(sf, mon, c.AllRegions)
59+
60+
// Did we expect an error?
61+
if c.ExpectError {
62+
// Did it fail to arrive?
63+
if !mon.ErrorOccured {
64+
t.Error("Expected an error to occur, but it did not... :^(")
65+
}
66+
} else if mon.ErrorOccured {
67+
t.Errorf("Unexpected error occurred: %s", mon.ErrorMessage)
68+
} else if actualCount != c.ExpectedCount {
69+
t.Errorf("Error: EC K8 SubcountInstances returned %d; expected %d", actualCount, c.ExpectedCount)
70+
} else if mon.ProgramExited {
71+
t.Errorf("Unexpected Exit: The program unexpected exited with status code=%d", mon.ExitCode)
72+
}
73+
}
74+
}

ec2_test.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package main
1010
import (
1111
"errors"
1212
"reflect"
13+
"slices"
1314
"strings"
1415
"testing"
1516

@@ -50,7 +51,7 @@ var ec2Regions *ec2.DescribeRegionsOutput = &ec2.DescribeRegionsOutput{
5051
// This is our map of regions and the instances in each
5152
var ec2InstancesPerRegion = map[string][]*ec2.DescribeInstancesOutput{
5253
// US-EAST-1 illustrates a case where DescribeInstancesPages returns two pages of results.
53-
// First page: 2 different reservations (1 running instance, then 2 instances [1 is spot])
54+
// First page: 2 different reservations (1 running instance, then 3 instances [1 is k8 related vm, 1 is a spot instance])
5455
// Second page: 1 reservation (2 instances, 1 of which is stopped)
5556
"us-east-1": {
5657
&ec2.DescribeInstancesOutput{
@@ -71,6 +72,14 @@ var ec2InstancesPerRegion = map[string][]*ec2.DescribeInstancesOutput{
7172
Name: aws.String("running"),
7273
},
7374
},
75+
{
76+
Tags: []*ec2.Tag{
77+
{Key: aws.String("aws:eks:cluster-name"), Value: aws.String("cluster-name")},
78+
},
79+
State: &ec2.InstanceState{
80+
Name: aws.String("running"),
81+
},
82+
},
7483
{
7584
InstanceLifecycle: aws.String("spot"),
7685
State: &ec2.InstanceState{
@@ -276,13 +285,26 @@ func instanceSatisfiesFilter(reflectStruct reflect.Value, filter *ec2.Filter) bo
276285

277286
// Get our field value from the path
278287
fieldValue, ok := resolvePathByReflection(reflectStruct, fieldNamePath)
279-
if !ok {
288+
if !ok && !slices.Contains(fieldNamePath, "TagKey") {
280289
return false
281290
}
282291

283292
// Does this match one of the filter values?
284293
for _, value := range filter.Values {
285-
// Does it match?
294+
// if we get a filter for "tag-key", check the "Tags" portion of an ec2 instance
295+
// loop through the tags slice checking each of the "Kay" values in the tags struct
296+
// return true if we find any matching values
297+
if slices.Contains(fieldNamePath, "TagKey") {
298+
tags := reflectStruct.FieldByName("Tags")
299+
if !tags.IsValid() || tags.IsNil() || tags.Kind() != reflect.Slice {
300+
return false
301+
}
302+
for i := 0; i < tags.Len(); i++ {
303+
if tagKey := tags.Index(i).Elem().FieldByName("Key").Elem(); tagKey.IsValid() && tagKey.Kind() == reflect.String && tagKey.String() == *value {
304+
return true
305+
}
306+
}
307+
}
286308
if *value == fieldValue {
287309
return true
288310
}
@@ -455,7 +477,7 @@ func TestEC2Counts(t *testing.T) {
455477
}{
456478
{
457479
RegionName: "us-east-1",
458-
ExpectedCount: 3,
480+
ExpectedCount: 4,
459481
}, {
460482
RegionName: "us-east-2",
461483
ExpectedCount: 5,
@@ -467,7 +489,7 @@ func TestEC2Counts(t *testing.T) {
467489
ExpectError: true,
468490
}, {
469491
AllRegions: true,
470-
ExpectedCount: 8,
492+
ExpectedCount: 9,
471493
},
472494
}
473495

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ var date string = "<<never built>>"
3030
//
3131
// This command requires access to a valid AWS Account. For now, it is assumed that
3232
// this is stored in the user's ".aws" folder (located in $HOME/.aws).
33-
//
3433
func main() {
3534
/* =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
3635
* Command line processing
@@ -92,6 +91,7 @@ func main() {
9291
results.Append("Timestamp", time.Now().Format(time.RFC3339))
9392
results.Append("Region", displayRegion)
9493
results.Append("# of EC2 Instances", EC2Counts(serviceFactory, monitor, settings.allRegions))
94+
results.Append("# of EC2 K8 related VMs Sub-instances", EC2K8SubInstances(serviceFactory, monitor, settings.allRegions))
9595
results.Append("# of Spot Instances", SpotInstances(serviceFactory, monitor, settings.allRegions))
9696
results.Append("# of EBS Volumes", EBSVolumes(serviceFactory, monitor, settings.allRegions))
9797
results.Append("# of Unique Containers", UniqueContainerImages(serviceFactory, monitor, settings.allRegions))

0 commit comments

Comments
 (0)