ステートフルなECサイトもLambdaで動くんです!! AWS Lambda の Custom Runtimes を利用してEC-CUBEを動かしてみる #reinvent

Custom Runtimesを使えばEC-CUBEだって動くんです!!
2018.12.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

サーバーレス開発部@大阪の岩田です。 この記事は AWS Lambda Custom Runtimes芸人 Advent Calendar 2018 の 17日目 です。

はじめに

これまでLambdaのCustom Runtimesを利用してPHPで遊んで来ました。

今回はPHPシリーズの仕上げとしてECサイトを構築できるOSSとして有名なEC-CUBEをLambdaで動かしてみます。 なお、heroku使えよという突っ込みは受け付けておりません。

最終的な成果物はGitHubで公開しているので、必要に応じて参照して下さい。

目標

EC-CUBEの全機能を動かすのはハードルが高いので、シンプルな購入フローが正常終了し、DBに受注データが生成されるまでを目標として設定しました。 HTTPSへの対応も行いません。

環境

今回構築する環境です

  • EC-CUBE:4.0.1
  • PHP:7.1
  • カスタムランタイム:STACKERY社提供のレイヤーarn:aws:lambda:${AWS::Region}:887080169480:layer:php71:5をベースに使用
  • DB:MySQL5.7(RDSを利用)

PHPのExtension用レイヤー作成

まずはEC-CUBEの動作に必要なPHPのExtensionを準備します。

build.shという名前で以下のシェルスクリプトを作成します。

build.sh

#!/bin/bash
yum install -y php71-pdo php71-intl php71-mysqlnd php71-mbstring

mkdir -p /tmp/layer/
cd /tmp/layer

mkdir -p lib/php/7.1/modules

for lib in intl.so pdo.so mysqlnd.so pdo_mysql.so mbstring.so; do
  cp "/usr/lib64/php/7.1/modules/${lib}" lib/php/7.1/modules
done

zip -r /opt/layer/eccube_ext.zip .

STACKERY社推奨のDockerイメージを利用して上記シェルスクリプトを実行し、必要なExtensionをZIPに固めます。

docker run --rm -v $(PWD):/opt/layer lambci/lambda:build-nodejs8.10 /opt/layer/build.sh

ZIPが作成できたらレイヤーを作成します

aws lambda publish-layer-version --layer-name eccube_ext --zip-file fileb://eccube_ext.zip

bootstrap用のレイヤー作成

STACKERY社から提供されているレイヤーをそのまま利用するとEC-CUBEが正常に動作しないため、独自にレイヤーを作成してbootstrapを上書きます。

STACKERY社のGitHubリボジトリからbootstrapを取得し、修正していきます。

curlのリダイレクトを無効化

STACKERY社提供のbootstrapはレスポンスヘッダのLocationを再帰的に辿る仕様となっており、EC-CUBE側でリダイレクトをかけた時にERR_RESPONSE_HEADERS_MULTIPLE_LOCATIONが発生してしまいます。 Locationを辿るのはブラウザに任せるように該当ロジックをコメントアウトします。

//curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);

HTTPメソッドがPOSTの場合に、バックエンドにデータを送信するように修正

STACKERY社提供のbootstrapはPOSTのリクエストを受けた際に、バックエンドのPHPビルトインサーバーにデータを流してくれません。(普通にバグってる気が・・・) データを流すように下記のコードを追加します。

※2018/12/28追記

このPOST対応のプルリクがマージされたので、バージョン6以後のレイヤーは修正無しでそのまま利用できそうです

if($event['httpMethod'] === 'POST'){
    curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}

最終的なbootstrapのコードです

bootstrap

#!/opt/bin/php -c/opt/php.ini
<?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 start_webserver() {
  $pid = pcntl_fork();
  switch($pid) {
    case -1:
      die('Failed to fork webserver process');

    case 0:
      // exec the command
      $HANDLER = getenv('_HANDLER');
      chdir('/var/task');
      exec("PHP_INI_SCAN_DIR=/opt/etc/php-7.1.d/:/var/task/php-7.1.d/ php -S localhost:8000 -c /var/task/php.ini -d extension_dir=/opt/lib/php/7.1/modules '$HANDLER'");
      exit;

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

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

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

  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, TRUE);

  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) {
    if($event['httpMethod'] === 'POST'){
      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['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";
    }
    $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);
}
?>

