AWS Lambda(Go)でHTMLのスクリーンショットを撮影して保存する【provided.al2023編】

こんにちは、証券フロンティア事業部の西永です。
普段はPHPでAPI開発をしたりGoでバックエンドをやっています。
前回同記事名でLambda上からChromeを起動してスクリーンショットを撮る方法を紹介しました。 その際はGo1.xマネージドランタイムで動かす方法を紹介していましたが、時が経ちGo1.x環境が廃止されてしまいました…

Lambda の Go 1.x マネージドランタイムは非奨励になりました。Go 1.x ランタイムを使用する関数がある場合は、関数を provided.al2023 または provided.al2 に移行する必要があります。 https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-golang.html

今後はprovided.al2またはprovided.al2023環境でGoを動かす必要があります。
Go1.xのコンテナを用意してLambdaをコンテナで動かす方法もありますが、今回はprovided.al2023環境で動かす方法を紹介します。

本題

コード

まずはコードの方から。
今回ライブラリはchromedp(chromedp/chromedp)を使用しています。
こちらを使用するとchromedriverが不要になります。
他言語の場合はchromedriverをダウンロードして使用すれば同様のことが出来るかと思います。

package main

import (
    "context"
    "encoding/base64"
    "log"
    "os"
    "os/exec"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/chromedp/chromedp"
    "github.com/codeclysm/extract"
)

func handler(_ context.Context) (string, error) {
    // レイヤーは「/opt/」の中にある
    // Lambda上では「/tmp/」でしか自由にディレクトリの操作が行えない

    // レイヤーを結合させてtar.gzを展開
    if _, err := exec.Command("sh", "-c", "cat /opt/chromium.tar.gz.* > /tmp/chromium.tar.gz").Output(); err != nil {
        return "", err
    }

    file, err := os.Open("/tmp/chromium.tar.gz")
    if err != nil {
        return "", err
    }
    defer file.Close()

    if err := extract.Gz(context.Background(), file, "/tmp/", nil); err != nil {
        return "", err
    }

    os.Setenv("FONTCONFIG_PATH", "/tmp/fonts")
    os.Setenv("LD_LIBRARY_PATH", "/tmp/lib")

    // Chromium起動
    opts := []chromedp.ExecAllocatorOption{
        chromedp.Headless,
        chromedp.NoSandbox,
        chromedp.Flag("disable-setuid-sandbox", true),
        chromedp.Flag("disable-dev-shm-usage", true),
        chromedp.Flag("single-process", true),
        chromedp.Flag("no-zygote", true),
        // ウィンドウサイズを指定したい場合
        // chromedp.WindowSize(width, height),
        chromedp.ExecPath("/tmp/chromium"),
    }
    ctx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
    defer cancel()

    ctx, cancel = chromedp.NewContext(ctx, chromedp.WithErrorf(log.Printf))
    defer cancel()

    var imageBuf []byte
    err = chromedp.Run(ctx, chromedp.Tasks{
        // ローカルのHTMLを開く場合は「file::///○○.html」等で指定
        chromedp.Navigate("https://www.tecotec.co.jp/"),
        chromedp.FullScreenshot(&imageBuf, 90),
    })

    // base64で出力(S3に保存する場合はここから書き換えてください)
    return "data:image/png;base64," + base64.StdEncoding.EncodeToString(imageBuf), err
}

func main() {
    lambda.Start(handler)
}

コードを見れば分かるかと思いますが、今回はLambdaレイヤーで用意しているchromium.tar.gz.XXが肝になります。

では次にLambdaレイヤーを用意します。

レイヤー

1.brotliのインストール

# apt-get install brotli

等を行い、

# brotli --version
brotli 1.0.7

が出来るようになれば完了です。
こちらはこの後実行するShellでbrファイルを展開するために使用します。

2.フォントのダウンロード

今回は前回と同じくNoto Sans Japaneseを使用します。
ダウンロード&展開してNotoSansJP-Regular.ttfを用意します。

3.Shellの用意

下記ShellをNotoSansJP-Regular.ttfと同じ階層に用意します。
Shell名は何でもよいです。今回はchromium.shとします。

#!/bin/bash

# 自身のshellの位置に移動
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"

# ディレクトリの作成&移動
mkdir -p tmp
cd tmp

# ダウンロードするファイルのURL
DOWNLOAD_URL="https://github.com/Sparticuz/chromium/releases/download/v131.0.1/chromium-v131.0.1-pack.tar"

# ダウンロード先のファイル名
OUTPUT_FILE="chromium-v131.0.1-pack.tar"

# ダウンロードコマンドを実行
curl -o $OUTPUT_FILE -L $DOWNLOAD_URL

# ダウンロードしたファイルを展開
tar -xvf $OUTPUT_FILE

# ダウンロードしたファイルを削除
rm $OUTPUT_FILE

