告别 Shell 脚本:用 Laravel Envoy 实现干净可复用的部署

告别 Shell 脚本:用 Laravel Envoy 实现干净可复用的部署

如果你部署代码有段时间了,很可能某个地方有个叫 deploy.sh 的文件。

也许一开始只有几行:

bash 复制代码
git pull origin main
php artisan migrate --force

一两年后,它变成了一堵 bash 墙:各种条件判断、环境标志、几行注释掉的旧代码(来自某次事故),还有一个神秘的 sleep 5,没人敢动。

它能跑。

但也很吓人。

Laravel Envoy 就是那种默默解决这个问题的工具:可重复、可读的部署自动化,如果你习惯 SSH 和 shell 命令,用起来还很熟悉。Envoy 让你用干净的、类 PHP 的方式组织部署流程,同时仍然通过 SSH 在真实服务器上运行真实命令,而不是维护又一个巨大的 shell 脚本。

本文将介绍:

  • Laravel Envoy 是什么(以及不是什么)
  • 它如何改进纯 shell 脚本
  • 如何将真实的 deploy.sh 迁移到 Envoy.blade.php
  • 处理多环境(staging、QA、production)
  • 安全特性(确认、钩子、通知)
  • Envoy 如何融入 CI/CD

读完后,你应该能把现有部署流程转换成更干净、可复用、团队更容易理解的东西。

原文链接 告别 Shell 脚本:用 Laravel Envoy 实现干净可复用的部署

为什么纯 shell 脚本会变得痛苦

