• בלוג
  • עמוד 3
  • מדריך: הכנת סביבה והרצת קוד פייתון ב AWS Lambda

מדריך: הכנת סביבה והרצת קוד פייתון ב AWS Lambda

16/02/2025

אם יש משהו שמאפיין את AWS זה הפער בין התיאוריה לפרקטיקה. קל מאוד לתאר מה אני רוצה לעשות אבל בדרך להפוך את זה לקוד הרבה דברים מסתבכים. במדריך היום נראה איך להפעיל תוכנית פייתון על Lambda, מה יכול להשתבש בדרך ואיך לתקן.

1. התקנת הכלים

לצורך המדריך אני מניח שיש לכם על המכונה את הכלים הבאים מותקנים:

  1. כלי שורת הפקודה של AWS. התקנה מכאן: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html.

  2. כלי שורת הפקודה SAM. התקנה מכאן: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html

  3. דוקר. התקנה מכאן: https://docs.docker.com/

  4. מאוד מומלץ להריץ את המדריך ממכונת לינוקס בארכיטקטורה x86_64 כי זו הארכיטקטורה עליה רץ ה Lambda. בעבודה על המק נתקלתי בהמון תקלות מוזרות תוך כדי שנעלמו לגמרי כשעברתי ללינוקס.

וכמובן אני מניח שיש לכם משתמש על AWS.

בפוסט נשתמש ב SAM שהוא הרחבה של CloudFormation, כלומר כלי לתיאור ארכיטקטורה באמצעות קובץ yml והרצתה מקומית מחוץ ל AWS כדי לבדוק שפונקציות ה Lambda שיצרנו עובדות. בהשוואה ל localstack עליו כתבתי בפוסטים קודמים, סאם מריץ רק פונקציות Lambda ולא כולל את שירותי AWS האחרים.

2. תבנית הפרויקט

בשביל הדוגמה אני בונה פרויקט שיוצר פודקסט עם ספריית podcastfy. יצרתי את תבנית הפרויקט דרך כלי שורת הפקודה SAM עם הפקודה:

$ sam init

אתם יכולים לעשות את זה ולבחור מהתפריט ליצור פרויקט פייתון על בסיס Image, או לקחת את הקבצים מתבנית הפרויקט של הפוסט בגיטהאב כאן:

https://github.com/ynonp/lambda-podcastfy/tree/main

לצורך הדוגמה אני אריץ תוכנית פייתון שמשתמשת בספריית podcastfy ליצירת פודקסטים. הספריה תתן לנו הזדמנות לגעת במספר נקודות רגישות של Lambda. אם אתם לוקחים את הקוד שלי מגיטהאב תצטרכו ליצור בתוך תיקייה hello_world קובץ .env במבנה הבא:

GEMINI_API_KEY=
OPENAI_API_KEY=

וכן יש למלא את המפתחות בפנים בשביל שזה יעבוד.

3. בנייה והרצה מקומית

בשביל לבנות את הפרויקט מקומית נוכל להריץ:

$ sam build --use-container

התוספת --use-container גורמת ל SAM להריץ את הבנייה. בנייה בהקשר הזה זה בנייה של אימג' שבתוכו תרוץ ה Lambda שלנו. אפשר לראות ולעדכן את ה Dockerfile בקובץ hello_world/Dockerfile והתוכן שלו בפרויקט הדוגמה הוא:

FROM public.ecr.aws/lambda/python:3.12

WORKDIR /var/task