# brotliファイルを展開
# 展開時点では5つファイルが生成されるが
# - al2.tar.brはAmazonLinux2023環境では不要(AmazonLinux2環境で必要)
# - fonts.tar.brは日本語に対応してないのと同階層にフォントを予め用意しているので不要
brotli -d al2023.tar.br
brotli -d chromium.br
brotli -d swiftshader.tar.br

# 展開したファイルを削除
rm *.br

# 権限追加
chmod +x chromium

# tarファイルを展開
tar -xvf al2023.tar
tar -xvf swiftshader.tar

# tarディレクトリを削除
rm *.tar

# fontsディレクトリの作成
mkdir -p fonts

# ディレクトリ移動
cd fonts

# 再度fontsディレクトリの作成
mkdir -p fonts

XML_CONTENT='<?xml version="1.0" ?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
  <dir>/var/task/.fonts</dir>
  <dir>/var/task/fonts</dir>
  <dir>/opt/fonts</dir>
  <dir>/tmp/fonts</dir>
  <cachedir>/tmp/fonts-cache/</cachedir>
  <config></config>
</fontconfig>'

# fonts.confの作成
echo "$XML_CONTENT" > fonts.conf

# 予め用意しておいたフォントをコピー
cp -r ../../NotoSansJP-Regular.ttf fonts

# ディレクトリ位置を戻す
cd ..

# tar.gzファイルを作成
tar -zcvf ../chromium.tar.gz *

# ディレクトリ位置を戻す
cd ..

# tmpディレクトリの削除
rm -rf tmp

# 分割
split -b 45m -d chromium.tar.gz chromium.tar.gz.

# chromium.tar.gzの削除
rm -rf chromium.tar.gz

# zipファイルの作成と元ファイルの削除
for file in chromium.tar.gz.*; do
  zip -r $file.zip $file
  rm $file
done

今回はSparticuz/chromiumを使用しています。
こちらはLambda上でNodeJSからChroimiumを動かすライブラリですが、必要なものだけ引き抜いてGoに応用しています。*1
これでディレクトリ内にフォント(NotoSansJP-Regular.ttf)とchromium.sh2つが用意された状態になるかと思います。

chromium.sh置いた直後のディレクトリ内

4.Shellの実行

# bash chromium.sh

でchromium.shを実行します。

実行途中のログ
すると、新たにchromium.tar.gz.00.zipとchromium.tar.gz.01.zipができます。
これらがレイヤーになります。2つに分かれているのは1つにまとめると50MBを超えるためです。 *2
2つに分けることでS3に上げる手間を省いてます。 *3

chromium.sh実行後ディレクトリ内

これでレイヤーの作成は完了です。

AWSへ反映

では実際にLambdaに反映していきます。
まず「Lambda > レイヤー > レイヤーの作成」より、先程作成したchromium.tar.gz.00.zipとchromium.tar.gz.01.zipをそれぞれアップロードします。
オプションは画像の通り。

レイヤーのアップロード

そしてLambdaを作成します。

Lambdaの作成

次にLambdaのアップロードを行います。
コンパイル&Zipの作成方法については公式のものがありますので説明は割愛します。

Lambdaのアップロード

今度はレイヤーの設定を行います。
「Lambda > レイヤー > レイヤーを追加」より、上で追加したレイヤーをそれぞれ両方とも設定します。

レイヤーの設定

最後に、「設定 > 一般設定」よりメモリのサイズを1024MB以上に変更&タイムアウト時間も伸ばしてください。

メモリ量を上げないとメモリ不足に陥る

では実際にLambdaを実行してみます。
「テスト > テストイベント」よりサンプルそのままで実行できます。

実行結果

無事出力に成功しました。
出力の最初と最後のダブルクォーテーションを省いた部分をブラウザのURLに貼り付ければスクショ結果が確認できます。

ウィンドウサイズを指定しなかったのでデフォルトで縦長になっている

おわりに

provided.al2023環境(Amazon Linux 2023)でLambda上からブラウザを動かす方法を紹介しましたがいかがでしたでしょうか。
今回はGoで行いましたが他言語にも応用できるかと思いますので、機会があればお試しください。

Amazon Web Servicesおよびかかる資料で使用されるその他の AWS 商標 は、米国および/またはその他の諸国における、Amazon.com, Inc. またはその関連会社の商標です。

テコテックの採用活動について

テコテックでは新卒採用、中途採用共に積極的に募集をしています。 採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。 ご興味を持っていただけましたら是非ご覧ください。 tecotec.co.jp

*1:Chromiumのバージョンもv131と最新です

*2:Chromium単体でも170MB位あります

*3:S3に上げる場合、AWS接続にIP制限を課している環境だとaws:ViaAWSServiceで許可していないとアップロードできないので注意