PHP7.3環境のCustom Runtimeを作ってAWS Lambdaを動かしてみた

こんにちは、エンジニアの @akase244 です。

LambdaでPHPが使えたらいいのに。と思ってたらre:Invent 2018でLambdaのCustom Runtimeという機能が発表されました。(というネタで既に多くのブログ記事が公開されていますがお付き合いいただけると幸いです)

aws.amazon.com

上記のブログでは、すでに各言語に応じたCustom RuntimeがAWS社やAWSのパートナー企業により提供されていることについて触れられており、PHPのCustom RuntimeはStackery社が提供しているものを利用できます。

しかし、Stackery社が提供している最新のCustom RuntimeのPHPのバージョンを確認すると7.1となっています。

PHP7.1はEnd of lifeにはまだなってませんがActive supportの期間が2018年12月1日に終了しているため、今回はせっかくなのでPHPの最新安定版である7.3.1のCustom Runtimeを作成してAWS Lambdaを動かしてみようと思います。

AWS Lambdaの大まかな仕組み

AWS Lambdaをこれまで一度も使ったことがなかったので仕組みがよくわかっていなかったのですが、Lambdaを利用する場合は各言語(標準でサポートされているPythonやGoなど)に応じたランタイムを選択して、関数を準備することで動作するようです。

f:id:akase244:20190203110219p:plain:w500
関数作成時にランタイムを選択する

今回のPHPのように標準サポート以外の言語を利用する場合はランタイムインターフェイスに基づいたカスタムランタイムを作成し、レイヤーとしてアップロードします。

アップロードしたレイヤーは、Lambdaの実行環境(後述)の「/opt」ディレクトリ配下に配置されます。 Stackery社のbootstrapファイル(カスタムランタイムを構成するファイルでLambda実行時に呼び出される)を参考に見てみると、「/opt/〜」という記述がちらほら登場するのはこういった理由です。

f:id:akase244:20190203111527p:plain:w400
レイヤーの作成画面でカスタムランタイムをアップロードする

Lambdaの実行環境のOSは「Amazon Linux(amzn-ami-hvm-2017.03.1.20170812-x86_64-gp2)」が採用されているようで、この環境上で関数の実行が行われます。(2019年2月3日時点の情報)

PHPをビルドする環境について

最初に参考にして読んでいた「Serverless Anything: Using AWS Lambda Layers to build custom runtimes」という記事では、Amazon Linux AMI 2018.03 Release Notesの中から「amzn-ami-hvm-.*-gp2」に該当するAMIを探してEC2インスタンスを作成してPHPのビルドを行ったとのこと。Lambdaの実行環境は先述したとおりAmazon Linuxですので、実行環境と同等環境でビルドするのは理にかなってそうです
ただし、文中に「Don’t forget to terminate your large EC2 instance」との警告が書いてあり、ビルドのためにわざわざインスタンスを作るのは面倒そうだなぁという感想を持ちました。

そんな時はStackery社を参考に。。。と該当のMakefileを見てみるとビルドはDockerを利用しており、Dockerイメージとして「lambci/lambda:build-nodejs8.10」が指定されています。Docker Hubには「Images that (very closely) mimic the live AWS Lambda environment」という説明があり、Lambdaの実行環境と近しい環境を提供してくれるようです。また、GitHubを見てみるとgcc-c++、autoconfなどPHPのビルド時に必要なパッケージ群が事前に入っていることもわかります。

色々と調べているうちにAWSパートナーのブログ記事を見つけ、このページを参考にEC2インスタンスを作成するのではなく「amazonlinux:2017.03.1.20170812」のDockerイメージを利用することに決めました。

ビルド環境についてはやり方が色々あると思うので、自分の好みで決めちゃってよいと思います。

ビルド実行時の注意点について

カスタムランタイムを作成するスクリプトについては以下のリポジトリに上げてます。

github.com

今回、PHPのビルドを行った際にいくつかつまづいたので解説しておきます。

PHPをyumコマンドでインストールしていない理由

以下の実行結果のとおり、PHP7.3系はまだyumではインストールできないためソースファイルからインストール行っています。

$ docker container run amazonlinux:2017.03.1.20170812 yum search php7 |grep 'p7.\.x86'
php70.x86_64 : PHP scripting language for creating dynamic web sites
php71.x86_64 : PHP scripting language for creating dynamic web sites
php72.x86_64 : PHP scripting language for creating dynamic web sites

