2

EDIT 3:
So the problem may likely be in the set-up and configuration of my Lambda Layer Dependencies. I have a /bin directory containing 3 files:

  • lambdazip.sh
  • pdftk
  • libgcj.so.10

pdftk is a pdf library, and libgcj is a dependency for PDFtk.
lambdazip.sh seems to set & modify PATH Variables.

I have tried uploading all 3 as 1 lambda layer.
I have tried uploading all 3 as 3 separate lambda layers.
I have not tried customizing the .zip file names, I know sometimes the Lambda Layer wants you to name the .zip file a specific name dependent on the language.
I have not tried customizing the "compatible architectures" & "compatible runtime" lambda layer settings.

EDIT 2:
I tried renaming the Lambda Layer as Python.zip because I heard that sometimes you need a specific naming convention for the Lambda Layer to work correctly. This also failed & produced the same error.

EDIT:
I have tried pulling the .py files out of the /surveys directory, so when they are zipped, they are in the root folder, but I still receive the same error: Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'surveys

Which files do I need to zip? Do I need to move certain files to the root?
I learned that I had accidentally zipped the directory which commonly caused this error.
I needed to zip the contents of the directory, which is a common solution.
Unfortunately this did not work for me.
enter image description here

I have a Lambda Function, and the code I have uploaded is a zipped folder of my /Archive directory.
From what I understand, many of the people who run into this "[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function':" have issues because of their Lambda Handler.

My Lambda handler is: lambda_function.lambda_handler so this doesn't appear to be my issue.

Another common problem I've noticed on Stackoverflow, appears to be with how people are compressing & zipping the files they upload to the Lambda Function.

Do I need to move my lambda_function.py? Sometimes this CloudWatch error occurs because the lambda_function.py is not in the ROOT directory.

Does my survey directory need to move?

I think the folders & directories I have here may be causing my issue.

Do I need to zip the directories individually?

Can I resolve this error by Zipping the entire project?

For more information, I also have a Lambda Layer for PDF Toolkit, called pyPDFtk in the codebase. In that Lambda layer is a zipped /bin with binaries inside.

If there is anything I can alter/change within my code or AWS configuration, please let me know, and I can return new CloudWatch error logs for you.

lambda_function.py

"""
cl_boost-pdfgen manages form to
pdf merge and mail
"""
import json, base64
import os, sys
from string import Template
from boost import PageCalc, AwsWrapper
from boost.tools import helper 
from boost.surveys import ALLOWED_SURVEYS

os.environ['LAMBDA_TASK_ROOT'] = os.environ['LAMBDA_TASK_ROOT'] if 'LAMBDA_TASK_ROOT' in os.environ else '/usr/local'
os.environ['PDFTK_PATH'] = os.environ['LAMBDA_TASK_ROOT'] + '/bin/pdftk'
os.environ['LD_LIBRARY_PATH'] = os.environ['LAMBDA_TASK_ROOT'] + '/bin'
# must import after setting env vars for pdftk
import pypdftk

# Constants
BUCKET_NAME = os.environ['BUCKET_NAME'] if 'BUCKET_NAME' in os.environ else 'cl-boost-us-east-1-np'

RAW_MESSAGE = Template(b'''From: ${from}
To: ${to}
Subject: MySteadyMind Survey results for ${subjname}
MIME-Version: 1.0
Content-type: Multipart/Mixed; boundary = "NextPart"

--NextPart
Content-Type: multipart/alternative; boundary="AlternativeBoundaryString"

--AlternativeBoundaryString
Content-Type: text/plain;charset="utf-8"
Content-Transfer-Encoding: quoted-printable

See attachment for MySteadyMind report on ${subjname}

--AlternativeBoundaryString
Content-Type: text/html;charset="utf-8"
Content-Transfer-Encoding: quoted-printable

<html>
  <body>=0D
    <p>See attachment for MySteadyMind Report on </b> ${subjname} </b>.</p>=0D
  </body>=0D
</html>=0D

--AlternativeBoundaryString--

--NextPart
Content-type: application / pdf
Content-Type: application/pdf;name="${filename}"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;filename="${filename}"

${pdfdata}

--NextPart--''')

EMAIL_TAKER = True
#DEFAULT_EMAIL = os.environ['DEFAULT_EMAIL'] if 'DEFAULT_EMAIL' in os.environ else 'support@mysteadymind.com'
DEFAULT_EMAIL = os.environ['DEFAULT_EMAIL'] if 'DEFAULT_EMAIL' in os.environ else 'marshall@volociti.com'
SUBJECT = 'Evaluation for %s'
NAME_PATH = ['Entry', 'Name']
#EXTRA_EMAILS = os.environ['EXTRA_EMAILS'].split(",") if 'EXTRA_EMAILS' in os.environ else ['seth@mysteadymind.com']
EXTRA_EMAILS = os.environ['EXTRA_EMAILS'].split(",") if 'EXTRA_EMAILS' in os.environ else ['marshall@volociti.com']

