AWS STUDY NOTE

Deploy Laravel to Lambda With Bref by AWS CDK

試著用 AWS CDK 來部署搭配 Bref 的 Laravel 網站到 AWS Lambda 上去,紀錄整個過程,包括所有遇到的問題。

Azole (小賴)

--

這個練習會需要有 AWS CDK 跟 Bref 的基礎了解(很基礎就可以了),如果還不知道這兩個是什麼,可以參照下方列出的資料去玩玩看。另外,也需要知道 AWS Lambda 與 AWS API Gateway。

Laravel

Laravel 是一個被廣泛使用的 PHP 框架,有龐大的社群支援,我自己也用 Laravel 開發過很多個網站。

Bref

AWS Lambda 並不直接支援 PHP,必須要自己做 Custom Runtime,還蠻麻煩的。Bref 就是來協助我們解決這個問題的,只需要用 composer 安裝一下,就可以利用 Bref 的 PHP Runtime Layer 來把 PHP 專案做成 Serverless。

Bref 依賴於 PHP 7.2+ 跟 serverless framework。如果不想用 serverless framework,你可以手動部署、用 CloudFormation 或是用 AWS CDK(本文練習的目標)。

AWS CDK

AWS CDK 是一個開發框架,可以幫助我們實現 ”Infrastructure IS Code”,有注意到嗎? 是 IS,不是 AS。因為 AWS CDK 可以讓我們用自己熟悉的語言來開發雲端部署,目前支援的語言有 TypeScript, Javascript, Python, JAVA, C#。

今天要做什麼

這次的練習主要是想要用 AWS CDK 來部署一個搭配 Bref 的 Laravel 專案。如果你有玩過 AWS CDK,相信你會跟我一樣喜歡 AWS CDK,用物件導向、模組化的方式來開發部署,真的是很好玩,也有很多優點可以享受,作為一個開發工程師,很難不被吸引啊~

怎麼開始呢

以下是我個人的歷程,也許你可以試試直接開始,不一定要按照這樣的步驟做,我個人是好奇 bref 跟 serverless framework 做了什麼。

首先,先試著把這個 github repo https://github.com/azole/laravel58-bref 裡的步驟完成。完成以後,你應該已經能成功地用 Bref 跟 serverless framework 來部署一個 Laravel 專案了。serverless framework 在部署到 AWS 時,底層其實也是依賴於 AWS CloudFormation,所以在 serverless deploy 被執行過後,可以在 ./serverless 這個檔案夾中看到兩個 AWS CloudFormation Template json 檔案被建立:

  • ./serverless/cloudformation-template-create-stack.json 這只是建立一個 S3 Bucket,好用來放後續需要上傳的程式碼。
  • ./serverless/cloudformation-template-update-stack.json 這一個就複雜多了,也是我們要研究的重點。

為了簡化一點流程,我把 serverless.yml 裡、建立 artisan lambda function 的部分移掉,單純先看網頁的部分,想要一起做的朋友,可以參考這裡 https://github.com/azole/laravel58-bref-cdk/blob/master/doc/serverless.yml

用了這個新的 serverless.yml 後,需要重新部署一下,讓他產生新的 json檔,觀察一下這個 json 檔,即便我們已經簡化過、只部署了網站這個 lambda,它仍需要做以下這些工作任務:

  • ServerlessDeploymentBucket: 建立用來放 Lambda 程式碼的 S3 Bucket
  • WebsiteLogGroup: 建立一個 CloudWatch Log Group
  • IamRoleLambdaExecution: 建立一個給 lambda function 用的 IAM role
  • WebsiteLambdaFunction: 要被部署的 lambda function
  • WebsiteLambdaVersiongXXXXXX: 為剛剛建立的 lambda function 建立版本
  • ApiGatewayRestApi: 建立一個 API Gateway Rest API
  • ApiGatewayResourceProxyVar: 為剛剛那個 Rest API 建立一個 proxy resource
  • ApiGatewayMethodAny: 為剛剛那個 Rest API 設定一個 ANY method
  • ApiGatewayMethodProxyVarAny: 為剛剛那個 proxy resource 設定一個 ANY method
  • ApiGatewayDeploymentXXXX: 部署這個 Rest API
  • WebsiteLambdaPermissionApiGateway: 開權限給這個 Rest API

