目录
[一、SSH 介绍](#一、SSH 介绍)
[四、Podman on ECS + Jenkins 落地方案](#四、Podman on ECS + Jenkins 落地方案)
一、SSH 介绍
SSH(Secure Shell) 是一种在网络上安全登录远程电脑、并在上面执行命令的协议/工具。
日常说的用 SSH,通常指:
bash
ssh user@服务器IP
连上之后,就像坐在那台机器前一样操作终端。
它解决什么问题?
早期远程登录常用 Telnet,内容几乎是明文,密码、命令都可能被窃听。
SSH 的核心价值:
- 加密传输:通信内容加密,中间人难以直接偷看
- 身份认证:确认"你是谁"、以及"这台服务器是不是你要连的那台"
- 远程执行:不只登录,还能远程跑命令、传文件
所以在运维、Jenkins 部署到 ECS 时,SSH 很常用。
基本组成
| 角色 | 说明 |
|---|---|
| SSH 客户端 | 本机 / Jenkins 所在机器上的 ssh |
| SSH 服务端 | 目标机器上的 sshd(监听常见端口 22) |
| 用户账号 | 如 centos、deploy |
| 认证方式 | 密码,或更推荐的密钥 |
一次连接大致是:
电脑/Jenkins --加密通道--> ECS 上的 sshd --验证身份--> 给出一个远程 Shell
两种常见登录方式
1. 密码登录
bash
ssh centos@192.168.1.10
# 然后输入密码
简单,但密码可能泄露、也不适合自动化。
2. 密钥登录(推荐)
本地生成一对密钥:
- 私钥(private key):只有你自己保管,不能泄露
- 公钥(public key):放到服务器的
~/.ssh/authorized_keys
之后登录不再输密码:
bash
ssh -i ~/.ssh/id_rsa centos@192.168.1.10
Jenkins 部署 ECS 时,通常把私钥存进Jenkins 凭据,流水线里用它去 SSH。
常用能力(不只是登录)
1、远程执行一条命令
bash
ssh user@ecs-ip 'sudo systemctl restart myapp'
2、传文件
bash
# 上传
scp app.jar user@ecs-ip:/opt/app/
# 下载
scp user@ecs-ip:/var/log/app.log ./
更强的同步工具还有 rsync(底层也常走 SSH)。
3、隧道/端口转发(进阶)
把远程服务"映射"到本地,例如安全访问远程数据库端口。
和 Jenkins / ECS 的关系
在Jenkins 部署到 ECS里,SSH 通常负责最后一步:
Jenkins 构建完成
→ 通过 SSH 连上 ECS
→ scp/rsync 传包,或让机器 docker pull
→ ssh 远程执行重启、健康检查
所以:
- SSH:安全通道(怎么连、怎么传、怎么远程执行)
- Ansible:在 SSH 之上做批量、标准化部署
- kubectl:那是连 K8s,不是 SSH 那套路
安全上要注意什么
- 私钥当密码看:泄露等于别人能登录你的机器
- 尽量禁用密码登录,只保留密钥
- 不要用 root 日常部署,用权限受限的部署账号
- 限制来源 IP / 安全组,不要对公网无脑敞开 22 端口
- 换默认端口、加 fail2ban 等属于加固手段,不是必须第一步就做
几个容易混淆的词
| 词 | 含义 |
|---|---|
| SSH | 协议/工具本身 |
| sshd | 服务器上的 SSH 服务进程 |
| Shell | 命令行环境(bash 等);SSH 是"安全地获得远程 Shell" |
| SSL/TLS | 另一类加密协议,常用于 HTTPS;和 SSH 不是同一个东西 |
二、Jenkins存放秘钥的方式
Jenkins 通过 SSH 访问 ECS 时,私钥不要当普通文件随便扔在某个共享目录里长期使用。
正确做法是放进Jenkins 凭据(Credentials),由 Jenkins 加密保管,流水线按 ID 引用。
「都放在同一个目录」------可以临时用,但有明显风险,生产不推荐。
推荐:存在 Jenkins Credentials 里
怎么存
- Manage Jenkins → Credentials(凭据)
- 添加 SSH Username with private key
- 填:
- Username:如
deploy - Private Key:粘贴私钥内容,或从文件录入
- Username:如
- 记下凭据 ID,如
ecs-ssh-key
流水线怎么用
bash
sshagent(credentials: ['ecs-ssh-key']) {
sh 'ssh -o StrictHostKeyChecking=no deploy@ECS_IP "hostname"'
}
或用 ssh-agent / Publish Over SSH 等插件,本质都是:用凭据 ID,不在脚本里写私钥路径明文。
为什么这样好
- 私钥不进 Git、不进
Jenkinsfile- Jenkins 主目录里加密保存(依赖 Jenkins 自身安全机制)
- 权限可控:谁能看凭据、哪个 Job 能用
- 轮换密钥时只改凭据,不用改一堆脚本路径
如果放在同一目录,会有什么问题?
比如都放在:
bash
/var/lib/jenkins/.ssh/
id_ecs_a
id_ecs_b
id_ecs_c
或某个共享目录 /data/keys/:
| 问题 | 说明 |
|---|---|
| 权限风险 | 目录若对多人/多 Job 可读,一把钥匙被共用就扩大泄露面 |
| 误用风险 | 脚本写错文件名,可能连错机器、用错账号 |
| 备份/拷贝泄露 | 目录被打包、同步、误提交到仓库都很容易出事 |
| Agent 不一致 | 多节点构建时,私钥若只在某台机器目录,别的 Agent 找不到 |
| 审计困难 | 不如凭据系统能管"谁在用哪把钥匙" |
| 轮换麻烦 | 改文件要同步所有机器和脚本 |
所以:
- 同一目录放多把密钥:文件系统上可行
- 安全与运维上:容易乱、容易漏,不适合作为正式方案
更稳妥的实践
一把钥匙一个用途(最小权限)
- 不要一把 root 私钥打天下
- 最好:
deploy用户 + 仅部署所需 sudo每套环境/每类机器可分开
ecs-test-deploy-keyecs-prod-deploy-key
生产与测试分开,避免测试流水线能登生产权限尽量严(若必须落盘)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_xxx
chown jenkins:jenkins ~/.ssh -R
私钥永不进仓库
.gitignore掉- 也不要写在 Job 明文参数里
定期轮换
- 人员离职、怀疑泄露时立刻换钥,并废止旧公钥
和「同一目录」相关的直接回答
| 做法 | 是否建议 |
|---|---|
多把私钥都丢在 /var/lib/jenkins/keys/ 给所有 Job 读 |
不建议 |
| 每把钥进 Credentials,流水线用不同 credentialId | 推荐 |
开发机本地 ~/.ssh 自己用 |
可以,那是个人环境 |
| Jenkins 正式访问 ECS | 用 Credentials,不要靠共享目录 |
「同一目录」本身不是协议错误,问题在共享、权限、误用和泄露面。密钥一多,目录方案几乎一定会乱。
小提示:公钥放哪?
- 私钥:Jenkins Credentials(保密)
- 公钥:ECS 上
~deploy/.ssh/authorized_keys
两边成对,缺一不可。
用 Jenkins 凭据存私钥,按环境/用途分开;不要图方便把所有私钥堆在同一个可共享目录里。
三、部署到ECS时的常见问题
1、用哪个 Linux 用户部署?
- 要问:root 还是普通用户?
- 方案:专用
deploy用户,不要日常用 root
2、私钥存在哪?
- 要问:文件目录还是 Jenkins 凭据?
- 方案:Jenkins Credentials;不要堆在共享目录,不要进 Git
3、一把密钥打天下?
- 要问:测试键能不能登生产?
- 方案:环境隔离密钥;生产单独凭据 ID
4、sudo 权限怎么控?
- 要问:deploy 能否随意 root?
- 方案:
sudoers只允许重启服务、切软链等必要命令
5、known_hosts / 主机指纹
- 要问:第一次 SSH 交互确认导致流水线卡住?
- 方案:预先写入 known_hosts,或发布机侧统一管理;慎用长期关闭主机校验
6、配置文件放哪?
- 要问:配置打进包,还是机器上外置?
- 方案:配置与代码分离;每环境一份配置,不进镜像/包里的密钥
7、产物是什么、放哪?
- 要问:jar 直接 scp,还是先推 Nexus/OSS?
- 方案:正式建议先推制品库,ECS 再下载/拉取,便于追溯
8、旧版本保留几个?
- 方案:保留最近 N 个(如 5~10),支持快速回滚
9、目录结构怎么设计?
bash
/opt/app/
releases/ # 历史版本
current -> ... # 当前软链
conf/ # 配置
logs/ # 日志
- 方案:用软链切换版本,避免直接覆盖唯一文件
10、触发方式?
- 方案:测试环境 Webhook 自动;生产建议手动或
input审批
11、并发部署怎么办?
- 方案:同一应用加互斥锁,防止两次发布互相踩
12、多分支怎么发?
- 方案:
main→生产,develop→测试;禁止随意分支直发生产
13、失败后哪里看?
- 方案:统一看 Jenkins Console + ECS
journalctl -u 服务名
14、通知谁?
- 方案:成功/失败钉钉或邮件;至少包含环境、版本、构建链接
15、能否接受短暂中断?
- 单机 stop/start 会有中断
- 方案:可接受就单机发布;不可接受就多机滚动 + SLB
16、如何优雅停止?
- 方案:先停流量(从 SLB 摘除)→ 停服务 → 换版本 → 启动 → 探活 → 加回流量
17、健康检查做什么?
- 方案:
curl /health重试多次;失败即回滚,不要只看进程在不在
18、回滚谁触发?
- 方案:健康检查失败自动回滚;也保留"一键回滚到上一版本"任务
19、数据库变更怎么办?
- 要问:是否有迁移脚本?是否可回滚?
- 方案:DB 变更与应用发布拆开策略;先兼容再切换,危险变更人工值守
20、建议采用的最小可行方案(MVP)
如果现在是小白,先不要上太复杂:
- 1 台测试 ECS + deploy 用户 + SSH 密钥进 Jenkins 凭据
- systemd 跑一个 jar(或一个 docker 容器)
- Jenkins 流水线后半段固定 6 步:上传 → 停服 → 切版本 → 启动 → 健康检查 → 通知
- 保留上一版本,失败可回滚
- 测试跑顺后,再复制到生产(换机器、换密钥、加审批)
四、Podman on ECS + Jenkins 落地方案
总体流程:
代码提交
→ Jenkins:podman build
→ podman push 到镜像仓库
→ SSH 登录 ECS
→ podman pull
→ 停旧容器
→ 起新容器
→ 健康检查
→ 失败则回滚
1、ECS 上先准备(只做一次)
(1)安装 Podman
bash
# CentOS Stream / RHEL 9 示例
sudo dnf -y install podman
podman --version
(2)创建部署用户(推荐)
bash
sudo useradd -m deploy
sudo mkdir -p /opt/app
sudo chown deploy:deploy /opt/app
(3)配置 SSH 免密(Jenkins 用)
在 ECS 上:
bash
sudo -u deploy mkdir -p /home/deploy/.ssh
sudo -u deploy chmod 700 /home/deploy/.ssh
# 把 Jenkins 公钥写入:
sudo -u deploy tee -a /home/deploy/.ssh/authorized_keys <<< '你的公钥内容'
sudo -u deploy chmod 600 /home/deploy/.ssh/authorized_keys
(4)让 deploy 能跑 Podman
优先试 rootless(deploy 自己跑):
bash
# 以 deploy 登录后
podman info
若 rootless 在所处环境不好用,再退而求其次给有限 sudo(按需):
bash
# 仅示例:允许 deploy 执行 podman(生产请更精细)
echo 'deploy ALL=(root) NOPASSWD: /usr/bin/podman' | sudo tee /etc/sudoers.d/deploy-podman
(5)登录镜像仓库(ECS 上)
bash
podman login registry.example.com
# 输入仓库账号/密码
可把登录信息留给 deploy 用户,避免每次交互。
(6)安全组
- 放行 Jenkins → ECS 的 SSH
- 放行业务端口(如 8080/80/443)
2、项目里准备 Dockerfile
项目根目录示例:
bash
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY target/app.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app/app.jar"]
前端/其他语言同理,关键是:构建上下文和端口要固定。
3、镜像命名规范(务必统一)
bash
registry.example.com/myproj/app:1.0.0-b128-a1b2c3d
registry.example.com/myproj/app:latest-test # 可选,仅测试
建议至少包含:
- 项目名
- 构建号或版本号
- Git commit 短 SHA
4、ECS 上的运行约定
统一容器名、端口、数据目录:
bash
APP_NAME=app
HOST_PORT=8080
CONTAINER_PORT=8080
DATA_DIR=/opt/app/data
CONF_DIR=/opt/app/conf
首次建目录:
bash
mkdir -p /opt/app/data /opt/app/conf /opt/app/releases
记录当前版本(方便回滚):
bash
/opt/app/releases/current_tag.txt
/opt/app/releases/previous_tag.txt
5、ECS 部署脚本(建议做成文件)
放到 ECS:/opt/app/deploy.sh
bash
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="app"
IMAGE_REPO="registry.example.com/myproj/app"
NEW_TAG="$1" # 例如 1.0.0-b128-a1b2c3d
HOST_PORT=8080
CONTAINER_PORT=8080
HEALTH_URL="http://127.0.0.1:${HOST_PORT}/health"
RELEASE_DIR="/opt/app/releases"
mkdir -p "$RELEASE_DIR"
NEW_IMAGE="${IMAGE_REPO}:${NEW_TAG}"
OLD_TAG="$(cat ${RELEASE_DIR}/current_tag.txt 2>/dev/null || true)"
echo "[1] pull image: $NEW_IMAGE"
podman pull "$NEW_IMAGE"
echo "[2] record previous tag"
if [ -n "${OLD_TAG}" ]; then
echo "$OLD_TAG" > "${RELEASE_DIR}/previous_tag.txt"
fi
echo "[3] stop old container"
podman stop "$APP_NAME" 2>/dev/null || true
podman rm "$APP_NAME" 2>/dev/null || true
echo "[4] start new container"
podman run -d \
--name "$APP_NAME" \
--restart=always \
-p "${HOST_PORT}:${CONTAINER_PORT}" \
-v /opt/app/conf:/app/conf:ro \
-v /opt/app/data:/app/data \
"$NEW_IMAGE"
echo "[5] health check"
ok=0
for i in $(seq 1 20); do
if curl -fsS "$HEALTH_URL" >/dev/null 2>&1; then
ok=1
break
fi
sleep 3
done
if [ "$ok" -ne 1 ]; then
echo "health check failed, rollback..."
podman stop "$APP_NAME" 2>/dev/null || true
podman rm "$APP_NAME" 2>/dev/null || true
if [ -n "${OLD_TAG}" ]; then
podman run -d \
--name "$APP_NAME" \
--restart=always \
-p "${HOST_PORT}:${CONTAINER_PORT}" \
-v /opt/app/conf:/app/conf:ro \
-v /opt/app/data:/app/data \
"${IMAGE_REPO}:${OLD_TAG}"
echo "rolled back to ${OLD_TAG}"
fi
exit 1
fi
echo "$NEW_TAG" > "${RELEASE_DIR}/current_tag.txt"
echo "deploy success: $NEW_TAG"
授权:
bash
chmod +x /opt/app/deploy.sh
若健康检查路径不是
/health,改HEALTH_URL。
6、Jenkins 流水线(Podman 版)
(1)Jenkins 节点也要有 Podman
构建机(Jenkins controller 或 agent)安装 Podman,并能 podman login。
(2)凭据准备
ecs-ssh-key:SSH 私钥(访问 ECS)registry-cred:镜像仓库账号(Username/Password)
(3)Jenkinsfile 示例
bash
pipeline {
agent any
environment {
REGISTRY = 'registry.example.com'
IMAGE_NAME = 'myproj/app'
ECS_IP = '192.168.1.10' // 改成你的 ECS
ECS_USER = 'deploy'
IMAGE_TAG = "b${BUILD_NUMBER}-${GIT_COMMIT.take(7)}"
FULL_IMAGE = "${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Build App') {
steps {
// 按你的语言改,这里以 Java 为例
sh 'mvn -B clean package -DskipTests'
}
}
stage('Build Image (Podman)') {
steps {
sh "podman build -t ${FULL_IMAGE} ."
}
}
stage('Push Image') {
steps {
withCredentials([usernamePassword(
credentialsId: 'registry-cred',
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS'
)]) {
sh '''
echo "$REG_PASS" | podman login "$REGISTRY" -u "$REG_USER" --password-stdin
podman push "$FULL_IMAGE"
'''
}
}
}
stage('Deploy to ECS') {
steps {
sshagent(credentials: ['ecs-ssh-key']) {
sh """
ssh -o StrictHostKeyChecking=accept-new ${ECS_USER}@${ECS_IP} \
'/opt/app/deploy.sh ${IMAGE_TAG}'
"""
}
}
}
}
post {
success { echo "部署成功: ${FULL_IMAGE}" }
failure { echo "部署失败,请看 Console 和 ECS 上 podman logs app" }
}
}
需要思考的关键问题
| 问题 | 建议 |
|---|---|
| rootless 还是 rootful? | 先 rootless;WSL/部分 ECS 不稳再 rootful |
| 和 Docker 命令差异 | 大多可替换;Compose 用 podman compose |
| 镜像仓库地址 | 生产尽量内网仓库,更快更稳 |
| 配置怎么进容器 | -v /opt/app/conf:/app/conf,别把密钥打进镜像 |
| 数据会不会丢 | 数据目录必须挂卷 |
| 端口冲突 | 一台机一应用端口规划清楚 |
| 重启策略 | --restart=always,机器重启容器自动起 |
| 回滚 | 脚本里保留 previous_tag.txt |
| 日志怎么看 | podman logs -f app |
7、常用运维命令(ECS 上)
bash
podman ps
podman logs -f app
podman inspect app
podman stop app
podman rm app
# 手动回滚到上一版
PREV=$(cat /opt/app/releases/previous_tag.txt)
/opt/app/deploy.sh "$PREV"
8、最小验收步骤
- 本地/Jenkins 能
podman build - 能
podman push - ECS 能
podman pull - 手动执行:
/opt/app/deploy.sh 某测试tag - 浏览器或
curl访问健康检查成功 - 再让 Jenkins 全自动跑一遍
9、起步顺序(别一次做完所有环境)
- 先在 一台测试 ECS 跑通
- 镜像仓库先用测试项目
- Jenkins 只接测试环境
- 稳定后复制到生产:换
ECS_IP、换 SSH 凭据、加input审批