ちょっと話題の記事

【禁忌解放】WordPressのコンテナイメージをLambda上で動かしてみた #reinvent

動いちゃいました
2020.12.13

CX事業本部@大阪の岩田です。 この記事はServerless Advent Calendar 2020の13日目 です。

先日ブログでご紹介したように、Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました。

というわけで、WordPressのコンテナイメージを作成してLambda実行環境上で動かしてみます。

注意事項

このブログはネタです。アンチパターンをバリバリ利用しています。このブログを参考にWordPressの本番環境をLambda上に構築するのは一切オススメしませんので、ネタとして割り切って読んで頂ければと思います。

Lambda上でWordPressを動かす際の課題

普通にApacheやNginxなどのWebサーバーを立ててWordpressを動かす場合と比べて、Lambda上でWordpressを動かすためにはいくつかクリアすべき課題があります。まずはこれらの課題を解決するためのアーキテクチャから考えていきます。

HTTPリクエストとイベントデータの変換

通常のWordPressはHTTPリクエストをトリガーにPHPのプログラムが起動しますが、LambdaのhandlerはAWS上のイベントからトリガーされ、対象イベントに関する情報(HTTPリクエストであればメソッドやパスの情報など)はイベントデータから取得する必要があります。Lambdaのイベントデータを処理するようにWordPressのロジックを修正するのは大変なので、Lambda実行環境内でWebサーバーを稼働させつつ、handlerから起動したPHPスクリプトでイベントデータをHTTPリクエストに変換してWebサーバーにプロキシするような構成をとります。これはPHP用のカスタムランタイムPHP Layer For AWS Lambdaと同じアプローチです。

今回はコンテナイメージを利用するということもあり、PHP Layer For AWS LambdaのようにPHPのビルトインWebサーバーを稼働させるのではなく、コンテナ内でApache & mod_phpを稼働させる構成に挑戦します。

セッション管理

LambdaのアーキテクチャではユーザーからのリクエストとLambda実行環境が1:1で紐づきます。そのため、複数のリクエストを跨いでユーザーのステートを管理するためにはPHPのセッションを外部のデータストアに保存する必要があります。PHPのセッション保存先をデフォルトのファイルではなくElastiCacheに変更することで...と思って検証していたところ、WordPressはPHPのセッションを使っていないということを知りました。なので、セッション管理については考慮しなくて良さそうです。

WordPress Doesn’t Use PHP Sessions, and Neither Should You

画像のアップロード先

セッション管理については考慮不要と分かりましたが、画像類はどうでしょうか?Lambda実行環境Aからアップロードした画像はLambda実行環境Bからも参照できる必要があります。今回は画像のアップロード先としてEFSを利用することでLambda実行環境を跨いだ画像の共有を実現します。WordPressのプラグインを使ってS3 & CloudFrontを利用する構成の方がベターだと思いますが、プラグインの調査/選定が面倒だったのと、今年のLambda関連アップデートを振り返るという意味でEFSを使うことに決めました。

構成

アーキテクチャの検討が終わりました。今回はこんな構成を作ります。

  • ユーザーからのリクエストをALBで受け付けてLambdaを起動
  • Lambdaのhandlerから起動したPHPのスクリプトがイベントデータをHTTPリクエストに変換し、Lambda実行環境内で動作するApache & mod_phpにリクエストをプロキシ
  • Apache & mod_phpは普通にWordPressを実行
    • DBにはRDSを利用
    • 画像はEFSにアップロード

ざっくりこのような流れになります。

環境構築

それでは環境を構築していきましょう。なお、今回はWordPressのバージョン5.5.3で環境を構築しています。

ローカル環境でWordPressの環境構築

まずローカル環境にWordPressをインストールしてDBダンプを準備します。WordPressがインストールできたら管理者権限で /wp-admin/options.phpにアクセスし、upload_pathの値を/mnt/efs/uploadsに変更しておきます。

後ほどLambda実行環境の/mnt/efsにEFSをマウントすることで画像のアップロード先ディレクトリとしてEFSが利用可能になります。

続いて生成されたwp-config.phpを以下のように編集し、諸々の情報を環境変数から取得するように変更します。

...略
// ** MySQL 設定 - この情報はホスティング先から入手してください。 ** //
/** WordPress のためのデータベース名 */
define( 'DB_NAME', getenv('DB_NAME'));

