GitHub Actions + 校园网服务器自动部署
GitHub Actions + 校园网服务器自动部署
概述
在我们的开发流程中,代码主要在本地主机上编写和测试,每次更新后,我们会将代码推送(Push)到 GitHub 的远程仓库。而部署环境则是在校园网内部的一台服务器上。过去每次部署,都需要登录服务器手动拉取代码、重启服务,操作繁琐、效率低下。是否能够实现一种自动化机制:每当我们将代码 Push 到 GitHub 时,服务器能够自动拉取最新代码并完成部署。这正是现代软件开发中广泛采用的 CI/CD(持续集成 / 持续部署) 的典型应用场景。简要来说:
- CI(Continuous Integration):自动执行代码构建、测试、静态检查等步骤,确保主分支代码质量;
- CD(Continuous Deployment):将构建/测试通过的代码自动部署到目标环境,减少手动干预。
那么如何实现这样的自动化流程呢?GitHub 为我们提供了一套内置工具 —— GitHub Actions。它是一套基于事件驱动的自动化工作流系统,能够在以下事件发生时自动执行用户定义的操作:
- 推送代码(push)
- 创建 Pull Request
- 发布版本(release)
- 甚至定时任务(schedule)
通过编写简单的工作流配置文件(YAML 格式),我们可以轻松实现自动构建、测试、部署等操作。在我们的场景中,需求其实很简单:Push 之后自动远程登录到服务器,拉取代码并重启服务。然而,这里有一个额外的现实问题:
由于服务器位于校园网内网,无法直接通过公网 SSH 登录。
清华的校园网屏蔽了包括 22(SSH 默认端口)、3389(远程桌面)、8000~8100 等常用端口,导致外部网络无法直接访问校园网主机。因此,除了配置 GitHub Actions,我们还需要借助一个公网中转节点(如腾讯云 VPS)通过 SSH 反向隧道 建立内外连接通道。这个博客梳理一下这个自动化部署流程的配置步骤,涉及的主要技术包括:
- systemd:服务器端服务管理与自启动
- SSH 反向隧道:穿透校园网内网,实现远程连接
- GitHub Actions:自动触发与部署控制
目标是搭建一个 轻量、自动、稳定的 CI/CD 方案,适用于校园网络环境下的个人服务器部署。
systemd
什么是服务?
我们经常提到“服务(service)”这个词,我们在服务器上启动的也是一个自己开发的”服务“,但它具体指的是什么?简单来说,服务就是在后台持续运行、等待处理请求的程序进程。比如:
sshd
:提供 SSH 登录功能;httpd
:提供 HTTP 网站服务;mysqld
:提供数据库服务。
由于系统中可能同时运行着几十上百个服务,因此我们很自然会想:有没有一种统一的方法,来管理这些服务?例如统一地启动、停止、重启、监控状态,甚至设置开机自启等。这正是系统初始化工具(init system)所负责的任务。
SysVinit 与 systemd 的演进
在较早的 Linux 系统中,使用的是 System V init(SysVinit)。它的基本原理是:将每个服务对应的启动脚本统一放在 /etc/init.d/
目录下,通过调用这些脚本来控制服务的启动与关闭:
1 | sudo /etc/init.d/cron start |
以上命令的含义是启动名为 cron
的服务。我们可以查看这些脚本的内容来了解其具体实现:
1 | cat /etc/init.d/cron |
脚本中定义了服务的启动命令、PID 文件、日志路径、以及对 start
、stop
、restart
、status
等命令的响应逻辑。
但 SysVinit 存在以下几个明显的缺点:
- 串行启动:导致系统启动缓慢;
- 依赖管理薄弱:无法很好地处理服务之间的依赖关系;
- 脚本风格不统一:各服务的管理方式五花八门,缺乏一致性;
- 状态管理不清晰:无法准确判断服务是否已经启动、是否运行正常。
为了解决这些问题,现代 Linux 系统普遍采用了 systemd。
什么是 systemd?
systemd 是一个全新的系统初始化与服务管理框架,自 2010 年推出以来,已成为多数主流 Linux 发行版的默认 init 系统(包括 Fedora、CentOS、Ubuntu 等)。
它的核心特性包括:
- 并行启动服务,显著加快开机速度;
- 原生支持服务依赖关系;
- 使用统一命令
systemctl
管理服务; - 集成日志系统(
journald
); - 支持挂载点、网络、定时任务等多种资源的一体化管理。
systemd 框架里还包含了一个叫作 systemd 的程序(
/bin/systemd
),它是用户空间第一个启动的守护进程(PID 1),它负责系统初始化,并接管所有后续服务的管理工作。
systemd 框架包括多个组件,比如:
systemctl
:服务管理命令行工具;journald
:日志收集系统;logind
:用户会话管理;networkd
:网络配置工具;resolved
:DNS 解析守护进程;- ……等等。
Unit(单元)
systemd 是怎么进行的一体化整合的呢?计算机领域有一句名言:“计算机科学中的所有问题都可以通过增加一个中间层来解决。” 实际上一切整合类的工作,都可以采用同样的思路:构建统一的更高级别的抽象。
systemd 的设计核心是 unit(单元)这一抽象概念。每一个 unit 描述一个可管理资源,比如服务、挂载点、定时任务等。
unit 文件通常位于以下路径:
1 | /usr/lib/systemd/system/ # 系统安装的 unit 文件 |
常见 unit 类型如下表:
Unit 类型 | 描述 | 文件后缀 |
---|---|---|
service |
后台服务进程(daemon) | .service |
socket |
套接字激活的服务 | .socket |
target |
一组 unit 的逻辑集合(类似运行级别) | .target |
timer |
定时任务(类似 cron ) |
.timer |
mount |
文件系统挂载点 | .mount |
device |
内核识别的设备 | .device |
path |
路径监控(如文件创建/修改触发) | .path |
这种统一的 unit 抽象,使得 systemd 能够对各种系统资源进行一致性管理。
systemctl:服务管理命令
systemctl
是 systemd
提供的主要命令行工具,用于操作和查询服务状态。常用命令示例如下:
操作 | 命令 |
---|---|
启动服务 | systemctl start nginx |
停止服务 | systemctl stop nginx |
重启服务 | systemctl restart nginx |
查看服务状态 | systemctl status nginx |
设置开机自启 | systemctl enable nginx |
取消开机自启 | systemctl disable nginx |
列出所有服务 | systemctl list-units --type=service |
重载 unit 文件 | systemctl daemon-reload |
重新执行 systemd | systemctl daemon-reexec |
journalctl:日志管理工具
journald
是 systemd 体系中的 后台日志收集守护进程(daemon),它的任务是收集内核日志、标准输出/错误、Syslog 消息、服务日志等内容,并统一保存在一种二进制日志格式中(通常在 /var/log/journal/
)。 journalctl
是 systemd 提供的 用户命令行工具,用于查询、过滤、查看由 journald
收集的日志数据。journalctl
的常用参数包括:
功能 | 命令示例 |
---|---|
查看全部日志 | journalctl |
查看某服务日志 | journalctl -u nginx |
查看最近日志 | journalctl -r |
实时滚动日志 | journalctl -f |
指定时间查看 | journalctl --since "2024-01-01" |
查看最近启动日志 | journalctl -b |
自定义 .service 文件示例
假设我们要部署的是一个基于 Flask 编写的 Web 应用,我们将其作为服务托管给 systemd 管理,可以新建如下的最简 unit 文件(例如 /etc/systemd/system/myapp.service
):
1 | [Unit] |
然后执行以下命令启用和启动服务:
1 | sudo systemctl daemon-reload # 重新加载 unit 文件 |
这个 .service
文件分为三个主要部分:
[Unit]
:服务的元信息与依赖设置
Description=
:对服务的简要描述,会显示在systemctl status
等命令中。After=
:表示该服务启动的顺序依赖,network.target
表示“在网络就绪后再启动本服务”。
[Service]
:服务的核心配置
WorkingDirectory=
:指定服务运行时的工作目录,通常是你的项目目录。ExecStart=
:服务的启动命令(必须使用绝对路径)。Restart=
:当服务异常退出时是否自动重启。always
表示无论什么原因退出都重启。
[Install]
:服务如何被“安装”或激活
WantedBy=
:指定此服务在哪个“target”中被启用。multi-user.target
是一个常用的运行级别,表示该服务在系统正常启动后自动启动。
sudo 权限
当我们配置好了 systemd 服务后,如果运行:
1 | sudo systemctl start myapp |
默认会要求输入密码。而自动部署时,由于脚本执行是非交互式的,无法输入密码,因此就会失败。为了解决这个问题,我们需要配置服务器的 sudoer 文件,让某个用户在执行特定命令时不需要输入密码。
在 Linux 中,sudo
的行为是由以下这些文件控制的:
配置位置 | 作用说明 |
---|---|
/etc/sudoers |
🔧 主配置文件,定义所有用户/组的 sudo 权限,是 sudo 的核心配置 |
/etc/sudoers.d/ |
📁 补充配置目录,存放额外的“分模块配置文件”,可按需单独管理特定用户/服务 |
当我们想新增 sudo
配置的时候,不要直接修改 /etc/sudoers
本体,否则就可能出错。主配置文件 /etc/sudoers
中显式包含了对 /etc/sudoers.d/
的引用,只需要在 /etc/sudoers.d/
中新建一个文件,写入我们要配置的内容即可,sudo
启动时会自动读取并解析这些补充文件。
对于我们的需求,我们可以新建一个配置文件,例如 depoly
:
1 | sudo visudo -f /etc/sudoers.d/deploy |
注意,一定要用 visudo
而不是直接编辑,visudo
会帮你做语法检查,防止配置错误导致无法 sudo
。
添加如下内容:
1 | ubuntu ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp.service |
意思是用户 ubuntu
可以以 root 身份(ALL)运行 /bin/systemctl restart myapp.service
,并且不需要输入密码(NOPASSWD)。更安全的是,这条权限只允许执行该一条命令,而不是 ALL
。sudoer
文件中规则的核心格式是这样的:
1 | <用户或组> <主机列表>= (<可切换身份>) <可执行命令> |
因此对于我们的命令,各部分的含义如下:
部分 | 含义 |
---|---|
ubuntu |
被授权的用户(也可以是 %groupname 表示组) |
ALL (主机列表) |
表示在任意主机上都生效(用于集群配置,单机上通常写 ALL) |
(ALL) (切换身份) |
表示 ubuntu 用户可以以 任何身份(包括 root) 来执行指定命令 |
NOPASSWD: |
执行时不要求输入密码 |
/bin/systemctl... |
被授权执行的命令(精确路径) |
SSH 反向隧道
SSH 是什么?
我们经常使用 SSH 客户端(如 ssh
命令或 Xshell 等)远程登录服务器。那么 SSH 到底是什么?
许多时候,我们会直接把它等同于“远程登录服务器的工具”。实际上,SSH(Secure Shell)是一种协议规范,本质上用于在不安全的网络中构建加密的通信通道。所谓的 ”SSH 软件“,实际上只是实现了 SSH 协议的产品。
SSH 协议定义了在网络上传输数据时的三大核心机制:
- 认证(Authentication):验证通信双方身份;
- 加密(Encryption):对通信内容进行加密,防止被窃听;
- 完整性校验(Integrity):确保数据传输过程中未被篡改。
总而言之,SSH 在计算机之间建立网络连接,并能充分保障连接的双方是真实可信的,SSH 还能确保使用该连接传输的所有数据不会被窃听者读取或者修改。“远程登录”、“安全文件传输(如 scp
)”、“端口转发(Port Forwarding)”这些只是利用 SSH 安全通信能力在应用层之上的特定功能实现,本质上都只是在一条加密通道上传送不同类型的数据。因此我们需要格外注意:
✅ SSH 是网络通信安全的基础设施,而不是某一种具体的工具用途。
SSH 隧道
基于 SSH,可以实现一种被称为端口转发(port forwarding)或隧道(tunneling)的技术,可以实现对 TCP/IP 进行重新路由,使其通过 SSH 连接传输,并且透明底进行端到端地加密。
假设我们在本地(HostA),要连接公司的内网数据库服务 HostC(db.internal.company.com:3306),但这台数据库服务器不能直接从外网访问,只允许公司内网访问。不过公司有一台“跳板机”或“堡垒机”(HostB),它既能从外部访问,也能访问内网数据库。这个时候,我们就可以使用 SSH 本地转发(-L
)。
ssh -L
本地端口转发命令,其通用语法如下:
1 | ssh -L [本地端口]:[目标主机]:[目标端口] user@跳板机 |
含义如下:
本地端口
:在本地机器监听的端口;目标主机
:是从跳板机所能访问的内部机器,可以是 localhost,也可以是另外一台服务器;目标端口
:目标主机上的服务端口;跳板机
(也称“SSH 网关”):你要通过 SSH 连接的机器,它作为中转。
这条命令可以翻译成:
“在本地监听端口
本地端口
,把通过这个端口的所有数据,通过 SSH 连接发给user@跳板机
,由user@跳板机
再把数据转发到目标主机:目标端口
上去。”
整个过程如下图所示。这里需要注意的是,HostB 需要写成 user@HostB
的形式,这是因为本地客户端需要主动发起一条 SSH 连接到 HostB,因此这里会涉及到 SSH 登录以及认证。而 HostC 并不是通过 SSH 被连接的对象,它只是 HostB 能访问的目标机器或者服务地址。HostC 和 HostB 之间的通信就是普通的 TCP 请求,而不是 SSH 登录,因为在内网之间我们也无须如此防范。换言之,整个数据流可以总结为:
1 | 本地程序 → 本地端口 → SSH 加密通道 → HostB(中转机) → 普通 TCP → HostC:目标端口 |
sequenceDiagram participant A as 本地客户端 (HostA) participant B as 跳板机/SSH Server (HostB) participant C as 目标服务主机 (HostC) Note over A,C: SSH 建立连接:ssh -L 端口1:HostC:端口2 user@HostB A->>B: SSH 连接(加密通道) Note over A: 本地监听 localhost:端口1 A->>A: 启动本地监听端口(端口1) Note over A,B: 通过 SSH 通道发送应用层数据 A-->>B: 请求转发到 HostC:端口2(加密传输) Note over B,C: HostB 普通 TCP 请求转发 B-->>C: 普通 TCP 请求 → HostC:端口2(未加密) C-->>B: 普通 TCP 响应数据(未加密) B-->>A: 返回响应(通过 SSH 加密通道) Note over A: 最终应用通过 localhost:端口1 完成加密通信
回到我们数据库的例子,我们就可以执行:
1 | ssh -L 3307:db.internal.company.com:3306 [email protected] |
这条命令会在本地的 3307 端口和内网的 3306 端口之间建立一条 ”隧道“,使得访问本地的 3307 端口,就好像访问内网的 3306 端口一样。接下来在本地执行:
1 | mysql -h 127.0.0.1 -P 3307 -u dbuser -p |
实际上就是通过本地的 127.0.0.1:3307
连接到了远端的内网数据库 db.internal.company.com:3306
。
参考文献:Barrett D J, Silverman R E. SSH 权威指南. 中国电力出版社, 2003.
SSH 反向隧道(Reverse Tunnel)
在前面的本地端口转发(ssh -L
)中,我们假设有一个可以被公网访问的跳板机,用它作为中转来访问目标服务器。
然而,在 清华校园网的实际情况中,并不存在这样一台“公网可访问”的跳板机,反而是:
- ❌ 外部机器无法连接校园网内主机的 22 端口(因为端口被屏蔽);
- ✅ 校园网内部主机可以主动连接公网机器(比如 VPS 服务器);
在这种场景下,如果我们想要从外部访问校园网内部的服务器,就需要反过来建立连接——由内网主机主动发起 SSH 连接,预先建立一条“隧道”,供外部访问使用。这种技术叫作 SSH 反向隧道,也常被称为“内网穿透”。命令如下:
1 | ssh -R <远程端口>:<目标主机>:<目标端口> <用户>@<host2> |
在这里,目标主机是运行实际服务的主机,比如数据库服务,服务运行在 <目标端口>
上。host1
就是执行反向隧道命令的主机,它在内网中。在通常的情况下,host1
和 target
是同一个主机,这样 <目标主机>
部分就是 localhost
。host2
是公网服务器,作为端口暴露和隧道中转。整个流程如下所示:
sequenceDiagram participant Target as 内网目标主机 (target) participant Host1 as 内网客户端 (host1,执行 ssh -R) participant Host2 as 公网服务器 (host2,VPS) participant Client as 外部访问者 Note over Host1,Host2: host1 执行:ssh -R <远程端口>:<目标主机>:<目标端口> user@host2 Host1->>Host2: 建立 SSH 连接(加密通道) Host2->>Host2: 在本地监听 <远程端口> Note over Client,Host2: 外部访问者连接 host2:<远程端口> Client->>Host2: 发起 TCP 连接请求(未加密) Host2->>Host1: 通过 SSH 隧道转发请求(加密) Note over Host1,Target: host1 普通 TCP 转发到 target:<目标端口> Host1->>Target: 发起普通 TCP 请求(未加密) Target-->>Host1: 响应数据返回(未加密) Host1-->>Host2: 响应数据通过 SSH 隧道返回(加密) Host2-->>Client: 返回 TCP 响应(未加密)
首先
host1
(在内网)主动通过 SSH 连接host2
(公网 VPS),通过命令建立了一条 SSH 加密隧道,同时要求host2
开发<远程端口>
并做好转发准备;连接建立后,
host2
会在本地监听<远程端口>
,等待外部访问;某个外部主机(
client
)访问host2:<远程端口>
,发起普通 TCP 请求,就像访问一台正常服务;host2
把这个请求通过SSH 加密隧道转发给host1
;host1
收到后,发起普通 TCP 请求访问<目标主机>:<目标端口>
,也就是最终的服务运行位置。注意:<目标主机>
可以是localhost
(host1 本机);- 也可以是
host1
能访问的内网其他机器(比如某台数据库服务器)。
服务响应返回后,数据沿着原路返回:
从
target
返回给host1
(普通 TCP);host1
再通过 SSH 加密隧道回传给host2
;host2
最后将数据返回给最初发起连接的外部client
。
在整个过程中,只有 host1
和 host2
之间是通过 SSH 隧道加密通信的,因此需要 host2
可以认证 host1
,比如存有 host1
的 SSH 公钥,或者输入用户密码。
数据路径 | 是否加密 | 描述 |
---|---|---|
host1 ↔︎ host2 |
✅ 是 | SSH 隧道,加密通信 |
host1 ↔︎ target |
❌ 否 | 局域网普通 TCP 请求 |
client ↔︎ host2 |
❌ 否 | 公开网络 TCP 请求(如 6000) |
回到我们的场景,我们希望实现的是:GitHub Actions 能自动登录校园网内的服务器,拉取最新代码并重启服务,也就是一次完整的远程自动部署。也就是说,GitHub Actions 所在的服务器(GitHub 的 Runner)必须能访问这台服务器的 SSH(22 端口)。因此,我们采用SSH 反向隧道,在校园网服务器(内网)上主动向公网 VPS 建立连接,并把自己的 22
端口映射到 VPS 的某个公网端口上(例如 2222
):
1 | ssh -N -f -R 2222:localhost:22 [email protected] |
在校园网内服务器(host1)上执行,建立 SSH 连接到公网 VPS(host2),要求在 VPS 上监听端口 2222
。此后所有访问 VPS 2222
的连接,都会被反向转发回校园服务器的 localhost:22
,这样就实现了:从公网访问 VPS:2222 = 实际访问 校园网服务器:22。常用的 SSH 选项含义如下所示:
选项 | 语义/全拼(概念) | 作用说明 |
---|---|---|
-L |
Local Port Forwarding | 本地端口转发,将本地端口映射到远程服务 |
-R |
Remote Port Forwarding | 反向端口转发,将远程端口映射回本地服务 |
-N |
No Command | 不执行远程命令,仅建立连接(常用于端口转发) |
-f |
Fork to background | 建立连接后立即转入后台运行 |
-i |
Identity File | 指定私钥文件(用于密钥登录) |
-p |
Port | 指定远程主机的 SSH 端口 |
-T |
Disable pseudo-terminal | 不分配伪终端,适用于脚本调用 |
-v |
Verbose | 显示详细调试信息(排查连接问题时非常有用) |
-C |
Compression | 启用压缩传输,适合低带宽网络 |
autossh
autossh
是一个非常实用的小工具,它可以自动监控并重连断掉的 SSH 会话,常用于保持端口转发或反向隧道长期稳定运行。我们在创建了一个 SSH 隧道之后,网络偶尔中断、SSH 会话被踢掉、VPS 重启等问题都可能导致隧道失效。这时候,就需要 autossh
——一个能在隧道断开时自动重新连接的工具。autossh
可以像正常的 ssh
一样启动一个 SSH 会话,但它会周期性地探测隧道是否活着。如果隧道断了,它会自动重新连接,直到恢复为止。
假设我们想让内网主机一直维持一个反向 SSH 隧道到 VPS:
1 | autossh -M 0 -f -N -R 2222:localhost:22 [email protected] |
默认情况下,autossh 会额外建立一组“探测连接”,用来监控实际 SSH 隧道是否可用。它通过两个额外端口进行双向通信测试,如果这些端口的通信失败,就判断 SSH 隧道可能挂了,然后自动重启。比如当我们运行:
1 | autossh -M 20000 ... |
autossh 会在本地监听 20000 和远程监听 20001 两个端口,用它们之间的数据往返作为“心跳探测”。如果这两个端口间的 echo 测试失败,说明 SSH 连接断了,autossh 会自动重连。使用 -M 0
表示禁用 autossh 的内建监控端口机制,只依赖 SSH 自身的重连检测,避免额外占用端口或被防火墙干扰。
Github Actions
概述
GitHub Actions 是一套自动化平台,包含:事件触发机制 + 执行环境(Runner)+ 步骤控制系统。在项目中启用 Github Actions 非常简单,只需要使用一个 YAML 格式的配置文件即可,放在代码仓库路径:
1 | .github/workflows/your-workflow-name.yml |
只要这个文件存在,GitHub 就会自动启用该工作流(Workflow),并在后台自动分配一台虚拟机(Runner)来执行我们定义的所有步骤。你在 .yml
里写的每一个 run:
命令,都是在这台虚拟机上执行的。这台虚拟机是 GitHub 提供的临时环境,和我们的本地电脑无关。所以 GitHub Actions 本质上就是:自动调度云端虚拟机来执行我们写的步骤,用 Github Actions 的服务器跑我们设定好的流程。所以我们可以把整个流程想象成:
- GitHub 检测到我们 push 了代码;
- GitHub 立刻找来一台干净的云服务器(虚拟机);
- 下载我们的项目代码;
- 按照我们的
.yml
文件中定义的步骤逐一执行; - 所有的脚本、命令、SSH 操作、部署行为,都在那台临时云服务器上完成。
下面是一个最小可用的 GitHub Actions 示例,用于在 push 到 main 分支时,执行一条 shell 脚本:
1 | name: Deploy to Server |
主要字段说明:
字段 | 说明 |
---|---|
name |
工作流名称,会显示在 GitHub Actions 页面 |
on |
触发条件,可以是 push , pull_request , schedule 等 |
jobs |
工作流中要执行的一个或多个任务(job) |
runs-on |
指定 runner 的操作系统,如 ubuntu-latest , windows-latest |
steps |
任务的执行步骤,每一步是 shell 命令或使用 uses 引用其他 action |
uses |
调用别人已经封装好的 Action,比如 actions/checkout@v3 |
run |
执行一条 shell 命令 |
在这里,actions/checkout@v3
是一个 官方 GitHub Action,用于将我们项目的代码仓库克隆(checkout)到运行环境(Runner)里。当 GitHub Actions 启动时,它只是一台“空”的云主机。默认情况下,它并不会自动包含我们的项目代码。所以必须使用 actions/checkout@v3
把代码“拉取下来”,才能在后续步骤中使用这些文件、脚本、配置等。这个命令表示,使用 actions
组织下的 checkout
模块,并指定版本 v3
(目前稳定主流版本)。同时,把当前仓库的代码拉取到当前工作目录中(默认是 /home/runner/work/your-repo
)。
run: bash scripts/deploy.sh
的意思是,运行项目代码中 scripts/deploy.sh
脚本。在 .yml
中可以直接使用相对路径,因为默认当前工作目录就是项目根目录。
当然,我们还可能依赖于特定版本的工具,Github Actions 也提供了官方工具 Action 来设置运行时的环境,比如:
1 | - name: Set up Python |
如果要设置特定版本的 GCC/CMake/Java 等,可以使用 apt-get install
来安装依赖,也可以使用官方提供的 setup-*
系列 action 来自动安装指定版本的工具,如 setup-java
, setup-go
, setup-ruby
等。整个流程其实和 Docker 的配置是比较相近的。
执行多行命令
使用 run: |
可以运行多行 shell 脚本,格式如下:
1 | - name: 安装依赖并构建 |
run: |
表示多行命令块开始(YAML 中叫“多行块字面量”,就像 Python 的 """
一样),每行都是标准 Bash 命令,所有命令会在同一个 shell 会话中执行。使用 |
后,在 YAML 中,每行的缩进和行尾空白都会被去掉,而额外的缩进会被保留:
1 | # YAML |
如果要在 SSH 登录后再执行多条命令,可以使用 Here Document:
1 | - name: SSH 远程部署 |
这种写法,属于 Bash 的 Here Document 语法,是一种向命令传递多行字符串的方式。Here Document 的语法是:
1 | command <<[标识符] |
这个语法的意思是把“多行内容”作为标准输入(stdin),送入前面的命令执行。对于我们的例子,整段代码的完整含义是:
用 SSH 连接到远程主机
vps.example.com
,端口2222
;然后在连接后执行
<<EOF
之间的所有命令(这些命令会在远程主机上执行);类似于登录进 SSH 后你手动一条一条输命令,但现在你把它打包好一次性送进去。
片段 | 含义 |
---|---|
ssh -p 2222 user@host |
使用 SSH 连接远程主机,端口 2222 ,用户名 user |
<<'EOF' |
开启 Here Document,并用 'EOF' 作为“终止标志” |
cd ~/myapp ... |
是一组命令,会通过 SSH 被传到远程服务器,并在服务器上执行 |
EOF (最后一行) |
表示 Here Document 的结尾,告诉 Bash:“命令结束了” |
Secrets 和 Variables
在 Github Actions 中,我们可以使用变量,比如定义服务器 IP、端口、或者私钥等等。在 Github Actions 中,有两种变量,Secrets 和 Variables。Secrets 是我们不希望公开的敏感信息,比如 SSH 私钥、API 密钥、数据库密码。我们可以在 Github 仓库中添加:
Settings → Secrets and variables → Actions → New repository secret
添加后,可以在工作流中这样使用:
1 | - name: Connect to server |
Variables 是非敏感的环境变量,比如环境名称(dev、prod)、自定义部署路径、标记版本号等。它们的配置入口和 Secrets 是同一地方,也可以在 .yml
中直接定义:
1 | env: |
然后在步骤中使用 ${{ env.DEPLOY_ENV }}
。
在自动部署中,我们需要 GitHub Actions 在代码 push 后远程登录服务器,执行命令(如 git pull
、systemctl restart
),我们有两种认证选择。
方式一:使用账号 + 密码登录
把密码以明文形式保存在 GitHub 的 Secrets 里,然后配合 sshpass
工具进行非交互登录:
1 | sshpass -p ${{ secrets.SSH_PASSWORD }} ssh user@host "some-command" |
方式二:使用 SSH 密钥对
📍 步骤 1:生成密钥对(在本地)
1 | ssh-keygen -t rsa -b 4096 -m PEM -f cicd_rsa |
-t rsa
:生成 RSA 密钥;-b 4096
:4096 位强度;-m PEM
:强制使用传统 PEM 格式(这才是-----BEGIN RSA PRIVATE KEY-----
);-f cicd_rsa
:文件名为cicd_rsa
和cicd_rsa.pub
。
会生成:
- 私钥:
~/.ssh/cid_rsa
- 公钥:
~/.ssh/cid_rsa.pub
📍 步骤 2:将公钥加入服务器的信任列表
登录到服务器,执行:
1 | mkdir -p ~/.ssh |
然后将 公钥内容(cid_rsa.pub) 粘贴进去保存。
📌 路径是
~/.ssh/authorized_keys
,表示“允许哪些公钥可以无密码 SSH 登录我”。
📍 步骤 3:把私钥配置到 GitHub Secrets
- 打开项目仓库 →
Settings
→Secrets and variables
→Actions
- 点击
New repository secret
- 添加一项名称为
SSH_PRIVATE_KEY
- 把私钥内容(
cid_rsa
文件)复制粘贴进去
📍 步骤 4:GitHub Actions 脚本中使用它
1 | - name: Setup SSH key |
然后就可以无密码远程执行命令了:
1 | - name: Deploy |
个人项目配置
最后,对于我们所说的项目配置情景,可以使用如下所示的 yml 配置,具体字段含义均显示在 yml 文件的注释中:
1 | # deploy.yml |
整个部署流程如下图所示:
sequenceDiagram participant Dev as 本地开发机 participant GitHub as GitHub Actions participant VPS as 腾讯云中转机 (sheny@vps) participant Campus as 校园服务器 (sheny@kepler) %% 建立反向隧道(常驻过程) Campus->>VPS: ssh -R 2222:localhost:22 sheny@vps note over VPS,Campus: VPS 上开放 2222 端口,反向连接 Campus:22 %% 部署过程开始 Dev->>GitHub: git push 到 main 分支 GitHub->>GitHub: 触发 Actions Workflow GitHub->>VPS: SSH 连接 VPS:2222(跳板端口) VPS->>Campus: 通过反向隧道转发 SSH 到校园服务器 GitHub->>Campus: (透过隧道)执行部署脚本 Campus->>Campus: git pull origin main Campus->>Campus: systemctl restart app Campus-->>GitHub: 返回执行结果 GitHub-->>Dev: 显示部署成功 / 失败日志
小结
本文结合实际校园网络环境,构建了一套基于 GitHub Actions + SSH 反向隧道 + systemd 的轻量化自动部署方案,核心流程为:
- Push 代码到 GitHub → 触发 Actions;
- GitHub Actions 通过 VPS 转发 SSH 连接 → 登录校园服务器;
- 执行部署脚本 → 拉取最新代码并重启服务。
在配置过程中,我们还介绍了:
systemd
的服务管理机制与.service
文件写法;SSH
反向隧道的原理与实用命令;GitHub Actions
的基础结构、运行环境、Secrets 管理方法;sudoers.d
的免密码配置,保障部署流程稳定自动执行。
借助这些工具,即使在内网受限的场景下,我们也可以优雅实现自动化部署,从此告别手动登录拉代码的繁琐流程。