AWS Lambda のCustom RuntimeでPHP用のLayer作成(API Gatewayを使わない)

##前置き

この記事が初投稿になります。
アウトプットする機会を増やすために投稿することにしました。

初投稿の記事はAWS lambdaのCustom Runtime機能でPHPを扱うものになります。
PythonのようにHandlerとeventを扱いたいと思ったので、自分用にまとめた記事です。

API Gate Wayを使用したくない&Handlerでファイル名.関数を記述したい&phpファイルにmain();を書きたくない人向けです。

Stackery社のphp-lambad-layerを改造します。
pythonみたいにhandlerにファイル名.関数を記述すると、該当のファイルの関数が呼ばれるLayerにすることが目的です。

## 更新
2019/03/30 build.shでのphpのインストールをzipファイルからremiリポジトリでのインストールに変更しました。php7.3に変更しました。

## 作業環境

LayerのZip作成にdockerを、AWSへのアップロードにaws-cliを使います。

- pyenv
    - 1.2.9-2
- pip
    - 19.0.1(python2.7)
- aws-cli
    - 1.16.96(python2.7.15) 
- docker
    - 19.09.2   

作業はLinuxサーバー内で行いました。
~~暇なときにMacOSでも試してみます。~~
DockerをいれたMacOSでもLayer作成とAWSへのアップロードできました。

## 手順

Stackery社のphp-lambda-layerをcloneします。

```
git clone https://github.com/stackery/php-lambda-layer.git
```

ファイルの変更箇所が非常におおいため、ファイルの中身を丸ごと載せます。

#### Makefile: 

```makefile:Makefile
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))

php73.zip:
	docker run --rm -v $(ROOT_DIR):/opt/layer lambci/lambda:build-nodejs8.10 /opt/layer/build.sh
```

#### bootstrap:
ビルトインサーバーで扱うファイルを変更し、不要な処理を全て削除します。
ビルトインサーバーとして起動するファイルに、handlerをファイル名と関数にわけたものを環境変数で渡します。

```php7:bootstrap
#!/opt/bin/php -c/opt/php.ini
<?php

error_reporting(E_ALL | E_STRICT);

$AWS_LAMBDA_RUNTIME_API = getenv('AWS_LAMBDA_RUNTIME_API');
$HTTP_METHOD = 'POST';

function start_webserver() {
  $pid = pcntl_fork();
  switch($pid) {
    case -1:
      die('Failed to fork webserver process');
    case 0:
      $HANDLER = getenv('_HANDLER');
      $HANDLER_ARRAY = explode('.', $HANDLER);
      $handler_function = $HANDLER_ARRAY["1"];
      $handler_components = explode('/', $HANDLER_ARRAY["0"]);
      $handler_path = implode('/', array_merge(['/var/task'], $handler_components));
      $handler_path .= ".php";
      putenv("handler_function=$handler_function");
      putenv("handler_path=$handler_path");

      exec("PHP_INI_SCAN_DIR=/opt/etc/php-7.3.d/:/var/task/php-7.3.d/ php -S localhost:8000 -c /var/task/php.ini -d extension_dir=/opt/lib/php/7.3/modules '/opt/init.php'");

      exit;
    // return the child pid to parent
    default:
      // Wait for child server to start
      sleep(1);
      return $pid;
  }
}

start_webserver();

while (true) {
  $ch = curl_init("http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/next");
  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
  curl_setopt($ch, CURLOPT_FAILONERROR, TRUE);
  $invocation_id = '';
  curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $header) use (&$invocation_id) {
    if (!preg_match('/:\s*/', $header)) {
      return strlen($header);
    }
    [$name, $value] = preg_split('/:\s*/', $header, 2);
    if (strtolower($name) == 'lambda-runtime-aws-request-id') {
      $invocation_id = trim($value);
    }
    return strlen($header);
  });
  $body = '';
  curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$body) {
    $body .= $chunk;
    return strlen($chunk);
  });
  curl_exec($ch);
  if (curl_error($ch)) {
    die('Failed to fetch next Lambda invocation: ' . curl_error($ch) . "\n");
  }
  if ($invocation_id == '') {
    die('Failed to determine Lambda invocation ID');
  }
  curl_close($ch);
  $ch = curl_init("http://localhost:8000");
  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $HTTP_METHOD);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
  curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  curl_setopt($ch, CURLOPT_INFILESIZE, strlen($body));
  curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $fd, $length) use ($body) {
    return $body;
  });
  $response = array();
  $response['body'] = '';
  curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$response) {
    $response['body'] .= $chunk;
    return strlen($chunk);
  });
  curl_exec($ch);
  curl_close($ch);
  $ch = curl_init("http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/$invocation_id/response");
  
  echo $response['body'];
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $response['body']);
  
  curl_exec($ch);
  curl_close($ch);
}
?>
```

