AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda function to create VPC in any region

Parameters:
  cidr:
    Type: String
    Default: ""
    Description: "Lambda picks up CIDR range automatically when input cidr is blank. You can override it by using a user defined value."
  VPCRegion:
    Type: String
    Default: us-east-1
    Description: "The region where the VPC will be created"
    AllowedValues:
      - ap-south-2
      - ap-south-1
      - eu-south-1
      - eu-south-2
      - me-central-1
      - il-central-1
      - ca-central-1
      - eu-central-1
      - eu-central-2
      - us-west-1
      - us-west-2
      - af-south-1
      - eu-north-1
      - eu-west-3
      - eu-west-2
      - eu-west-1
      - ap-northeast-3
      - ap-northeast-2
      - me-south-1
      - ap-northeast-1
      - sa-east-1
      - ap-east-1
      - ca-west-1
      - ap-southeast-1
      - ap-southeast-2
      - ap-southeast-3
      - ap-southeast-4
      - us-east-1
      - ap-southeast-5
      - us-east-2
    ConstraintDescription: "Must be a valid AWS region."

Resources:
  # IAM Role for Lambda with required permissions
  VPCCreationLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: 
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: !Sub "VPCCreationPolicy-${AWS::Region}"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateVpc
                  - ec2:CreateSubnet
                  - ec2:CreateRoute
                  - ec2:CreateRouteTable
                  - ec2:CreateNatGateway
                  - ec2:AllocateAddress
                  - ec2:CreateInternetGateway
                  - ec2:AttachInternetGateway
                  - ec2:AssociateRouteTable
                  - ec2:DescribeVpcs
                  - ec2:ModifyVpcAttribute
                  - ec2:DescribeSubnets
                  - ec2:DescribeRouteTables
                  - ec2:DescribeInternetGateways
                  - ec2:DescribeNatGateways
                  - ec2:CreateTags
                  - ec2:DescribeAvailabilityZones
                  - ec2:CreateVpcEndpoint
                  - ec2:DescribeVpcEndpoints
                  - ec2:DisassociateRouteTable
                  - ec2:DescribeNetworkInterfaces
                  - ec2:DescribeSecurityGroups
                  - ec2:DescribeAddresses
                  - ec2:DeleteNetworkInterface
                  - ec2:DetachInternetGateway
                  - ec2:DeleteInternetGateway
                  - ec2:DeleteVpcEndpoints
                  - ec2:DeleteNatGateway
                  - ec2:ReleaseAddress
                  - ec2:DeleteRoute
                  - ec2:DeleteSubnet
                  - ec2:DeleteRouteTable
                  - ec2:DeleteVpc
                  - servicequotas:GetServiceQuota
                Resource: "*"
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "arn:aws:logs:*:*:*"
      RoleName: !Sub "VPCCreationLambdaExecutionRole-${AWS::Region}"

  # Lambda function to create VPC
  VPCCreationFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "VPCCreationLambda-${AWS::Region}"
      Handler: index.main  # This is just an alias since we use ZipFile for inline code
      Runtime: python3.9
      MemorySize: 128
      Timeout: 600
      Role: !GetAtt VPCCreationLambdaRole.Arn
      Code:
        ZipFile: |
          import boto3
          import time
          import ipaddress
          import cfnresponse

          def get_service_quota(service_code, quota_code, region="us-east-1"):
              client = boto3.client("service-quotas", region_name=region)
              try:
                  quota = client.get_service_quota(ServiceCode=service_code, QuotaCode=quota_code)
                  return quota["Quota"]["Value"]
              except client.exceptions.NoSuchResourceException:
                  print(f"No quota found for {service_code} with QuotaCode {quota_code}.")
                  return None
              
          def pre_check_vpc_creation(region="us-east-1"):
              ec2 = boto3.client("ec2", region_name=region)
              limit_exceeded_message = None

              try:
                  # Step 1: Describe all Elastic IPs in the account
                  eips = ec2.describe_addresses()["Addresses"]
                  
                  # Step 2: Iterate over each EIP and release if it is not associated with any resource
                  for eip in eips:
                      if "AssociationId" not in eip:  # Unassociated EIP
                          allocation_id = eip["AllocationId"]
                          ec2.release_address(AllocationId=allocation_id)
                          print(f"Released unassociated Elastic IP: {allocation_id}")
                      else:
                          print(f"Elastic IP {eip['AllocationId']} is in use, skipping.")

                  # Check 1: Elastic IP usage
                  eips = ec2.describe_addresses()["Addresses"]
                  used_eips = len([eip for eip in eips if "AssociationId" in eip])
                  eip_limit = get_service_quota("ec2", "L-0263D0A3", region)

                  if used_eips >= eip_limit:
                      limit_exceeded_message = "Failed to create VPC: Elastic IP limit reached."

                  # Check 2: NAT Gateway usage
                  nat_gateways = ec2.describe_nat_gateways(Filters=[{"Name": "state", "Values": ["available", "pending"]}])["NatGateways"]
                  used_nats = len(nat_gateways)
                  nat_limit = get_service_quota("vpc", "L-FE5A380F", region)

                  if used_nats >= nat_limit:
                      limit_exceeded_message = "Failed to create VPC: NAT Gateway limit reached."

                  # Check 3: VPC usage
                  vpcs = ec2.describe_vpcs()["Vpcs"]
                  used_vpcs = len(vpcs)
                  vpc_limit = get_service_quota("vpc", "L-F678F1CE", region)

                  if used_vpcs >= vpc_limit:
                      limit_exceeded_message = "Failed to create VPC: VPC limit reached."

                  # If any limit exceeded, return the message
                  if limit_exceeded_message:
                      print(limit_exceeded_message)
                      return False, limit_exceeded_message
                  
                  # If all checks pass, return success
                  return True, "Pre-checks passed. Proceeding with VPC creation."
                          
              except Exception as e:
                  error_message = f"Error during pre-checks: {str(e)}"
                  print(error_message)
                  return False, error_message

          def release_unused_eips(region="us-east-1"):
            ec2 = boto3.client("ec2", region_name=region)
            try:
                # Step 1: Describe all Elastic IPs in the account
                eips = ec2.describe_addresses()["Addresses"]
                
                # Step 2: Iterate over each EIP and release if it is not associated with any resource
                for eip in eips:
                    if "AssociationId" not in eip:  # Unassociated EIP
                        allocation_id = eip["AllocationId"]
                        ec2.release_address(AllocationId=allocation_id)
                        print(f"Released unassociated Elastic IP: {allocation_id}")
                    else:
                        print(f"Elastic IP {eip['AllocationId']} is in use, skipping.")
            except Exception as e:
                print(f"Error releasing unused Elastic IPs: {str(e)}")

          def is_default_security_group_in_use(vpc_id, region):
              ec2 = boto3.client('ec2', region_name=region)

              try:
                  # Step 1: Get the default security group for the VPC
                  security_groups = ec2.describe_security_groups(
                      Filters=[
                          {'Name': 'vpc-id', 'Values': [vpc_id]}
                      ]
                  )['SecurityGroups']

                  if not security_groups:
                      print(f"No default security group found for VPC {vpc_id}.")
                      return False  # No default security group found

                  default_sg_id = security_groups[0]['GroupId']
                  print(f"Default Security Group ID for VPC {vpc_id}: {default_sg_id}")

                  # Step 2: Check if the default security group is in use
                  network_interfaces = ec2.describe_network_interfaces(
                      Filters=[{'Name': 'group-id', 'Values': [default_sg_id]}]
                  )['NetworkInterfaces']

                  if network_interfaces:
                      print(f"Default security group {default_sg_id} is in use by {len(network_interfaces)} resources.")
                      return True  # Default security group is in use, meaning there are external dependencies

                  print(f"Default security group {default_sg_id} is not in use.")
                  return False  # Default security group is not in use

              except Exception as e:
                  print(f"Error while checking default security group usage: {str(e)}")
                  return False  # In case of any errors, assume it's safe to delete

          def delete_eni_in_subnet(ec2, subnet_id, retries=10, delay=30):
              # Describe and delete all ENIs associated with the subnet
              enis = ec2.describe_network_interfaces(Filters=[{'Name': 'subnet-id', 'Values': [subnet_id]}])['NetworkInterfaces']
              
              for eni in enis:
                  eni_id = eni['NetworkInterfaceId']
                  for attempt in range(retries):
                          try:
                              ec2.delete_network_interface(NetworkInterfaceId=eni_id)
                              print(f"Successfully deleted ENI: {eni_id}")
                              return  # Exit the loop if the ENI is successfully deleted

                          except ec2.exceptions.ClientError as e:
                              error_code = e.response['Error']['Code']

                              # Handle the case where ENI is still in use
                              if error_code == 'InvalidNetworkInterface.InUse':
                                  print(f"ENI {eni_id} is still in use, waiting before retrying... (Attempt {attempt+1}/{retries})")
                              
                              # Handle the case where ENI no longer exists
                              elif error_code == 'InvalidNetworkInterfaceID.NotFound':
                                  print(f"ENI {eni_id} not found, assuming it has been deleted.")
                                  return  # Exit successfully, as the ENI has been deleted

                              else:
                                  # Log any other unexpected errors
                                  print(f"Unexpected error while deleting ENI {eni_id}: {str(e)}")

                          # Wait before retrying
                          time.sleep(delay)

                  print(f"Failed to delete ENI {eni_id} after {retries} attempts.")

          def find_available_cidr(existing_cidrs, base_cidr=None, subnet_prefix=16):
              """
              Find an available CIDR block that doesn't overlap with existing VPCs.

              Args:
                  existing_cidrs (list): A list of existing CIDRs to avoid overlaps with.
                  base_cidr (str, optional): The base CIDR range to search in (e.g., 10.0.0.0/8 or 172.16.0.0/12).
                                          If not provided, it is determined based on the existing CIDRs.
                  subnet_prefix (int): The prefix size of the subnet you want to allocate (e.g., 16 for /16 subnets).

              Returns:
                  str: An available CIDR block, or None if no CIDR is available.
              """
              # Determine the base_cidr if not explicitly provided
              if base_cidr is None:
                  if any(cidr.startswith("10.") for cidr in existing_cidrs):
                      base_cidr = "10.0.0.0/8"
                  elif any(cidr.startswith("172.") for cidr in existing_cidrs):
                      base_cidr = "172.16.0.0/12"
                  else:
                      base_cidr = "10.0.0.0/8"  # Default to 10.0.0.0/8

              base_network = ipaddress.IPv4Network(base_cidr)

              # Iterate through all possible subnets in the base CIDR range
              for subnet in base_network.subnets(new_prefix=subnet_prefix):
                  if all(not subnet.overlaps(ipaddress.IPv4Network(cidr)) for cidr in existing_cidrs):
                      return str(subnet)

              # Return None if no available CIDR is found
              return None
              
          def get_existing_vpc_cidrs(region):
              """
              Get a list of CIDR blocks for all existing VPCs in the specified region.
              """
              ec2 = boto3.client('ec2', region_name=region)
              vpcs = ec2.describe_vpcs()['Vpcs']
              
              # Extract the list of existing VPC CIDR blocks
              existing_cidrs = [vpc['CidrBlock'] for vpc in vpcs]
              return existing_cidrs

          def delete_vpc_resources(vpc_id, region):
            ec2 = boto3.client('ec2', region_name=region)
            print(f"Deleting resources for VPC: {vpc_id}")
            
            # Step 2: Delete NAT Gateway and release EIP
            nat_gateways = ec2.describe_nat_gateways(Filters=[{"Name": "vpc-id", "Values": [vpc_id]}])['NatGateways']
            for nat_gw in nat_gateways:
                try:
                    natGWID = nat_gw['NatGatewayId']
                    if natGWID:
                        ec2.delete_nat_gateway(NatGatewayId=natGWID)
                        print(f"Deleted NAT Gateway: {natGWID}")
                except Exception as e:
                    print(f"Failed to delete NAT: {str(e)}")
                time.sleep(30)  # Give time for NAT Gateway deletion to complete
                # After waiting, release the associated Elastic IP (EIP)
                for address in nat_gw['NatGatewayAddresses']:
                    alloc_id = address.get('AllocationId')
                    if alloc_id:
                        try:
                            ec2.release_address(AllocationId=alloc_id)
                            print(f"Released EIP: {alloc_id}")
                        except Exception as e:
                            print(f"Unable to release EIP {alloc_id}. It is attached to a network interface. Will attempt again later to release it.")

            # Step 3: Delete S3 Gateway Endpoint
            s3_endpoints = ec2.describe_vpc_endpoints(Filters=[{'Name': 'vpc-id', 'Values': [vpc_id]}])
            for s3_endpoint in s3_endpoints['VpcEndpoints']:
                ec2.delete_vpc_endpoints(VpcEndpointIds=[s3_endpoint['VpcEndpointId']])
                print(f"Deleted S3 Gateway Endpoint: {s3_endpoint['VpcEndpointId']}")

            # Step 4: Disassociate subnets from route tables before deleting them
            route_tables = ec2.describe_route_tables(Filters=[{"Name": "vpc-id", "Values": [vpc_id]}])['RouteTables']
            for route_table in route_tables:
                associations = route_table.get('Associations', [])
                
                # Disassociate any associated subnets from this route table
                for association in associations:
                    if not association.get('Main', False):  # Skip the main route table
                        association_id = association['RouteTableAssociationId']
                        try:
                            ec2.disassociate_route_table(AssociationId=association_id)
                            print(f"Disassociated route table {route_table['RouteTableId']} from subnet {association['SubnetId']}")
                        except Exception as e:
                            print(f"Failed to disassociate route table {route_table['RouteTableId']}: {str(e)}")
            
            # Step 5: Delete subnets after disassociating them from route tables
            subnets = ec2.describe_subnets(Filters=[{'Name': 'vpc-id', 'Values': [vpc_id]}])['Subnets']
            for subnet in subnets:
                # Ensure ENIs are deleted from the subnet before deleting the subnet
                delete_eni_in_subnet(ec2, subnet['SubnetId'])
                try:
                    ec2.delete_subnet(SubnetId=subnet['SubnetId'])
                    print(f"Deleted Subnet: {subnet['SubnetId']}")
                except Exception as e:
                    print(f"Failed to delete subnet {subnet['SubnetId']}: {str(e)}")
                
            # Step 6: Delete routes (except the local route) and route tables
            for route_table in route_tables:
                route_table_id = route_table['RouteTableId']
                # Delete non-local routes in the route table (skip the local route)
                routes = route_table.get('Routes', [])
                for route in routes:
                    if route.get('NatGatewayId') or route.get('GatewayId'):
                        # Skip the local route
                        if route.get('DestinationCidrBlock') == f'{vpc_id}/16':
                            print(f"Skipping local route {route['DestinationCidrBlock']} in route table {route_table_id}")
                            continue
                        # Delete non-local routes
                        try:
                            ec2.delete_route(RouteTableId=route_table_id, DestinationCidrBlock=route['DestinationCidrBlock'])
                            print(f"Deleted route {route['DestinationCidrBlock']} from route table {route_table_id}")
                        except Exception as e:
                            print(f"Failed to delete route: {str(e)}")   
                # Now, attempt to delete the route table if it is not the main route table
                if 'Associations' in route_table and len(route_table['Associations']) > 0:
                    # Check if it's the main route table and skip if true
                    if not route_table['Associations'][0].get('Main', False):
                        try:
                            ec2.delete_route_table(RouteTableId=route_table_id)
                            print(f"Deleted Route Table: {route_table_id}")
                        except Exception as e:
                            print(f"Failed to delete route table {route_table_id}: {str(e)}")
                else:
                    # Route table has no associations, so we can delete it
                    try:
                        ec2.delete_route_table(RouteTableId=route_table_id)
                        print(f"Deleted Route Table: {route_table_id}")
                    except Exception as e:
                        print(f"Failed to delete route table {route_table_id}: {str(e)}")
            
            # Step 7: Delete Internet Gateway
            igws = ec2.describe_internet_gateways(Filters=[{'Name': 'attachment.vpc-id', 'Values': [vpc_id]}])['InternetGateways']
            for igw in igws:
                ec2.detach_internet_gateway(InternetGatewayId=igw['InternetGatewayId'], VpcId=vpc_id)
                ec2.delete_internet_gateway(InternetGatewayId=igw['InternetGatewayId'])
                print(f"Detached and deleted Internet Gateway: {igw['InternetGatewayId']}")
            
            # Step 8: Delete the VPC
            try:
                ec2.delete_vpc(VpcId=vpc_id)
                print(f"Deleted VPC: {vpc_id}")
                return True
            except Exception as e:
                print(f"Failed to delete VPC resources: {str(e)}")
                return False

          def generate_subnet_cidrs(available_cidr, num_azs):
              base_network = ipaddress.IPv4Network(available_cidr)
              
              # Ensure that the base CIDR block is large enough to accommodate the subnets
              if base_network.prefixlen > 16:
                  raise ValueError("Base CIDR block must be /16 or larger for this allocation scheme.")
              
              cidr_blocks = {
                  "public": [],
                  "private": [[] for _ in range(num_azs)]
              }

              # Define the starting point for public and private subnets within the available CIDR block
              subnet_iterator = base_network.subnets(new_prefix=22)  # Subnets with /22 prefix

              for i in range(num_azs):
                  # Get the next /22 subnet for the public subnet
                  public_subnet = next(subnet_iterator)
                  cidr_blocks["public"].append(str(public_subnet))

                  # Get the next two /22 subnets for the private subnets
                  for j in range(2):
                      private_subnet = next(subnet_iterator)
                      cidr_blocks["private"][i].append(str(private_subnet))

              return cidr_blocks

          try:
            def main(event, context):
              request_type = event.get("RequestType", "Create")
              region = event.get("ResourceProperties", {}).get("region", "us-east-1")
              cidr_ip_override = event.get("ResourceProperties", {}).get("cidr", "")
              print(f"region: {region} cidr: {cidr_ip_override}")

              if request_type == 'Create':
                pre_check_passed, message = pre_check_vpc_creation(region)
                if not pre_check_passed:                
                  response_data =  {"status": "FAILED", "message": message}
                  if 'ResponseURL' in event:
                    cfnresponse.send(event, context, cfnresponse.FAILED, response_data, physicalResourceId=None)
                  print(f"Pre-check failed: {response_data}")
                  return
                
                ec2 = boto3.client("ec2", region_name=region)
                print(f"boto3 version: {boto3.__version__}")

                # Step 1: Get the list of existing VPC CIDRs
                if cidr_ip_override == "":
                    existing_cidrs = get_existing_vpc_cidrs(region)
                    print(f"Existing VPC CIDRs: {existing_cidrs}")
                    # Step 2: Find an available CIDR non-overlapping block
                    available_cidr = find_available_cidr(existing_cidrs)    
                    print(f"Available CIDR: {available_cidr}")              
                else:
                    # Use manual CIDR supplied
                    available_cidr = cidr_ip_override
                    print(f"Using provided CIDR: {available_cidr}")
                    
                # Step 3: Create a VPC with a default route
                vpc = ec2.create_vpc(CidrBlock=available_cidr)
                vpc_id = vpc["Vpc"]["VpcId"]
                ec2.modify_vpc_attribute(VpcId=vpc_id, EnableDnsSupport={"Value": True})
                ec2.modify_vpc_attribute(VpcId=vpc_id, EnableDnsHostnames={"Value": True})
                print(f"Created VPC: {vpc_id}")

                # Step 4: Get the default route and make it private route and assign it a friendly name
                default_route_table = ec2.describe_route_tables(
                Filters=[
                    {"Name": "vpc-id", "Values": [vpc_id]},
                    {"Name": "association.main", "Values": ["true"]}
                ])['RouteTables'][0]
                
                private_route_table_id = default_route_table['RouteTableId']
                
                ec2.create_tags(
                    Resources=[private_route_table_id],
                    Tags=[{"Key": "Name", "Value": "pri_route_table"}])  # Friendly name for private route table
                
                # Step 5: Get all availability zones in the region
                azs = ec2.describe_availability_zones()["AvailabilityZones"]
                print(f"Found {len(azs)} Availability Zones: {[az['ZoneName'] for az in azs]}")
                
                # Step 6: Create Internet Gateway and attach it to the VPC
                igw = ec2.create_internet_gateway()
                igw_id = igw["InternetGateway"]["InternetGatewayId"]

                # Assign a friendly name (alias) to the private route table
                ec2.create_tags(
                    Resources=[igw_id],
                    Tags=[{"Key": "Name", "Value": "internet-gateway"}]) 
                ec2.attach_internet_gateway(InternetGatewayId=igw_id, VpcId=vpc_id)
                print(f"Created and attached Internet Gateway: {igw_id}")
                
                # Step 7: Create route table for public subnets
                public_route_table = ec2.create_route_table(VpcId=vpc_id)["RouteTable"]
                public_route_table_id = public_route_table['RouteTableId']
                
                # Assign a friendly name (alias) to the public route table
                ec2.create_tags(
                    Resources=[public_route_table_id],
                    Tags=[{"Key": "Name", "Value": "pub_route_table"}]  # Friendly name for public route table
                )

                # Step 8: Create a route in the public route table to route all traffic to the Internet Gateway
                public_rt_id = public_route_table["RouteTableId"]
                ec2.create_route(RouteTableId=public_route_table_id, DestinationCidrBlock="0.0.0.0/0", GatewayId=igw_id)
                print(f"Created public route table: {public_route_table_id}")

                # Step 9: Generate CIDR block for 2 private and 1 public subnet
                subnet_cidr_blocks = generate_subnet_cidrs(available_cidr, len(azs))

                # Step 10: Create subnets for each availability zone and associate the route table
                for i, az in enumerate(azs):
                    az_name = az["ZoneName"]

                    # Public Subnet
                    public_subnet = ec2.create_subnet(
                        VpcId=vpc_id,
                        CidrBlock=subnet_cidr_blocks["public"][i],
                        AvailabilityZone=az_name
                    )
                    public_subnet_id = public_subnet["Subnet"]["SubnetId"]
                    ec2.create_tags(Resources=[public_subnet_id],
                                    Tags=[{"Key": "Name", "Value": f"public-subnet-1-{az_name}"}])
                    ec2.associate_route_table(RouteTableId=public_route_table_id, SubnetId=public_subnet["Subnet"]["SubnetId"])
                    print(f"Created Public Subnet in {az_name}: {public_subnet_id}")

                    # Private Subnets (2 per AZ)
                    for j in range(2):
                        private_subnet = ec2.create_subnet(
                            VpcId=vpc_id,
                            CidrBlock=subnet_cidr_blocks["private"][i][j],
                            AvailabilityZone=az_name
                        )
                        private_subnet_id = private_subnet["Subnet"]["SubnetId"]
                        ec2.create_tags(Resources=[private_subnet_id],
                                        Tags=[{"Key": "Name", "Value": f"private-subnet-{j+1}-{az_name}"}])
                        # Associate each private subnet with the private route table
                        ec2.associate_route_table(RouteTableId=private_route_table_id, SubnetId=private_subnet_id)                
                        print(f"Created Private Subnet-{j+1} in {az_name}: {private_subnet_id}")


                # Step 11: Create a NAT gateway and assign a friendly name
                # Release any unused Elastic IPs
                release_unused_eips(region)
                eip = ec2.allocate_address(Domain="vpc")
                nat_eip_id = eip["AllocationId"]
                nat_gateway = ec2.create_nat_gateway(SubnetId=public_subnet['Subnet']['SubnetId'], AllocationId=nat_eip_id)
                nat_gw_id = nat_gateway["NatGateway"]["NatGatewayId"]

                # Assign a friendly name (alias) to the NAT
                ec2.create_tags(
                    Resources=[nat_gw_id],
                    Tags=[{"Key": "Name", "Value": "nat-gateway"}]) 

                print(f"Creating NAT Gateway: {nat_gw_id}...")
                time.sleep(30)  # NAT Gateway takes some time to become available

                # Step 12: Add a route to the private route table to route internet-bound traffic through the NAT Gateway
                ec2.create_route(
                    RouteTableId=private_route_table_id,
                    DestinationCidrBlock="0.0.0.0/0",
                    NatGatewayId=nat_gw_id)
                    
                # Step 13: Associate S3 Gateway Endpoint with the private route table for access to Amazon S3
                s3_endpoint = ec2.create_vpc_endpoint(
                    VpcId=vpc_id,
                    ServiceName=f"com.amazonaws.{region}.s3",
                    VpcEndpointType="Gateway",
                    RouteTableIds=[private_route_table_id]
                )
                s3_endpoint_id = s3_endpoint['VpcEndpoint']['VpcEndpointId']

                # Assign a friendly name (alias) to the S3 gateway
                ec2.create_tags(
                    Resources=[s3_endpoint_id],
                    Tags=[{"Key": "Name", "Value": "s3-endpoint-gw"}]) 
                print(f"Created S3 Gateway Endpoint: {s3_endpoint_id}")

                print("VPC setup complete!")
                if 'ResponseURL' in event:
                  response_data =  {"VpcId": vpc_id}
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, physicalResourceId=vpc_id)
                  print(f"Created VPC: {vpc_id} and returned to CloudFormation.")
              elif request_type == 'Delete':
                # Step 1: Get the VPC ID from the PhysicalResourceId
                vpc_id = event.get("PhysicalResourceId", "NA")
                if vpc_id == "NA" or not vpc_id.startswith("vpc-") :
                  response_data =  {"status": "FAILED", "message": f"Invalid vpc_id={vpc_id} received"}
                  print(f"Invalid vpc_id={vpc_id} received")
                  if 'ResponseURL' in event:
                     cfnresponse.send(event, context, cfnresponse.FAILED, response_data, physicalResourceId=None)
                else:
                  # Check if the default security group is in use (indicating external dependencies)
                  if is_default_security_group_in_use(vpc_id, region):
                      error_message=f"The stack will be deleted but the VPC vpc_id={vpc_id} will remain intact because the default security group is in use. You will have to delete vpc manually."
                      print(error_message)
                      response_data =  {"status": "SUCCESS", "message": error_message}
                      if 'ResponseURL' in event:
                         cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, physicalResourceId=None)
                      return
                  # Delete the VPC resources in the correct order
                  result = delete_vpc_resources(vpc_id, region)
                  if 'ResponseURL' in event:
                      if result:
                        success_message=f"Successfully deleted vpc_id={vpc_id} resources"
                        response_data =  {"status": "SUCCESS", "message": success_message}
                        cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, physicalResourceId=vpc_id)
                      else:
                        error_message=f"Failed to do vpc_id={vpc_id} resources deletion. Manual cleanup may be required."
                        response_data =  {"status": "FAILED", "message": error_message}
                        cfnresponse.send(event, context, cfnresponse.FAILED, response_data, physicalResourceId=None)
              else:
                if 'ResponseURL' in event:
                   cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physicalResourceId=None)
          except Exception as e:
            print(f"Error: {e}")
            if 'ResponseURL' in event:
               response_data =  {"status": "FAILED", "message": f"Error: {e}"}
               cfnresponse.send(event, context, cfnresponse.FAILED, response_data, "CustomResourcePhysicalID")  

  # Custom Resource to trigger the Lambda function once
  VPCCreationCustomResource:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: !GetAtt VPCCreationFunction.Arn  # The Lambda ARN will be used to trigger the function
      region: !Ref VPCRegion  # Pass the AWS region as a property to the Lambda
      cidr: !Ref cidr # Pass cidr as a property to the Lambda

Outputs:
  VPCLambdaFunction:
    Description: "VPC Creation Lambda Function ARN"
    Value: !GetAtt VPCCreationFunction.Arn