/** MySQL データベースのユーザー名 */
define( 'DB_USER', getenv('DB_USER') );

/** MySQL データベースのパスワード */
define( 'DB_PASSWORD', getenv('DB_PASSWORD') );

/** MySQL のホスト名 */
define( 'DB_HOST', getenv('DB_HOST') );

/** データベースのテーブルを作成する際のデータベースの文字セット */
define( 'DB_CHARSET', 'utf8mb4' );

/** データベースの照合順序 (ほとんどの場合変更する必要はありません) */
define( 'DB_COLLATE', '' );

define('WP_HOME', getenv('WP_HOME'));
define('WP_SITEURL', getenv('WP_SITEURL'));
...略

WordPress用コンテナイメージのビルド

ECRにPushするためのコンテナイメージをビルドします。以下の構成でビルド用のディレクトリを用意します。

├── 99-wp-lambda.conf                           ... Lambda実行環境用に調整したApacheの設定ファイル
├── Dockerfile                                  ... コンテナイメージビルド用のDockerfile
├── bootstrap					... bootstrapとして利用するPHPのスクリプト
├── entry.sh					... コンテナイメージのENTRYPOINTとして指定するシェルスクリプト
└── html					... WordPressのソースコード

ビルド時にhtmlディレクトリの中身をコンテナイメージに突っ込むので、先程準備したWordPressのソースコードをhtmlディレクトリに配置して下さい。

1つづつ中身を見ていきましょう。まずDockerfileです。

FROM amazon/aws-lambda-provided:al2

RUN curl https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -O && \
    rpm -ivh epel-release-latest-7.noarch.rpm && \
    yum install -y http://rpms.famillecollet.com/enterprise/remi-release-7.rpm && \
    yum install -y php74-php php74-php-pecl-mysql httpd && \
    rm -rf /var/cache/yum/* && \
    yum clean all && \
    ln -sf /usr/bin/php74 /usr/bin/php && \
    sed -i 's/.*Group apache/#&/g' /etc/httpd/conf/httpd.conf &&  \
    sed -i 's/.*User apache/#&/g' /etc/httpd/conf/httpd.conf && \
    sed -i 's/.*ErrorLog.*/#&/g' /etc/httpd/conf/httpd.conf && \
    sed -i 's/.*CustomLog.*/#&/g' /etc/httpd/conf/httpd.conf && \
    sed -i 's/.*Listen.*/#&/g' /etc/httpd/conf/httpd.conf

COPY html /var/www/html/
COPY 99-wp-lambda.conf /etc/httpd/conf.d/99-wp-lambda.conf
COPY bootstrap /usr/local/bin
COPY entry.sh /
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod 755 /usr/bin/aws-lambda-rie
ENTRYPOINT [ "/entry.sh" ]

5,6行目でPHPとApacheをインストールした後、10 ~ 14行目でLambda実行環境でApacheを動作させるために邪魔な設定をコメントアウトしています。この辺りの設定項目は別途追加の設定ファイルとデーモン起動時のオプションで指定します。

17行目で追加しているApacheの設定ファイル99-wp-lambda.confの中身です。

Alias /wp-content/uploads/ /mnt/efs/uploads/
<Directory /mnt/efs/uploads/>
    Require all granted
</Directory>
PidFile /tmp/httpd.pid
Group daemon
Listen 8000
CustomLog /dev/stdout common
ErrorLog /dev/stderr

今回画像のアップロード先としてEFSを利用することにしました。Lambda実行環境にEFSをマウントする場合、利用可能なパスは/mntから始まるパスに制限されます。今回は/mnt/efs/というパスを利用するのですが、このパスはコンテナ内で動作するApacheのドキュメントルート外のディレクトリになります。そのためエイリアスを設定しつつアクセス許可を追加します。これでクライアントがhttp://xxxxxxxxx/wp-content/uploads/2020/12/xxxx.jpg のようなURLにリクエストした場合でもEFS上の画像ファイルを返却できるようになります。

Groupの指定はdaemonに変更しています。Lambda実行環境ではapacheというグループが利用できないためです。

PidFileの指定は/tmp/httpd.pidに変更しています。Lambda実行環境ではEFSを除くと/tmp以下のディレクトリしか書き込み権限が無いためです。同様にログファイルについても権限の問題を回避するために出力先を標準出力と標準エラー出力に変更しています。

18行目で追加しているbootstrapの中身です。PHP Layer For AWS Lambdaをベースに少し加工しています。

#!/usr/bin/php
<?php

error_reporting(E_ALL | E_STRICT);

$AWS_LAMBDA_RUNTIME_API = getenv('AWS_LAMBDA_RUNTIME_API');

/* https://gist.github.com/henriquemoody/6580488 */
$http_codes = [100=>'Continue',101=>'Switching Protocols',102=>'Processing',200=>'OK',201=>'Created',202=>'Accepted',203=>'Non-Authoritative Information',204=>'No Content',205=>'Reset Content',206=>'Partial Content',207=>'Multi-Status',208=>'Already Reported',226=>'IM Used',300=>'Multiple Choices',301=>'Moved Permanently',302=>'Found',303=>'See Other',304=>'Not Modified',305=>'Use Proxy',306=>'Switch Proxy',307=>'Temporary Redirect',308=>'Permanent Redirect',400=>'Bad Request',401=>'Unauthorized',402=>'Payment Required',403=>'Forbidden',404=>'Not Found',405=>'Method Not Allowed',406=>'Not Acceptable',407=>'Proxy Authentication Required',408=>'Request Timeout',409=>'Conflict',410=>'Gone',411=>'Length Required',412=>'Precondition Failed',413=>'Request Entity Too Large',414=>'Request-URI Too Long',415=>'Unsupported Media Type',416=>'Requested Range Not Satisfiable',417=>'Expectation Failed',418=>'I\'m a teapot',419=>'Authentication Timeout',420=>'Enhance Your Calm',420=>'Method Failure',422=>'Unprocessable Entity',423=>'Locked',424=>'Failed Dependency',424=>'Method Failure',425=>'Unordered Collection',426=>'Upgrade Required',428=>'Precondition Required',429=>'Too Many Requests',431=>'Request Header Fields Too Large',444=>'No Response',449=>'Retry With',450=>'Blocked by Windows Parental Controls',451=>'Redirect',451=>'Unavailable For Legal Reasons',494=>'Request Header Too Large',495=>'Cert Error',496=>'No Cert',497=>'HTTP to HTTPS',499=>'Client Closed Request',500=>'Internal Server Error',501=>'Not Implemented',502=>'Bad Gateway',503=>'Service Unavailable',504=>'Gateway Timeout',505=>'HTTP Version Not Supported',506=>'Variant Also Negotiates',507=>'Insufficient Storage',508=>'Loop Detected',509=>'Bandwidth Limit Exceeded',510=>'Not Extended',511=>'Network Authentication Required',598=>'Network read timeout error',599=>'Network connect timeout error'];

function fail($AWS_LAMBDA_RUNTIME_API, $invocation_id, $message) {
  $ch = curl_init("http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/$invocation_id/response");

  $response = array();

  $response['statusCode'] = 500;
  $response['body'] = $message;

  $response_json = json_encode($response);

  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $response_json);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'Content-Length: ' . strlen($response_json)
  ));

  curl_exec($ch);
  curl_close($ch);
}

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\n");
  }

  curl_close($ch);

  if (!$body) {
    die("Empty Lambda invocation response\n");
  }

  $event = json_decode($body, TRUE);
  
  if (!array_key_exists('requestContext', $event)) {
    fail($AWS_LAMBDA_RUNTIME_API, $invocation_id, 'Event is not an API Gateway request');
    continue;
  }

  $uri = $event['path'];

  if (array_key_exists('multiValueQueryStringParameters', $event) && $event['multiValueQueryStringParameters']) {
    $first = TRUE;
    foreach ($event['multiValueQueryStringParameters'] as $name => $values) {
      foreach ($values as $value) {
        if ($first) {
          $uri .= "?";
          $first = FALSE;
        } else {
          $uri .= "&";
        }

        $uri .= $name;

        if ($value != '') {
          $uri .= '=' . $value;
        }
      }
    }
  }

  $ch = curl_init("http://localhost:8000$uri");

  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE);

  if (array_key_exists('multiValueHeaders', $event)) {
    $headers = array();

    foreach ($event['multiValueHeaders'] as $name => $values) {
      foreach ($values as $value) {
        array_push($headers, "${name}: ${value}");
      }
    }

    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  }

  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $event['httpMethod']);

  if (array_key_exists('body', $event)) {
    $body = $event['body'];
    if (array_key_exists('isBase64Encoded', $event) && $event['isBase64Encoded']) {
      $body = base64_decode($body);
    }
  } else {
    $body = '';
  }

  if (strlen($body) > 0) {
    curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  }

  $response = array();
  $response['multiValueHeaders'] = array();
  $response['body'] = '';

  curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $header) use (&$response) {
    if (preg_match('/HTTP\/1.1 (\d+) .*/', $header, $matches)) {
      $response['statusCode'] = intval($matches[1]);
      return strlen($header);
    }

    if (!preg_match('/:\s*/', $header)) {
      return strlen($header);
    }

    [$name, $value] = preg_split('/:\s*/', $header, 2);

    $name = trim($name);
    $value = trim($value);

    if ($name == '') {
      return strlen($header);
    }

    if (!array_key_exists($name, $response['multiValueHeaders'])) {
      $response['multiValueHeaders'][$name] = array();
    }

    array_push($response['multiValueHeaders'][$name], $value);

    return strlen($header);
  });

  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");

  $isALB = array_key_exists("elb", $event['requestContext']);

  if ($isALB) { // Add Headers For ALB
    $status = $response["statusCode"];
    if (array_key_exists($status, $http_codes)) {
        $response["statusDescription"] = "$status ". $http_codes[$status];
    } else {
        $response["statusDescription"] = "$status Unknown";
    }

    $content_type = $response['multiValueHeaders']['Content-Type'][0];
    if (preg_match('/^image\/.*$/', $content_type)) {
      $response['body'] = base64_encode($response['body']);
      $response["isBase64Encoded"] = true;
    } else {
      $response["isBase64Encoded"] = false ;
    }


    
  }
  $response_json = json_encode($response);

  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $response_json);
  if (!$isALB){
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
      'Content-Type: application/json',
      'Content-Length: ' . strlen($response_json)
    ));
  }
  curl_exec($ch);
  curl_close($ch);
}

?>

PHP Layer For AWS Lambdaからの変更点は以下の通りです。

1c1
< #!/usr/bin/php
---
> #!/opt/bin/php -c/opt/php.ini
10a11,46
> function start_webserver() {
>   $SERVER_STARTUP_TIMEOUT = 1000000; // 1 second
>
>   $pid = pcntl_fork();
>   switch($pid) {
>     case -1:
>       die("Failed to fork webserver process\n");
>
>     case 0:
>       // exec the command
>       $HANDLER = getenv('_HANDLER');
>       $phpMinorVersion = PHP_MINOR_VERSION;
>       $handler_components = explode('/', $HANDLER);
>       $handler_filename = array_pop($handler_components);
>       $handler_path = implode('/', array_merge(['/var/task'], $handler_components));
>       chdir($handler_path);
>       exec("PHP_INI_SCAN_DIR=/opt/etc/php-7.${phpMinorVersion}.d/:/var/task/php-7.${phpMinorVersion}.d/ php -S localhost:8000 -c /var/task/php.ini -d extension_dir=/opt/lib/php/7.${phpMinorVersion}/modules '$handler_filename'");
>       exit;
>
>     default:
>       // Wait for child server to start
>       $start = microtime(true);
>
>       do {
>         if (microtime(true) - $start > $SERVER_STARTUP_TIMEOUT) {
>           die("Webserver failed to start within one second\n");
>         }
>
>         usleep(1000);
>         $fp = @fsockopen('localhost', 8000, $errno, $errstr, 1);
>       } while ($fp == false);
>
>       fclose($fp);
>   }
> }
>
32a69,70
> start_webserver();
>
80c118
<
---
>
183d220
<
191,201c228
<
<     $content_type = $response['multiValueHeaders']['Content-Type'][0];
<     if (preg_match('/^image\/.*$/', $content_type)) {
<       $response['body'] = base64_encode($response['body']);
<       $response["isBase64Encoded"] = true;
<     } else {
<       $response["isBase64Encoded"] = false ;
<     }
<
<
<
---
>     $response["isBase64Encoded"] = false;
204d230
<

変更内容には以下の通りです

  • シェバンの指定を#!/opt/bin/php -c/opt/php.iniから#!/usr/bin/phpに変更
  • PHPのビルドインサーバー起動ロジックを削除
  • レスポンスが画像の場合の考慮を追加

最後に19行目で追加しているentry.shの中身です。

#!/bin/sh

if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
    /sbin/httpd -c "User  apache" -DFOREGROUND & 
    exec /usr/bin/aws-lambda-rie /usr/local/bin/bootstrap
else
    /sbin/httpd -c "User  $(whoami)" -DFOREGROUND & 
    exec /usr/local/bin/bootstrap
fi

Apacheのプロセスを起動した後、Lambdaのbootstrap処理を実行します。環境変数AWS_LAMBDA_RUNTIME_APIの有無でLambda実行環境/ローカル環境の判定を行い、処理を分岐しています。Lambda実行環境の場合はapacheユーザーが利用できないので、whoamiコマンドの実行結果をApache実行用のユーザーに指定します。

また、ローカル環境の場合はデバッグしやすいようにRIEを起動しています。RIEが不要であればこのロジックと、Dockerfileの20,21行目は削除して大丈夫です。イメージサイズを小さくするという意味では削除しておいた方が良いですね。

このDockerfileからコンテナイメージをビルドしてECRのリポジトリにPushしましょう

$ aws ecr create-repository --repository-name wp-lambda --image-tag-mutability MUTABLE --image-scanning-configuration scanOnPush=true
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com
$ docker build -t wp-lambda .
$ docker tag wp-lambda:latest <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/wp-lambda:latest
$ docker push <AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/wp-lambda:latest

その他AWS環境の構築

コンテナイメージがビルドできたら以下のSAMテンプレートからその他AWSリソースを一気に作成します。※RDSのパスワードをオンコードしてたり適当なので流用される場合は要注意です。

