写PHP写了快十年,之前每次听到Serverless都觉得这是Node、Go那帮人的主场。直到我用Serverless Framework和Bref把一个PHP接口真正部署到AWS Lambda上,才发现PHP在无服务器场景下并没有想象中那么别扭。这篇文章打算用一个极其简单的HTTP示例,走完本地开发到线上部署的完整流程,同时把为什么不那么顺的地方也讲透。适合想试试Serverless PHP但不知道从哪里下手的后端工程师,也适合正在评估是否把现有小接口迁移到Lambda的人。
1. 先搞明白:Lambda上跑PHP究竟靠什么
1.1 自定义运行时这块拼图
AWS Lambda原生支持的运行时里没有PHP,但Lambda留了一个通用入口:自定义运行时。说简单点,Lambda只要求你提供一个可执行文件,这个文件必须实现一套叫Lambda Runtime API的HTTP协议,负责从Lambda控制面拉取事件、把结果传回去。至于控制面内部跑的是Java、Python还是PHP,它不关心,只要执行环境里有这个二进制能跑就行。
Bref做的事,就是把一个预编译好的PHP解释器,连同那个bootstrap可执行文件,一起打包成Lambda Layer。部署的时候Bref插件会把层挂到你的函数上,于是Lambda沙箱内部就有了PHP能力。请求到达以后,bootstrap进程通过Runtime API拿到事件,再用某种方式触发PHP代码,最终把结果返回。整个过程不需要你自己维护EC2,也不需要去管PHP-FPM进程常驻在哪个服务器上。
这里有个容易误解的地方:很多人以为Serverless PHP就是“把php运行在容器里”。其实Lambda的隔离环境本身确实像一个小容器,但计费粒度是毫秒级请求,不是按分钟租虚拟机。Bref补上的,正是“事件进来之后如何通知PHP代码执行”这段胶水层。所以站在纯后端工程师角度,你关心的不再是“进程挂了要不要重启”,而是“请求进来后,我的代码怎么拿到这个事件”。
1.2 Bref的两种运行时壳:事件函数和PHP-FPM
Bref根据你函数入口的不同,提供了两种使用方式。理解这个区别,比你急着抄配置重要得多。
第一种叫事件函数,对应的运行时识别名类似于php-82。它主要处理S3文件上传、SQS消息、EventBridge定时任务这类事件。入口文件返回一个callable,Lambda每次调用就执行这个callable,参数是AWS传过来的原始事件数组。这适合写后台任务,不需要模拟HTTP环境。
第二种叫HTTP应用,对应的运行时识别名是php-82-fpm。Bref在Lambda环境里启动了一个真实的PHP-FPM进程,把API Gateway或ALB转发过来的HTTP事件,翻译成PHP-FPM能理解的传统HTTP请求。你的代码就像跑在一个普通PHP-FPM服务器上一样,能用$_GET、$_POST、php://input这些原生的东西。对老PHP项目来说,这种形态迁移成本最低。
我把两者放在一张表里对比:
| 形态 | 运行时识别名 | 前缀示例 | 典型入口 | 数据从哪来 |
|---|---|---|---|---|
| 事件驱动函数 | php-82 | SQS、EventBridge、S3 | src/worker.php | Lambda事件数组 |
| Web应用 | php-82-fpm | API Gateway、ALB | public/index.php | 超全局变量、请求头、php://input |
第一版Bref出来的时候,很多示例文件写得像事件函数那样返回一个数组。做HTTP API你也想用自己的框架、用现有的路由,那就走PHP-FPM模式更自然。因为框架底层依赖的$_SERVER['REQUEST_METHOD']、getallheaders()这些,只有在FPM模式下才完整可用。自己解析API Gateway事件数组再一笔笔手工拼HttpFoundation Request,那工作量就上去了。
1.3 什么类型的业务适合立刻上这套组合
不是所有PHP应用塞进Lambda都合适。就我实际体验,下面这几类场景收益最大:
- 低频但会被偶然调到的API,比如管理后台的回调接口、内部审批通知接口。
- 定时任务,每天跑一次数据汇总,跑完就释放,省去一台24小时开机的服务器。
- 事件型消费者,比如从消息队列读消息做通知推送。
- 短生命周期工具页或对外H5,流量波动大,又不想为峰值预留一堆机器。
但如果你要做WebSocket长连接、基于PHP内置Session做严格状态管理、或者一次请求要处理几十MB的文件,Lambda的模型会让你别扭。函数最大执行时长、临时磁盘空间/tmp只有512MB到几GB、本地文件系统本来就不该存业务数据,这些限制不是Bref能解除的。Serverless适合的是无状态、可快速伸缩的业务,不适合长期占用资源的在线应用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 部署前的工具箱和账号准备
2.1 本机依赖:版本和坑
开始之前,先把本机环境确认一遍。Serverless PHP的链路里,只要版本错一个,后续排查会非常烦躁。
需要装的东西有:
- PHP 8.1以上,命令行版可用就行,语法要求不高。
- Composer 2.x,用来安装Bref和之后部署时生成vendor目录。
- Node.js 16以上,因为Serverless Framework本身是个npm包。
- Serverless Framework CLI,全局安装即可。
严格来说,Bref的运行时层和本地PHP版本不需要完全一致。你本地写代码时用的是PHP 8.2,部署后线上用的是Bref预编译的PHP 8.2层,两者功能上基本对齐,但别指望你的代码能用上操作系统层面某个本地扩展。如果依赖了没打进层的扩展,只能在出错时才被发现,后面我会讲到这个坑。
检查的时候用一条命令:
bash复制php -v
composer -V
node -v
serverless --version
Serverless Framework的版本号最好稳定在3.x或更新的兼容版本。老教程里一堆serverless.yml写法是基于2.x的,直接抄到新版上会出现字段废弃提示。示例里用到的都是比较常规的字段,兼容性没问题。
2.2 AWS账号和本地凭证
部署目标是AWS Lambda,所以你得有一个AWS账号。严格的生产实践是给CI/CD创建最小权限的IAM角色,但个人尝试阶段,我建议先用一个带AdministratorAccess的开发用IAM用户,省去一长串权限报错。
拿到密钥后,用AWS CLI配置通常最省事:
bash复制aws configure
按提示输入Access Key ID、Secret Access Key、默认region。如果你不想装AWS CLI,也可以直接在环境变量里写:
bash复制export AWS_ACCESS_KEY_ID=你的KeyId
export AWS_SECRET_ACCESS_KEY=你的Secret
export AWS_DEFAULT_REGION=ap-southeast-1
这里有两个新手最容易踩的坑。第一个是region不一致,你serverless.yml里写的region如果和APIGateway、CloudWatch Logs的预期不一样,会出现资源创建成功但访问端点却报错的情况。第二个是临时凭证,如果你用的是STS临时token,别忘了额外设置AWS_SESSION_TOKEN变量,否则Serverless Framework会一直报403。
我一般会把尝试环境的region固定为ap-southeast-1,也就是新加坡区域。它在东南亚的延迟不算差,而且一些Lambda相关服务默认都可用。
2.3 Serverless Framework和Bref各自负责什么
这里大家最容易把职责搞混。Serverless Framework是一个基础设施编排工具,它读取serverless.yml,然后把解析结果转换成AWS CloudFormation模板,帮你创建Lambda函数、API Gateway、日志组、IAM角色。Bref是一个PHP运行时和插件,它负责告诉Serverless Framework“这个函数该怎么套上PHP运行时层”。
所以你在serverless.yml里看到的runtime: php-82-fpm并不是AWS原生认识的值。是Bref插件把这个字符串翻译成了对应的Lambda Layer ARN,再配合你指定的handler路径,生成一个能运行PHP的Lambda函数。插件还负责处理一些部署细节,比如把web handler对应的PHP-FPM启动方式映射成正确的函数配置。
理解这一层后,调试时你就不会像个无头苍蝇。部署失败先去想是权限问题还是资源定义问题;函数能创建但执行报错,再去想是PHP代码问题还是Bref运行时问题。Serverless Framework最终会展示CloudFormation的错误信息,但很多报错信息比较底层,你要能联系到“这可能是Bref plugin版本和框架版本不匹配”。
3. 把示例应用写出来并跑起来
3.1 从空目录初始化项目
我建议把示例放在一个全新目录,避免本地原有项目干扰。示例名叫hello-bref。
先建立基本目录:
bash复制mkdir hello-bref && cd hello-bref
mkdir -p public
手动创建composer.json,内容保持最简单:
json复制{
"name": "example/hello-bref",
"type": "project",
"require": {
"php": ">=8.1",
"bref/bref": "^2.0"
}
}
然后执行:
bash复制composer install
这里composer install会生成vendor/目录,其中包含Bref插件。后续serverless.yml里要引用./vendor/bref/bref这个plugin路径,所以必须先装Composer,再写Serverless配置。
如果你本机的Composer内存受限,可以加上:
bash复制COMPOSER_MEMORY_LIMIT=-1 composer install
否则偶尔会在下载大依赖包时卡住或者爆内存。
3.2 serverless.yml的关键配置
在项目根目录创建serverless.yml:
yaml复制service: hello-bref
frameworkVersion: '3'
provider:
name: aws
runtime: provided.al2
region: ap-southeast-1
plugins:
- ./vendor/bref/bref
functions:
web:
handler: public/index.php
runtime: php-82-fpm
timeout: 20
memorySize: 1024
events:
- httpApi: '*'
逐个看关键项:
provider.runtime: provided.al2表示底层使用Amazon Linux 2自定义运行时。真正的PHP运行时能力来自Bref插件挂载的Layer。functions.web.runtime: php-82-fpm不是AWS原生字段,而是Bref插件的快捷写法。如果项目需要PHP 8.3,可以写成php-83-fpm。但要注意,你本地PHP版本和线上runtime版本最好保持主版本一致,不然容易踩“本地没问题、线上报函数不存在”的坑。
handler: public/index.php是HTTP应用模式下的前端控制器。HTTP API的每一个请求只要匹配到事件,都会走到这个入口文件。timeout: 20给第一次冷启动留足余量,memorySize: 1024不是必须,但调大内存的同时Lambda会分到更多CPU,对PHP这类计算密集的解释型语言有明显好处。events里的httpApi: '*'表示创建一个默认的API Gateway HTTP API路由,所有路径都转发到这个函数。
3.3 写一个能返回请求信息的PHP入口
现在写真正被执行的public/index.php。为了让示例能验证HTTP环境是否完整,我让它把请求里的关键信息原样吐出来:
php复制<?php
declare(strict_types=1);
$requestTime = gmdate(DATE_ATOM);
$bodyText = file_get_contents('php://input');
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($uri, PHP_URL_PATH);
$responseData = [
'message' => 'Hello from Bref on Lambda',
'path' => $path,
'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET',
'query' => $_GET,
'body' => $bodyText,
'request_time' => $requestTime,
'php_version' => PHP_VERSION,
];
header('Content-Type: application/json');
echo json_encode($responseData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
这段代码没有引入任何PHP框架,完全是原生PHP写法。它读取请求URI、方法、query参数和请求体,组装成一个JSON返回。好处是能清晰验证Bref在web模式下是不是真的把HTTP语义完整交给了PHP-FPM。
本地先跑一下,用PHP内置服务器的路由模式:
bash复制php -S 127.0.0.1:8000 public/index.php
注意不要用php -S 127.0.0.1:8000 -t public。那种方式虽然会把public作为文档根目录,但当你访问/health这类路径时,内置服务器会尝试去找public/health文件,找不到就直接404。而把public/index.php作为入口参数,相当于所有请求都会先走这个文件,更接近之后API Gateway把任意路径转发到同一个handler的行为。
打开另一个终端:
bash复制curl 'http://127.0.0.1:8000/?source=local'
应该能看到一条JSON,里面包含path、method、query等字段。这就说明PHP逻辑本身没问题。跑完本地测试后,停掉PHP内置服务器,准备部署。
4. 从deploy到线上排错,完整实操记录
4.1 serverless deploy到底做了什么
在项目根目录执行:
bash复制serverless deploy
如果本地aws凭证没问题,Serverless Framework会开始执行部署。第一次部署通常需要一到三分钟,因为它不光创建Lambda函数,还要创建API Gateway HTTP API、日志组、相关IAM角色,这一整套基础资源需要通过CloudFormation栈来管理。
部署完成后,控制台会出现类似下面的输出:
code复制✔ Service deployed to stack hello-bref-dev
endpoint: https://xxxxxxxxxx.execute-api.ap-southeast-1.amazonaws.com
functions:
web: hello-bref-dev-web
看到endpoint后,直接用curl请求线上地址:
bash复制curl 'https://xxxxxxxxxx.execute-api.ap-southeast-1.amazonaws.com/?source=lambda'
不出意外会返回和本地几乎一样的JSON,差别可能只是request_time和php_version中的版本号。到这里,一个简单的Serverless PHP应用就算真正跑通了。
这里要注意,serverless deploy每次部署都会生成一份新的CloudFormation变更集。如果你只改了PHP代码而没改serverless.yml,可以不用重新执行整个部署,直接使用:
bash复制serverless deploy function -f web
它只更新指定函数代码,速度快很多。但如果你改动了环境变量、runtime、handler路径,必须完整执行serverless deploy,否则函数配置不会更新。
4.2 线上调用失败,应该去哪里查日志
很多人在Lambda里写PHP,第一次线上请求得到500或502就懵了,其实排查路径非常固定。
先分清类型。如果API Gateway返回502,通常表示Lambda运行时报错,进程没能正常返回HTTP响应。这时候去CloudWatch Logs看函数日志。Serverless Framework提供了一条很省