コンパイルオプション「--without-pear」について

ご存知の方もいらっしゃるかと思いますが、現在、PEARが利用できない状態となっています

PEARのサイトにhttpでアクセスすると「PEAR server is down」と表示され、httpsでアクセスすると証明書のエラーが発生するような状況です。PHPをコンパイルしようとすると「--with-pear」がデフォルトのようで、そのまま実行すると以下のエラーが発生します。この問題を回避するために、コンパイルオプションに「--without-pear」を追加しています。

Warning: fopen(): Unable to find the wrapper "https" - did you forget to enable it when you configured PHP? in /tmp/php-src-php-7.3.1/pear/fetch.php on line 66

Warning: fopen(https://pear.php.net/install-pear-nozlib.phar): failed to open stream: No such file or directory in /tmp/php-src-php-7.3.1/pear/fetch.php on line 66

Error..
fopen(https://pear.php.net/install-pear-nozlib.phar): failed to open stream: No such file or directory
make: *** [install-pear] Error 1

コンパイルオプション「--with-openssl」について

今回の検証では「--with-openssl」は本来不要だったのですが、もし関数実行時にOpenSSLの機能を利用する際にはビルド時に注意しておく点があります。
通常、Amazon Linuxの環境でPHPコンパイル時に「--with-openssl」を指定する場合は、事前にopenssl-develのパッケージをインストールしておくことでうまくいきます。
今回利用しているDockerイメージである「amazonlinux:2017.03.1.20170812」の環境で「yum install openssl-devel」を実行すると関連して「openssl-1.0.2k-16.146.amzn1.x86_64」がインストールされるのですが、この状態でLambdaを実行すると、以下のようなエラーログが出力されます。

/opt/bin/php: /lib64/libcrypto.so.10: version `OPENSSL_1.0.2' not found (required by /opt/bin/php)

「openssl-1.0.1k.tar.gz」のソースファイルを元にインストールを行い、PHPのコンパイルオプションとして「--with-openssl=/usr/local/ssl」を指定している理由は、以下のコマンドの結果のとおり「amazonlinux:2017.03.1.20170812」の環境ではOpenSSL 1.0.1kが導入されているためです。

$ docker container run amazonlinux:2017.03.1.20170812 openssl version
OpenSSL 1.0.1k-fips 8 Jan 2015

ZIPファイルの構成と実行環境の配置場所について

カスタムランタイム(runtime.zip)のファイル構成(アップロードするとLambda実行環境の「/opt」配下に配置されます)

├── bin
│   └── php
└── bootstrap

関数(function.zip)のファイル構成(アップロードするとLambda実行環境の「/var/task」配下に配置されます)

└── src
    └── function.php

ハンドラの指定について

bootstrapファイルはStackery社のものをほぼそのまま流用させてもらったのですが、注目すべきは以下の箇所です。

<?php
$HANDLER = getenv('_HANDLER');
$handler_components = explode('/', $HANDLER);
$handler_filename = array_pop($handler_components);
$handler_path = implode('/', array_merge(['/var/task'], $handler_components));
chdir($handler_path);
exec("php -S localhost:8000 '$handler_filename'");

「getenv('_HANDLER')」の値はLambdaの画面の「ハンドラ」と対応していますので、今回は「src/function.php」と入力しました。

f:id:akase244:20190204015813p:plain:w500
Lambdaの関数コード入力画面

これにより、$handler_pathには「/var/task/src」、$handler_filenameには「function.php」という値がセットされ正しく動作します。

AWS LambdaでPHPを動かしてみる

レイヤーを作成

Lambdaのレイヤー作成画面で必要な項目を入力し、アップロードボタンをクリックして作成済みのカスタムランタイムのZIPファイルを選択します。

f:id:akase244:20190205150629p:plain:w400
レイヤー作成画面

レイヤーが作成できたらARNの値を控えておきます。

f:id:akase244:20190205151237p:plain:w500
レイヤー作成完了時

関数を作成

Lambdaの関数作成画面で「名前」を入力し、「ランタイム」から「関数コードまたはレイヤーでカスタムランタイムを使用」を選択します。 Lambda用の「ロール」が作成済みであれば「既存のロールを選択」を選んでロールを指定してください。

f:id:akase244:20190205151909p:plain:w500
関数作成画面

未作成であれば「カスタムロールの作成」を選択すると、IAMの画面が開きますので「許可」をクリックします。

f:id:akase244:20190205152638p:plain:w500
IAMロール作成画面

すべての入力が終わったら「関数の作成」ボタンをクリックします。

f:id:akase244:20190205153151p:plain:w500
関数作成画面で入力内容が揃った状態

Lambdaの編集画面が開き、初期表示では「関数名(λ phpinfo)」が選択されている状態になっていますので、「Layers (0)」をクリックします。

f:id:akase244:20190205153637p:plain
初期表示時

f:id:akase244:20190205153659p:plain
Layersを選択

レイヤー追加画面で控えておいたARNを入力します。

f:id:akase244:20190205154732p:plain:w400
レイヤー追加画面

Lambdaの編集画面にレイヤーが追加されました。

f:id:akase244:20190205154931p:plain:w500
レイヤーが追加された状態

「関数名(λ phpinfo)」をクリックして関数コードの入力画面を表示します。「コード エントリ タイプ」を「.zipファイルをアップロード」に変更し、作成済み関数ファイルを選択します。ハンドラの値はZIPファイル内の階層構造とbootstrapファイルの_HANDLERの読み込みを考慮しつつ入力します。

f:id:akase244:20190205155623p:plain:w500
関数コード入力画面

ここまで入力が終わったら右上の「保存」ボタンを押しましょう。保存が終わったら関数コードの画面でアップロードしたファイルの中身が確認できるはずです。

f:id:akase244:20190205160010p:plain:w500
アップロードした関数ファイルの中身が確認できた

次はAPI Gatewayの画面で作業を行います。

API GatewayでAPIを作成

APIの作成画面でこのように入力します。

f:id:akase244:20190205145512p:plain:w500
API GatewayのAPI作成画面

「アクション」から「リソースの作成」をクリックします。

f:id:akase244:20190205145836p:plain:w300
アクション → リソースの作成

「プロキシリソースとして設定」にチェックを入れてリソースの作成を行います。

f:id:akase244:20190205150134p:plain:w500
リソース作成画面

「Lambda 関数」に作成済みの関数名を入力します。

f:id:akase244:20190205160933p:plain:w500
リソース画面

警告が表示されるのでOKをクリックします。

f:id:akase244:20190205161127p:plain:w500
権限に関する警告

「アクション」から「APIのデプロイ」をクリックします。

f:id:akase244:20190205161223p:plain
アクション → APIのデプロイ

このように入力し「デプロイ」をクリックします。

f:id:akase244:20190205161326p:plain:w400
デプロイ画面

次はLambdaの画面で作業を行います。

トリガーの設定

Lambdaの画面で「トリガーの追加」から「API Gateway」を選択します。

f:id:akase244:20190205161720p:plain:w500
トリガーの追加

「トリガーの設定」にAPI Gatewayで作成した内容を入力して、「追加」をクリックします。

f:id:akase244:20190205161845p:plain:w500
トリガーの設定

f:id:akase244:20190205171529p:plain:w400
トリガーが追加された状態

右上の「保存」ボタンを押すと「API エンドポイント」が表示されます。

f:id:akase244:20190205162953p:plain:w500
APIのエンドポイントが表示される

APIのエンドポイントにアクセスしてみる

見れた!!!!!

f:id:akase244:20190205174326p:plain:w500
phpinfoが表示された

まとめ

AWS LambdaとAPI Gatewayはまったく触れたことがなかったので、つまづくことが多かったですがなんとか動くところまで到達できたので達成感がありました。

今回はStackery社のbootstrapファイルをほぼそのまま流用する形で実現しましたが、ランタイムインターフェイスの理解を深めて作り変えにチャレンジしてみたいと思っています。

あと、正直なところマネジメントコンソールの画面からの操作はとても面倒なので、awsコマンドでやったほうがスッキリすると思います。 (違うんや。前回の記事のとおりコマンドの導入自体は終わってたんやが、Python2系にしてもPython3系にしても自分の環境でaws lambdaコマンドがうまいこと動いてくれなくて。。。)