總共有 11 個動作要做,json 檔案有 347 行。(可以參考這裡: https://github.com/azole/laravel58-bref-cdk/blob/master/doc/cloudformation-template-update-stack.json

藉由分析 serverless framework 所建立出來的 CloudFormation Template json檔案,我們大致上知道了要做哪些事才能取代 serverless framework,接下來讓我們來試試看怎麼用 AWS CDK 完成這些步驟吧。

1. 建立一個採用 Bref 的 Laravel 專案

在 PHP 7.2+的環境下,執行以下命令:

composer create-project laravel/laravel laravel58-cdk --prefer-distcd laravel58-cdkcomposer require bref/bref

並且對這個專案做一些 Bref 需要的調整,這個請參考這篇文章: Deploy Serverless Laravel by Bref (中文),如果沒有 PHP 7.2+ 的環境,文章裡也提供了一個 github repo,有附上一個 Dockerfile,讓你用 docker container 來解決環境問題。

2. Create a CDK Deploy Project

回到跟 Laravel 專案同一層的檔案夾(或是,你找得到的地方就好),建立一個 AWS CDK專案,如果不知道 CDK 怎麼用,請參考 CDK 初探 這篇文章。

mkdir laravel58-bref-cdk && cd laravel58-bref-cdkcdk init -l typescriptcdk bootstrap

3. Deploy a Lambda Function

在剛剛列出那麼多的工作項目中,最核心的應該還是把 Laravel 專案給放到 Lambda 上去,所以,第一步我們就先來完成這個吧。

如果有先看過 AWS CDK 的文件,或是看過 CDK 初探這篇文章,就會知道這件事一點都不難,大概就是查找 AWS CDK API 文件、安裝需要的 package、引用他。

npm i @aws-cdk/aws-lambda

安裝完需要的 package 後,用你心愛的編輯器打開 lib/laravel58-bref-cdk-stack.ts(看你用什麼專案名稱),開發以下程式碼:

import cdk = require('@aws-cdk/core');
import lambda = require('@aws-cdk/aws-lambda');
export class Laravel58CdkDeployStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Get Bref layer ARN from https://runtimes.bref.sh/
// At this page, select correct Region and PHP version
const phpRuntimeLayer = lambda.LayerVersion.fromLayerVersionArn(this, 'lambda-php72-runtime', 'arn:aws:lambda:us-west-2:209497400698:layer:php-72-fpm:10');
const fn = new lambda.Function(this, 'CDK-Bref-fn', {
runtime: lambda.Runtime.PROVIDED, // for custom runtime
code: lambda.Code.fromAsset('../laravel58-cdk'),
handler: 'public/index.php',
layers: [phpRuntimeLayer],
});
}

這份程式碼做了以下三件事:

  • 引用需要的 package
  • 找到我們要用的 PHP Runtime Layer,因為我們是用 Bref,Bref 提供了不同版本跟區域的 AWS Lambda Layer 給我們用,大家可以在這裡https://runtimes.bref.sh/ (Bref 真的好佛心啊~)
  • 最後,就是部署這個 lambda 函式。這邊要特別注意 code 的設定,記得要改成你放 laravel 專案的地方

是不是很簡單呢~

4. 測試與調整 AWS Lambda 的設定

當你用 AWS CDK 部署完成後,應該可以在 AWS Lambda 上看到有一個新增的 function,可以進去檢查看看,是不是如我們所預期的,有正確地引用了 Bref 的 PHP Runtime Layer 等。

如果要測試,可以選擇 Amazon API Gateway AWS Proxy 這個 template,然後把裡面的 path 跟 httpMethod 改一下:

{
"body": "eyJ0ZXN0IjoiYm9keSJ9",
"resource": "/{proxy+}",
"path": "/",
"httpMethod": "GET",
"isBase64Encoded": true,
....
}

然後就可以點下去測試。