#  Lambda response
def respond(err, res=None):
    """
    parameters are expected to either be
    None or valid JSON ready to go.

    :param err:
    :param res:
    :return:
    """
    return {
        'statusCode': '400' if err else '200',
        'body': err if err else res,
        'headers': {
            'Content-Type': 'application/json',
        },
    }


def check_basic_auth(headers):
    """
    pull out the auth header and validate.

    :param headers:
    :return:

    # Retrieve values from env
    vid = os.environ['uid']
    vpw = os.environ['pwd']

    encoded = "Basic " + base64.b64encode("%s:%s" % (vid,vpw))

    # compare
    return headers['Authorization'] == encoded
    """
    return True


def lambda_handler(event, context):
    """
    receive JSON, produce PDF, save to S3,
    email via SES.... bring it!
    """
    err = None
    rsp = None

    #Have to none out addresses for future lambda runs to not cause issues with appending.
    ADDRESSES = None
    ADDRESSES = {'from': "marshall@volociti.com",
                 'to': [DEFAULT_EMAIL] + EXTRA_EMAILS}
    """
    ADDRESSES = {'from': "support@mysteadymind.com",
                 'to': [DEFAULT_EMAIL] + EXTRA_EMAILS}
    """

    # check auth
    if not check_basic_auth(event['headers']):
        print ("Failed to authenticate")
        return False

    # get dataq
    data = json.loads(event['body'])

    # make sure its legit
    if (data['Form']['InternalName'] not in ALLOWED_SURVEYS):
        return False

    # read in template and prep survey type and scoreit
    pcalc = PageCalc(data, data['Form']['InternalName'])
    pcalc.score()

    pcalc.flat['Name'] = data['Section']['FirstName'] + \
        " " + data['Section']['LastName']

    # output pdf to temp space
    # baseName = str(data['Entry']['Number']) + "-" + pcalc.survey + "-" + \
    #     data['Section']['LastName'].replace(' ','') + ".pdf"
    baseName = str(data['Entry']['Number']) + "-MySteadyMind-" + \
         data['Section']['LastName'].replace(' ','') + ".pdf"
    filename = "/tmp/" + baseName

    pypdftk.fill_form(pcalc.pdf_path, pcalc.flat, out_file=filename)

    # -- Post Processing after PDF Generation -- #
    # fetch the client wrapper(s)
    aws = AwsWrapper()

    # get PDF data and prep for email
    try:
        # save the pdf to S3
        print("save %s to S3" % filename)
        aws.save_file(BUCKET_NAME, pcalc.survey,filename)

        # read in the pdf file data and
        # base64 encode it
        buff = open(filename, "rb")
        pdfdata = base64.b64encode(buff.read())
        buff.close()

        ADDRESSES['to'].append(data['Section']['Email']) if EMAIL_TAKER else None

        # gather data needed for email body
        data = {"from": ADDRESSES['from'],
                "to": ', '.join(ADDRESSES['to']),
                "subjname": pcalc.flat["Name"],
                "filename": baseName,
                "pdfdata": pdfdata
               }

        print("sending email via SES to: %s" % ', '.join(ADDRESSES['to']))

        # build MMM email and send via SES
        response = aws.send_raw_mail(ADDRESSES['from'],
                                     ADDRESSES['to'],
                                     RAW_MESSAGE.substitute(data))

        # send JSON response
        rsp = '{"Code": 200, "Message": "%s"}' % response

    except Exception as ex:
        # error trap for all occassions
        errmsg = "Exception Caught: %s" % ex

        # notify local log
        print(errmsg)

        # and lambda response
        err = '{"Code":500, "Message":"%s"}' % errmsg

    # done
    return respond(err, rsp)


if __name__ == '__main__':
    # use this option to manually generate from raw csv of cognitoforms
    if len(sys.argv) > 1:
        import csv
        with open(sys.argv[1], 'rU') as csvfile:
            csvreader = csv.DictReader(csvfile, delimiter=',',)
            for row in csvreader:
                jsondata = helper.create_json(row)
                fakeevent = {'body': json.dumps(jsondata), "headers": []}
                lambda_handler(fakeevent, None)

    # use this option to manually generate from raw json webhook response from cognito in a dir called generated/
    else:
        rng = range(3,4)
        print (rng)
        print ("Attempting to parse files: " + str(rng))
        for i in rng:
            try:
                print ('./generated/queue/' + str(i) + '.json')
                f = open('./generated/queue/' + str(i) + '.json', 'r')
                jsondata = f.read().replace('\n', '')
                f.close()

                #jdata = json.loads(jsondata)
                fakeevent = {'body': jsondata, "headers": []}
                lambda_handler(fakeevent, None)
            except:
                print ("error. file not found: " + str(i))

