【问题标题】:AWS Lambda Container Running Selenium With Headless Chrome Works Locally But Not In AWS Lambda使用 Headless Chrome 运行 Selenium 的 AWS Lambda 容器可以在本地工作,但不能在 AWS Lambda 中运行
【发布时间】:2021-04-02 09:46:21
【问题描述】:

我目前正在开发一个 Python 程序,该程序有一段使用 Chrome 和 Selenium 的无头版本来执行重复过程。我的目标是在 Lambda 上运行程序。

整个程序有大约 1GB 的依赖项,因此不能选择使用 .zip archive 的标准方法,包含我所有的函数代码和依赖项,因为函数和所有层的总解压缩大小可以' t 超过 250 MB 的解压缩部署包大小限制。

所以,这就是新的AWS Lambda – Container Image Support(我使用这个链接的教程来开发整个实现,所以如果您需要更多信息,请阅读)进来。这允许我将我的 Lambda 函数打包和部署为容器映像最大 10 GB。

我正在使用由 AWS 提供的 ECR Public 中托管的基本映像,它运行 Amazon Linux 2。首先 - 在我的 Dockerfile I 中:

  • 下载基础镜像。
  • 定义一些全局变量。
  • 复制我的文件。
  • 安装我的 pip 附录
  • 使用 yum 安装一些包。

最后 - 我安装了 Chrome(阅读时为 87.0.4280.88)和 Chromedriver(87.0.4280.88)

  • 最后下载安装最新版本的 Chrome 和 Chromedriver

这可能是问题所在,但我非常怀疑这两者是相同的版本 - ChromeDriver uses the same version number scheme as Chrome

这是我的 Dockerfile

# 1) DOWNLOAD BASE IMAGE.
FROM public.ecr.aws/lambda/python:3.8

# 2) DEFINE GLOBAL ARGS.
ARG MAIN_FILE="main.py"
ARG ENV_FILE="params.env"
ARG REQUIREMENTS_FILE="requirements.txt"
ARG FUNCTION_ROOT="."
ARG RUNTIME_VERSION="3.8"

# 3) COPY FILES.
# Copy The Main .py File.
COPY ${MAIN_FILE} ${LAMBDA_TASK_ROOT}
# Copy The .env File.
COPY ${ENV_FILE} ${LAMBDA_TASK_ROOT}
# Copy The requirements.txt File.
COPY ${REQUIREMENTS_FILE} ${LAMBDA_TASK_ROOT}
# Copy Helpers Folder.
COPY helpers/ ${LAMBDA_TASK_ROOT}/helpers/
# Copy Private Folder.
COPY priv/ ${LAMBDA_TASK_ROOT}/priv/
# Copy Source Data Folder.
COPY source_data/ ${LAMBDA_TASK_ROOT}/source_data/

# 4) INSTALL DEPENDENCIES.
RUN --mount=type=cache,target=/root/.cache/pip python3.8 -m pip install --upgrade pip
RUN --mount=type=cache,target=/root/.cache/pip python3.8 -m pip install wheel
RUN --mount=type=cache,target=/root/.cache/pip python3.8 -m pip install urllib3
RUN --mount=type=cache,target=/root/.cache/pip python3.8 -m pip install -r requirements.txt --default-timeout=100

# 5) DOWNLOAD & INSTALL CHROMEIUM + CHROMEDRIVER.
#RUN yum -y upgrade
RUN yum -y install wget unzip libX11 nano wget unzip xorg-x11-xauth xclock xterm

# Install Chrome
RUN wget https://intoli.com/install-google-chrome.sh
RUN bash install-google-chrome.sh

# Install Chromedriver
RUN wget https://chromedriver.storage.googleapis.com/87.0.4280.88/chromedriver_linux64.zip
RUN unzip ./chromedriver_linux64.zip
RUN rm ./chromedriver_linux64.zip
RUN mv -f ./chromedriver /usr/local/bin/chromedriver
RUN chmod 755 /usr/local/bin/chromedriver

# 5) SET CMD OF HANDLER.
CMD [ "main.lambda_handler" ]

此图像始终可以毫无问题地构建并按预期创建我的图像。

还有我的 docker-compose.yml 文件:

version: "3.7"
services:
  lambda:
    image: tbg-lambda:latest
    build: .
    ports:
      - "8080:8080"
    env_file:
     - ./params.env

所以 - 现在图像已构建,我可以使用 cURL 在本地进行测试。在这里,我传递了一个空的 JSON 有效负载:

curl -XPOST "http://localhost:8080/2015-03-31/functions/function/invocations" -d '{}'

