魔法合体:Remix 和 Vercel/AWS 双重部署

* 一个前提条件:你的应用没有和 AWS 或者 Vercel 的各种数据库生态高度融合。

看了一下,网上似乎没有相关教程。

大概是在国庆期间吧,Vercel 在大陆访问特别不稳,然后当时恰巧成功注册了 AWS 账号,就想,能不能把自己手里面一个 Remix 应用迁移到 AWS 上。AWS 在国庆期间大陆访问还是挺稳的。

但是国庆都过完了,结果咕咕咕到现在才开工。

准备工作

你需要一个 AWS CLI,里面的 AWS 凭据需要有效并且具有 AdministratorAccess 权限。目前这个是官方文档推荐的做法。

需要注意的是,Architect(后文需要使用的工具)不支持包含非 ASCII 字符的路径,所以先确保项目路径里面不含非 ASCII 字符。(都什么年代了还在用传统 ASCII?快来试试我这款最新最热 Unicode(x

基本操作

Architect 主要是用于使用 AWS 构建数据库支持的 Web 应用程序。因为它与 Remix 有相关的集成,所以这里就选用了它。

Architect 默认的初始化工具似乎并不能识别 pnpm,所以这里提供一种手动安装 architect 的方法。

首先安装这些依赖包:

pnpm install @architect/architect @remix-run/architect # 安装 architect 和相关包

然后在项目根目录创建 app.arc 文件(Architect 配置文件):

# app.arc
@app
your-application-name

@http
/*
  method any
  src backend

@aws
# profile default
region us-west-2
architecture arm64

这个配置文件的意思是:

  • @app:这个应用名称叫 your-application-name,会影响到最终部署到 AWS 上的一堆资源名称;
  • @http:对于路由 /*,转发全部 HTTP 方法(method any)到 backend 目录下的 handler;
  • @aws:这个应用将会部署在俄勒冈州(us-west-2),并运行于 arm64 架构的机器上。为什么选 arm 架构呢?因为比 x86 便宜。

那么,自然,你需要一个 backend 目录。叫什么其实随意啦,把上面的配置项对应着改一下就好啦。

backend 目录下创建 index.js

/* backend/index.js */
const { createRequestHandler } = require("@remix-run/architect");
exports.handler = createRequestHandler({
  build: require("./build"),
});

以及对应的 config.arc

# backend/config.arc
@aws
runtime nodejs18.x
# 单位为 MB,与分配的 vCPU 成正比,参见 AWS 文档
memory 256
# timeout 单位为秒
timeout 15

这个意思是,从 backend 目录下导入 build(Remix 构建好的 bundle),然后用 @remix-run/architect 转译一下交给 Architect 处理。

那么我们肯定需要修改一下 Remix 的构建设置,以将构建输出到 backend/build 文件夹下。但是因为我们仍然需要构建到 Vercel 上,可以考虑使用环境变量区分:

/* remix.config.cjs */
module.exports = {
  ignoredRouteFiles: ["**/.*"],
  serverModuleFormat: "cjs",
  browserBuildDirectory: "./public/build",
  serverBuildPath: process.env.ARC ? "./backend/build/index.js" : undefined,
  publicPath: process.env.ARC ? "/_static/build/" : undefined,
};

这个配置的意思是,如果定义了环境变量 ARC 并且值为真,则将 server bundle 构建到 backend/build/index.js 文件,并且所有资源的静态 assets 会从 URL /_static/build/ 下访问,而不是原来的默认设置。静态资源需要重写到 /_static/ 下,这个是 Architect 的特性。所以还要注意你的 favicon.ico,建议使用 HTML 的 <link rel="icon"> 设置图标。

然后这个项目就大概能用了。

构建并测试

构建的时候,只需要将环境变量 ARC 设置成 1 就可以正常构建了:

ARC=1 pnpm build

最后 pnpm arc sandbox 可以查看效果。

可以考虑把这一系列操作都写进脚本或者 package.json/scripts

环境变量

环境变量啥的看文档啦。这个不是本文的重点。

部署

这里只说 AWS 的部署了。很简单,就一个指令:

# 预备环境
pnpm arc deploy --staging -v --prune
# 生产环境
pnpm arc deploy --production -v --prune

记住,一定把 sam.yamlsam.json 写进 .gitignore!!!环境变量什么的都会存在里面,而且 Architect 也不需要这些文件判断环境变量!!!

奇技淫巧

这部分内容有些 project-related。

  • 我的项目里页脚有一个链接「Powered by Vercel」,但是如果我项目部署在 AWS 上就不就 ntr 了吗(x
  • 我要读取 public 目录下的脚本,但是 Vercel 和 Architect 的静态资源目录不一样诶。

所以可以考虑在 loader 中读取 ARC_ENV 环境变量,服务端进行一个特判就好啦。

export const loader = () => {
  return process.env.ARC_ENV ?? null;
};

export default function App() {
  const arcEnv = useLoaderData<string | null>();
  return (
    <>
      ...
      {arcEnv ? (
        <script src="/_static/clarity.js"></script>
      ) : (
        <script src="/clarity.js"></script>
      )}
    </>
  );
}

需要注意的是,在 arc sandbox 的时候,仍然需要手动设置 ARC_ENV 环境变量。

ARC_ENV=development pnpm arc sandbox

关于 Prisma

现在存在一个问题,就是 Prisma ORM 在这套方案上会出现一些大问题,似乎是因为没法跑 prisma generate。暂时不知道怎么解决,求教qwq

持续集成

贴一份 GitHub Actions 的一个模板。

name: AWS deploy

on: [ push, pull_request ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Cancel previous runs
        uses: styfle/[email protected]
      - name: Checkout repository
        uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
      - name: Setup pnpm and Install dependencies
        uses: pnpm/action-setup@v2
        with:
          version: 8
          run_install: |
            - recursive: true
              args: [--frozen-lockfile, --strict-peer-dependencies]

      - name: Build for AWS
        run: ./awsbuild.sh

      - name: Arc hydrate
        run: pnpm arc hydrate

      - name: Staging deploy
        if: github.ref != 'refs/heads/main'
        run: pnpm arc deploy --staging -v --prune
        env:
          AWS_ACCESS_KEY_ID: {{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: {{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Production deploy
        if: github.ref == 'refs/heads/main'
        run: pnpm arc deploy --production -v --prune
        env:
          AWS_ACCESS_KEY_ID: {{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: {{ secrets.AWS_SECRET_ACCESS_KEY }}

上面的 YAML 请自行替换环境变量语法,因为这个 Markdown 解析器太傻了甚至会解析代码块里的美元符号。


关于 Architect 的更多用法,还是看文档好一些啦。