Shell 脚本很适合快速搞定事情。它们:

  • 无处不在(每台服务器都有 /bin/sh
  • 上手简单
  • 适合做胶水工作

问题是:部署往往会膨胀。一个典型的 Laravel 部署脚本经常变成这样:

bash 复制代码
#!/usr/bin/env bash
set -e
APP_DIR=/var/www/myapp
BRANCH=${1:-main}
echo "Deploying branch $BRANCH to $APP_DIR"
cd "$APP_DIR"
echo "Pulling latest code..."
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
echo "Installing composer dependencies..."
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
echo "Running migrations..."
php artisan migrate --force
echo "Caching config & routes..."
php artisan config:cache
php artisan route:cache
echo "Restarting queues..."
php artisan queue:restart
echo "Done!"

这看起来还算温和,但随着需求增长,你开始堆叠:

  • 多环境(staging vs production)
  • 不同环境用不同分支
  • 资源、Horizon、队列、缓存预热的额外步骤
  • 条件判断("只在生产环境运行这个"、"没有变更就跳过迁移")
  • 通知(Slack、邮件)
  • 用符号链接和 releases/20251208000000 文件夹实现零停机发布

你可以继续用 bash 做这些,但最终会重新实现其他工具已经提供的结构:分组、钩子、可复用的片段、任务组合。

这就是 Envoy 的用武之地。

Laravel Envoy 是什么?

Laravel Envoy 是一个小型的 PHP SSH 任务运行器。你在一个叫 Envoy.blade.php 的文件中用类 Blade 语法定义任务,Envoy 通过 SSH 在远程服务器上执行这些任务。

几个要点:

  • 它不限于 Laravel 应用。你可以用它部署任何能通过 shell 命令管理的东西:Node 应用、静态站点、后台 worker 等。
  • 它在本地(或 CI 中)运行,通过 SSH 连接服务器,就像你在终端里做的一样。
  • 官方支持 macOS 和 Linux;在 Windows 上通常通过 WSL2 使用。

Envoy 提供的不是一个巨大的脚本,而是:

  • @servers --- 命名的 SSH 目标
  • @task --- 在这些目标上运行的 shell 命令块
  • @story --- 组成部署流水线的任务序列
  • @setup --- 用于变量、分支逻辑、配置的 PHP 代码
  • 钩子(@before@after@error@success@finished)用于日志和通知等横切关注点
  • 额外功能如确认、并行执行和 Slack 通知

你仍然用你已经熟悉的部署语言写核心逻辑:shell 命令。Envoy 只是给它们加上结构。

在项目中安装 Envoy

你有两个主要选择:全局安装或按项目安装。现在按项目安装通常更干净、更可复现。

1. 通过 Composer 安装 Envoy

在 Laravel 项目根目录:

bash 复制代码
composer require laravel/envoy --dev

这会把 Envoy 添加为开发依赖,并在 vendor/bin/envoy 暴露其二进制文件。

确认安装成功:

bash 复制代码
php vendor/bin/envoy --version

2. 创建 Envoy.blade.php

在项目根目录:

bash 复制代码
touch Envoy.blade.php

这个文件是所有 Envoy 配置和任务的所在地。Envoy 默认查找这个文件。

添加一个最小示例:

blade 复制代码
@servers(['web' => 'deploy@your-server'])
@task('hello', ['on' => 'web'])
    echo "Hello from {{ gethostname() }}";
@endtask

运行它:

bash 复制代码
php vendor/bin/envoy run hello

Envoy 会以 deploy 用户 SSH 到你的服务器并远程执行 echo 命令。

从这里开始,你可以扩展这个文件来匹配你的部署流程。

deploy.sh 到 Envoy.blade.php 的逐步迁移

让我们把一个典型的 Laravel 部署脚本转换成 Envoy。

原始 bash 部署脚本

我们从这样的脚本开始:

bash 复制代码
#!/usr/bin/env bash
set -e
APP_DIR=/var/www/myapp
BRANCH=${1:-main}
echo "Deploying branch $BRANCH to $APP_DIR"
cd "$APP_DIR"
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan queue:restart

它能用,但所有东西都纠缠在一起。没有简单的方法只重新运行其中一部分(比如只运行迁移),或者在不复制修改脚本的情况下在环境间共享片段。

步骤 1:定义服务器

开始我们的 Envoy.blade.php

blade 复制代码
@servers(['web' => 'deploy@your-server'])

你可以定义任意多个命名服务器(如 staging、prod-1、prod-2、workers)。

步骤 2:设置共享变量

Envoy 允许你在任务执行前在 @setup 块中运行 PHP 代码。这非常适合配置、路径和分支。

blade 复制代码
@setup
    $appDir = '/var/www/myapp';
    // 如果没传分支则默认为 main:--branch=feature/something
    $branch = isset($branch) ? $branch : 'main';
@endsetup

注意:运行 Envoy 时可以传递选项如 --branch=develop,它们会作为 $branch 在 Blade 模板中可用。

步骤 3:把部署拆分成任务

不是一个长脚本,我们创建几个专注的任务:

blade 复制代码
@task('git', ['on' => 'web'])
    echo "Deploying branch {{ $branch }}";
    cd {{ $appDir }};
    git fetch origin {{ $branch }};
    git reset --hard origin/{{ $branch }};
@endtask

@task('composer', ['on' => 'web'])
    cd {{ $appDir }};
    echo "Installing composer dependencies...";
    COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader;
@endtask

@task('migrate', ['on' => 'web'])
    cd {{ $appDir }};
    echo "Running migrations...";
    php artisan migrate --force;
@endtask

@task('optimize', ['on' => 'web'])
    cd {{ $appDir }};
    echo "Caching config & routes...";
    php artisan config:cache;
    php artisan route:cache;
@endtask

@task('restart-queues', ['on' => 'web'])
    cd {{ $appDir }};
    echo "Restarting queues...";
    php artisan queue:restart;
@endtask

每个 @task 就是 bash,但分组和命名得很好。

步骤 4:把任务组合成 "story"

story 是按顺序运行的任务序列。这是定义部署流水线的地方。

blade 复制代码
@story('deploy')
    git
    composer
    migrate
    optimize
    restart-queues
@endstory

现在完整部署只需要:

bash 复制代码
php vendor/bin/envoy run deploy --branch=main

如果你需要只运行其中一部分(比如只运行迁移):

bash 复制代码
php vendor/bin/envoy run migrate

到这里,我们已经复现了原始脚本------但它更干净、可组合、更容易修改。

干净地支持多环境

真实部署几乎总是涉及至少 staging 和 production。让我们扩展 Envoy 设置来支持这一点。

1. 声明多个服务器

blade 复制代码
@servers([
    'staging' => 'deploy@staging.example.com',
    'prod'    => 'deploy@prod.example.com',
])

2. 用 --server 选项选择环境

我们在 @setup 中添加一些 PHP 逻辑来要求 --server 参数,并为每个环境选择正确的分支:

blade 复制代码
@setup
    if (! isset($server)) {
        throw new Exception("Please pass --server=staging or --server=prod");
    }
    $appDir = '/var/www/myapp';
    // 环境与分支的映射
    $branches = [
        'staging' => 'develop',
        'prod'    => 'main',
    ];
    if (! array_key_exists($server, $branches)) {
        throw new Exception("Unknown server '{$server}'. Allowed: staging, prod.");
    }
    $branch = isset($branch) ? $branch : $branches[$server];
@endsetup

Envoy 会把 CLI 选项如 --server=staging 传入 $server,所以我们可以在任务和配置中使用它。

3. 让任务感知环境

我们现在可以定义 on 参数是动态的任务:

blade 复制代码
@task('deploy-code', ['on' => $server])
    echo "Deploying {{ $branch }} to {{ $server }}";
    cd {{ $appDir }};
    git fetch origin {{ $branch }};
    git reset --hard origin/{{ $branch }};
@endtask

@task('run-migrations', ['on' => $server])
    cd {{ $appDir }};
    php artisan migrate --force;
@endtask

@task('optimize', ['on' => $server])
    cd {{ $appDir }};
    php artisan optimize:clear;
@endtask

@story('deploy')
    deploy-code
    run-migrations
    optimize
@endstory

同一个 Envoy story 现在适用于任何环境:

bash 复制代码
# 部署到 staging(使用 develop 分支)
php vendor/bin/envoy run deploy --server=staging

# 部署到 prod(使用 main 分支)
php vendor/bin/envoy run deploy --server=prod

# 如果确实需要,可以覆盖分支
php vendor/bin/envoy run deploy --server=staging --branch=feature/checkout-redesign

注意我们没有重复任务。只有配置(哪个服务器、哪个分支)根据输入变化。

添加安全措施:确认、钩子和通知

Envoy 自带一些有用的"护栏",用纯 shell 脚本很难做得这么好。

1. 确认危险任务

对于可能搞坏东西的任务(如部署到生产环境),你可以让 Envoy 提示确认:

blade 复制代码
@task('deploy-prod', ['on' => 'prod', 'confirm' => true])
    cd {{ $appDir }};
    git fetch origin {{ $branch }};
    git reset --hard origin/{{ $branch }};
    php artisan migrate --force;
@endtask

使用 'confirm' => true,Envoy 会在运行任务前显示"确定吗?"风格的提示。

2. 钩子:@before、@after、@error、@success、@finished

钩子让你在每个任务周围插入行为,而不用重复自己。Envoy 在本地以 PHP 执行这些,不是在服务器上。

例如,简单的日志:

blade 复制代码
@before
    echo "About to run task: {$task}";
@endbefore

@after
    echo "Finished task: {$task}";
@endafter

@error
    echo "Task {$task} failed!";
@enderror

@success
    echo "All tasks completed successfully!";
@endsuccess

或者在部署完成后发送 Slack 通知:

blade 复制代码
@finished
    if ($exitCode === 0) {
        @slack('https://hooks.slack.com/services/XXX/YYY/ZZZ', '#deployments')
    }
@endfinished

Envoy 的 @slack 指令通过 webhook 向你选择的频道或用户发送消息,非常适合"deployments"或"ops"频道。

对比用纯 bash 做同样的事情,尤其是当你想在每个任务周围都加上这些而不只是在最后。

部署到多台服务器(可选并行)

如果你的应用在负载均衡器后面的多台 web 服务器上运行,你可能需要部署到所有服务器。

Envoy 让这变得非常简单:

blade 复制代码
@servers([
    'web-1' => 'deploy@web1.example.com',
    'web-2' => 'deploy@web2.example.com',
])

@setup
    $appDir = '/var/www/myapp';
    $branch = isset($branch) ? $branch : 'main';
@endsetup

@task('deploy-code', ['on' => ['web-1', 'web-2']])
    cd {{ $appDir }};
    git fetch origin {{ $branch }};
    git reset --hard origin/{{ $branch }};
@endtask

默认情况下,Envoy 串行运行任务:完成 web-1,然后转到 web-2。如果你想加快速度,并且确信部署可以安全地并发运行,可以启用并行执行:

blade 复制代码
@task('deploy-code', ['on' => ['web-1', 'web-2'], 'parallel' => true])
    cd {{ $appDir }};
    git fetch origin {{ $branch }};
    git reset --hard origin/{{ $branch }};
@endtask

只需改一行就能从串行变成跨多台服务器的并行部署------这在 bash 中当然也能做到,但远没有这么方便。

迈向零停机:releases 和符号链接

对于很多应用,"原地 git pull"就够了。但一旦你关心零停机部署,通常会开始使用这样的模式:

  • /var/www/myapp/releases/20251208090000(新发布目录)
  • /var/www/myapp/current(指向活动发布的符号链接)
  • /var/www/myapp/shared(共享存储 .env、uploads 等)

Envoy 非常适合用可读的方式编码这种模式。

这是一个简化的例子:

blade 复制代码
@setup
    $appDir      = '/var/www/myapp';
    $releasesDir = $appDir . '/releases';
    $currentDir  = $appDir . '/current';
    $branch = isset($branch) ? $branch : 'main';
    date_default_timezone_set('UTC');
    $release = date('YmdHis');
    $newReleaseDir = $releasesDir . '/' . $release;
@endsetup

@story('deploy-zero-downtime')
    prepare-release
    install-dependencies
    migrate
    optimize
    switch-symlink
    restart-queues
@endstory

@task('prepare-release', ['on' => 'prod'])
    echo "Creating new release: {{ $newReleaseDir }}";
    mkdir -p {{ $newReleaseDir }};
    cd {{ $newReleaseDir }};
    git clone --depth=1 --branch={{ $branch }} git@github.com:your-org/your-repo.git .;
@endtask

@task('install-dependencies', ['on' => 'prod'])
    cd {{ $newReleaseDir }};
    COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader;
@endtask

@task('migrate', ['on' => 'prod'])
    cd {{ $newReleaseDir }};
    php artisan migrate --force;
@endtask

@task('optimize', ['on' => 'prod'])
    cd {{ $newReleaseDir }};
    php artisan config:cache;
    php artisan route:cache;
@endtask

@task('switch-symlink', ['on' => 'prod'])
    echo "Switching symlink to {{ $newReleaseDir }}";
    ln -nfs {{ $newReleaseDir }} {{ $currentDir }};
@endtask

@task('restart-queues', ['on' => 'prod'])
    cd {{ $currentDir }};
    php artisan queue:restart;
@endtask

这基本上就是用 Envoy 写的经典"Capistrano 风格"发布流程。有些细节你可能想完善(共享存储、.env 文件符号链接、清理旧发布),但结构读起来像一个故事:

  1. 准备发布目录
  2. 安装依赖
  3. 运行迁移
  4. 优化
  5. 把 current 符号链接指向新发布
  6. 重启队列

因为每个步骤都是独立的任务,如果需要调试,你也可以单独重新运行某个步骤。

将 Envoy 集成到 CI/CD

到目前为止,我们假设你从笔记本电脑运行 Envoy,这已经比临时 SSH 登录好很多了。但 Envoy 也能很好地配合 CI/CD 系统。

核心上,CI 只需要:

  1. 能访问你的 SSH 密钥(或某个部署密钥)
  2. 检出你的仓库
  3. 运行 php vendor/bin/envoy run deploy ...

例如,一个非常简单的 GitHub Actions 工作流可能是这样:

yaml 复制代码
name: Deploy
on:
  workflow_dispatch:
  push:
    branches:
      - main
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - name: Install dependencies
        run: composer install --no-dev --prefer-dist --no-interaction
      - name: Configure SSH
        run: |
          mkdir -p ~/.ssh
          echo "$DEPLOY_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan your-server.com >> ~/.ssh/known_hosts
      - name: Run Envoy deploy
        run: php vendor/bin/envoy run deploy --server=prod
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

如果你想要更干净的抽象,还有专门用于运行 Envoy stories 的社区 GitHub Action。

这把你的部署从"某人手动运行的 CLI 命令,可能忘了记录"变成"存在版本控制中的可重复工作流,可以在推送时或手动触发"。

什么时候不用 Envoy

Envoy 是一个"刚刚好"的工具:

  • 它比完整的 CI/CD 平台加单独的部署工具更简单
  • 它比一次性 shell 脚本或随机命令更有结构

但是,有些时候它可能不是正确的选择:

  • 你已经深度使用另一个部署系统(如 Laravel Forge、Envoyer、基于 Kubernetes 的部署,或平台特定的流水线)
  • 你的团队更喜欢用 PHP 以外的语言做运维工具,想要一切都用 Go 或 Python
  • 你的基础设施太复杂,需要超出 SSH 任务运行器的编排功能

但如果你处于很常见的情况------"我们 SSH 到几台 VPS 运行命令来部署"或"我们有个不太信任的 bash 脚本"------Envoy 是很自然的升级。

在真实项目中使用 Envoy 的实用技巧

最后,这里有一些通常效果不错的模式和习惯:

把 Envoy.blade.php 放在仓库里

把它当作应用代码。代码审查你的部署变更。如果部署流程变了,应该在 git 历史中可见。

用意图命名任务,而不是实现

使用 deploywarm-cacherestart-workersrollback 这样的名字,而不是 task1step2 等。这让你的 stories 读起来像真正的文字。

用 stories 作为你的"公共 API"

人们运行 envoy run deployenvoy run rollback。内部,这些 stories 可以由更小的任务组成,你可以自由重组而不改变人们与部署交互的方式。

快速失败并大声报错

在合理的地方使用 set -e 风格的行为(Envoy 已经暴露退出码并支持 @error 钩子)。不要静默吞掉错误。如果迁移失败,你希望部署停止并报错。

明确环境

--server(或 --env)成为必需的。如果没传环境,永远不要默认到生产环境。强制调用者明确意图。

从小处开始,然后重构

你不需要在第一天就有完美的零停机、多阶段、多服务器流水线。先把你当前的步骤包装成 Envoy 任务和一个 story。稳定后,再添加环境、钩子和更高级的流程。

总结

你不用抛弃所有关于部署的知识才能获得更干净的自动化。

Laravel Envoy 让你:

  • 继续写你已经在用的 shell 命令
  • 把它们包装在 Blade 风格的、PHP 驱动的配置文件中
  • 与应用代码一起分享和版本控制这个文件
  • 添加结构:命名任务、stories、钩子和环境
  • 安全地从"VPS 上的一台服务器"扩展到"多服务器、零停机部署"

用纯 bash 脚本做所有事情可以吗?当然可以。但随着项目------和团队------的增长,有一个读起来几乎像文档、同时又是可执行代码的部署流程是非常有价值的。

如果你有个让人紧张的 deploy.sh,试试这个:

  1. 在项目中安装 Envoy
  2. 把现有命令包装成 @task
  3. 把它们组合成 @story('deploy')
  4. 添加一个环境标志和一个钩子

从那里开始,你可以逐步迭代成健壮、可读、可复用的东西------超越 shell 脚本,但不放弃你已经熟悉的工具。

相关推荐
BingoGo2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack3 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo4 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
BingoGo5 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·laravel
JaguarJack5 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理5 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082855 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php