【禁忌解放】WordPressのコンテナイメージをLambda上で動かしてみた #reinvent
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とか動かしてみても面白いかもしれません。
最後に繰り返しですが、本番環境でこんな構成組まないで下さいね!!