告别 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 脚本,但不放弃你已经熟悉的工具。

相关推荐
Cache技术分享2 小时前
267. Java 集合 - Java 开发必看:ArrayList 与 LinkedList 的全方位对比及选择建议
前端·后端
2501_921649492 小时前
亚太股票数据API:日股、韩股、新加坡股票、印尼股票市场实时行情,实时数据API-python
开发语言·后端·python·websocket·金融
爱上妖精的尾巴3 小时前
6-1WPS JS宏 new Set集合的创建
前端·后端·restful·wps·js宏·jsa
在坚持一下我可没意见3 小时前
Spring 后端安全双剑(上篇):JWT 无状态认证 + 密码加盐加密实战
java·服务器·开发语言·spring boot·后端·安全·spring
就像风一样抓不住3 小时前
SpringBoot静态资源映射:如何让/files/路径访问服务器本地文件
java·spring boot·后端
sszdlbw3 小时前
前后端在服务器的部署
运维·服务器·前端·后端
tingyu3 小时前
Maven聚合插件2.0版本发布:功能全面升级,开发效率再提升
后端·intellij idea
启山智软3 小时前
【单体系统与分布式系统是两种根本不同的软件架构模式】
java·vue.js·spring boot·后端·spring