Skip to content

Commit 3a9b47b

Browse files
authored
Merge pull request #7740 from justinsb/aws_janitor_iam_role
AWS Janitor: Clean up leaked IAM roles
2 parents ceb25ea + eb73e75 commit 3a9b47b

File tree

1 file changed

+110
-14
lines changed

1 file changed

+110
-14
lines changed

maintenance/aws-janitor/main.go

+110-14
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ var awsResourceTypes = []awsResourceType{
6666
dhcpOptions{},
6767
volumes{},
6868
addresses{},
69+
iamRoles{},
6970
}
7071

7172
type awsResource interface {
@@ -75,6 +76,10 @@ type awsResource interface {
7576
// intended to be globally unique across regions and accounts, so
7677
// that works.
7778
ARN() string
79+
80+
// ResourceKey() returns a per-resource key, because ARNs might conflict if two objects
81+
// with the same name are created at different times (e.g. IAM roles)
82+
ResourceKey() string
7883
}
7984

8085
// awsResourceSet keeps track of the first time we saw a particular
@@ -122,24 +127,24 @@ func (s *awsResourceSet) Save(sess *session.Session, p *s3path) error {
122127
// on whether it should be deleted. If Mark(r) returns true, the TTL
123128
// has expired for r and it should be deleted.
124129
func (s *awsResourceSet) Mark(r awsResource) bool {
125-
arn := r.ARN()
130+
key := r.ResourceKey()
126131
now := time.Now()
127132

128-
s.marked[arn] = true
129-
if t, ok := s.firstSeen[arn]; ok {
133+
s.marked[key] = true
134+
if t, ok := s.firstSeen[key]; ok {
130135
since := now.Sub(t)
131136
if since > s.ttl {
132-
s.swept = append(s.swept, arn)
137+
s.swept = append(s.swept, key)
133138
return true
134139
}
135-
glog.V(1).Infof("%s: seen for %v", r.ARN(), since)
140+
glog.V(1).Infof("%s: seen for %v", key, since)
136141
return false
137142
}
138-
s.firstSeen[arn] = now
139-
glog.V(1).Infof("%s: first seen", r.ARN())
143+
s.firstSeen[key] = now
144+
glog.V(1).Infof("%s: first seen", key)
140145
if s.ttl == 0 {
141146
// If the TTL is 0, it should be deleted now.
142-
s.swept = append(s.swept, arn)
147+
s.swept = append(s.swept, key)
143148
return true
144149
}
145150
return false
@@ -150,14 +155,14 @@ func (s *awsResourceSet) Mark(r awsResource) bool {
150155
// resources have been marked.
151156
func (s *awsResourceSet) MarkComplete() int {
152157
var gone []string
153-
for arn := range s.firstSeen {
154-
if !s.marked[arn] {
155-
gone = append(gone, arn)
158+
for key := range s.firstSeen {
159+
if !s.marked[key] {
160+
gone = append(gone, key)
156161
}
157162
}
158-
for _, arn := range gone {
159-
glog.V(1).Infof("%s: deleted since last run", arn)
160-
delete(s.firstSeen, arn)
163+
for _, key := range gone {
164+
glog.V(1).Infof("%s: deleted since last run", key)
165+
delete(s.firstSeen, key)
161166
}
162167
if len(s.swept) > 0 {
163168
glog.Errorf("%d resources swept: %v", len(s.swept), s.swept)
@@ -221,6 +226,10 @@ func (i instance) ARN() string {
221226
return fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", i.Region, i.Account, i.InstanceID)
222227
}
223228

229+
func (i instance) ResourceKey() string {
230+
return i.ARN()
231+
}
232+
224233
// AutoScalingGroups: https://docs.aws.amazon.com/sdk-for-go/api/service/autoscaling/#AutoScaling.DescribeAutoScalingGroups
225234

226235
type autoScalingGroups struct{}
@@ -276,6 +285,10 @@ func (asg autoScalingGroup) ARN() string {
276285
return asg.ID
277286
}
278287

288+
func (asg autoScalingGroup) ResourceKey() string {
289+
return asg.ARN()
290+
}
291+
279292
// LaunchConfigurations: http://docs.aws.amazon.com/sdk-for-go/api/service/autoscaling/#AutoScaling.DescribeLaunchConfigurations
280293

281294
type launchConfigurations struct{}
@@ -317,6 +330,10 @@ func (lc launchConfiguration) ARN() string {
317330
return lc.ID
318331
}
319332

333+
func (lc launchConfiguration) ResourceKey() string {
334+
return lc.ARN()
335+
}
336+
320337
// Subnets: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeSubnets
321338

322339
type subnets struct{}
@@ -359,6 +376,10 @@ func (sub subnet) ARN() string {
359376
return fmt.Sprintf("arn:aws:ec2:%s:%s:subnet/%s", sub.Region, sub.Account, sub.ID)
360377
}
361378

379+
func (sub subnet) ResourceKey() string {
380+
return sub.ARN()
381+
}
382+
362383
// SecurityGroups: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeSecurityGroups
363384

364385
type securityGroups struct{}
@@ -441,6 +462,10 @@ func (sg securityGroup) ARN() string {
441462
return fmt.Sprintf("arn:aws:ec2:%s:%s:security-group/%s", sg.Region, sg.Account, sg.ID)
442463
}
443464

465+
func (sg securityGroup) ResourceKey() string {
466+
return sg.ARN()
467+
}
468+
444469
// InternetGateways: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeInternetGateways
445470

446471
type internetGateways struct{}
@@ -508,6 +533,10 @@ func (ig internetGateway) ARN() string {
508533
return fmt.Sprintf("arn:aws:ec2:%s:%s:internet-gateway/%s", ig.Region, ig.Account, ig.ID)
509534
}
510535

536+
func (ig internetGateway) ResourceKey() string {
537+
return ig.ARN()
538+
}
539+
511540
// RouteTables: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeRouteTables
512541

513542
type routeTables struct{}
@@ -563,6 +592,10 @@ func (rt routeTable) ARN() string {
563592
return fmt.Sprintf("arn:aws:ec2:%s:%s:route-table/%s", rt.Region, rt.Account, rt.ID)
564593
}
565594

595+
func (rt routeTable) ResourceKey() string {
596+
return rt.ARN()
597+
}
598+
566599
// VPCs: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeVpcs
567600

568601
type vpcs struct{}
@@ -614,6 +647,10 @@ func (vp vpc) ARN() string {
614647
return fmt.Sprintf("arn:aws:ec2:%s:%s:vpc/%s", vp.Region, vp.Account, vp.ID)
615648
}
616649

650+
func (vp vpc) ResourceKey() string {
651+
return vp.ARN()
652+
}
653+
617654
// DhcpOptions: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeDhcpOptions
618655

619656
type dhcpOptions struct{}
@@ -718,6 +755,10 @@ func (dhcp dhcpOption) ARN() string {
718755
return fmt.Sprintf("arn:aws:ec2:%s:%s:dhcp-option/%s", dhcp.Region, dhcp.Account, dhcp.ID)
719756
}
720757

758+
func (dhcp dhcpOption) ResourceKey() string {
759+
return dhcp.ARN()
760+
}
761+
721762
// Volumes: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeVolumes
722763

723764
type volumes struct{}
@@ -757,6 +798,10 @@ func (vol volume) ARN() string {
757798
return fmt.Sprintf("arn:aws:ec2:%s:%s:volume/%s", vol.Region, vol.Account, vol.ID)
758799
}
759800

801+
func (vol volume) ResourceKey() string {
802+
return vol.ARN()
803+
}
804+
760805
// Elastic IPs: https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeAddresses
761806

762807
type addresses struct{}
@@ -801,6 +846,57 @@ func (addr address) ARN() string {
801846
return fmt.Sprintf("arn:aws:ec2:%s:%s:address/%s", addr.Region, addr.Account, addr.ID)
802847
}
803848

849+
func (addr address) ResourceKey() string {
850+
return addr.ARN()
851+
}
852+
853+
// IAM Roles
854+
855+
type iamRoles struct{}
856+
857+
func (iamRoles) MarkAndSweep(sess *session.Session, acct string, region string, set *awsResourceSet) error {
858+
svc := iam.New(sess, &aws.Config{Region: aws.String(region)})
859+
860+
var toDelete []*iamRole // Paged call, defer deletion until we have the whole list.
861+
if err := svc.ListRolesPages(&iam.ListRolesInput{}, func(page *iam.ListRolesOutput, _ bool) bool {
862+
for _, r := range page.Roles {
863+
l := &iamRole{arn: aws.StringValue(r.Arn), roleID: aws.StringValue(r.RoleId), roleName: aws.StringValue(r.RoleName)}
864+
if set.Mark(l) {
865+
glog.Warningf("%s: deleting %T: %v", l.ARN(), r, r)
866+
toDelete = append(toDelete, l)
867+
}
868+
}
869+
return true
870+
}); err != nil {
871+
return err
872+
}
873+
874+
for _, r := range toDelete {
875+
_, err := svc.DeleteRole(
876+
&iam.DeleteRoleInput{
877+
RoleName: aws.String(r.roleName),
878+
})
879+
if err != nil {
880+
glog.Warningf("%v: delete failed: %v", r.ARN(), err)
881+
}
882+
}
883+
return nil
884+
}
885+
886+
type iamRole struct {
887+
arn string
888+
roleID string
889+
roleName string
890+
}
891+
892+
func (lc iamRole) ARN() string {
893+
return lc.arn
894+
}
895+
896+
func (lc iamRole) ResourceKey() string {
897+
return lc.roleID + "::" + lc.ARN()
898+
}
899+
804900
// ARNs (used for uniquifying within our previous mark file)
805901

806902
type arn struct {

0 commit comments

Comments
 (0)