使用 Chrome 无头模式完美运行整个程序,没有错误。

太棒了 - Docker 容器在本地运行,并且符合预期。

让我们将它上传到 ECR,以便我可以将它与我的 Lambda 函数一起使用(为安全起见已更改 ECR URL):

aws ecr create-repository --repository-name tbg-lambda:latest --image-scanning-configuration scanOnPush=true
docker tag tbg-lambda:latest 123412341234.dkr.ecr.sa-east-1.amazonaws.com/tbg-lambda:latest
aws ecr get-login-password | docker login --username AWS --password-stdin 123412341234.dkr.ecr.sa-east-1.amazonaws.com
docker push 123412341234.dkr.ecr.sa-east-1.amazonaws.com/tbg-lambda:latest 

一切都按预期进行 - 然后我创建新的 lambda 函数,选择“容器映像”作为函数选项,并为 IAM 角色附加我需要的所有权限:

我将内存设置为最大值只是为了确保这不是问题:

好的,让我们进入故障点:

我使用测试事件通过控制台调用函数:

一切都运行完美,直到它使用 Chrome 创建 webdriver 驱动程序的代码:

    options = Options()
    options.add_argument('--no-sandbox')
    options.add_argument('--headless')
    options.add_argument('--single-process')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--remote-debugging-port=9222')
    options.add_argument('--disable-infobars')
    driver = webdriver.Chrome(
        service_args=["--verbose", "--log-path={}".format(logPath)],
        executable_path=f"/usr/local/bin/chromedriver",
        options=options
    )

PS:logPath只是项目目录下的另一个文件夹——这里的日志输出和预期的一样,日志如下所示。

这是 Cloudwatch 日志中突出显示错误的部分:

Caught WebDriverException Error: unknown error: Chrome failed to start: crashed.

