Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,47 @@ Resource handler returned message: "Error occurred during operation 'ECS Deploym
\_ processImmediate (node:internal/timers:464:21)
```

### GuardDuty VPC Endpoint and Stack Deletion

#### Problem

When destroying CloudFormation stacks that use ECS Fargate with GuardDuty
Runtime Monitoring enabled, VPC deletion fails with errors like:

```
The subnet 'subnet-xxx' has dependencies and cannot be deleted.
The vpc 'vpc-xxx' has dependencies and cannot be deleted.
```

#### Root Cause

AWS GuardDuty ECS Runtime Monitoring automatically creates AWS-managed
resources outside of CloudFormation:

1. **VPC Endpoint**: `com.amazonaws.us-east-1.guardduty-data` (Interface endpoint)

- Creates Elastic Network Interfaces (ENIs) in private subnets
- Required for GuardDuty agent sidecar containers to communicate with GuardDuty service

2. **Security Group**: `GuardDutyManagedSecurityGroup-vpc-*`
- Attached to the GuardDuty VPC endpoint
- Allows traffic between ECS tasks and GuardDuty service

These resources are created when you:
- Enable GuardDuty Runtime Monitoring for ECS
- Deploy Fargate tasks (which automatically get GuardDuty agent sidecar containers)

Because these resources are created outside CloudFormation, they are
**not tracked in the CDK stack** and don't get deleted when you destroy
the stack, causing the VPC deletion to fail.

#### Solution

Create the VPC endpoint in the [network stack](./scr/network_stack.py)
so that it can be managed by this CDK project which will allow
the CDK to destroy the resource.


# Deployment

## Bootstrap
Expand Down
14 changes: 14 additions & 0 deletions src/network_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,17 @@ def __init__(self, scope: Construct, construct_id: str, vpc_cidr, **kwargs) -> N
self.vpc = ec2.Vpc(
self, "Vpc", max_azs=2, ip_addresses=ec2.IpAddresses.cidr(vpc_cidr)
)

# Create VPC endpoint for GuardDuty
# This is required for ECS Runtime Monitoring and must be explicitly
# managed to avoid orphaned resources during stack deletion
self.guardduty_endpoint = ec2.InterfaceVpcEndpoint(
self,
"GuardDutyEndpoint",
vpc=self.vpc,
service=ec2.InterfaceVpcEndpointAwsService.GUARDDUTY_DATA,
# Place endpoints in private subnets for security
subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS),
# Enable private DNS to use standard GuardDuty endpoint name
private_dns_enabled=True,
)
78 changes: 78 additions & 0 deletions tests/unit/test_network_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,81 @@ def test_vpc_created():
network = NetworkStack(app, "NetworkStack", vpc_cidr)
template = assertions.Template.from_stack(network)
template.has_resource_properties("AWS::EC2::VPC", {"CidrBlock": vpc_cidr})


def test_guardduty_vpc_endpoint_created():
"""Test that GuardDuty VPC endpoint is created with proper configuration."""
app = core.App()
vpc_cidr = "10.254.192.0/24"
network = NetworkStack(app, "NetworkStack", vpc_cidr)
template = assertions.Template.from_stack(network)

# Check that GuardDuty VPC endpoint is created
template.has_resource_properties(
"AWS::EC2::VPCEndpoint",
{
"VpcEndpointType": "Interface",
"PrivateDnsEnabled": True,
},
)

# Verify the service name contains guardduty-data (using partial match)
guardduty_endpoints = template.find_resources("AWS::EC2::VPCEndpoint")
assert len(guardduty_endpoints) == 1

# Check that the endpoint has a security group
template.has_resource("AWS::EC2::SecurityGroup", {})


def test_vpc_endpoint_security():
"""Test that VPC endpoint is placed in private subnets for security."""
app = core.App()
vpc_cidr = "10.254.192.0/24"
network = NetworkStack(app, "NetworkStack", vpc_cidr)
template = assertions.Template.from_stack(network)

# Verify that VPC endpoint exists
template.resource_count_is("AWS::EC2::VPCEndpoint", 1)

# Check that private subnets are created (VPC with private subnets)
template.has_resource_properties(
"AWS::EC2::Subnet",
{
"MapPublicIpOnLaunch": False,
},
)

# Verify that the VPC endpoint has a dedicated security group
template.has_resource_properties(
"AWS::EC2::SecurityGroup",
{
"GroupDescription": "NetworkStack/GuardDutyEndpoint/SecurityGroup",
},
)


def test_guardduty_vpc_endpoint_integration():
"""Test that GuardDuty VPC endpoint is properly integrated with the VPC."""
app = core.App()
vpc_cidr = "10.254.192.0/24"
network = NetworkStack(app, "NetworkStack", vpc_cidr)
template = assertions.Template.from_stack(network)

# Check that VPC endpoint references the correct VPC
template.has_resource_properties(
"AWS::EC2::VPCEndpoint",
{
"VpcId": {"Ref": assertions.Match.any_value()},
"VpcEndpointType": "Interface",
},
)

# Verify that the VPC has the expected configuration for GuardDuty
template.has_resource_properties(
"AWS::EC2::VPC",
{
"CidrBlock": vpc_cidr,
"EnableDnsHostnames": True,
"EnableDnsSupport": True,
},
)
Loading