Dynamically clean up lambda

Below snippet shows how to clean up a lambda resource and its associated ENI


---
Resources:
  LambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Handler: "index.lambda_handler"
      Role: !GetAtt LambdaFunctionRole.Arn
      Code:
        ZipFile: !Sub |
          def lambda_handler(event, context):
            print('Hello World')
      Runtime: "python2.7"
      Timeout: "60"
      VpcConfig:
        SecurityGroupIds:
          - !Ref SecurityGroup1
          - !Ref SecurityGroup2
        SubnetIds:
          - !Ref Subnet1
          - !Ref Subnet2
  VPCLambdaCleanupCustomResource:
    Type: Custom::LambdaCleanup
    Properties:
      ServiceToken: !GetAtt VPCLambdaCleanupLambdaFunction.Arn
      Region: !Ref "AWS::Region"
      LambdaFunctionName: !Ref LambdaFunction
  VPCLambdaCleanupLambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Handler: "index.lambda_handler"
      Role: !GetAtt LambdaFunctionRole.Arn
      Code:
        ZipFile: !Sub |
          import boto3
          from botocore.exceptions import ClientError
          import time
          import cfnresponse

          ec2 = boto3.client('ec2')

          def detach_interface(attachment_id):
            ec2.detach_network_interface(AttachmentId=attachment_id, Force=True)
            print('Detached attachment [{0}]'.format(attachment_id))


          def delete_interface(interface_id):
            retries = 6

            # We need to retry because the detach can take some time and this will fail if you try too quicker after the detach
            while retries > 0:
              try:
                # Delete the ENI, if successful drop the retry count to 0 so we do not try again
                ec2.delete_network_interface(NetworkInterfaceId=interface_id)
                print('Deleted interface [{0}]'.format(interface_id))
                retries = 0
              except ClientError as delete_error:
                # Get the error code and do not retry on NotFound
                error_code = delete_error.response.get("Error", {}).get("Code", "")

                if error_code == 'InvalidNetworkInterfaceID.NotFound':
                  retries = 0
                  print('Interface [{0}] has already been deleted'.format(interface_id))
                # Default Case
                else:
                  # If we encounter an error decrement the retry count by 1 and retry after sleeping for 30s
                  retries -= 1
                  print('Failed to delete interface [{0}] - retries remaining [{1}]. Error: {2}'.format(interface_id, retries, delete_error))
                  time.sleep(30)


          def get_attachment_id_for_eni(eni):
            if 'Attachment' in eni and 'AttachmentId' in eni['Attachment']:
              return eni['Attachment']['AttachmentId']
            return None


          def get_eni_id(eni):
            if 'NetworkInterfaceId' in eni:
              return eni['NetworkInterfaceId']
            return None


          def clean_up_enis_for_lambda_function(function_name):
            try:
              # Get the associated ENIs
              response = ec2.describe_network_interfaces(
                Filters=[
                  {
                    'Name': 'requester-id',
                    'Values': ['*:{0}'.format(function_name)]
                  },
                  {
                    'Name': 'description',
                    'Values': ['AWS Lambda VPC ENI*']
                  }
                ]
              )

              # Check there is any interfaces
              if 'NetworkInterfaces' in response and len(response['NetworkInterfaces']) > 0:
                print('{0} ENIs to clean up for [{1}]'.format(len(response['NetworkInterfaces']), function_name))

                # Get the list of attachments we need to detach
                eni_attachment_ids = filter(lambda eaid: eaid is not None, map(lambda eni: get_attachment_id_for_eni(eni), response['NetworkInterfaces']))

                # Get the list of ENIs
                eni_ids = filter(lambda eid: eid is not None, map(lambda eni: get_eni_id(eni), response['NetworkInterfaces']))

                # Print out what we are going to do
                print('Detaching the following attachments [{0}]'.format(",".join(eni_attachment_ids)))
                print('Deleting the following interfaces [{0}]'.format(",".join(eni_ids)))

                # Detach each ENI
                for eni_attachment_id in eni_attachment_ids:
                  detach_interface(eni_attachment_id)

                # Delete each ENI
                for eni_id in eni_ids:
                  delete_interface(eni_id)
              else:
                print('No ENIs to clean up for [{0}]'.format(function_name))

            # We would rather let the Custom Resource "Delete" than not clean up. Print the error and continue
            except Exception as clean_up_error:
              print('Failed to cleanup ENIs for function [{0}]. Error: '.format(function_name, clean_up_error))

          def lambda_handler(event, context):
            request_type = event['RequestType']
            responseData = {}

            if request_type == 'Delete':
              function_name = event['ResourceProperties']['LambdaFunctionName']
              clean_up_enis_for_lambda_function(function_name)
              cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
            else:
              cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
      Runtime: "python2.7"
      Timeout: "240"
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: "10.0.0.0/16"
  Subnet1:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: 'us-west-2a'
      CidrBlock: "10.0.1.0/24"
      VpcId: !Ref VPC
  Subnet2:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: 'us-west-2b'
      CidrBlock: "10.0.2.0/24"
      VpcId: !Ref VPC
  SecurityGroup1:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "Open SecurityGroup1"
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: '1'
          ToPort: '65535'
          CidrIp: 0.0.0.0/0
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '1'
          ToPort: '65535'
          CidrIp: 0.0.0.0/0
      VpcId: !Ref VPC
  SecurityGroup2:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: "Open SecurityGroup2"
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: '1'
          ToPort: '65535'
          CidrIp: 0.0.0.0/0
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '1'
          ToPort: '65535'
          CidrIp: 0.0.0.0/0
      VpcId: !Ref VPC
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action: ['sts:AssumeRole']
          Effect: Allow
          Principal:
            Service: [lambda.amazonaws.com]
        Version: '2012-10-17'
      Path: /
      Policies:
        - PolicyName: LambdaRole
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Effect: Allow
                Resource:
                  - !Sub "arn:aws:logs:*:*:*"
              - Action:
                  - 'ec2:*'
                Effect: Allow
                Resource: "*"
  LambdaCleanUpFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action: ['sts:AssumeRole']
          Effect: Allow
          Principal:
            Service: [lambda.amazonaws.com]
        Version: '2012-10-17'
      Path: /
      Policies:
        - PolicyName: LambdaRole
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Effect: Allow
                Resource:
                  - !Sub "arn:aws:logs:*:*:*"
              - Action:
                  - 'ec2:*'
                Effect: Allow
                Resource: "*"

Comments

Popular posts from this blog

Integrate an API with lambda - Part 5

Custom authorizer

Integrate an API with lambda - Part 3