結果,爆炸了,你會得到一個像是這樣的錯誤訊息:

很明顯,是 timeout 了。

我先試著手動在 AWS Console 畫面上去調整 Memory Size 跟 timeout,分別設定成 1024 MB 跟 30 秒,儲存、測試。

成功獲得 200 OK

確定是要調整 Memory 跟 Timeout 後,就來試試看怎麼用 AWS CDK 設定,總不能每次都在部署完 Lambda Function 後都在去 AWS Console 畫面手動設定一次對吧。

參考以下文件:

const fn = new lambda.Function(this, 'CDK-Bref-fn', {
runtime: lambda.Runtime.PROVIDED, // for custom runtime
code: lambda.Code.fromAsset('../laravel58-cdk'),
handler: 'public/index.php',
layers: [phpRuntimeLayer],
// set timeout to 30 seconds
timeout: Duration.seconds(30),
// set memory to 1024 MB
memorySize: 1024,

});

部署、測試、完成。

5. 建立 Rest API 跟設定 ANY Method

到目前為止,其實我們已經成功地利用 Bref 跟 AWS CDK 把一個 laravel 網站放到 AWS Lambda 上去了,也就是完成了 IamRoleLambdaExecution 跟 WebsiteLambdaFunction 這兩項任務。

但目前仍無法用瀏覽器打開這個 Laraval 網站,我們需要 AWS API Gateway 來協助我們把這個網站發布出去。

首先,我們先建立一個 Rest API 跟幫這個 Rest API 與為這個 Rest API 設定一個 ANY method,但要怎麼做呢?文件是你的好朋友,參考以下這些文件:

// ... after lambda function// ApiGatewayRestApi
const api = new apigw.RestApi(this, 'CDK-Bref-api', {
endpointTypes: [apigw.EndpointType.EDGE],
});
// Integration with Lambda function
const postAPIIntegration = new apigw.LambdaIntegration(fn, {
proxy: true,
});
// ApiGatewayMethodAny
api.root.addMethod('ANY', postAPIIntegration);

是不是很簡單,只要三個函式,就成功地建立的一個 API Gateway 的 Rest API、為他設定了一個 ANY method、幫這個 ANY method 綁定了先前部署的 lambda function。

至於有些設定為什麼會知道要這樣設,其實我也是參考 serverless framework 產出的 CloudFormation Template 跟另外去研究一些 AWS API Gateway 文件才知道的。

用 CDK 部署一下目前的進展,在 AWS Console > API Gateway 裡會找到這個被部署上去的 Rest API,進到 Stages、找到網址:

AWS API Gateway

點開他:

到目前為止,我們已經成功地把 Laravel 網站部署上 AWS Lambda,也發佈出去了。

6. 建立 Proxy Resource

一個網站應該不會只有首頁,改一下網址、試試看其他網頁,例如 http://[Invoke URL]/prod/test,會得到這樣的訊息:

{
"message": "Missing Authentication Token"
}

上網做了一點研究*,發現是需要幫這個 Rest API 建立一個 Proxy Resource,關於這一點,請參考一下網頁,本文主題在怎麼用 AWS CDK 部署 Laravel 專案,其他的就暫且不表。

註:其實 serverless framework 產生的 CloudFormation Template json file 乙也已經有提示要建立 Proxy Resource 了喔。

在 AWS CDK 要做到這件事,也真的是非常簡單,少少的兩行就解決了。

...
// ApiGatewayResourceProxyVar
// create a proxy resource
const resource = api.root.addResource("{proxy+}");
// set a ANY method to this proxy resource
resource.addMethod('ANY', postAPIIntegration);
...

再一次,部署、測試。

現在你如果打開 http://[Invoke URL]/prod/test,會得到:

Laravel 404 Response

這裡拿到 404 是因為我們沒有開發 test 這個 route,Laravel 網站回我們 404,至少 Request 是送到 Laravel 了。

去 Laravel 專案加一下這個 route 的處理,然後回到 CDK 專案、重新部署一下:

小結

