「AWS無料相談会」をオンラインで開催中

Lambda上でwgetコマンドを実行できるか?

エンジニアの内山です。今回は、Lambda上でwgetコマンドを入れるときに色々と手間取ったので、その知見を共有させていただきます。

Lambdaにはwgetコマンドがない

Lambdaには、デフォルトでwgetコマンドはインストールされていません。そのため、Lambda上にwgetバイナリファイルをアップロードする必要があります。

しかし、どこかのサーバにインストールされているwgetバイナリファイルを、単純にアップロードして実行すると、以下のようなエラーが出てしまいます。

/var/task/handlers/bin/wget: 
error while loading shared libraries: 
libgnutls.so.28: cannot open shared object file:
 No such file or directory

このエラーは、wgetが依存しているShared Objectが存在しないために起こります。そのため、関連するShared ObjectもLambdaにアップロードする必要があります。

今回は、以下のような手順で、wgetコマンドをLambda上で、実行できるようにしました。

  • Dockerコンテナ上でwgetをソースからビルド
  • Dockerコンテナ上からwgetバイナリファイルをコピーして、Lambdaにアップロード
  • Dockerコンテナ上からwgetが依存しているShared Objectをコピーして、Lambda Layerにアップロード
  • Lambda関数にLambda Layerを関連付ける

以下、それぞれの詳細を紹介していきます。

Dockerコンテナ上でwgetをソースからビルド

以下のDockerfileを作り、wgetをDockerコンテナ上でビルドします。

FROM lambci/lambda:build-python3.8

ENV VERSION wget-1.18

RUN yum install -y tar gzip gcc make gnutls-devel pkgconfig

RUN curl -o ${VERSION}.tar.gz ftp.gnu.org/pub/gnu/wget/${VERSION}.tar.gz && 
    tar xzvf ${VERSION}.tar.gz && 
    cd ${VERSION} && 
    ./configure --with-gnu-ld  --disable-pcre --disable-threads --disable-ipv6 --disable-ntlm && 
    make && 
    mv src/wget /

今回はPythonからwgetコマンドを実行する形にしているため、Dockerイメージはlambda:build-pythonを使用しています。

wgetをソースからビルドしているのは、不要な機能はオフにして、依存するShared Objectを最小限にするためです。

ビルドは、以下のように実行します。

$ docker build -t wget:1 .

Dockerコンテナ上からwgetバイナリファイルをコピーして、Lambdaにアップロード

ビルドが終了したら、以下のように、Lambdaに上げる用のディレクトリを作成し、ホスト側にwgetバイナリファイルをコピーしてきます。

$ mkdir handlers/bin
$ docker cp $(docker run -d  wget:1 bash):/wget handlers/bin

また、wgetコマンドを実行するPythonスクリプトを以下のように実装しました。

import subprocess
import tempfile
import os

basedir=os.path.dirname(__file__)

def lambda_handler(event, context):
    with tempfile.TemporaryDirectory() as dir_name:
        wget(dir_name, 'https://example.com/')


def wget(dir_name, url: str) -> str:
    cmd = '{}/bin/wget -k  -r 1 -P {} {}'.format(basedir, dir_name, url)

    result = subprocess.run(
        cmd.split(" "),
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT
    )
    print(result.stdout.decode())

    return dir_name
$ zip -r main.zip handlers

main.zipは、Lambdaにアップロードします。

Dockerコンテナ上からwgetが依存しているShared Objectをコピーして、Lambda Layerにアップロード

今回は以下のShared Objectが必要でした。

  • /lib64/libgnutls.so.28
  • /lib64/libnettle.so.4
  • /lib64/libhogweed.so.2
  • /lib64/libgmp.so.10

これらすべてがシンボリックリンクなので注意してください。

Lambda Layer用のディレクトリを作成し、必要なShared ObjectをDockerコンテナからコピーしてきます。

$ mkdir lib
$ docker cp $(docker run -d  wget:1 bash):/lib64/libgnutls.so.28.43.3 lib/libgnutls.so.28
$ docker cp $(docker run -d  wget:1 bash):/lib64/libnettle.so.4.7 lib/libnettle.so.4
$ docker cp $(docker run -d  wget:1 bash):/lib64/libhogweed.so.2.5 lib/libhogweed.so.2
$ docker cp $(docker run -d  wget:1 bash):/lib64/libgmp.so.10.2.0 lib/libgmp.so.10

Lambdaの実行環境によっては、必要になってくるShared Objectが違うかもしれないので、適宜必要なものをコピーしてください。

コピーしたものは、zipで固めます。

$ zip layer.zip lib/*

layer.zipをレイヤーにアップロードします。今回は、Python3.8ランタイムで実行する想定です。

Lambda関数にLambda Layerを関連付ける

以下の画面で、レイヤーをLambda関数に関連付けます。

Lambdaを実行して、wgetコマンドが実行できていることを確認

Lambdaを試しに実行すると、以下のようなメッセージが出力され、wgetコマンドを実行できていることを確認できると思います。

--2020-08-07 04:56:56--  https://example.com/
Resolving example.com... 93.184.216.34
Connecting to example.com|93.184.216.34|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1256 (1.2K) [text/html]
Saving to: ‘/tmp/tmpnpvavroa/example.com/index.html’
     0K .                                                     100% 59.1M=0s

おわりに

色々と手間取りましたが、Lambda上でwgetコマンドを実行できるようになりました。
参考になれば幸いです。