lambdazip.sh

#!/bin/bash
PYTHON_PATH=$VIRTUAL_ENV/..
BASE_PATH=$PWD


cd $VIRTUAL_ENV/lib/python3.9/site-packages/
zip -x "*pyc" -r9 $BASE_PATH/dist/cl-boost.zip pypdftk*

cd $BASE_PATH
zip -r $BASE_PATH/dist/cl-boost.zip bin

cd $BASE_PATH
zip -x "*pyc" -r9 $BASE_PATH/dist/cl-boost.zip boost* pdf_surveys
Stephen Stilwell
  • 518
  • 1
  • 5
  • 17
  • I suggest you share the lambda code, or at least enough of it so it's a reproducible example – Paolo Nov 09 '21 at 18:53
  • added lambda_function.py & I can include more upon request. – Stephen Stilwell Nov 09 '21 at 21:02
  • I learned that I was zipping the project incorrectly. Initially, I was just zipping the root directory. Now I have each file zipped correctly, but I am getting the same 'no module named /surveys' error – Stephen Stilwell Nov 10 '21 at 17:18
  • The "No module named" error seems to hint at the fact that the layers you have configured do not have the required libraries. See https://stackoverflow.com/a/64462403/3390419 – Paolo Nov 10 '21 at 17:21
  • 'surveys' is a directory in my project, containing 5 .py files. I will look at my Layers & at the link you have provided. – Stephen Stilwell Nov 10 '21 at 17:58
  • I have tried pulling the .py files out of the /surveys directory, so when they are zipped, they are in the root folder, but I still receive the same error: Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'surveys – Stephen Stilwell Nov 12 '21 at 16:02
  • Some things to try: 1) Although it may not needed, add an `__init__.py` file to boost folder. 2) Make sure that you test locally with the same python version as in the lambda environment 3) Check that your `__init__.py` files are correct – kgiannakakis Nov 12 '21 at 16:56
  • Is there any chance that I need to move the files out of the directories they are currently in? Other people with this error have suggested I move certain files to the ROOT directory. – Stephen Stilwell Nov 12 '21 at 17:03
  • For the case you are talking about: Instad of moving files to the root directory use: import sys sys.path.append('/where/are/my/files') and then you definitely do not need an __init__.py (only a .py file name) so they are in the searchpath. Adding stuff to the root is usually not a great idea – Arbor Chaos Nov 19 '21 at 13:43

3 Answers3

2

I tried to replicate the issue, but it all works as expected. My setup was (Lambda with Python 3.9):

enter image description here

It seems to me that either your directory struct is not what you posted in the question. Similarly your real code that you present in SO could be different.

Marcin
  • 215,873
  • 14
  • 235
  • 294
  • I have added the lambdazip.sh file contents to the bottom of my original post. Maybe we can find some errors in this file, or help push us in the right direction. To my understanding, the past developer may have used this lambdazip.sh file to test the lambda function locally before deploying to AWS. I was previously instructed not upload this .sh file to a Lambda Layer. Do you believe this .sh file needs to be uploaded to the Lambda Layer as a .zip? – Stephen Stilwell Nov 15 '21 at 14:55
  • @StephenStilwell Sadly your new code does not help. I don't have such libraries nor project setup, thus can't run your `lambdazip.sh`. In other words, `lambdazip.sh` is not reproducible. – Marcin Nov 15 '21 at 22:47
1

Considering Marcin could not reproduce the error: This error might be connected to multiple installations and their libraries. If you have the wrong one in your path variables, the correct one cannot be found.

Arbor Chaos
  • 149
  • 6
  • I have added the lambdazip.sh file contents to the bottom of my original post. Maybe we can find some errors in this file, or help push us in the right direction. To my understanding, the past developer may have used this lambdazip.sh file to test the lambda function locally before deploying to AWS. I was previously instructed not upload this .sh file to a Lambda Layer. Do you believe this .sh file needs to be uploaded to the Lambda Layer as a .zip? – Stephen Stilwell Nov 15 '21 at 14:54
0

My code was written in Python 2.7, a deprecated language for AWS Lambda.

While updating the code to Python 3.9, I learned that I also need to update my dependencies, binaries & AWS Lambda Layers.

The Binary that I am using for Python PDF Toolkit, or PDFtk, is also out-dated and incompatible.

So I needed to launch a new EC2 instance of CentOS 6 to produce the Binary.

This has come with its own problems, but those details are best for another thread.

Mainly, CentOS 6 is EOL & instructions for producing the binary no longer work.

I need to go change the base url inside the /etc/yum.repos.d/ to access the vault.centos.org

Stephen Stilwell
  • 518
  • 1
  • 5
  • 17