#### build.sh
PHPで必要なパッケージをインストールするようにbuildも変更します。
ここで作成するzipファイルの中身がCustom Runtimeのベース環境になるので、過不足ないようにします。

```shell:build.sh
#!/bin/bash

echo "Building layer for PHP 7.3 - using Remi repository"

yum install -y wget
yum install -y yum-utils
wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm
wget https://rpms.remirepo.net/enterprise/remi-release-6.rpm
rpm -Uvh remi-release-6.rpm
rpm -Uvh epel-release-latest-6.noarch.rpm

yum-config-manager --enable remi-php73

yum install -y httpd
yum install -y postgresql-devel
yum install -y libargon2-devel

yum install -y --disablerepo="*" --enablerepo="remi,remi-php73" php php-mbstring php-pdo php-mysql php-pgsql


mkdir /tmp/layer
cd /tmp/layer
cp /opt/layer/bootstrap bootstrap
cp /opt/layer/init.php init.php
cp /opt/layer/php.ini php.ini

mkdir bin
cp /usr/bin/php bin/

mkdir lib
for lib in libncurses.so.5 libtinfo.so.5 libpcre.so.0; do
  cp "/lib64/${lib}" lib/
done

cp /usr/lib64/libedit.so.0 lib/
cp /usr/lib64/libargon2.so.0 lib/
cp /usr/lib64/libpq.so.5 lib/

mkdir -p lib/php/7.3
cp -a /usr/lib64/php/modules lib/php/7.3/

zip -r /opt/layer/php73.zip .
```

#### init.php

ビルトインサーバーで使用するphpファイルを新規作成。
bootstrapから渡される環境変数をもとに、Lambda関数のphpファイルにある関数を呼びます。
bootstrapとinit.phpによってHandlerにファイル名.関数名で記述できるようになります。

```php:init.php
<?php
$handler_path = getenv('handler_path');
$handler_function = getenv('handler_function');
require_once($handler_path);
$lambda_event_json = file_get_contents('php://input');
$lambda_event = json_decode($lambda_event_json, TRUE);
if(function_exists($handler_function))
{
  $handler_function($lambda_event);
}
```


変更後、makeでzip作成

```make:php72.zip
make php72.zip
```

Stackery社のMakefileをそのまま使用してAWSにアップロードしてもいいですが、今回は直接アップロードします。
(個人環境へのアップロードで、使用しているregionも1つならaws-cli直の方が安心かと)

```
aws lambda --profile <profile設定> publish-layer-version --layer-name <Layer名> --zip-file fileb://php72.zip --description "PHP7.2 Custom Runtime."
```

## まとめ

Custom Runtime Layerの作成は以上になります。
今回はPHP7.2.12を使用していますが、公式からzipさえ落とせるのであれば、どのバージョンでも問題なさそうです。

変更した箇所のdiffを見ていただければわかりますが、APIではなく、Invoke起動などにも幅広く対応させています。
Lambda側に処理を書くことになりますが、CloudWatchのイベント、Invoke起動など全てに対応可能です。

**API GateWayを使用する場合は素直にStackery社のものをそのまま使いましょう。**

今回のLayerを用いたPHP関数と作成手順は、後日qiitaにあげます。

## 参考

[Stackery/php-lambda-layer](https://github.com/stackery/php-lambda-layer)
[PHP7.2が使えるLambdaのカスタムランタイム環境を作ってみた #reinvent](https://dev.classmethod.jp/cloud/aws/create-lambda-custom-runtime-php72/)
[CentOS 7 PHP 7.2.5のソースファイルからのインストール](https://qiita.com/knutpb1205/items/c970e653466591fb2545)