share facebook facebook facebook twitter twitter menu hatena pocket slack

2021.04.19 MON

LaravelをAWS Lambdaで動作させてデータベースにはAurora Serverlessを使ってみた

甲斐 甲

WRITTEN BY 甲斐 甲

LaravelをAWS Lambdaで動作させることができる素敵な時代になっていたので、Laravel SailとLaravel Breezeでログイン機能を実装したプロジェクトをServerless Frameworkを利用してデプロイしてみました。

前提

こちらのGitHubリポジトリを利用してデプロイ環境を構築する手順です。
環境構築後の内容はこちらのブランチにおいています。

手順

Serverless Frameworkのインストール

セットアップスクリプトが提供されているのでそれを利用します。

> curl -o- -L https://slss.io/install | bash
> serverless -v
Framework Core: 2.30.3 (standalone)
Plugin: 4.5.1
SDK: 4.2.0
Components: 3.7.4

Serverless Getting Started Guide
https://www.serverless.com/framework/docs/getting-started/

Brefを利用してAWS Lambda用にLaravelを構成する

Brefというパッケージを利用するとAWS LambdaでLaravelを動作させる環境構築がとても簡単にできます。
Bref – Serverless PHP made simple
https://bref.sh/
brefphp/laravel-bridge: Package to use Laravel on AWS Lambda with Bref
https://github.com/brefphp/laravel-bridge
LaravelをコンテナにしてLambdaでデプロイするのが超簡単になった2021年 – Qiita
https://qiita.com/umihico/items/514cf792d30bf3706ef5
パッケージをインストールしてServerless Framework用の設定ファイルを生成します。

> ./vendor/bin/sail composer require bref/bref bref/laravel-bridge
> ./vendor/bin/sail artisan vendor:publish --tag=serverless-config

serverless.ymlの編集