來跟我們一開始從 serverless framework 拿到的 CloudFormation Template json file 比較一下,我們沒有在 AWS CDK 中開發到以下這些任務:

  • There is no need to create S3 bucket.
  • There is no need to handle IAM Role and Permission.
  • There is no need to deploy API Gateway.
  • There is no need to grant invoke permission to the API

那沒有做嗎?並不是,而是 AWS CDK 封裝掉了這些工作。

還記得嗎,那個 json file 有 347 行,而 AWS CDK,我自己這邊只寫了 43 行,包含空白與註解。

當然,你還是可以用 Bref 原本搭配的 serverless framework,這也是一個好工具,下一節,我會展示為什麼我這麼喜歡 AWS CDK。

優化:模組化

AWS CDK 的優點在他的官網及 CDK 初探這篇文章中,都有提及了,其中很重要的一點是,它是程式碼,也就是你可以模組化它。

在 CDK 專案中,讓我們在 lib 檔案夾裡建立一個名叫 php-deployer.ts 的檔案,先在裡面準備好這些內容:

import cdk = require('@aws-cdk/core');export interface PHPDeployerProps {
phpRuntimeLayerARN: string,
phpCodePath: string,
handler: string,

}
export class PHPDeployer extends cdk.Construct { constructor(scope: cdk.Construct, id: string, props: PHPDeployerProps) {
super(scope, id);
// 準備開發 construct 的地方 }
}

然後回到 Laravel58CdkDeployStack class,把程式都搬過來,把原本寫死的字串改成參數,完整程式碼請參考 lib/php-develpers.ts

編輯 laravel58-cdk-deploy-stack.ts,改成用我們自己開發的這個 PHPDeployer:

import cdk = require('@aws-cdk/core');
import { PHPDeployer } from './php-deployer';
export class Laravel58CdkDeployStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Get Bref layer ARN from https://runtimes.bref.sh/
new PHPDeployer(this, 'HelloPHPDeployer', {
phpRuntimeLayerARN: 'arn:aws:lambda:us-west-2:209497400698:layer:php-72-fpm:10',
phpCodePath: '../laravel58-cdk',
handler: 'public/index.php'
});

}
}

可以把原本部署的 stack 用 CDK destroy 刪掉,改用這個版本重新部署一次看看。

進一步,我們可以把 PHPDeployer 發佈、分享出去,例如做成一個 node package 發佈到 npm 去。

是不是很棒啊~

結論

在這個練習中,我們試著用 AWS CDK 把一個搭配 Bref 的 Laravel 專案部署到 AWS Lambda 上去,其實原本 Bref 用 serverless framework 已經很棒了,但我想用自己喜歡的 AWS CDK 來試試看,並且比較一下兩種做法的感受。

我的個人意見是,我喜歡 AWS CDK,大概是因為我是開發工程師,用程式語言還是我最熟悉的方式。

你可以自己親自動手做、自己親自感受,看看哪一種方式最適合你、最適合你的團隊。沒有完美的解答,只有這個方法與工具是不是你想要的、有沒有解決到你的問題。

享受整個過程,總是會學到比自己預期還要多的東西。#工程師小確幸

補充

完整的程式碼與過程紀錄在這個 github repo: laravel58-bref-cdk,這裡面還有多了一個小節,是記錄怎麼用 AWS CDK 來開啟 API Gateway 的 Log,還記得我們還沒建立 Proxy Resource 時,開啟首頁以外的頁面會拿到 “Missing Authentication Token” 這個錯誤訊息嗎?我當時想看看 Log 中會不會有詳實地記錄,老實說,我對這個錯誤訊息有點疑惑,跟沒有建立正確地 Proxy Resource 連不起來。

不過很遺憾,雖然大多數的時候,Log 是除錯的好幫手,但在這裡、這個錯誤,竟然不會被 AWS API Gateway 給記錄起來。所以本文中就略過不提了,在 repo 的 README 裡還是詳實地記錄了,工程師不會有白走的路,至少學會了怎麼開啟 log。

--

--

Azole (小賴)

As a passionate software engineer and dedicated technical instructor, I have a particular fondness for container technologies and AWS.