Skip to content

Instantly share code, notes, and snippets.

@atheiman
Last active October 18, 2024 18:03
Show Gist options
  • Save atheiman/e619c25e68163fb10c48df8090cadb5c to your computer and use it in GitHub Desktop.
Save atheiman/e619c25e68163fb10c48df8090cadb5c to your computer and use it in GitHub Desktop.
aws-delete-all-organization-default-vpcs
Description: >-
Scheduled Lambda function to delete Default VPC from all accounts in the organization. Deploys an IAM role via
CloudFormation stackset to the organization root and schedules a Lambda function to assume the IAM roles in each
account and attempt to delete the Default VPCs in every Region. If any operation fails, or if a Default VPC is not
empty, or if a Default VPC has a peering connection, the function invocation fails.
Parameters:
OrgRootId:
Type: String
AllowedPattern: '^r-[a-zA-Z0-9]+$'
Description: >-
Organization root ID. Example: "r-abcd"
CrossAcctRoleName:
Type: String
Default: DeleteDefaultVpc
Description: Name of IAM role to be created in all accounts in the organization with permission to delete VPCs.
ScheduleExpression:
Type: String
Default: 'cron(0 18 * * ? *)'
Description: >-
Frequency to invoke the Lambda function to attempt to delete all Default VPCs in the organization. Default cron
expression invokes the function once daily at 6:00pm UTC. See
https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html
Resources:
StackSet:
Type: AWS::CloudFormation::StackSet
Properties:
StackSetName: !Ref AWS::StackName
AutoDeployment:
Enabled: True
RetainStacksOnAccountRemoval: False
Capabilities: [CAPABILITY_NAMED_IAM]
OperationPreferences:
RegionConcurrencyType: PARALLEL
FailureTolerancePercentage: 25
MaxConcurrentPercentage: 100
PermissionModel: SERVICE_MANAGED
StackInstancesGroup:
- DeploymentTargets:
OrganizationalUnitIds: [!Ref OrgRootId]
Regions: [!Ref AWS::Region]
Tags:
- Key: CfnStackId
Value: !Ref AWS::StackId
Parameters:
- ParameterKey: AssumeRolePrincipal
ParameterValue: !Sub '${DeleteAllOrganizationDefaultVpcsFunctionRole.Arn}'
- ParameterKey: RoleName
ParameterValue: !Ref CrossAcctRoleName
TemplateBody: |
Parameters:
AssumeRolePrincipal:
Type: String
RoleName:
Type: String
Resources:
CrossAcctRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Ref RoleName
Path: '/'
Description: Used by organization automation to delete Default VPCs
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub '${AssumeRolePrincipal}'
Action: sts:AssumeRole
Tags:
- Key: CfnStackId
Value: !Ref AWS::StackId
ManagedPolicyArns:
- !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonEC2ReadOnlyAccess'
Policies:
- PolicyName: Inline
PolicyDocument:
Version: '2012-10-17'
Statement:
# Be sure to keep these permissions in sync between stackset role and org mgmt acct role
- Effect: Allow
Action:
- ec2:DescribeRegions
- ec2:DetachInternetGateway
- ec2:DeleteInternetGateway
- ec2:DeleteSubnet
- ec2:DeleteSecurityGroup
- ec2:DeleteVpc
Resource: '*'
CrossAcctRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Ref CrossAcctRoleName
Path: '/'
Description: Used by organization automation to delete Default VPCs
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
AWS: !Sub '${DeleteAllOrganizationDefaultVpcsFunctionRole.Arn}'
Action: sts:AssumeRole
Tags:
- Key: CfnStackId
Value: !Ref AWS::StackId
ManagedPolicyArns:
- !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonEC2ReadOnlyAccess'
Policies:
- PolicyName: Inline
PolicyDocument:
Version: '2012-10-17'
Statement:
# Be sure to keep these permissions in sync between stackset role and org mgmt acct role
- Effect: Allow
Action:
- ec2:DescribeRegions
- ec2:DetachInternetGateway
- ec2:DeleteInternetGateway
- ec2:DeleteSubnet
- ec2:DeleteSecurityGroup
- ec2:DeleteVpc
Resource: '*'
DeleteAllOrganizationDefaultVpcsFunction:
Type: AWS::Lambda::Function
Properties:
Description: Deletes all Default VPCs across the AWS organization
Role: !Sub '${DeleteAllOrganizationDefaultVpcsFunctionRole.Arn}'
Handler: index.handler
Timeout: 300
Runtime: python3.8
Tags:
- Key: CfnStackId
Value: !Ref AWS::StackId
Environment:
Variables:
ASSUME_ROLE_ARN_FMT_STR: !Sub 'arn:${AWS::Partition}:iam::{acct_id}:role/${CrossAcctRoleName}'
Code:
ZipFile: |
import boto3
import botocore
import json
import os
import traceback
import cfnresponse
orgs = boto3.client("organizations", region_name=os.environ["AWS_REGION"])
sts = boto3.client("sts", region_name=os.environ["AWS_REGION"])
local_ec2 = boto3.client("ec2", region_name=os.environ["AWS_REGION"])
def handler(event, context):
print(json.dumps(event, default=str))
# Capture errors and do not raise until finished configuring all monitoring
errors = []
regions = local_ec2.describe_regions()["Regions"]
accts = []
for pg in orgs.get_paginator('list_accounts').paginate():
accts += pg['Accounts']
for acct in accts:
print(f"Info - Deleting Default VPCs in account '{acct['Id']}'")
role_arn = os.environ["ASSUME_ROLE_ARN_FMT_STR"].format(acct_id=acct['Id'])
creds = sts.assume_role(RoleArn=role_arn, RoleSessionName=os.environ["AWS_LAMBDA_FUNCTION_NAME"][:63])[
"Credentials"
]
boto3_client_args = {
"aws_access_key_id": creds["AccessKeyId"],
"aws_secret_access_key": creds["SecretAccessKey"],
"aws_session_token": creds["SessionToken"],
}
for region in regions:
regional_ec2_client = boto3.client("ec2", region_name=region["RegionName"], **boto3_client_args)
print(f"Info - Deleting Default VPC in account '{acct['Id']}', Region '{region['RegionName']}'")
try:
delete_default_vpc(regional_ec2_client)
except Exception as e:
print(
f"Error - Failed to delete Default VPC in account '{acct['Id']}', Region '{region['RegionName']}' - {repr(e)} - "
f"{traceback.format_exc()}"
)
errors.append(e)
if errors:
raise Exception("Errors encountered:", errors)
def delete_default_vpc(ec2):
try:
default_vpc = ec2.describe_vpcs(Filters=[{"Name": "is-default", "Values": ["true"]}])["Vpcs"][0]
except IndexError:
print(f"Info - Default VPC not found")
return
skip_delete = False
net_ifs = ec2.describe_network_interfaces(
Filters=[{"Name": "vpc-id", "Values": [default_vpc["VpcId"]]}]
)["NetworkInterfaces"]
if net_ifs:
skip_delete = True
print(
f"Warning - Found network interfaces in Default VPC '{default_vpc['VpcId']}'. "
"Delete will likely fail, see network interfaces below:"
)
print(json.dumps(net_ifs, default=str))
pcxs = ec2.describe_vpc_peering_connections(
Filters=[
{"Name": "requester-vpc-info.vpc-id", "Values": [default_vpc["VpcId"]]},
{"Name": "status-code", "Values": ["pending-acceptance", "failed", "expired", "provisioning", "active", "deleting"]},
]
)["VpcPeeringConnections"]
pcxs += ec2.describe_vpc_peering_connections(
Filters=[
{"Name": "accepter-vpc-info.vpc-id", "Values": [default_vpc["VpcId"]]},
{"Name": "status-code", "Values": ["pending-acceptance", "failed", "expired", "provisioning", "active", "deleting"]},
]
)["VpcPeeringConnections"]
if pcxs:
skip_delete = True
print(
f"Warning - Found peering connections in Default VPC '{default_vpc['VpcId']}'. "
"Delete will likely fail, see peering connections below:"
)
print(json.dumps(pcxs, default=str))
if skip_delete:
raise Exception(
f"Found dependencies in Default VPC '{default_vpc['VpcId']}', see above. "
"Not attempting to delete."
)
igws = ec2.describe_internet_gateways(
Filters=[{"Name": "attachment.vpc-id", "Values": [default_vpc["VpcId"]]}]
)["InternetGateways"]
for igw in igws:
print(f"Info - Detaching internet gateway '{igw['InternetGatewayId']}' from Default VPC")
ec2.detach_internet_gateway(InternetGatewayId=igw["InternetGatewayId"], VpcId=default_vpc["VpcId"])
print(f"Info - Deleting internet gateway '{igw['InternetGatewayId']}'")
ec2.delete_internet_gateway(InternetGatewayId=igw["InternetGatewayId"])
subnets = ec2.describe_subnets(Filters=[{"Name": "vpc-id", "Values": [default_vpc["VpcId"]]}])["Subnets"]
for subnet in subnets:
print(f"Info - Deleting Default VPC subnet '{subnet['SubnetId']}'")
ec2.delete_subnet(SubnetId=subnet["SubnetId"])
security_groups = ec2.describe_security_groups(
Filters=[{"Name": "vpc-id", "Values": [default_vpc["VpcId"]]}]
)["SecurityGroups"]
for security_group in security_groups:
if security_group["GroupName"] == "default":
continue
print(
f"Info - Deleting Default VPC security group '{security_group['GroupId']}' "
f"('{security_group['GroupName']}')"
)
ec2.delete_security_group(GroupId=security_group["GroupId"])
print(f"Info - Deleting Default VPC '{default_vpc['VpcId']}'")
ec2.delete_vpc(VpcId=default_vpc["VpcId"])
print(f"Info - Deleted Default VPC '{default_vpc['VpcId']}'")
DeleteAllOrganizationDefaultVpcsFunctionLogGroup:
Type: AWS::Logs::LogGroup
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
LogGroupName: !Sub /aws/lambda/${DeleteAllOrganizationDefaultVpcsFunction}
RetentionInDays: 90
DeleteAllOrganizationDefaultVpcsFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Tags:
- Key: CfnStackId
Value: !Ref AWS::StackId
ManagedPolicyArns:
- !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
Policies:
- PolicyName: Inline
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- organizations:ListAccounts
- ec2:DescribeRegions
- sts:AssumeRole
Resource: '*'
DeleteAllOrganizationDefaultVpcsFunctionEventsRule:
Type: AWS::Events::Rule
Properties:
State: ENABLED
ScheduleExpression: !Ref ScheduleExpression
Targets:
- Id: DeleteAllOrganizationDefaultVpcsFunction
Arn: !Sub '${DeleteAllOrganizationDefaultVpcsFunction.Arn}'
DeleteAllOrganizationDefaultVpcsFunctionPermissionEventsRule:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref DeleteAllOrganizationDefaultVpcsFunction
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !Sub '${DeleteAllOrganizationDefaultVpcsFunctionEventsRule.Arn}'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment