この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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とか動かしてみても面白いかもしれません。
最後に繰り返しですが、本番環境でこんな構成組まないで下さいね!!