Skip to content

Commit 69e8d81

Browse files
authored
Merge pull request #46953 from JMRacke/guardduty-vpc-cleanup
r/aws_vpc, r/aws_subnet: Add GuardDuty managed resource cleanup on destroy
2 parents 790f80b + 9b801e8 commit 69e8d81

12 files changed

Lines changed: 848 additions & 23 deletions

File tree

.changelog/46953.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:enhancement
2+
resource/aws_vpc: Automatically detect and remove GuardDuty-managed VPC endpoints and security groups during `terraform destroy` when they block VPC deletion
3+
```
4+
5+
```release-note:enhancement
6+
resource/aws_subnet: Automatically detect and dissociate GuardDuty-managed VPC endpoints during `terraform destroy` when they block subnet deletion
7+
```

internal/service/ec2/consts.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ const (
7070
vpnStateModifying = "modifying"
7171
)
7272

73+
const (
74+
guardDutyServiceNamePattern = "*guardduty-data*"
75+
guardDutySecurityGroupPrefix = "GuardDutyManagedSecurityGroup-"
76+
guardDutyManagedTagKey = "GuardDutyManaged"
77+
guardDutyManagedTagValue = "true"
78+
)
79+
7380
// See https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html#check-import-task-status
7481
const (
7582
ebsSnapshotImportStateActive = "active"

internal/service/ec2/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ const (
143143
errCodeNatGatewayNotFound = "NatGatewayNotFound"
144144
errCodeNetworkACLEntryAlreadyExists = "NetworkAclEntryAlreadyExists"
145145
errCodeOperationNotPermitted = "OperationNotPermitted"
146+
errCodeOperationInProgress = "OperationInProgress"
146147
errCodePrefixListVersionMismatch = "PrefixListVersionMismatch"
147148
errCodeResourceNotReady = "ResourceNotReady"
148149
errCodeRouteAlreadyExists = "RouteAlreadyExists"

internal/service/ec2/exports_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ var (
319319
VPCMigrateState = vpcMigrateState
320320
VPNGatewayRoutePropagationParseID = vpnGatewayRoutePropagationParseID
321321
WaitVolumeAttachmentCreated = waitVolumeAttachmentCreated
322+
FindGuardDutyVPCEndpoints = findGuardDutyVPCEndpoints
323+
FindGuardDutySecurityGroupsForVPC = findGuardDutySecurityGroupsForVPC
324+
GuardDutySecurityGroupNameForVPC = guardDutySecurityGroupNameForVPC
322325
)
323326

324327
type (

internal/service/ec2/vpc_.go

Lines changed: 167 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import (
1717
"github.com/aws/aws-sdk-go-v2/service/ec2"
1818
awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types"
1919
"github.com/hashicorp/aws-sdk-go-base/v2/tfawserr"
20+
"github.com/hashicorp/terraform-plugin-log/tflog"
2021
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
2122
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
2223
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
2324
"github.com/hashicorp/terraform-provider-aws/internal/conns"
2425
"github.com/hashicorp/terraform-provider-aws/internal/enum"
2526
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
27+
"github.com/hashicorp/terraform-provider-aws/internal/logging"
2628
"github.com/hashicorp/terraform-provider-aws/internal/provider/sdkv2/importer"
2729
"github.com/hashicorp/terraform-provider-aws/internal/retry"
2830
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
@@ -389,13 +391,15 @@ func resourceVPCDelete(ctx context.Context, d *schema.ResourceData, meta any) di
389391
var diags diag.Diagnostics
390392
conn := meta.(*conns.AWSClient).EC2Client(ctx)
391393

392-
log.Printf("[INFO] Deleting EC2 VPC: %s", d.Id())
394+
ctx = tflog.SetField(ctx, logging.KeyResourceId, d.Id())
395+
tflog.Info(ctx, "Deleting EC2 VPC")
396+
393397
input := ec2.DeleteVpcInput{
394398
VpcId: aws.String(d.Id()),
395399
}
396-
_, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, d.Timeout(schema.TimeoutDelete), func(ctx context.Context) (any, error) {
397-
return conn.DeleteVpc(ctx, &input)
398-
}, errCodeDependencyViolation)
400+
401+
// First attempt at deletion.
402+
_, err := conn.DeleteVpc(ctx, &input)
399403

400404
if tfawserr.ErrCodeEquals(err, errCodeInvalidVPCIDNotFound) {
401405
return diags
@@ -407,6 +411,52 @@ func resourceVPCDelete(ctx context.Context, d *schema.ResourceData, meta any) di
407411
return diags
408412
}
409413

414+
// Defers checking for GuardDuty-managed resources until we get a DependencyViolation error so that no new permissions,
415+
// such as ec2:DescribeVpcEndpoints, are required for users who do not have GuardDuty monitoring enabled for their VPCs.
416+
var guardDutyDiags diag.Diagnostics
417+
if tfawserr.ErrCodeEquals(err, errCodeDependencyViolation) {
418+
tflog.Debug(ctx, "VPC deletion failed with DependencyViolation, checking for GuardDuty resources", map[string]any{
419+
"error": err,
420+
})
421+
422+
endpointsErr := detectAndDeleteGuardDutyVPCEndpoints(ctx, conn, d.Id())
423+
if endpointsErr != nil {
424+
if isUnauthorizedError(endpointsErr) {
425+
guardDutyDiags = sdkdiag.AppendWarningf(guardDutyDiags,
426+
"While deleting EC2 VPC %q, the provider was unable to do check for or dissociate GuardDuty-managed resources.\n"+
427+
"If GuardDuty monitoring is enabled for this VPC, the missing permissions will prevent deletion of the Subnet\n\n"+
428+
"Error: %s", d.Id(), endpointsErr.Error(),
429+
)
430+
} else {
431+
return sdkdiag.AppendErrorf(diags, "deleting GuardDuty VPC endpoints for EC2 VPC %q: %s", d.Id(), endpointsErr)
432+
}
433+
}
434+
435+
sgErr := detectAndDeleteGuardDutySecurityGroups(ctx, conn, d.Id())
436+
if sgErr != nil {
437+
if isUnauthorizedError(sgErr) {
438+
guardDutyDiags = sdkdiag.AppendWarningf(guardDutyDiags,
439+
"While deleting EC2 VPC %q, the provider was unable to do check for or dissociate GuardDuty-managed resources.\n"+
440+
"If GuardDuty monitoring is enabled for this VPC, the missing permissions will prevent deletion of the Subnet\n\n"+
441+
"Error: %s", d.Id(), sgErr.Error(),
442+
)
443+
} else {
444+
return sdkdiag.AppendErrorf(diags, "deleting GuardDuty VPC security groups for EC2 VPC %q: %s", d.Id(), sgErr)
445+
}
446+
}
447+
448+
// Retry the deletion now that GuardDuty resources have been cleaned up.
449+
_, err = tfresource.RetryWhenAWSErrCodeEquals(ctx, d.Timeout(schema.TimeoutDelete), func(ctx context.Context) (any, error) {
450+
return conn.DeleteVpc(ctx, &input)
451+
}, errCodeDependencyViolation)
452+
}
453+
454+
// Only append GuardDuty-related warnings if we're still seeing a DependencyViolation:
455+
// If there's no longer a DependencyViolation, any GuardDuty-related warings are not relevant.
456+
if tfawserr.ErrCodeEquals(err, errCodeDependencyViolation) {
457+
diags = append(diags, guardDutyDiags...)
458+
}
459+
410460
if err != nil {
411461
return sdkdiag.AppendErrorf(diags, "deleting EC2 VPC (%s): %s", d.Id(), err)
412462
}
@@ -756,3 +806,116 @@ func resourceVPCFlatten(ctx context.Context, client *conns.AWSClient, vpc *awsty
756806
func vpcARN(ctx context.Context, c *conns.AWSClient, accountID, vpcID string) string {
757807
return c.RegionalARNWithAccount(ctx, names.EC2, accountID, "vpc/"+vpcID)
758808
}
809+
810+
func detectAndDeleteGuardDutySecurityGroups(ctx context.Context, conn *ec2.Client, vpcID string) error {
811+
tflog.Debug(ctx, "Detecting GuardDuty security groups in VPC")
812+
813+
sgs, err := findGuardDutySecurityGroupsForVPC(ctx, conn, vpcID)
814+
if err != nil {
815+
if isUnauthorizedError(err) {
816+
return err
817+
}
818+
return fmt.Errorf("listing GuardDuty security groups for VPC %q: %w", vpcID, err)
819+
}
820+
821+
if len(sgs) == 0 {
822+
tflog.Debug(ctx, "No GuardDuty security groups found in VPC")
823+
return nil
824+
}
825+
826+
tflog.Debug(ctx, "Found GuardDuty security group(s) in VPC", map[string]any{
827+
"count": len(sgs),
828+
})
829+
830+
for _, sg := range sgs {
831+
groupID := aws.ToString(sg.GroupId)
832+
ctx := tflog.SetField(ctx, "group_id", groupID)
833+
834+
tflog.Debug(ctx, "Deleting GuardDuty security group")
835+
836+
deleteInput := ec2.DeleteSecurityGroupInput{
837+
GroupId: aws.String(groupID),
838+
}
839+
_, err := conn.DeleteSecurityGroup(ctx, &deleteInput)
840+
if err != nil {
841+
if isUnauthorizedError(err) {
842+
return err
843+
}
844+
return fmt.Errorf("deleting GuardDuty security group %q: %w", groupID, err)
845+
}
846+
847+
tflog.Debug(ctx, "Successfully deleted GuardDuty security group")
848+
}
849+
850+
return nil
851+
}
852+
853+
func detectAndDeleteGuardDutyVPCEndpoints(ctx context.Context, conn *ec2.Client, vpcID string) error {
854+
tflog.Debug(ctx, "Checking for GuardDuty VPC endpoints for deletion")
855+
856+
endpoints, err := findGuardDutyVPCEndpoints(ctx, conn, vpcID)
857+
if err != nil {
858+
if isUnauthorizedError(err) {
859+
return err
860+
}
861+
return fmt.Errorf("listing GuardDuty VPC endpoints for VPC %q: %w", vpcID, err)
862+
}
863+
864+
if len(endpoints) == 0 {
865+
tflog.Debug(ctx, "No GuardDuty VPC endpoints found")
866+
return nil
867+
}
868+
869+
tflog.Debug(ctx, "Found GuardDuty VPC endpoints", map[string]any{
870+
"count": len(endpoints),
871+
})
872+
873+
for _, endpoint := range endpoints {
874+
endpointID := aws.ToString(endpoint.VpcEndpointId)
875+
ctx := tflog.SetField(ctx, "endpoint_id", endpointID)
876+
877+
tflog.Debug(ctx, "Deleting GuardDuty VPC endpoint")
878+
879+
deleteInput := ec2.DeleteVpcEndpointsInput{
880+
VpcEndpointIds: []string{endpointID},
881+
}
882+
_, err := conn.DeleteVpcEndpoints(ctx, &deleteInput)
883+
if err != nil {
884+
if isUnauthorizedError(err) {
885+
return err
886+
}
887+
if tfawserr.ErrCodeEquals(err, errCodeInvalidVPCEndpointIdNotFound) {
888+
tflog.Debug(ctx, "GuardDuty VPC endpoint not found during deletion")
889+
continue
890+
}
891+
return fmt.Errorf("deleting GuardDuty VPC endpoint %q in VPC %q: %w", endpointID, vpcID, err)
892+
}
893+
894+
if err := waitVPCEndpointDeleted(ctx, conn, endpointID, vpcEndpointDeletionTimeout); err != nil {
895+
if tfawserr.ErrCodeEquals(err, errCodeInvalidVPCEndpointIdNotFound) {
896+
tflog.Debug(ctx, "GuardDuty VPC endpoint not found while waiting for deleted state")
897+
continue
898+
}
899+
return fmt.Errorf("waiting for GuardDuty VPC endpoint %q to reach deleted state in VPC %q: %w", endpointID, vpcID, err)
900+
}
901+
902+
tflog.Debug(ctx, "Successfully deleted GuardDuty VPC endpoint")
903+
}
904+
905+
return nil
906+
}
907+
908+
func guardDutySecurityGroupNameForVPC(vpcID string) string {
909+
return guardDutySecurityGroupPrefix + vpcID
910+
}
911+
912+
func findGuardDutySecurityGroupsForVPC(ctx context.Context, conn *ec2.Client, vpcID string) ([]awstypes.SecurityGroup, error) {
913+
groupName := guardDutySecurityGroupNameForVPC(vpcID)
914+
return findSecurityGroups(ctx, conn, &ec2.DescribeSecurityGroupsInput{
915+
Filters: newAttributeFilterList(map[string]string{
916+
"vpc-id": vpcID,
917+
"group-name": groupName,
918+
"tag:" + guardDutyManagedTagKey: guardDutyManagedTagValue,
919+
}),
920+
})
921+
}

internal/service/ec2/vpc_endpoint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ func resourceVPCEndpointDelete(ctx context.Context, d *schema.ResourceData, meta
515515
return sdkdiag.AppendErrorf(diags, "deleting EC2 VPC Endpoint (%s): %s", d.Id(), err)
516516
}
517517

518-
if _, err = waitVPCEndpointDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil {
518+
if err = waitVPCEndpointDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil {
519519
return sdkdiag.AppendErrorf(diags, "waiting for EC2 VPC Endpoint (%s) delete: %s", d.Id(), err)
520520
}
521521

0 commit comments

Comments
 (0)