生成されたserverless.ymlファイルにAurora Serverlessに必要となるリソースを追加します。下記の記事を参考にさせてもらいました。(感謝

Serverless FrameworkでAurora Postgres + VPC Lambda + RDS Proxyをデプロイする | DevelopersIO
https://dev.classmethod.jp/articles/rds-proxy-deploy-with-serverless-framework/
Aurora Serverless をCloudFormationを利用して設置してみた | DevelopersIO
https://dev.classmethod.jp/articles/aurora-serverless-by-cfn/
Brefを使って完全サーバレスな、Symfony + Vue.js + Aurora Serverless製TODOリストを作る – Qiita
https://qiita.com/ippey_s/items/bbb4e329c0a919d1a8b5

serverless.ymlと設定ファイルは下記のようになりました。

serverless.yml

service: laravel

provider:
  name: aws
  # The AWS region in which to deploy (us-east-1 is the default)
  region: ap-northeast-1
  # The stage of the application, e.g. dev, production, staging… ('dev' is the default)
  stage: dev
  runtime: provided.al2

custom:
  defaultStage: dev
  profiles:
    dev: sls-itg
    stg: sls-stg
    prd: sls-prd
  environments: ${file(./config/config.${opt:stage, self:custom.defaultStage}.yml)}
  secret: ${file(./config/secret/.secret.${opt:stage, self:custom.defaultStage}.yml)}

package:
  # Directories to exclude from deployment
  exclude:
    - node_modules/**
    - public/storage
    - resources/assets/**
    - storage/**
    - tests/**

functions:
  # This function runs the Laravel website/API
  web:
    handler: public/index.php
    timeout: 28 # in seconds (API Gateway has a timeout of 29 seconds)
    layers:
      - ${bref:layer.php-74-fpm}
    events:
      - httpApi: "*"
      # This function lets us run artisan commands in Lambda
    vpc:
      securityGroupIds:
        - !Ref LambdaSecurityGroup
      subnetIds:
        - !Ref PrivateSubnetA
        - !Ref PrivateSubnetC
    environment:
      DB_PORT: ${self:custom.environments.DB_PORT}
      DB_HOST: !GetAtt DBCluster.Endpoint.Address
      DB_PASSWORD: ${self:custom.secret.PASSWORD}

  artisan:
    handler: artisan
    timeout: 120 # in seconds
    layers:
      - ${bref:layer.php-74} # PHP
      - ${bref:layer.console} # The "console" layer
    vpc:
      securityGroupIds:
        - !Ref LambdaSecurityGroup
      subnetIds:
        - !Ref PrivateSubnetA
        - !Ref PrivateSubnetC
    environment:
      DB_PORT: ${self:custom.environments.DB_PORT}
      DB_HOST: !GetAtt DBCluster.Endpoint.Address
      DB_PASSWORD: ${self:custom.secret.PASSWORD}

plugins:
  # We need to include the Bref plugin
  - ./vendor/bref/bref

resources:
  Resources:
    # The S3 bucket that stores the assets
    Assets:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.environments.AWS_BUCKET}
    # The policy that makes the bucket publicly readable
    AssetsBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref Assets # References the bucket we defined above
        PolicyDocument:
          Statement:
            - Effect: Allow
              Principal: "*" # everyone
              Action: "s3:GetObject" # to read
              Resource: !Join ["/", [!GetAtt Assets.Arn, "*"]] # things in the bucket
              # alternatively you can write out Resource: 'arn:aws:s3:::<bucket-name>/*'
    ## VPC Resource
    VPC:
      Type: AWS::EC2::VPC
      Properties:
        CidrBlock: 10.0.0.0/24
        Tags:
          - { Key: Name, Value: Sample VPC }
    PrivateSubnetA:
      Type: AWS::EC2::Subnet
      Properties:
        VpcId: !Ref VPC
        CidrBlock: 10.0.0.0/25
        AvailabilityZone: ap-northeast-1a
        Tags:
          - { Key: Name, Value: Sample Private A }
    PrivateSubnetC:
      Type: AWS::EC2::Subnet
      Properties:
        VpcId: !Ref VPC
        CidrBlock: 10.0.0.128/25
        AvailabilityZone: ap-northeast-1c
        Tags:
          - { Key: Name, Value: Sample Private C }
    LambdaSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        GroupDescription: SecurityGroup for Lambda Functions
        VpcId: !Ref VPC
        Tags:
          - Key: "Name"
            Value: "LambdaSecurityGroup"
    AuroraSecurityGroup:
      Type: AWS::EC2::SecurityGroup
      Properties:
        GroupDescription: SecurityGroup for Aurora
        VpcId: !Ref VPC
        SecurityGroupIngress:
          - IpProtocol: tcp
            FromPort: ${self:custom.environments.DB_PORT}
            ToPort: ${self:custom.environments.DB_PORT}
            CidrIp: 10.0.0.0/24
        Tags:
          - Key: "Name"
            Value: "AuroraSecurityGroup"
      DependsOn: VPC
    ## RDS Resource
    DBSubnetGroup:
      Type: AWS::RDS::DBSubnetGroup
      Properties:
        DBSubnetGroupDescription: "SampleDB subnet group"
        DBSubnetGroupName: sampledb-subnet-group
        SubnetIds:
          - !Ref PrivateSubnetA
          - !Ref PrivateSubnetC
    DBCluster:
      Type: AWS::RDS::DBCluster
      Properties:
        DatabaseName: ${self:custom.environments.DB_DATABASE}
        Engine: aurora-mysql
        EngineMode: serverless
        MasterUsername: ${self:custom.secret.USER_NAME}
        MasterUserPassword: ${self:custom.secret.PASSWORD}
        DBClusterParameterGroupName: !Ref DBClusterParameterGroup
        DBSubnetGroupName: !Ref DBSubnetGroup
        VpcSecurityGroupIds:
          - !Ref AuroraSecurityGroup
      DependsOn: DBSubnetGroup
    DBClusterParameterGroup:
      Type: AWS::RDS::DBClusterParameterGroup
      Properties:
        Description: A parameter group for aurora
        Family: aurora-mysql5.7
        Parameters:
          time_zone: "Asia/Tokyo"
          character_set_client: "utf8"
          character_set_connection: "utf8"
          character_set_database: "utf8"
          character_set_results: "utf8"
          character_set_server: "utf8"
    AuroraSecret:
      Type: AWS::SecretsManager::Secret
      Properties:
        Name: Sample/aurora
        SecretString: '{"username":"${self:custom.secret.USER_NAME}", "password":"${self:custom.secret.PASSWORD}"}'
    SecretTargetAttachment:
      Type: AWS::SecretsManager::SecretTargetAttachment
      Properties:
        SecretId: !Ref AuroraSecret
        TargetId: !Ref DBCluster
        TargetType: "AWS::RDS::DBCluster"
      DependsOn: DBCluster

config/secret/.secret.dev.yml

USER_NAME: root
PASSWORD: password

データベース名、バケット名は任意で指定します。

config/config.dev.yml

DB_PORT: 3306
DB_DATABASE: hoge_app
AWS_BUCKET: kai-laravel-test

Lambda関数に環境変数を追加する

Brefが自動生成してくれるLambda関数のリソースにAurora Serverlessへアクセスするための環境変数を設定します。Aurora Serverlessの場合、インスタンスは自動で生成されるので、DBCluster.Endpoint.Addressでエンドポイントを取得します。

functions:
  # This function runs the Laravel website/API
  web:
    handler: public/index.php
    timeout: 28 # in seconds (API Gateway has a timeout of 29 seconds)
    layers:
      - ${bref:layer.php-74-fpm}
    events:
      - httpApi: "*"
      # This function lets us run artisan commands in Lambda
    vpc:
      securityGroupIds:
        - !Ref LambdaSecurityGroup
      subnetIds:
        - !Ref PrivateSubnetA
        - !Ref PrivateSubnetC
    environment:
      DB_PORT: ${self:custom.environments.DB_PORT}
      DB_HOST: !GetAtt DBCluster.Endpoint.Address
      DB_PASSWORD: ${self:custom.secret.PASSWORD}

  artisan:
    handler: artisan
    timeout: 120 # in seconds
    layers:
      - ${bref:layer.php-74} # PHP
      - ${bref:layer.console} # The "console" layer
    vpc:
      securityGroupIds:
        - !Ref LambdaSecurityGroup
      subnetIds:
        - !Ref PrivateSubnetA
        - !Ref PrivateSubnetC
    environment:
      DB_PORT: ${self:custom.environments.DB_PORT}
      DB_HOST: !GetAtt DBCluster.Endpoint.Address
      DB_PASSWORD: ${self:custom.secret.PASSWORD}

クラスターの設定

EngineMode: serverlessと設定するとAurora Serverlessとしてクラスター作成できます。

AWS::RDS::DBCluster – AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html#cfn-rds-dbcluster-enginemode

    DBCluster:
      Type: AWS::RDS::DBCluster
      Properties:
        DatabaseName: ${self:custom.environments.DB_DATABASE}
        Engine: aurora-mysql
        EngineMode: serverless
        MasterUsername: ${self:custom.secret.USER_NAME}
        MasterUserPassword: ${self:custom.secret.PASSWORD}
        DBClusterParameterGroupName: !Ref DBClusterParameterGroup
        DBSubnetGroupName: !Ref DBSubnetGroup
        VpcSecurityGroupIds:
          - !Ref AuroraSecurityGroup
      DependsOn: DBSubnetGroup

DBClusterParameterGroupでcharacter_setを指定していますが、これはMySQL5.7.7未満だと標準のcharasetがutf8mb4となり、unique制約をつけたカラムで文字数オーバーになるのを回避するためです。
詳細は下記が詳しいです。

Laravel5.4以上、MySQL5.7.7未満 でusersテーブルのマイグレーションを実行すると Syntax error が発生する – Qiita
https://qiita.com/beer_geek/items/6e4264db142745ea666f

    DBClusterParameterGroup:
      Type: AWS::RDS::DBClusterParameterGroup
      Properties:
        Description: A parameter group for aurora
        Family: aurora-mysql5.7
        Parameters:
          time_zone: "Asia/Tokyo"
          character_set_client: "utf8"
          character_set_connection: "utf8"
          character_set_database: "utf8"
          character_set_results: "utf8"
          character_set_server: "utf8"

デプロイ

serverless deployコマンドでデプロイします。時間がかかるのでデプロイ中はお茶でものんでまったりと待ちます。

> ./vendor/bin/sail npm run prod
> serverless deploy

デプロイ後、エンドポイントにアクセスして以下のように表示されたらOKです。

静的ファイルの配置

S3バケットにプロジェクトのpublicディレクトリをコピーします。これはAWS Lambda関数へのアクセスだけだと、public/css/app.cssなどへアクセスできないので、デプロイで作成した、アセット用のS3バケットへファイルを配置するためです。
ファイルがないとブラウザでアクセスした場合、ログイン画面へアクセスすると以下のようになります。

> aws s3 sync public s3://kai-laravel-test/public --delete

ファイルコピー後、下記のようになっていたらOKです。

マイグレーション

Brefにはcliコマンドが提供されているので、それを利用してAWS Lambdaのlaravel-dev-artisan関数を利用してマイグレーションします。
cliコマンドはAWSのクレデンシャルファイルを参照するので、sailコマンドを挟まないほうが手っ取り早いです。

> ./vendor/bin/bref cli --region=ap-northeast-1 laravel-dev-artisan -- migrate

動作確認

実際にデプロイした環境にアクセスしています。

今回はメール送信については対応していないので、パスワード変更のためのメール送信はエラーとなります。

リソースの削除

動作確認ができてリソースが不要であれば削除します。S3バケットにファイルがあるまま削除処理を実行するとエラーになるので、ファイルを削除しておきます。

> aws s3 rm --recursive s3://kai-laravel-test/
> serverless remove

まとめ

実用するにはまだまだ対応するべきことがありますが、ひとまずはやりたいことが実現できました。
やったぜ

参考

Serverless Getting Started Guide
https://www.serverless.com/framework/docs/getting-started/
Bref – Serverless PHP made simple
https://bref.sh/
brefphp/laravel-bridge: Package to use Laravel on AWS Lambda with Bref
https://github.com/brefphp/laravel-bridge
LaravelをコンテナにしてLambdaでデプロイするのが超簡単になった2021年 – Qiita
https://qiita.com/umihico/items/514cf792d30bf3706ef5
Serverless FrameworkでAurora Postgres + VPC Lambda + RDS Proxyをデプロイする | DevelopersIO
https://dev.classmethod.jp/articles/rds-proxy-deploy-with-serverless-framework/
Aurora Serverless をCloudFormationを利用して設置してみた | DevelopersIO
https://dev.classmethod.jp/articles/aurora-serverless-by-cfn/
Brefを使って完全サーバレスな、Symfony + Vue.js + Aurora Serverless製TODOリストを作る – Qiita
https://qiita.com/ippey_s/items/bbb4e329c0a919d1a8b5
AWS::RDS::DBCluster – AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html#cfn-rds-dbcluster-enginemode
Laravel5.4以上、MySQL5.7.7未満 でusersテーブルのマイグレーションを実行すると Syntax error が発生する – Qiita
https://qiita.com/beer_geek/items/6e4264db142745ea666f
CreateDBClusterParameterGroup-Amazon Relational Database Service
https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBClusterParameterGroup.html
AWS::RDS::DBInstance – AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#cfn-rds-dbinstance-masterusername
エラー: The policy failed legacy parsing – Qiita
https://qiita.com/sot528/items/f248db0b69c7e1d34220
Cloudformationでエラー: Has prohibited field Resource – Qiita
https://qiita.com/sot528/items/2263fab0dbb55d6ce032
Aurora Serverlessの導入時に気をつけるべきこと | DevelopersIO
https://dev.classmethod.jp/articles/lessons-learned-from-up-and-running-aurora-serverless/
Console commands – Bref
https://bref.sh/docs/runtimes/console.html
Serverless Frameworkで環境変数を外部ファイルから読み込み、環境毎に自動で切り替えてみる | DevelopersIO
https://dev.classmethod.jp/articles/serverless-framework-conf-change/
データベースの使用-Bref
https://bref.sh/docs/environment/database.html
Brefが進化してたのでサーバーレスLaravel環境作ってみた – Qiita
https://qiita.com/ippey_s/items/25129dde8c7fe85479e4
Creating serverless PHP websites – Bref
https://bref.sh/docs/websites.html#architectures

元記事はこちら

LaravelをAWS Lambdaで動作させてデータベースにはAurora Serverlessを使ってみた」)

甲斐 甲

甲斐 甲

2018/7にJOIN。 最近の好みはサーバレスです。なんでもとりあえず試します。

cloudpack

cloudpackは、Amazon EC2やAmazon S3をはじめとするAWSの各種プロダクトを利用する際の、導入・設計から運用保守を含んだフルマネージドのサービスを提供し、バックアップや24時間365日の監視/障害対応、技術的な問い合わせに対するサポートなどを行っております。
AWS上のインフラ構築およびAWSを活用したシステム開発など、案件のご相談はcloudpack.jpよりご連絡ください。