文中使用到的 PlanetScale 資料庫服務,在 3 月 7 號發了一篇文章宣布 4 月 8 號後不再提供免費服務。🥲
假設你今天寫了一個網站,想要放到網路上讓其他人訪問,可以怎麼做?
最傳統的方式,就是找到一台伺服器,並將你的網站放上去運行。聽起來雖然簡單,但要讓程式在伺服器上運行,必須先在伺服器上建立運行程式的環境才行。以 Laravel 這個 PHP 框架為例,我們可以考慮使用常見的 LEMP 環境,即 Linux、Nginx、MySQL 與 PHP 的組合。
網站成功運行起來後,並非所有事情就告一段落了。伺服器需要定期維護,例如定期更新軟體以避免安全性問題。假設網站流量持續增加,你可能需要升級伺服器或增加更多伺服器來分擔流量,這又需要進行額外的負載平衡設定。
綜上所述,為了確保網站的穩定運行,除了寫好程式碼之外,你還需要經常關注伺服器相關事務。然而,如果你不想花太多時間處理伺服器問題,而是希望更專注於程式開發,那麼無伺服器運算 (Serverless) 也許是你可以考慮的部署選擇。這並不代表沒有伺服器存在,而是你無需操心伺服器的維護問題。只需將程式碼上傳,後續的伺服器維護工作都由雲服務供應商處理。
目前大多雲服務商都有 Serverless 的服務,如 Azure 的 Function、GCP 的 Cloud Function以及本篇文章的主角 - AWS Lambda。
前陣子為了玩 Serverless 與省錢,我把用 Laravel 開發的部落格從自架的 K3s 搬家到 AWS Lambda 上。所以本篇文章就來簡單分享一下,要如何把 Laravel 部署到 AWS Lambda 上。
本篇文章會操作 AWS 資源,想跟著做的朋友,請確保你的本機環境已經設定好 AWS 的相關權限 😊。
架構說明
在分享如何部署之前,我們先來看一下架構,並簡略說明架構中使用到的各種服務:
Lambda:為 AWS 的無伺服器運算服務,也是 Laravel 程式碼存放的地方。Lambda 上面的程式被稱為函式 (Function),並根據事件驅動來執行函式,例如用戶發出的請求就是事件的一種,所以 Lambda 是一種函式即服務(Function as a Service, FaaS)。Lambda 本身支援並行 (Concurrency) 功能,所以具有一定的 Auto Scaling 能力。沒有最低消費,根據程式碼執行次數與總執行時間收費,在用量不大的時候,費用很便宜。
Lambda 的優點是沒有低消,而缺點是沒有上限,用量大起來,費用會很驚人。正式環境使用可以搭配 Cloudflare 或 AWS WAF 等服務避免惡意 DDoS 導致帳單飄升的情況。
API Gateway:API Gateway 提供一個統一的訪問入口,讓外部使用者可以訪問部署在 Lambda 上面的服務,此外還提供速率限制 (Rate limiting) 與節流閥 (Throttling) 等功能,可以壓低這兩個數值達到省錢的目的。費用與 Lambda 一樣沒有低消,只需為傳入與傳出的資料量付費,在用量不大的時候,費用也不高。
S3:AWS 的物件存儲服務。Lambda 上面無法存放靜態資源,所以我們需要將其放在 S3 上面。在 Laravel 中只需要修改環境變數 ASSET_URL
,就可以將靜態資源的網址指向 S3 的網址。
DynamoDB:AWS 的 NoSQL 服務,這裡用來當作 Laravel 的 Cache,DynamoDB 是根據儲存量與單位讀寫次數收費,因此同樣在用量不大的時候,費用也不高。
SQS (Simple Query Service):這裡用來當作 Laravel 的 Queue,當寫入新的訊息到 SQS 時,同時也會觸發寫入事件,呼叫 Lambda 函式執行訊息中的任務。一樣根據用量收費,少量使用時費用幾乎可忽略不記。
PlanetScale:提供 MySQL 服務的雲提供商,Hobby Plan 提供 10 億次的資料列讀取次數,與 1000 萬次的資料列寫入次數,只要在這個額度內都是免費的,可以說是省錢的最佳選擇。
根據上述的說明,想必各位讀到這裡也應該看得出來 …
這是個專注於省錢的架構,在用量不大的情況下成本很低。
前置作業
在部署程式碼到 Lambda 之前,我們先來設定資料庫、環境變數與靜態資源。
在 PlanetScale 新增一個資料庫
註冊一個 PlanetScale 帳號後就可以開始來新建資料庫。Plan Type
選擇 Hobby
,輸入資料庫名稱與選擇建置資料庫的地區。注意地區請選擇待會 AWS Lambda 要部署的地區,這裡我選擇東京 (離台灣較近)。
雖然是免費,但還是需要輸入信用卡資訊。
接下來需要選擇框架並建立資料庫密碼。只要在框架上選擇 Laravel,PlanetScale 就會很貼心的顯示一段 .env
格式的資料庫連線資訊。
DB_CONNECTION=mysql
DB_HOST=aws.connect.psdb.cloud
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=<這是機密>
DB_PASSWORD=<這也是機密>
MYSQL_ATTR_SSL_CA=/etc/ssl/certs/ca-certificates.crt
密碼建立好之後就可以透過 GUI 工具或是 CLI 工具與資料庫進行連線。注意連線務必開啟 SSL 加密。如果你原本有資料的話,也可以在這個時候將資料匯入。
還有一點需要注意的是,PlanetScale 預設沒有開啟 Foreign key constraints 功能,需要在 Settings → Beta Features 中開啟。(當然沒用到就不需要開啟該功能)
使用 Systems Manager 的 Parameter Store 儲存敏感資訊
我們可以將一些敏感的資訊放在 AWS Systems Manager 的 Parameter Store 中,例如剛剛拿到的資料庫連線密碼。待會部署 Laravel 到 Lambda 時,可以從 Parameter Store 取得敏感資訊來配置 Lambda 函式的環境變數。
在 Parameter Store 頁面中,點擊 Create parameter 來儲存新的變數。在變數名稱的部分,你可以使用 /
來分類,方便你進行管理。注意 Type
可以選擇 SecureString
進行加密,然後在 Value
的地方輸入想儲存的資訊即可。
將所有想儲存的 Laravel 配置資訊放到 Parameter 上,包含剛剛拿到的資料庫連線資訊。
將靜態資源上傳至 S3 Bucket
Lambda 中無法儲存圖片與前端腳本等靜態資源,我們需要將靜態資源放置於 S3 中。首先建立一個 S3 Bucket ,然後使用 AWS CLI 的 S3 指令將靜態資源上傳上去。
# 移動到 Laravel 專案目錄下
cd your_laravel_project
# 打包前端資源
npm install && npm run build
# 將 Laravel 專案底下的 public 資料夾上傳到 S3 Bucket 中
aws s3 sync public s3://<你的 S3 Bucket 名稱>
接下來只要在 Laravel 中將 ASSET_URL
環境變數修改為 S3 Bucket 的網址,網頁上的靜態資源網址就會變成 S3 Bucket 的網址。
使用 Serverless 與 Bref 將 Laravel 部署至 Lambda
終於來到最重要的部分,將 Laravel 部署到 Lambda。首先在專案底下透過 npm
安裝 Serverless 工具。我們需要這個工具將 Laravel 部署到 Lambda。
# 下面兩種方式都可以,但我偏好只在專案中安裝,後續的操作也會以第一種方式為主
# 在專案中安裝 serverless
cd your_project
npm install serverless
# 在系統中安裝 serverless
npm install serverless -g
除了 serverless
,我們還需要安裝 serverless-lift
這個 Serverless 套件來建立 SQS 佇列。
npm install serverless-lift -D
因為 AWS Lambda 預設沒有 PHP 的 Runtime,所以我們需要使用社群維護 Lambda Runtime - Bref,來讓 Laravel 在 Lambda 上面運行起來。
使用 Composer 安裝 Bref。
composer require bref/bref bref/laravel-bridge --update-with-dependencies
Bref 安裝好之後,我們在專案底下新增一個描述檔 serverless.yaml
。這個描述檔會定義各種 AWS 資源,除了會將 Laravel 部署到 Lambda,還會建立 API Gateway 、DynamoDB 與 SQS 等相關資源。
首先設定要部署的服務名稱與此次部署會使用到的套件。
# serverless.yaml
# 設定要部署的服務名稱,Serverless 會根據這個名稱來追蹤服務的狀態
# 所以服務名稱必須是唯一的,不能與其他 Serverless 服務重複
service: docfunc
plugins:
- ./vendor/bref/bref
- serverless-lift
設定 AWS 的 Provider 與所有 Lambda 函式的環境變數。
provider:
name: aws
stage: production
# AWS 資源的地區選擇與 PlanetScale 相同的地區,減少連線延遲
region: ap-northeast-1
# API Gateway 建立好之後預設會生成一段可以訪問的網址
# 可以使用 Custom Domain 這個功能使用自訂的網址,並將這個網址設定為禁止訪問
httpApi:
disableDefaultEndpoint: false
# 省錢小撇步,如果伺服器 CPU 架構不影響你的程式
# 可以考慮使用 AWS 的 ARM CPU,價格更便宜
architecture: arm64
# 設定要給 Lambda 的 IAM 角色,我的部落格需要有存取 S3 與 DynamoDB 的權限
# S3 是我用來存放文章圖片的地方,DynamoDB 是我用來存放 Cache 的地方
# Lambda 不允許設定 AWS_ACCESS_KEY_ID 與 AWS_SECRET_ACCESS_KEY 這兩個環境變數
# 所以只能透過 IAM 角色來存取 AWS 資源
iam:
role:
statements:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource:
# ${ssm:} 是 serverless 提供的特殊語法
# 讓我們可以從 AWS Systems Manager 的 Parameter Store 中取得變數內容
- arn:aws:s3:::${ssm:/docfunc/production/aws-bucket}/*
- Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
# 這裡的 Resource 指定我們要存取的 DynamoDB 資源
# 待會會在 resources 區塊中建立這個資源
Resource: !GetAtt CacheTable.Arn
# Serverless 預設會將 Lambda 的 Log 存放在 CloudWatch,我們可以設定 Log 的保留天數
logRetentionInDays: 1
# Lambda 函式的環境變數設定
# Bref 預設會幫你開啟 stderr log,所以不需要特別設定 LOG_CHANNEL
environment:
# App settings
APP_NAME: DocFunc
APP_ENV: production
APP_KEY: ${ssm:/docfunc/production/app-key}
APP_DEBUG: false
APP_URL: ${ssm:/docfunc/production/app-url}
# 靜態資源網址為剛剛建立的 S3 Bucket 網址
ASSET_URL: ${ssm:/docfunc/production/asset-url}
# Database
DB_CONNECTION: mysql
DB_HOST: ${ssm:/docfunc/production/database-host}
DB_PORT: ${ssm:/docfunc/production/database-port}
DB_DATABASE: ${ssm:/docfunc/production/database-name}
DB_USERNAME: ${ssm:/docfunc/production/database-username}
DB_PASSWORD: ${ssm:/docfunc/production/database-password}
# 使用 Bref 的金鑰來加密資料庫連線
MYSQL_ATTR_SSL_CA: /opt/bref/ssl/cert.pem
# Cache
CACHE_DRIVER: dynamodb
DYNAMODB_CACHE_TABLE: !Ref CacheTable
# Session
SESSION_DRIVER: dynamodb
SESSION_LIFETIME: 120 # minutes
# Queue
QUEUE_CONNECTION: sqs
# 待會會在 construct 區塊中建立 SQS 佇列
SQS_QUEUE: ${construct:jobs.queueUrl}
# Filesystem
FILESYSTEM_DISK: s3
AWS_BUCKET: ${ssm:/docfunc/production/aws-bucket}
AWS_URL: ${ssm:/docfunc/production/aws-url}
AWS_USE_PATH_STYLE_ENDPOINT: false
建立用來做 Cache 的 DynamoDB 與用來存放 Laravel Job 的 SQS 佇列。
# 建立 DynamoDB 資源
resources:
Resources:
CacheTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
BillingMode: PAY_PER_REQUEST
TimeToLiveSpecification:
AttributeName: ttl
Enabled: true
KeySchema:
- AttributeName: id
KeyType: HASH
# 為 serverless-lift 套件提供的功能
# 我們可以使用 SQS 佇列來觸發 Lambda 函式
# 當 Lambda 函式寫入新的訊息到 SQS 時,就會觸發另外一個 Lambda 函式執行 SQS 訊息中的任務
constructs:
jobs:
type: queue
worker:
handler: Bref\LaravelBridge\Queue\QueueHandler
runtime: php-83
timeout: 60 # in seconds
想要在 Laravel 中使用 DynamoDB 當作 Cache,需要修改 config/cache.php
中的 DynamoDB 設定。
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
// 加上這一段設定
'attributes' => [
'key' => 'id',
'expiration' => 'ttl',
]
],
為了避免上傳不必要的檔案到 Lambda 上,我們可以在 package
中定義不想上傳檔案的 Patterns。
package:
# 避免將不必要的檔案上傳到 Lambda
patterns:
- '!node_modules/**'
- '!public/storage'
- '!resources/assets/**'
- '!storage/**'
- '!tests/**'
- '!.env'
最後設定 Lambda 函式。
# 這裡設定我們要部署的 Lambda 函式
functions:
# 部署可以讓用戶訪問的 Web 函式
web:
handler: public/index.php
# 使用 Bref 的 PHP FPM Runtime 處理請求
runtime: php-83-fpm
timeout: 28 # in seconds
# 如果有請求進來就觸發該函式來處理請求
events:
- httpApi: '*'
# 如果想執行 Laravel 的 Artisan Command,可以透過這個函式來執行
artisan:
handler: artisan
runtime: php-83-console
timeout: 720 # in seconds
# 設定一個定時任務,讓 Lambda 定期執行 Laravel 的 Schedule 任務
events:
- schedule:
rate: rate(1 hour)
input: '"schedule:run"'
serverless.yaml
寫好之後就可以使用 serverless deploy
指令,將 Laravel 部署到 Lambda 上。
# 部署之前,先清空開發環境上的快取
php artisan config:clear
# 開始進行部署
node_modules/.bin/serverless deploy
Serverless 會使用 AWS 的 Cloudformation 來建立 AWS 資源。部署成功之後,可以看到Serverless 建立了一個包含了三個函式的 Lambda 應用程式 (Application),並給了一段 API Gateway 的訪問網址。直接訪問網址就可以看到你部署的 Laravel 網站了!
Lambda 與 Bref 的原理
Lambda 的背後是使用由 AWS 開發 Firecracker 虛擬化技術,這種技術兼具容器的快速啟動與傳統 VM 的安全隔離。我們上傳的程式碼都會放在用 Firecracker 啟動的微型 VM (Micro VM),並透過事件驅動來執行。
如剛剛提到的,Lambda 原生並不提供 PHP 的 Runtime,那麼 Bref 是怎麼做到讓 PHP 應用在 Lambda 上面跑起來的呢?
這裡就要說說 Lambda Layer 這個功能。Bref 將 PHP 環境打包成一個 zip 檔案後上傳到 Lambda Layer,Lambda 會將這包 zip 檔案解壓縮後放置在 /opt
資料夾底下,因此 Lambda 函式就可以直接使用 /opt
資料夾底下的 PHP 環境。
如果你登入 AWS,並查看剛剛部署的 web 函式,你會發現它使用了 arm-php-83
的 Layer。
Layer 的好處在於可以同時給多個 Lambda 函式使用,複用性很高。如果你有一些工具想使用,但 Lambda 本身並沒有提供,就可以將其包成一個 zip 檔案並上傳到 Layer。
想知道更多 Bref 與 Lambda 背後的技術,可以看看 Bref 的作者在 PHP UK Conference 上的分享。
小小補充
Bref 除了上述提到的部分,還提供多種功能與部署選擇。
執行 Laravel Artisan 指令
如果你想要使用 artisan
函式來執行指令,例如執行資料庫遷移,可以使用 bref:cli
指令。
# 注意指令不能有交互式的互動,如果你的 APP_ENV 設定為 production
# 應該使用 --force 參數來避免需要輸入 yes 進行二次確認的情況
node_modules/.bin/serverless bref:cli --args="migrate --force"
使用 Laravel Octane
Bref 支援 Laravel Octane,而且不需要安裝 Swoole 或是 RoadRunner。Octane 支援持久性資料庫連接的功能,可以降低建立資料庫連線的成本。如果想提升網站的回應速度可以考慮使用。
functions:
web:
# 如果你有使用 Laravel Octane,你可以使用 Bref 提供的 OctaneHandler
handler: Bref\LaravelBridge\Http\OctaneHandler
runtime: php-83
# Bref OctaneHandler 提供持久性資料庫連接的功能
environment:
BREF_LOOP_MAX: 250
OCTANE_PERSIST_DATABASE_SESSIONS: 1
timeout: 28 # in seconds (API Gateway has a timeout of 29 seconds)
events:
- httpApi: '*'
在函式執行時才配置敏感資訊
剛剛設定敏感資訊配置的方式,是從 SSM 取得資訊後設定為 Lambda 的環境變數。如果想要更安全一點,Bref 提供冷啟動 (Cold Start) 的方式來設定敏感資訊的配置。不使用 Lambda 的環境變數,而是讓函式在第一次執行的時候去 SSM 取得敏感資訊。
Bref 也有提供冷啟動的功能,有興趣的人可以查看官方文件。我們公司內部也主要採用這種方式設定敏感資訊的配置 (但不是用在 Laravel 上就是了 😆)。
在 Lambda 上安裝其他 PHP 擴充套件
Bref 原本的 Runtime 只會包含常用的擴充套件 (Extension),如果你想安裝 PHP 的 Redis 擴充套件來操作 Redis,可以使用 Extra PHP Extension 這個套件。它會在 Lambda 上面新增一個包含 Redis Extension 的 Layer。
首先使用 Composer 安裝 Extra PHP Extension。
composer require bref/extra-php-extensions
然後修改 serverless.yaml
檔案。
# serverless.yml
service: docfunc
plugins:
- ./vendor/bref/bref
- serverless-lift
# 新增這個 Serverless 套件
- ./vendor/bref/extra-php-extensions
# ...
functions:
web:
handler: public/index.php
runtime: php-83-fpm
layers:
# 在函式這裡新增一個包含 PHP Redis 擴充套件的 Layer
- ${bref-extra:redis-php-83}
timeout: 28
events:
- httpApi: '*'
這種方法雖然方便,但是每個 Lambda 函式最多只能有 5 個 Layer。如果你需要安裝 2 個甚至是 3 個 PHP 擴充套件,建議自己包一個 Layer 上傳到 Lambda。
進入維護模式
Bref 在 Laravel 上也提供進入維護模式的功能,類似於 Laravel 本身提供的 php artisan down
指令。只要在 Lambda 中將環境變數 MAINTENANCE_MODE
設定為 1
就能讓你的網站進入維護模式。
可以在 serverless.yaml
中加入 MAINTENANCE_MODE
這個環境變數。
provider:
environment:
MAINTENANCE_MODE: ${param:maintenance, 0}
並在部署時,指定參數內容。
# 完整部署,透過 CloudFormation 更新所有的 Lambda Function
serverless deploy --param="maintenance=1"
# 快速更新所有 Lambda Function 的設定
serverless deploy function --function=web --update-config --param="maintenance=1"
serverless deploy function --function=artisan --update-config --param="maintenance=1"
serverless deploy function --function=<function-name> --update-config --param="maintenance=1"
看看另外一種架構,使用 RDS 與 ElastiCache
如果不想讓資料庫的訪問走公共網路, 可以將 PlanetScale 換成 AWS 的資料庫服務 RDS。
如果想加快 Cache 的讀寫速度,可以將 DynamoDB 換成 ElastiCache。
要讓 Lambda 連線到 RDS 與 ElastiCache,就需要把它們放在同一個 VPC 底下。但放在 VPC 底下的 Lambda 就無法直接訪問外部網路,如果你有使用 HTTP Client 在後端呼叫外部服務的 API,就會遇到連線問題。解決辦法是讓 Lambda 透過 NAT Gateway 訪問外部網路。
這個架構裡面的 NAT Gateway、RDS 與 ElastiCache 都有一定的基本費用,成本上會比剛剛的架構貴不少。
RDS 與 ElastiCache 都可以透過開小台一點的機器來省錢,但即使是最便宜的等級,一個月也都要 10 美金左右。NAT Gateway 一個月保底 30 美金,流量費用另外計算。
使用 GitHub Action 部署 Lambda
Serverless 有官方的 Action 可以使用,但其實直接用 npm
安裝 serverless
並執行 deploy
指令也沒問題。
這裡我使用 OIDC (OpenID Connect) 來取得操作 AWS 的權限。可以參考我的另一篇文章了解怎麼在 GitHub Action 中使用 OIDC 取得 AWS 權限。
name: Deploy to AWS Lambda
on:
# 測試結束後觸發部署流程
workflow_run:
workflows: [ "Run tests" ]
types:
- completed
# 使用 JWT 取得 OIDC token
permissions:
id-token: write
contents: read
jobs:
deploy-lambda:
# 如果測試失敗,則不執行此流程
if: ${{ github.event.workflow_run.conclusion == 'success' }}
environment: production
name: Deploy to AWS Lambda
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure PHP 8.3
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
coverage: none
- name: Install php packages
run: composer install --prefer-dist --optimize-autoloader --no-dev
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '21'
- name: Install javascript packages and build the frontend assets
run: |
npm install
npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::154471991214:role/github_action
aws-region: ap-northeast-1
- name: Deploy to AWS Lambda
run: |
npm install serverless serverless-lift
node_modules/.bin/serverless deploy --stage production
- name: Copy files to the S3 with the AWS CLI
run: |
aws s3 sync public s3://assets.docfunc.com
PlanetScale 宣布不再提供免費額度之後,決定改用 Neon 這個提供 Serverless PostgreSQL 服務的平台,資料庫也從 MySQL 換成 PostgreSQL。
有空的話,會再寫一篇如何轉移的文章。😀