AWSTemplateFormatVersion: 2010-09-09
Description: WordPress on Lambda Container Image
Transform: AWS::Serverless-2016-10-31
Parameters:
  ImageUri: 
    Type: String   
    Description: ECR Image URI For WordPress
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
  SubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
  SubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: ap-northeast-1c
      MapPublicIpOnLaunch: true
  VPCInternetGateway:
    Type: AWS::EC2::InternetGateway
  VPCAttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref VPCInternetGateway
  VPCPublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
  VPCRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref VPCPublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref VPCInternetGateway
  SubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetA
      RouteTableId: !Ref VPCPublicRouteTable
  SubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref SubnetC
      RouteTableId: !Ref VPCPublicRouteTable 
  MyDBSubnetGroup:
    Type: "AWS::RDS::DBSubnetGroup"
    Properties:
      DBSubnetGroupDescription: WP-DB Subnet 
      SubnetIds: 
        - !Ref SubnetA
        - !Ref SubnetC
  Database:
    Type: AWS::RDS::DBInstance
    Properties:
      VPCSecurityGroups:
      - Ref: RdsSecurityGroup
      AllocatedStorage: '20'
      DBInstanceClass: db.t3.micro
      Engine: mysql
      EngineVersion: 5.7.22
      MasterUsername: root
      MasterUserPassword: MySql10_
      DBSubnetGroupName: !Ref MyDBSubnetGroup
      DBName: wpdb
    DeletionPolicy: Delete
  RdsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Rds Security Group      
      VpcId: !Ref VPC
  RdsSecurityGroupInMysql:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref RdsSecurityGroup
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
      SourceSecurityGroupId: !Ref LambdaSecurityGroup  
  Alb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      SecurityGroups:
        - !Ref AlbSecurityGroup
      Subnets:
        - !Ref SubnetA
        - !Ref SubnetC
      Type: application
  AlbTarget:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: AlbTargetLambda 
      TargetGroupAttributes: 
        - Key: lambda.multi_value_headers.enabled
          Value: 'true'
      TargetType: lambda
      Targets: 
        - Id: !GetAtt WordPress.Arn
    DependsOn:
      - AlbInvokePermission
  AlbListner:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties: 
      DefaultActions: 
        - Type: forward
          TargetGroupArn: !Ref AlbTarget
      LoadBalancerArn: !Ref Alb
      Port: 80
      Protocol: HTTP
  AlbInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt  WordPress.Arn
      Action: lambda:InvokeFunction
      Principal: elasticloadbalancing.amazonaws.com
      SourceArn: !Sub arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:targetgroup/AlbTargetLambda/*
  AlbSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: ALB Security Group      
      VpcId: !Ref VPC
  AlbSecurityGroupInHTTP:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref AlbSecurityGroup
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      CidrIp: 0.0.0.0/0      
  EfsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Efs Security Group      
      VpcId: !Ref VPC
  EfsSecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref EfsSecurityGroup
      IpProtocol: tcp
      FromPort: 2049
      ToPort: 2049
      SourceSecurityGroupId: !Ref LambdaSecurityGroup
  Efs:
    Type: AWS::EFS::FileSystem
  EfsAccessPoint:
    Type: AWS::EFS::AccessPoint
    Properties: 
      FileSystemId: !Ref Efs
      PosixUser: 
        Gid: '1001'
        Uid: '1001'
      RootDirectory:
        CreationInfo:
          OwnerUid: '1001'
          OwnerGid: '1001'
          Permissions: '0755'
        Path: /wordpress
  EfsMountTargetA:
    Type: AWS::EFS::MountTarget
    Properties: 
      FileSystemId: !Ref Efs
      SecurityGroups: 
        - !Ref EfsSecurityGroup
      SubnetId: !Ref SubnetA
  EfsMountTargetC:
    Type: AWS::EFS::MountTarget
    Properties: 
      FileSystemId: !Ref Efs
      SecurityGroups: 
        - !Ref EfsSecurityGroup
      SubnetId: !Ref SubnetC
  WordPress:
    Type: AWS::Serverless::Function
    Properties:
      Description: WordPress Container
      PackageType: Image
      ImageUri: !Ref ImageUri
      MemorySize: 512
      Timeout: 30
      Tracing: Active
      FileSystemConfigs:
        - Arn: !GetAtt EfsAccessPoint.Arn
          LocalMountPath: /mnt/efs
      Policies:
        - Version: '2012-10-17'
          Statement:
           - Effect: Allow
             Action:
               - elasticfilesystem:ClientMount
               - elasticfilesystem:ClientWrite
               - elasticfilesystem:DescribeMountTargets 
             Resource: '*'
      VpcConfig:
        SecurityGroupIds:
          - !Ref LambdaSecurityGroup
        SubnetIds:
          - !Ref SubnetA
          - !Ref SubnetC
      Environment:
        Variables:
          DB_NAME: wpdb
          DB_USER: root
          DB_PASSWORD: MySql10_
          DB_HOST: !GetAtt Database.Endpoint.Address
          WP_HOME: !Sub http://${Alb.DNSName}
          WP_SITE_URL: !Sub http://${Alb.DNSName}
    DependsOn:
      - EfsMountTargetA
      - EfsMountTargetC
  LambdaSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Lambda Security Group
      VpcId: !Ref VPC
Outputs:
  DBEndPoint:
    Value: !GetAtt  Database.Endpoint.Address
    Description: Database Endpoint
  ALBEndPoint:
    Value: !GetAtt  Alb.DNSName
    Description: ALB Endpoint

デプロイします。パラメータに先程PUSHしたコンテナイメージのURIを指定して下さい。

$ sam package --template-file  sam.yml --output-template-file output.yml --s3-bucket cm-iwata --image-repository hello-container
$ sam deploy  --template-file   output.yml --stack-name lambda-wp --capabilities CAPABILITY_IAM --parameter-overrides ParameterKey=ImageUri,ParameterValue=<AWSアカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/wp-lambda:latest

AWSリソースのデプロイが完了したらローカル環境で取得しておいたDBダンプをRDSにリストアします。これでWordPress on Lambdaの準備完了です。

WordPressを動かしてみる

動作確認してみましょう。まずはログインして...

ダッシュボードへ

EFSと連携した画像アップロードもバッチリです

アップロードした画像を使いつつ、投稿を編集して更新

別のブラウザから一般ユーザーとして確認してみしょう

バッチリです!!

まとめ

LambdaからEFSが利用可能になった時に挑戦しようと思いながら後回しになっていたWordPress on Lambda、コンテナイメージを使いつつ実現できて満足です。今回コンテナ内でLambdaのメイン処理と別にApacheのプロセスを常駐させるような構成を作りましたが、こういうハックができるのもコンテナイメージの面白いところですね。fluentdとか動かしてみても面白いかもしれません。

最後に繰り返しですが、本番環境でこんな構成組まないで下さいね!!