使用 Headless Chrome 运行 Selenium 的 AWS Lambda 容器可以在本地运行,但不能在 AWS Lambda 中运行

Luk*_*ley 9 python selenium chromium docker aws-lambda

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

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

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

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

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

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

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

有可能这可能是问题所在,但我非常怀疑这是同一版本 - ChromeDriver 使用与 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" ]
Run Code Online (Sandbox Code Playgroud)

此图像始终可以正常构建并按预期创建我的图像。

和我的 docker-compose.yml文件:

version: "3.7"
services:
  lambda:
    image: tbg-lambda:latest
    build: .
    ports:
      - "8080:8080"
    env_file:
     - ./params.env
Run Code Online (Sandbox Code Playgroud)

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

curl -XPOST "http://localhost:8080/2015-03-31/functions/function/invocations" -d '{}'
Run Code Online (Sandbox Code Playgroud)

使用 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 
Run Code Online (Sandbox Code Playgroud)

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

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
    )
Run Code Online (Sandbox Code Playgroud)

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 
Run Code Online (Sandbox Code Playgroud)

这是完整的 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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

提前致谢。

小智 5

Python v3.6 很好用。我有一个bin目录chromedriver v2.41https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip)和headless-chrome v68.0.3440.84https://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" ]
Run Code Online (Sandbox Code Playgroud)

在我的 python 脚本中,我将在获得许可的情况下将bin目录 (Docker Container) 中的文件复制到/tmp/bin目录 (Amazon Linux 2),775因为这tmp是我们可以在 Amazon linux 2 中写入文件的唯一目录,因为这里将执行 lambda。

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" ]
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)


小智 5

Sandeep Kumar 的解决方案有效(已投票,但由于我是新用户而不起作用)。

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

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

  2. 要求.txt

selenium==3.14.0
Run Code Online (Sandbox Code Playgroud)
  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" ]
Run Code Online (Sandbox Code Playgroud)
  1. 应用程序.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()
Run Code Online (Sandbox Code Playgroud)
  1. 在AWS lambda中拥有默认的IAM权限就足够了

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


umi*_*ico 5

我可以在 AWS Lambda 中运行 3.7 和 3.8。您需要安装特定版本。我还不知道如何运行最新的 chrome tho。请访问我的存储库。

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

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

  • Python 3.8(需要在docker镜像容器中安装依赖)
  • serverless-chrome v1.0.0-37
  • 铬驱动程序 2.37
  • 硒 3.141.0(最新)

  • 我仍然不知道到底为什么,但这个答案确实有效。没有废话。它直接工作。 (2认同)
  • 该解决方案运行良好并且保持最新。有许多移动部件可能会导致回归或可能需要更改所使用的设置,这就是为什么拥有最新的 DockerFile 和测试示例至关重要。谢谢美希科! (2认同)