こんにちは、エンジニアの @akase244 です。
LambdaでPHPが使えたらいいのに。と思ってたらre:Invent 2018でLambdaのCustom Runtimeという機能が発表されました。(というネタで既に多くのブログ記事が公開されていますがお付き合いいただけると幸いです)
上記のブログでは、すでに各言語に応じた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など)に応じたランタイムを選択して、関数を準備することで動作するようです。
今回のPHPのように標準サポート以外の言語を利用する場合はランタイムインターフェイスに基づいたカスタムランタイムを作成し、レイヤーとしてアップロードします。
アップロードしたレイヤーは、Lambdaの実行環境(後述)の「/opt」ディレクトリ配下に配置されます。 Stackery社のbootstrapファイル(カスタムランタイムを構成するファイルでLambda実行時に呼び出される)を参考に見てみると、「/opt/〜」という記述がちらほら登場するのはこういった理由です。
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イメージを利用することに決めました。
ビルド環境についてはやり方が色々あると思うので、自分の好みで決めちゃってよいと思います。
ビルド実行時の注意点について
カスタムランタイムを作成するスクリプトについては以下のリポジトリに上げてます。
今回、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が利用できない状態となっています。
A security breach has been found on the https://t.co/dwKlscDEFf webserver, with a tainted go-pear.phar discovered.
— PEAR (@pear) January 19, 2019
The PEAR website itself has been disabled until a known clean site can be rebuilt. A more detailed announcement will be on the PEAR Blog once it's back online.
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」と入力しました。
これにより、$handler_pathには「/var/task/src」、$handler_filenameには「function.php」という値がセットされ正しく動作します。
AWS LambdaでPHPを動かしてみる
レイヤーを作成
Lambdaのレイヤー作成画面で必要な項目を入力し、アップロードボタンをクリックして作成済みのカスタムランタイムのZIPファイルを選択します。
レイヤーが作成できたらARNの値を控えておきます。
関数を作成
Lambdaの関数作成画面で「名前」を入力し、「ランタイム」から「関数コードまたはレイヤーでカスタムランタイムを使用」を選択します。 Lambda用の「ロール」が作成済みであれば「既存のロールを選択」を選んでロールを指定してください。
未作成であれば「カスタムロールの作成」を選択すると、IAMの画面が開きますので「許可」をクリックします。
すべての入力が終わったら「関数の作成」ボタンをクリックします。
Lambdaの編集画面が開き、初期表示では「関数名(λ phpinfo)」が選択されている状態になっていますので、「Layers (0)」をクリックします。
レイヤー追加画面で控えておいたARNを入力します。
Lambdaの編集画面にレイヤーが追加されました。
「関数名(λ phpinfo)」をクリックして関数コードの入力画面を表示します。「コード エントリ タイプ」を「.zipファイルをアップロード」に変更し、作成済み関数ファイルを選択します。ハンドラの値はZIPファイル内の階層構造とbootstrapファイルの_HANDLERの読み込みを考慮しつつ入力します。
ここまで入力が終わったら右上の「保存」ボタンを押しましょう。保存が終わったら関数コードの画面でアップロードしたファイルの中身が確認できるはずです。
次はAPI Gatewayの画面で作業を行います。
API GatewayでAPIを作成
APIの作成画面でこのように入力します。
「アクション」から「リソースの作成」をクリックします。
「プロキシリソースとして設定」にチェックを入れてリソースの作成を行います。
「Lambda 関数」に作成済みの関数名を入力します。
警告が表示されるのでOKをクリックします。
「アクション」から「APIのデプロイ」をクリックします。
このように入力し「デプロイ」をクリックします。
次はLambdaの画面で作業を行います。
トリガーの設定
Lambdaの画面で「トリガーの追加」から「API Gateway」を選択します。
「トリガーの設定」にAPI Gatewayで作成した内容を入力して、「追加」をクリックします。
右上の「保存」ボタンを押すと「API エンドポイント」が表示されます。
APIのエンドポイントにアクセスしてみる
見れた!!!!!
まとめ
AWS LambdaとAPI Gatewayはまったく触れたことがなかったので、つまづくことが多かったですがなんとか動くところまで到達できたので達成感がありました。
今回はStackery社のbootstrapファイルをほぼそのまま流用する形で実現しましたが、ランタイムインターフェイスの理解を深めて作り変えにチャレンジしてみたいと思っています。
あと、正直なところマネジメントコンソールの画面からの操作はとても面倒なので、awsコマンドでやったほうがスッキリすると思います。 (違うんや。前回の記事のとおりコマンドの導入自体は終わってたんやが、Python2系にしてもPython3系にしても自分の環境でaws lambdaコマンドがうまいこと動いてくれなくて。。。)