(unknown error: DevToolsActivePort file doesn't exist)

(The process started from chrome location /usr/bin/google-chrome is no longer running, so ChromeDriver is assuming that Chrome has crashed.)

END RequestId: 7c933bca-5f0d-4458-9529-db28da677444

REPORT RequestId: 7c933bca-5f0d-4458-9529-db28da677444 Duration: 59104.94 ms Billed Duration: 59105 ms Memory Size: 10240 MB Max Memory Used: 481 MB

RequestId: 7c933bca-5f0d-4458-9529-db28da677444 Error: Runtime exited with error: exit status 1 Runtime.ExitError 

这是完整的 Chromedriver 日志文件:

[1608748453.064][INFO]: Starting ChromeDriver 87.0.4280.88 (89e2380a3e36c3464b5dd1302349b1382549290d-refs/branch-heads/4280@{#1761}) on port 54581
[1608748453.064][INFO]: Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
[1608748453.064][INFO]: /dev/shm not writable, adding --disable-dev-shm-usage switch
[1608748453.679][SEVERE]: CreatePlatformSocket() failed: Address family not supported by protocol (97)
[1608748453.679][INFO]: listen on IPv6 failed with error ERR_ADDRESS_UNREACHABLE
[1608748454.432][INFO]: [13826d22c628514ca452d1f2949eb011] COMMAND InitSession {
   "capabilities": {
      "alwaysMatch": {
         "browserName": "chrome",
         "goog:chromeOptions": {
            "args": [ "--no-sandbox", "--headless", "--single-process", "--disable-dev-shm-usage" ],
            "extensions": [  ]
         },
         "platformName": "any"
      },
      "firstMatch": [ {

      } ]
   },
   "desiredCapabilities": {
      "browserName": "chrome",
      "goog:chromeOptions": {
         "args": [ "--no-sandbox", "--headless", "--single-process", "--disable-dev-shm-usage" ],
         "extensions": [  ]
      },
      "platform": "ANY",
      "version": ""
   }
}
[1608748454.433][INFO]: Populating Preferences file: {
   "alternate_error_pages": {
      "enabled": false
   },
   "autofill": {
      "enabled": false
   },
   "browser": {
      "check_default_browser": false
   },
   "distribution": {
      "import_bookmarks": false,
      "import_history": false,
      "import_search_engine": false,
      "make_chrome_default_for_user": false,
      "skip_first_run_ui": true
   },
   "dns_prefetching": {
      "enabled": false
   },
   "profile": {
      "content_settings": {
         "pattern_pairs": {
            "https://*,*": {
               "media-stream": {
                  "audio": "Default",
                  "video": "Default"
               }
            }
         }
      },
      "default_content_setting_values": {
         "geolocation": 1
      },
      "default_content_settings": {
         "geolocation": 1,
         "mouselock": 1,
         "notifications": 1,
         "popups": 1,
         "ppapi-broker": 1
      },
      "password_manager_enabled": false
   },
   "safebrowsing": {
      "enabled": false
   },
   "search": {
      "suggest_enabled": false
   },
   "translate": {
      "enabled": false
   }
}
[1608748454.433][INFO]: Populating Local State file: {
   "background_mode": {
      "enabled": false
   },
   "ssl": {
      "rev_checking": {
         "enabled": false
      }
   }
}
[1608748454.433][INFO]: Launching chrome: /usr/bin/google-chrome --disable-background-networking --disable-client-side-phishing-detection --disable-default-apps --disable-dev-shm-usage --disable-hang-monitor --disable-popup-blocking --disable-prompt-on-repost --disable-sync --enable-automation --enable-blink-features=ShadowDOMV0 --enable-logging --headless --log-level=0 --no-first-run --no-sandbox --no-service-autorun --password-store=basic --remote-debugging-port=0 --single-process --test-type=webdriver --use-mock-keychain --user-data-dir=/tmp/.com.google.Chrome.xgjs0h data:,
mkdir: cannot create directory ‘/.local’: Read-only file system
touch: cannot touch ‘/.local/share/applications/mimeapps.list’: No such file or directory
/usr/bin/google-chrome: line 45: /dev/fd/62: No such file or directory
/usr/bin/google-chrome: line 46: /dev/fd/62: No such file or directory
prctl(PR_SET_NO_NEW_PRIVS) failed
[1223/183429.578846:FATAL:zygote_communication_linux.cc(255)] Cannot communicate with zygote
Failed to generate minidump.[1608748469.769][INFO]: [13826d22c628514ca452d1f2949eb011] RESPONSE InitSession ERROR unknown error: Chrome failed to start: crashed.
  (unknown error: DevToolsActivePort file doesn't exist)
  (The process started from chrome location /usr/bin/google-chrome is no longer running, so ChromeDriver is assuming that Chrome has crashed.)
[1608748469.769][DEBUG]: Log type 'driver' lost 0 entries on destruction
[1608748469.769][DEBUG]: Log type 'browser' lost 0 entries on destruction

我可能认为问题在于 lambda 运行此容器的方式与我在本地运行它的方式。

很多人建议 NOT 不以 root 身份运行 chrome - 那么 Lambda 是否以 root 身份运行容器,这就是造成这种情况的原因吗?如果是这样,我如何告诉 Lambda 或 Docker 以非 root 用户身份运行代码。

这里提到:https://github.com/heroku/heroku-buildpack-google-chrome/issues/46#issuecomment-484562558

自从 AWS 宣布 lambda 容器以来,我一直在与这个错误作斗争,所以任何帮助都会很棒????如果我遗漏了什么,请询问更多信息!

提前致谢。

【问题讨论】:

  • 我正在阅读this 并以某种方式偶然发现了这个问题。可能该链接可以在某种程度上帮助调试
  • Java 遇到同样的问题

标签: python docker selenium aws-lambda chromium


【解决方案1】:

Python v3.6 效果很好。我有一个bin 目录,其中包含chromedriver v2.41 (https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip) 和headless-chrome v68.0.3440.84 (https://github.com/adieuadieu/serverless-chrome/releases/download/v1.0.0-53/stable-headless-chromium-amazonlinux-2017-03.zip)。

下面是我的 Dockerfile,我将 chromedriverheadless-chrome 从源 bin 目录复制到目标 bin 目录。有目的地bin目录的原因如下。

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

COPY app.py ${LAMBDA_TASK_ROOT}
COPY requirements.txt ${LAMBDA_TASK_ROOT}

RUN --mount=type=cache,target=/root/.cache/pip python3.6 -m pip install --upgrade pip
RUN --mount=type=cache,target=/root/.cache/pip python3.6 -m pip install -r requirements.txt

RUN mkdir bin

ADD bin bin/

CMD [ "app.handler" ]

在我的python脚本中,我将bin目录(Docker Container)中的文件复制到/tmp/bin目录(Amazon Linux 2)并具有775权限,因为tmp是我们可以写入文件的唯一目录在 Amazon linux 2 中,因为 lambda 将在此处执行。

BIN_DIR = "/tmp/bin"
CURR_BIN_DIR = os.getcwd() + "/bin"


def _init_bin(executable_name):
    if not os.path.exists(BIN_DIR):
        logger.info("Creating bin folder")
        os.makedirs(BIN_DIR)

    logger.info("Copying binaries for " + executable_name + " in /tmp/bin")

    currfile = os.path.join(CURR_BIN_DIR, executable_name)
    newfile = os.path.join(BIN_DIR, executable_name)

    shutil.copy2(currfile, newfile)

    logger.info("Giving new binaries permissions for lambda")

    os.chmod(newfile, 0o775)

handler 函数中,使用以下选项可以避免 chrome 驱动程序引发的少数异常。

def handler(event, context):

    _init_bin("headless-chromium")
    _init_bin("chromedriver")

    options = Options()

    options.add_argument("--headless")
    options.add_argument("--disable-gpu")
    options.add_argument("--no-sandbox")
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--disable-gpu-sandbox')
    options.add_argument("--single-process")
    options.add_argument('window-size=1920x1080')
    options.add_argument(
        '"user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36"')

    options.binary_location = "/tmp/bin/headless-chromium"
    browser = webdriver.Chrome(
        "/tmp/bin/chromedriver", options=options)

【讨论】:

    【解决方案2】:

    Sandeep Kumar 的解决方案有效(赞成,但由于我是新用户而无法正常工作)。

    这是在基于容器的 lambda 中运行 selenium 的最小设置。

    1. 下载并复制 Sandeep 提到的二进制文件(chromedriver v2.41 和 headless-chrome v68.0.3440.84)到 bin 文件夹中

    2. requirements.txt

    selenium==3.14.0
    
    1. Dockerfile(注意:python 3.8 不起作用)
    FROM public.ecr.aws/lambda/python:3.6
    
    COPY app.py ${LAMBDA_TASK_ROOT}
    COPY requirements.txt ${LAMBDA_TASK_ROOT}
    
    RUN pip install --upgrade pip
    RUN pip install -r requirements.txt
    
    RUN mkdir bin
    ADD bin /bin/
    
    RUN chmod 755 /bin/chromedriver
    
    CMD [ "app.handler" ]
    
    1. app.py
    from selenium import webdriver
    
    def handler(event, context):
        options = webdriver.ChromeOptions()
    
        options.add_argument("--headless")
        options.add_argument("--disable-gpu")
        options.add_argument("--no-sandbox")
        options.add_argument('--disable-dev-shm-usage')
        options.add_argument('--disable-gpu-sandbox')
        options.add_argument("--single-process")
        options.add_argument('window-size=1920x1080')
        options.add_argument(
            '"user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36"')
    
        options.binary_location = "/bin/headless-chromium"
        browser = webdriver.Chrome(
            executable_path="/bin/chromedriver", options=options)
    
        browser.get("https://feng.lu")
        print(browser.title)
    
        browser.quit()
    
    1. 在 AWS lambda 中拥有默认 IAM 权限就足够了

    注意:我没有像 Sandeep 那样将内容复制到 /tmp/bin 文件夹中,只是使用 bin 文件夹,并且我在 docker 文件中更新了 CHMOD 权限。

    【讨论】:

      【解决方案3】:

      我设法通过切换到 ubuntu 18.04 并安装 Chrome 需要运行的一些依赖项(参见步骤 #4、#5 和 #6)来解决此问题。

      这是我的 Dockerfile:

      # 1) DOWNLOAD BASE IMAGE.
      FROM public.ecr.aws/ubuntu/ubuntu:18.04_edge
      
      # 2) DEFINE GLOBAL ARGS.
      ARG MAIN_FILE="main.py"
      ARG ENV_FILE="params.env"
      ARG REQUIREMENTS_FILE="requirements.txt"
      ARG FUNCTION_ROOT="."
      ARG RUNTIME_VERSION="3.8"
      ARG LAMBDA_TASK_ROOT="/var/task"
      ARG LAMBDA_RUNTIME_DIR="/var/runtime"
      ENV LAMBDA_TASK_ROOT="/var/task"
      ENV LAMBDA_RUNTIME_DIR="/var/runtime"
      
      # 3) CREATE FUNCTION DIR
      WORKDIR ${LAMBDA_TASK_ROOT}
      
      # 4) COPY FILES.
      # Copy The Main .py File.
      COPY ${MAIN_FILE} ${LAMBDA_TASK_ROOT}
      # Copy The .env File.
      COPY ${ENV_FILE} ${LAMBDA_TASK_ROOT}
      # Copy The requirements.txt File.
      COPY ${REQUIREMENTS_FILE} ${LAMBDA_TASK_ROOT}
      # Copy Helpers Folder.
      COPY helpers/ ${LAMBDA_TASK_ROOT}/helpers/
      # Copy Private Folder.
      COPY priv/ ${LAMBDA_TASK_ROOT}/priv/
      # Copy Source Data Folder.
      COPY source_data/ ${LAMBDA_TASK_ROOT}/source_data/
      
      # 5) DOWNLOAD & INSTALL CHROMEIUM + CHROMEDRIVER.
      RUN apt-get update
      RUN apt-get install -y software-properties-common
      RUN add-apt-repository ppa:deadsnakes/ppa
      RUN apt-get install -y wget \
        unzip \
        libx11-dev \
        nano \
        g++ \
        make \
        cmake \
        unzip \
        python3.7 \
        python3-pip \
        libcurl4-openssl-dev \
        libfontconfig \
        xvfb \
        libnss3-dev \
        libosmesa6-dev \
        ffmpeg
      
      # 6) CREATE LINKS
      RUN ln -s /usr/bin/pip3 /usr/bin/pip
      RUN ln -s /usr/bin/python3.7 /usr/bin/python
      RUN ln -s /usr/lib/x86_64-linux-gnu/libOSMesa.so /usr/local/bin/libosmesa.so
      
      # 7) INSTALL DEPENDENCIES.
      RUN python -m pip install --upgrade pip
      RUN python -m pip install wheel
      RUN python -m pip install urllib3
      RUN python -m pip install awslambdaric
      RUN python -m pip install -r requirements.txt --default-timeout=100
      
      # 8) INSTALL CHROME
      RUN wget https://github.com/adieuadieu/serverless-chrome/releases/download/v1.0.0-37/stable-headless-chromium-amazonlinux-2017-03.zip
      RUN unzip ./stable-headless-chromium-amazonlinux-2017-03.zip
      RUN rm ./stable-headless-chromium-amazonlinux-2017-03.zip
      RUN mv -f ./headless-chromium /usr/local/bin/headless-chromium
      RUN chmod 755 /usr/local/bin/headless-chromium
      
      # 9) INSTALL CHROMEDRIVER
      RUN wget https://chromedriver.storage.googleapis.com/2.37/chromedriver_linux64.zip
      RUN unzip ./chromedriver_linux64.zip
      RUN rm ./chromedriver_linux64.zip
      RUN mv -f ./chromedriver /usr/local/bin/chromedriver
      RUN chmod 755 /usr/local/bin/chromedriver
      
      # 10) DEFINE ENV VARS
      ENV AWS_LAMBDA_RUNTIME_API=""
      ENV DISPLAY=":20"
      
      # 11) SET CMD OF HANDLER.
      ENTRYPOINT [ "/usr/bin/python", "-m", "awslambdaric" ]
      CMD [ "main.lambda_handler" ]
      

      还有我在 Python 中的 Selenium 设置:

      options = Options()
      options.binary_location = "/usr/local/bin/headless-chromium"
      options.add_argument("--headless")
      options.add_argument("--disable-gpu")
      options.add_argument("--window-size=1280x1696")
      options.add_argument("--disable-application-cache")
      options.add_argument("--disable-infobars")
      options.add_argument("--no-sandbox")
      options.add_argument("--hide-scrollbars")
      options.add_argument("--enable-logging")
      options.add_argument("--log-level=0")
      options.add_argument("--single-process")
      options.add_argument("--ignore-certificate-errors")
      options.add_argument("--homedir=/tmp")
      options.add_argument("--disable-gpu")
      driver = webdriver.Chrome(
          service_args=["--verbose", "--log-path={}".format(logPath)],
          executable_path=f"/usr/local/bin/chromedriver",
          options=options
      )
      

      感谢同时回答的其他人 - 我想最终切换回 Amazon Linux,因此这些回答会有所帮助。

      【讨论】:

      • 你能在本地测试吗?
      【解决方案4】:

      现在我们知道原生 chromium 可以在 lambda 上运行,并且您不再需要 serverless-chrome。在我的这个存储库中,最新的一个使用原生 chromium,但您也可以发现旧版本中正在使用 serverless-chrome。

      https://github.com/umihico/docker-selenium-lambda/

      到目前为止,我能找到的最新版本如下。

      • Python 3.9
      • 铬 89.0.4389.47
      • chromedriver 89.0.4389.23
      • 硒 4.0.0

      【讨论】:

      • 我仍然不知道确切的原因,但这个答案很有效。没有bs。它直接工作。
      • 这个解决方案运行良好,并且保持最新状态。有许多移动部件可能导致回归或可能需要更改所使用的设置,这就是为什么拥有最新的 DockerFile 和测试示例是关键。谢谢umihico!
      猜你喜欢
      • 2021-11-01
      • 2019-05-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多