bootstrap用のレイヤを作成します。

chmod +x bootstrap
zip bootstrap.zip bootstrap
aws lambda publish-layer-version --layer-name php71_bootstrap_ex --zip-file fileb://bootstrap.zip

このレイヤーをSTACKERY社提供のレイヤーより後からマージすることでbootstrapを上書きします。

EC-CUBEのソースを準備

次にEC-CUBEのソースを準備します。

git clone https://github.com/EC-CUBE/ec-cube

でソースコードを取得した後、README.mdを参考にローカル環境でライブラリの導入等EC-CUBEのインストール作業を完了させます。

ライブラリの導入

composer install --no-dev

初期設定

./bin/console e:i

インストールできたらLambda環境で動作するようにEC-CUBEのソースを修正していきます。

環境変数の取得方法を変更

STACKERY社提供のレイヤーではスーパーグローバル変数$_SERVERからLambdaの環境変数が取得できないので、$_SERVER$_ENVに置き換えていきます。

index.php

 // The check is to ensure we don't use .env in production
-if (!isset($_SERVER['APP_ENV'])) {
+if (!isset($_ENV['APP_ENV'])) {

-$env = isset($_SERVER['APP_ENV']) ? $_SERVER['APP_ENV'] : 'dev';
-$debug = isset($_SERVER['APP_DEBUG']) ? $_SERVER['APP_DEBUG'] : ('prod' !== $env);
+$env = isset($_ENV['APP_ENV']) ? $_ENV['APP_ENV'] : 'dev';
+$debug = isset($_ENV['APP_DEBUG']) ? $_ENV['APP_DEBUG'] : ('prod' !== $env);


-$trustedProxies = isset($_SERVER['TRUSTED_PROXIES']) ? $_SERVER['TRUSTED_PROXIES'] : false;
+$trustedProxies = isset($_ENV['TRUSTED_PROXIES']) ? $_ENV['TRUSTED_PROXIES'] : false;


-$trustedHosts = isset($_SERVER['TRUSTED_HOSTS']) ? $_SERVER['TRUSTED_HOSTS'] : false;
+$trustedHosts = isset($_ENV['TRUSTED_HOSTS']) ? $_ENV['TRUSTED_HOSTS'] : false;

ログの出力先を変更

次にログの出力先を標準エラー出力に変更します。

app/config/eccube/packages/dev/monolog.yml

         class: EasyCorp\EasyLog\EasyLogHandler
         public: false
         arguments:
-            - '%kernel.logs_dir%/%kernel.environment%/site.log'
+            - "php://stderr"

app/config/eccube/packages/prod/monolog.yml

         main_rotating_file:
-            type: rotating_file
-            max_files: 60
-            path: '%kernel.logs_dir%/%kernel.environment%/site.log'
-            formatter: eccube.log.formatter.line
-            level: debug
+            type:  stream
+            path:  "php://stderr"
+            level: debug

         front_rotating_file:
-            type: rotating_file
-            max_files: 60
-            path: '%kernel.logs_dir%/%kernel.environment%/front.log'
-            formatter: eccube.log.formatter.line
-            level: debug
+            type:  stream
+            path:  "php://stderr"
+            level: debug

         admin_rotating_file:
-            type: rotating_file
-            max_files: 60
-            path: '%kernel.logs_dir%/%kernel.environment%/admin.log'
-            formatter: eccube.log.formatter.line
+            type: stream
+            path: "php://stderr"

キャッシュの保存先を変更

次に各種のキャッシュファイル保存場所をLambdaから書き込めるディレクトリに変更します。一応ログの出力先も書き換えておきます。

src/Eccube/Kernel.php

     public function getCacheDir()
     {
-        return $this->getProjectDir().'/var/cache/'.$this->environment;
+        return '/tmp/var/cache/'.$this->environment;
     }
     
     public function getLogDir()
     {
-        return $this->getProjectDir().'/var/log';
+        return '/tmp/var/log';
     }

セッションの保存先を変更

セッションの保存先をファイルからDBに変更します。 詳細はSymfonyの公式ドキュメントを参照して下さい。

まずセッション情報を保存するためのテーブルを作成します。

CREATE TABLE `sessions` (
    `sess_id` VARCHAR(128) NOT NULL PRIMARY KEY,
    `sess_data` BLOB NOT NULL,
    `sess_time` INTEGER UNSIGNED NOT NULL,
    `sess_lifetime` MEDIUMINT NOT NULL
) COLLATE utf8_bin, ENGINE = InnoDB;

各種設定ファイルを修正していきます。

まずservices.yamlに下記の定義を追加します。

app/config/eccube/services.yaml

+    Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
+        public: false
+        arguments:
+             - '%env(DATABASE_URL)%'

セッションのハンドラとして追加した定義を指定します

app/config/eccube/packages/framework.yaml

     session:
-        handler_id: session.handler.native_file
-        save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
+        handler_id: 'Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler'

静的コンテンツの取得元を変更

JS,CSS,画像ファイルといった静的コンテンツはLambdaから取得せずにS3から取得するように設定します。

app/config/eccube/packages/framework.yaml

     assets:
-      base_path: '/html/template/%eccube.theme%'
+      base_urls: ['https://s3-%env(AWS_DEFAULT_REGION)%.amazonaws.com/%env(S3_ASSET_BUCKET_NAME)%/']
       packages:
         admin:
           base_path: '/html/template/admin'
         save_image:
-          base_path: '/html/upload/save_image'
+          base_urls: ['https://s3-%env(AWS_DEFAULT_REGION)%.amazonaws.com/%env(S3_ASSET_BUCKET_NAME)%/save_image']

php.iniの追加

PHPのExtensionを読み込むためにプロジェクトのルートディレクトリにphp.iniを用意します。

php.ini

extension=ctype.so
extension=curl.so
extension=dom.so
extension=iconv.so
extension=intl.so
extension=json.so
extension=mbstring.so
extension=mysqlnd.so
extension=pdo.so
extension=pdo_mysql.so
extension=phar.so
extension=simplexml.so
extension=tokenizer.so
extension=xml.so
extension=zip.so

AWS環境の構築

準備ができたのでAWS環境を構築していきます。 ALBのターゲットグループにLambdaを設定したいのですが、現状CloudFormationが対応していないようなので、ある程度までSAMテンプレートで構築しつつ、残りは手作業で構築します。

まずは下記のSAMテンプレートを使います。

AWSTemplateFormatVersion: 2010-09-09
Description: My PHP Application
Transform: AWS::Serverless-2016-10-31
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
    DependsOn: VPCInternetGateway
    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 
  LambdaSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Lambda Security Group      
      VpcId: !Ref VPC
  LambdaSecurityGroupMySQL:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref LambdaSecurityGroup
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
      SourceSecurityGroupId: !Ref LambdaSecurityGroup
  LambdaSecurityGroupHTTP:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref LambdaSecurityGroup
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80
      CidrIp: 0.0.0.0/0
  MyDBSubnetGroup:
    Type: "AWS::RDS::DBSubnetGroup"
    Properties:
      DBSubnetGroupDescription: EC-DB Subnet 
      SubnetIds: 
        - !Ref SubnetA
        - !Ref SubnetC
  Database:
    Type: AWS::RDS::DBInstance
    Properties:
      VPCSecurityGroups:
      - Ref: LambdaSecurityGroup
      AllocatedStorage: 20
      DBInstanceClass: db.t2.micro
      Engine: mysql
      EngineVersion: 5.7.22
      MasterUsername: root
      MasterUserPassword: hogehogehoge
      DBSubnetGroupName: !Ref MyDBSubnetGroup
      DBName: eccube      
    DeletionPolicy: Delete
  AssetBucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: PublicRead
      WebsiteConfiguration:
        IndexDocument: index.html
    DeletionPolicy: Delete
  AssetBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref 'AssetBucket'
      PolicyDocument:
        Statement:
          - Effect: Allow
            Principal: '*'
            Action: s3:GetObject
            Resource: !Join ['', ['arn:aws:s3:::', !Ref 'AssetBucket', /*]]

  Alb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      SecurityGroups:
        - !Ref LambdaSecurityGroup
      Subnets:
        - !Ref SubnetA
        - !Ref SubnetC
      Type: application
  phpserver:
    Type: AWS::Serverless::Function
    Properties:
      Description: PHP Webserver
      CodeUri: index.php
      Runtime: provided
      Handler: index.php
      MemorySize: 512
      Timeout: 30
      Tracing: Active
      Policies:
        - Version: '2012-10-17'
          Statement:
           - Effect: Allow
             Action:
               - ec2:CreateNetworkInterface
               - ec2:DescribeNetworkInterfaces
               - ec2:DeleteNetworkInterface
             Resource: '*'        
      VpcConfig:
        SecurityGroupIds:
          - !Ref LambdaSecurityGroup
        SubnetIds:
          - !Ref SubnetA
          - !Ref SubnetC
      Layers:
        - !Sub arn:aws:lambda:${AWS::Region}:887080169480:layer:php71:5
        # 自作したレイヤーのARNを設定する             
        - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:eccube_ext:1
        - !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:php71_bootstrap_ex:1
      Environment:
        Variables:
          APP_ENV: prod
          APP_DEBUG: 0
          DATABASE_URL:
            !Sub
              - "mysql://root:hogehogehoge@${EndPoint}/eccube"
              - { EndPoint : !GetAtt Database.Endpoint.Address}
          # ローカルでEC-CUBEをインストールした際に作成された.envファイルを参考に設定する
          DATABASE_SERVER_VERSION: 5
          MAILER_URL: "null://localhost"
          ECCUBE_AUTH_MAGIC: "hogehogehoge"
          ECCUBE_ADMIN_ROUTE: "admin"
          ECCUBE_TEMPLATE_CODE: default
          ECCUBE_LOCALE: ja
          S3_ASSET_BUCKET_NAME: !Ref AssetBucket
sam package --template-file sam.yml --s3-bucket <デプロイに使える適当なS3バケット名> --output-template-file output.yml
sam deploy --template-file output.yml --stack-name eccube --capabilities CAPABILITY_IAM

RDSにDBのダンプを復元

一時的にEC2を立てて、ローカル環境で取得したmysqlのダンプをRDSにリストアします。 詳細な手順は割愛します。

S3に静的コンテンツを同期

SAMテンプレートで作成したS3バケットに静的コンテンツを同期します

aws s3 sync html/template/default/assets s3://<SAMテンプレートで作成したS3バケット名>/assets
aws s3 sync html/upload/save_image s3://<SAMテンプレートで作成したS3バケット名>/save_image

Lambdaのデプロイ

ソースコードの準備ができたのでZIPに固めてデプロイします。

zip -r eccube.zip app bin codeception html src vendor php.ini index.php composer.json
aws s3 cp eccube.zip  s3://<デプロイに使える適当なS3バケット名>/eccube.zip
aws lambda update-function-code  --function-name <Lambda functionの名前> --s3-bucket <上記で指定したS3バケット名> --s3-key eccube.zip

ALBとLambdaの紐付け

ここからは手動でやっていきます。

ターゲットグループの作成

ターゲットグループを作成し、作成したLambda functionを実行するように設定します。「複数値のヘッダー」をONにするのを忘れないで下さい。

リスナーの作成

HTTPでアクセスを受け付けるようにリスナーを作成し、転送先に作成したターゲットグループを指定します。

動作確認

いよいよ動作確認です。 初回アクセス時はキャッシュの生成に結構な時間がかかるので、辛抱強く待ちましょう。

TOP

商品詳細

カート

ログイン

ゲスト購入

注文手続き

注文内容の確認

購入完了

まとめ

LambdaのCustom Runtimesを使ってEC-CUBEの購入フローを動かしてみました。 今回やってみた感想として、画像アップロード系の処理をS3と使うように修正したり、各種メール配信にSESを利用するように設定したりすれば、EC-CUBEのほとんどの機能はLambda上でも動かせそうな感触を得ました。 Custom Runtimesを利用することで、LambdaをPaaSチックに利用する可能性が広がって来たのではないでしょうか?

ログやセッションの保存先変更はステートフルなWebアプリをLambda上で動かす際の共通した注意事項になるので、この記事を参考にWordPressなど他のOSSを動かしてみるのも一興かと思います。