RUN mkdir -p /var/task/ffmpeg && \
    cd /var/task/ffmpeg && \
    curl -O https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && \
    python -c "import tarfile; tarfile.open('ffmpeg-release-amd64-static.tar.xz').extractall()" && \
    mv ffmpeg-*-amd64-static/* . && \
    rm -rf ffmpeg-*-amd64-static && \
    rm ffmpeg-release-amd64-static.tar.xz

ENV PATH="/var/task/ffmpeg:${PATH}"
RUN chmod -R +x /var/task/ffmpeg

COPY app.py requirements.txt ./
COPY .env ./

RUN python3.12 -m pip install -r requirements.txt -t .

# Command can be overwritten by providing a different command in the template directly.
CMD ["app.lambda_handler"]

בפרויקט הדוגמה אני רוצה להתקין את ספריית podcastfy והיא דורשת התקנה של ffmpeg על המכונה. אנחנו מתחילים עם אימג' של פייתון מאמזון ובתוכו אין כמעט כלום, הם כן שמו curl אבל אפילו tar לא מותקן על האימג' ולכן בשביל להתקין את ffmpeg אני מוריד את הבינארי שלו ומשתמש ב python -c כדי לפתוח את הארכיון. מוסיפים את ffmpeg לרשימת הנתיבים להרצה ומתקנים הרשאות ואז ממשיכים להעתקת קבצי התוכנית (בפרויקט שלי זה רק app.py) וקובץ משתני הסביבה. אחרי זה התקנת התלויות והפעלת הפונקציה המרכזית מתוך app.

הקוד ב app.py בזמן הבניה המקומית הוא:

import json
from podcastfy.client import generate_podcast
import os

def lambda_handler(event, context):
    """Sample pure Lambda function

    Parameters
    ----------
    event: dict, required
        API Gateway Lambda Proxy Input Format

        Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format

    context: object, required
        Lambda Context runtime methods and attributes

        Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html

    Returns
    ------
    API Gateway Lambda Proxy Output Format: dict

        Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
    """
    audio_file = generate_podcast(urls=["https://en.wikipedia.org/wiki/Podcast"])
    print(audio_file)

    return {
        "statusCode": 200,
        "body": json.dumps(
            {
                "message": "Audio File Created",
            }
        ),
    }

ובעצם הוא מייצר פודקסט מדף ב wikipedia שומר אותו בתיקייה המקומית ויוצא. אני יודע לא מלהיב במיוחד אבל מספיק טוב בשביל הדוגמה שלנו.

אחרי שבניתי את האימג' אני יכול להריץ את הפונקציה עם:

sam local invoke HelloWorldFunction --debug

ה --debug הוא אופציונאלי אבל מומלץ מאוד כי כשדברים נשברים אתם רוצים לדעת למה. אם לקחתם את פרויקט הדוגמה שלי זה כנראה יעבוד בזכות שתי הגדרות בקובץ ה template.yaml:

  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Image
      Timeout: 600
      MemorySize: 3008

הגדרת ה Timeout חשובה כי ליצור פודקסטים לוקח זמן וברירת המחדל 10 שניות קצרה מדי. גודל הזיכרון בברירת המחדל הוא 128 מגה שזה גם קטן מדי, לפי התיעוד אפשר להקצות עד 10 ג'יגה אבל מניסויים שלי 3 ג'יגה היה המקסימום שהצלחתי לקבל ולכן זה הגודל שהשארתי.

אם הפונקציה רצה מקומית והכל עובד אפשר להמשיך לשלב הבא.

4. יצירת משתמש AWS

בשביל להעלות את הקוד ל Lambda על AWS יש ליצור משתמש בסרביס IAM שלהם ולתת לו מספיק הרשאות כדי להעלות את הקוד. יכול להיות שאפשר להשתמש פה במשתמש הראשי שלכם (לא ניסיתי), ובכל מקרה אם אתם יוצרים משתמש ואין לו מספיק הרשאות תקבלו הודעת שגיאה ב deploy ואז אפשר להוסיף הרשאות לפי מה שהוא מבקש ויש גם כפתור בממשק שעושה את זה אוטומטית. ההרשאות שאני נתתי בסופו של דבר היו:

  1. AmazonAPIGatewayAdministrator
  2. AmazonS3FullAccess
  3. AWSCloudFormationFullAccess
  4. AWSLambda_FullAccess

ובנוסף גם:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole"
            ],
            "Resource": "arn:aws:iam::*:role/aws-service-role/apigateway.amazonaws.com/AWSServiceRoleForAPIGateway",
            "Condition": {
                "StringLike": {
                    "iam:AWSServiceName": "apigateway.amazonaws.com"
                }
            }
        }
    ]
}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:CreateRepository",
                "ecr:GetAuthorizationToken",
                "ecr:InitiateLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:CompleteLayerUpload",
                "ecr:PutImage",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:TagResource",
                "ecr:DescribeRepositories",
                "ecr:DeleteRepository",
                "ecr:SetRepositoryPolicy",
                "ecr:GetRepositoryPolicy"
            ],
            "Resource": [
                "arn:aws:ecr:us-east-1:124355662604:repository/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "ecr:GetAuthorizationToken",
            "Resource": "*"
        }
    ]
}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole",
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:GetRole",
                "iam:PutRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:PassRole",
                "iam:TagRole",
                "iam:UntagRole",
                "iam:AttachRolePolicy",
                "iam:DetachRolePolicy"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:CreateChangeSet",
                "cloudformation:CreateStack",
                "cloudformation:UpdateStack",
                "cloudformation:DeleteStack",
                "cloudformation:DescribeStacks",
                "cloudformation:ListStacks",
                "cloudformation:GetTemplateSummary",
                "cloudformation:ValidateTemplate",
                "s3:CreateBucket",
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "lambda:CreateFunction",
                "lambda:UpdateFunctionCode",
                "lambda:UpdateFunctionConfiguration",
                "lambda:DeleteFunction",
                "lambda:GetFunction",
                "lambda:ListFunctions",
                "lambda:PublishVersion",
                "lambda:CreateAlias",
                "lambda:DeleteAlias",
                "lambda:UpdateAlias",
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:GetRole",
                "iam:PutRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:PassRole"
            ],
            "Resource": "*"
        }
    ]
}

אחרי שיש משתמש נכנסים לטאב Security Credentials ויוצרים עבורו Access Key. משורת הפקודה נפעיל:

aws configure

ונכניס את ה Access Key ואת ה Secret כשהוא שואל אותנו.

5. העלאה ל AWS והרצת בדיקה מהממשק שלהם

אחרי חיבור AWS אפשר להפעיל:

sam deploy --guided

ולענות על השאלות כדי להעלות את הפונקציה ל AWS. אחרי סיום העלאה תוכלו למצוא את הפונקציה בדף הפונקציות בסרביס Lambda בממשק של AWS, ושם גם תמצאו כפתור Test שיפעיל את פונקציית ה Lambda החדשה שלנו.

6. תיקון הקוד והעלאה מחדש

ל Lambda יש מספר מגבלות חדשות שלא קיימות בהפעלה מקומית על SAM. הראשונה היא מגבלת הזיכרון המקסימלי (סאם מקומי יכול לעבוד עם עד 10 ג'יגה זיכרון, אבל בענן הם נתנו לי רק 3). השניה, יותר חשובה, היא שאי אפשר ליצור קבצים איפה שרוצים אלא רק בתיקיית /tmp. בעבודה עם podcastfy צריך להגיד לו ליצור את כל הקבצים שלו רק שם ואת זה עושים בשורה שיוצרת את הפודקסט עם פרמטר נוסף של הגדרות שיחה. זה נראה כך:

    audio_file = generate_podcast(
            urls=["https://en.wikipedia.org/wiki/Podcast"],
            conversation_config={
                "text_to_speech": {
                    "temp_audio_dir": "../../../../../../../../../../../../tmp/podcastify-demo/tmp",
                    "output_directories": {
                        "transcripts": "/tmp/podcastify-demo/transcripts",
                        "audio": "/tmp/podcastify-demo/audio"
                        }
                    }
                }
            )

את שני הערכים בתוך output_directories קל להבין, אבל מה הסיפור עם כל הנקודות ב temp_audio_dir ? נו, זה מעניין. בקוד הספריה podcastfy השורות הבאות מטפלות בערך שאני מעביר כאן:

def _setup_directories(self) -> None:
    """Setup required directories for audio processing."""
    self.output_directories = self.tts_config.get("output_directories", {})
    temp_dir = self.tts_config.get("temp_audio_dir", "data/audio/tmp/").rstrip("/").split("/")
    self.temp_audio_dir = os.path.join(*temp_dir)
    base_dir = os.path.abspath(os.path.dirname(__file__))
    self.temp_audio_dir = os.path.join(base_dir, self.temp_audio_dir)

הקוד לוקח את הערך למשתנה temp_dir, ואז מוחק את ה / מתחילת הנתיב ומחבר את זה לתיקייה בה נמצאת ספריית podcastfy עצמה. אני לא מבין למה הם עשו את זה אבל בפועל זה אומר שקבצים זמניים יישמרו לתת תיקייה בתוך תיקיית היישום, מה שלא עובד ב Lambda. בשביל לברוח בכל זאת לתיקיית /tmp בלי לכתוב את סימן / בתחילת שורת הנתיב השתמשתי בסימן .. שעובר תיקייה אחת למעלה, וכך נקבל נתיב שנראה בערך כך:

/var/task/app/podcastfy/../../../../../../../tmp/podcastfy-demo/tmp

והקבצים יישמרו בתיקיית /tmp דרך הנתיב היחסי.

7. מה נשאר

הפונקציה עובדת מכפתור Test בממשק של Lambda וזה טוב, עדיין נשארו שני דברים כדי שנוכל להשתמש בה בצורה נכונה:

  1. יש לחבר את הפונקציה לאירוע חיצוני שיטריג אותה. בקוד הדוגמה היא מופעלת דרך API Gateway בלי אותנטיקציה, כך שכל אחד מכל האינטרנט יכול להפעיל את הפונקציה. צריך לעדכן את זה כדי שהיא תופעל או מתוך אירוע שמגיע מתור או לפחות בצורה מוגנת דרך API Gateway כך שרק השרת שלי יוכל להטריג את ההפעלה.

  2. הפונקציה משתמשת בקובץ .env בתיקייה המקומית כדי להגדיר ערכים ל API Keys שהיא צריכה. ל AWS יש מנגנון יותר מאובטח בשם Secrets Manager. בעולם האמיתי נרצה להשתמש בו כדי שלא יגנבו לנו את מפתחות ה API.