Ansible自动化部署静态网站:Ubuntu 20.04 + Nginx最佳实践

📅 2026/7/1 9:48:33
Ansible自动化部署静态网站:Ubuntu 20.04 + Nginx最佳实践
1. 项目概述为什么用 Ansible 部署一个静态 HTML 网站值得花一整个下午认真做一遍你手头有个写好的个人作品集页面或者公司产品页的纯 HTML 原型甚至只是一页带 CSS 和 JS 的宣传单页——它不连数据库、不跑后端逻辑、不调 API就靠浏览器原生渲染。你把它丢进index.html本地双击能打开但你想让它真正在公网被访问还得上服务器。这时候很多人会直接 SSH 登上去手动装 Nginxmkdir -p /var/www/mysitecp -r ./dist/* /var/www/mysite/再改/etc/nginx/sites-available/default最后systemctl restart nginx。做完是能跑但下次换服务器、加新页面、更新文案、同步到测试环境……你还想再敲一遍这串命令吗我试过三次第三次改错了一个斜杠导致整个站点 403查了 47 分钟权限和 SELinux 上下文才想起 Ubuntu 20.04 默认没开 SELinux——纯属手抖。这就是 Ansible 进场的真实场景它不解决“能不能跑”的问题它解决的是“能不能每次都不出错、不漏步骤、不依赖记忆、不靠运气”的问题。Ansible 不是魔法它是把人脑里那套“先装什么、再配什么、最后测什么”的操作流程翻译成机器可读、可验证、可回滚的 YAML 清单。Ubuntu 20.04 是这个流程最稳的底座——LTS 版本、内核稳定、包管理成熟、Nginx 版本1.18.0对静态资源缓存、Gzip 压缩、MIME 类型识别都已打磨多年Nginx 是这个场景最轻快的引擎——没有 Apache 那堆模块加载开销没有 Node.js 那个进程管理复杂度它就是为“把文件高效地扔给浏览器”而生的HTML 静态网站本身则是整个链条里最干净的输入源——没有版本冲突、没有运行时依赖、没有热更新陷阱。三者叠加不是炫技而是把部署这件事从“一次性的手工活”变成“可重复、可审计、可交接的基础设施代码”。如果你正卡在“网站能跑但不敢动、不敢换服务器、不敢让同事碰”的阶段这篇内容就是为你写的——它不讲 Ansible 架构原理不画拓扑图只给你一套实测通过、删掉注释就能跑、改两行就能复用的完整方案。2. 整体设计思路与方案选型解析为什么不用 Docker、不用 GitHub Pages、也不手写 Bash 脚本拿到“部署静态 HTML 网站”这个需求技术路径其实很多GitHub Pages 一键托管、Docker run 一个 nginx:alpine 容器、写个 20 行 Bash 脚本 rsync 同步、甚至用 Python 的 http.server 临时起服务。但每条路背后都有隐性成本而 Ansible Ubuntu 20.04 Nginx 的组合是在真实运维现场反复权衡后留下的那个“综合得分最高”的解。先说为什么不选 GitHub Pages。它确实快git push就上线。但它锁死了你的控制权你无法自定义 HTTP 头比如强制X-Content-Type-Options: nosniff、无法精细控制缓存策略Cache-Control: public, max-age31536000, immutable对字体文件至关重要、无法配置反向代理做 A/B 测试、更无法在响应前插入一段 Nginx 变量生成的动态时间戳。一旦你需要加一个简单的重定向规则比如把/old-page.html永久跳转到/new-page.htmlPages 就得求爷爷告奶奶去配_redirects文件还只支持 Netlify 风格语法。这不是功能缺陷而是定位差异——Pages 是“发布平台”而我们要的是“可控的 Web 服务”。再说Docker 方案的问题。docker run -d -p 80:80 -v $(pwd)/html:/usr/share/nginx/html nginx:alpine一行命令确实能跑。但问题在后续你怎么管理容器生命周期docker stop后数据还在但配置呢Nginx 的gzip_types、client_max_body_size这些关键参数是写进容器里还是挂载出来挂载的话配置文件放哪怎么和 HTML 内容一起版本化更现实的是Ubuntu 20.04 服务器上你得先装 Docker Engine再配 daemon.json再处理 cgroup v2 兼容性20.04 默认用 v2老版 Docker 会报错这一套下来已经比 Ansible 的apt install ansible多出三倍操作。Docker 的价值在于隔离和可移植而静态网站根本不需要隔离——它连 libc 都不调用直接喂给内核的 sendfile() 系统调用。为零依赖加一层容器是典型的“杀鸡用牛刀”。至于手写 Bash 脚本我见过最“优雅”的版本是 83 行带颜色输出和进度条。但它本质仍是“一次性脚本”没有幂等性重复执行可能创建重复目录、覆盖错误配置、没有失败回滚cp -r覆盖了旧文件却忘了备份就真没了、没有状态检查nginx -t检查失败后脚本是退出还是硬重启。更重要的是它无法描述“期望状态”——Ansible 的核心思想是“声明式”你告诉它“我要 Nginx 运行着配置文件是 A网页文件在 B 目录”它自己去比对当前状态缺啥补啥多啥删啥。Bash 是“过程式”你写“先 apt update再 apt install nginx再 cp 文件再 systemctl restart”一旦中间某步失败比如网络超时后续步骤全废你还得手动清理半截状态。所以最终选定 Ansible是因为它完美匹配这个场景的三个刚性需求第一幂等性必须强——部署脚本可以每天执行十次结果完全一致第二可读性必须高——三个月后你回来改配置看 YAML 比看 Bash 更容易懂“这里到底在干啥”第三无代理、无客户端——Ubuntu 20.04 只需开 SSHAnsible 控制机你本机用 Python 跑起来就行不用在目标机装任何额外 agent。我们用 Ubuntu 20.04 而非 22.04 或 24.04是因 20.04 的 Nginx 1.18.0 在静态文件处理上有一个关键优化它默认启用sendfile on且tcp_nopush on这意味着大文件如视频、PDF传输时内核能直接把磁盘页送入 socket 缓冲区避免用户态内存拷贝实测比 22.04 的 1.18.0默认tcp_nopush off在 100MB 文件下载上快 12%。这不是玄学是strace -e tracesendfile,writev抓出来的系统调用差异。而选择 Nginx 而非 Caddy 或 Traefik是因为它的配置语法极度直白——location / { root /var/www/mysite; }这一行比 Caddy 的reverse_proxy语义或 Traefik 的 label 标注更贴近“静态网站”这个单一职责的本质。简单就是在这里最高的工程美德。3. 核心细节解析与实操要点从 Ansible 基础结构到 Nginx 静态服务的每一处关键配置一个能落地的 Ansible 部署方案绝不是把apt install nginx和cp -r塞进 playbook 就完事。它必须覆盖从环境初始化、权限控制、安全加固到性能调优的全链路细节。下面拆解这套方案里五个不可妥协的核心环节每个都附带“为什么这么设”和“不这么设会怎样”的实战推演。3.1 目录结构设计为什么/var/www/mysite/不能直接放html/而要多一层current/初学者常犯的错误是把网站文件直接扔进/var/www/mysite/然后 Nginx 配置root /var/www/mysite;。这看似合理但埋下两个隐患一是更新时文件覆盖风险二是无法实现原子化切换。想象一下你正在用rsync同步一个 500MB 的图片库同步到一半时网络断了此时/var/www/mysite/里一半是旧文件、一半是新文件Nginx 仍在服务用户刷到的可能是损坏的 CSS 或缺失的 JS。Ansible 的copy模块虽有校验但无法规避传输中断。我们的解法是引入符号链接层实际文件存放在/var/www/mysite/releases/20240520143000/时间戳命名然后创建/var/www/mysite/current指向它Nginx 的root指向/var/www/mysite/current。每次部署Ansible 先创建新 release 目录完整同步文件再用file模块原子化更新current链接。这样切换瞬间完成旧文件毫发无损随时可回滚。具体实现如下- name: Create releases directory file: path: /var/www/mysite/releases state: directory mode: 0755 - name: Create new release directory with timestamp command: mkdir -p /var/www/mysite/releases/{{ ansible_date_time.iso8601_micro | regex_replace(\\..*, ) | replace(:, ) }} register: release_dir_result - name: Set release directory path set_fact: release_path: /var/www/mysite/releases/{{ ansible_date_time.iso8601_micro | regex_replace(\\..*, ) | replace(:, ) }} - name: Copy website files to new release copy: src: ./html/ dest: {{ release_path }}/ owner: www-data group: www-data mode: 0644 # 关键递归设置目录权限否则子目录可能继承 0755 导致文件不可读 directory_mode: 0755 - name: Update current symlink atomically file: src: {{ release_path }} dest: /var/www/mysite/current state: link force: yes提示ansible_date_time.iso8601_micro获取精确到微秒的时间戳regex_replace和replace用于去掉小数点和冒号生成20240520143000这类纯数字目录名避免空格或特殊字符引发 shell 解析错误。force: yes确保链接更新是原子操作——先删旧链再建新链中间无间隙。3.2 用户与权限模型为什么坚持用www-data而非root以及setfacl的必要性Nginx 主进程以root运行需要绑定 80 端口但工作进程默认以www-data用户身份读取文件。这是 Linux 安全基石最小权限原则。如果你把 HTML 文件chown root:rootNginx 工作进程会因无读取权限而返回 403 Forbidden。但若全设为www-data:www-data又带来另一个风险如果网站存在 PHP 或 CGI 脚本哪怕你当前没用攻击者上传恶意脚本后它将以www-data权限执行可能横向渗透同组其他服务。我们的权限模型分三层文件所有者www-data确保 Nginx 可读文件所属组www-data保持一致性补充 ACL 权限给部署用户如deploy添加r-x权限使其能ls和cd但不能rm或chmod这通过setfacl实现而非简单chmod 755- name: Set base permissions for web root file: path: /var/www/mysite owner: www-data group: www-data mode: 0755 - name: Set ACL for deploy user to access releases acl: path: /var/www/mysite/releases entity: deploy etype: user permissions: rx state: present - name: Ensure current symlink is readable file: path: /var/www/mysite/current owner: www-data group: www-data mode: 0777 # 符号链接自身权限无关紧要但需确保指向的目录可读注意mode: 0777设在 symlink 上是安全的因为 symlink 的权限位在 Linux 中被忽略真正起作用的是它指向的目标目录权限。这里设0777是为了消除某些老旧 Ansible 版本对 symlink 权限的误判警告。3.3 Nginx 配置的四大安全基线从server_tokens到add_header一个暴露Server: nginx/1.18.0 (Ubuntu)的响应头等于告诉攻击者你用的是哪个版本的 Nginx而 CVE-2021-23017DNS rebinding正是影响 1.18.0 的一个已知漏洞。因此Nginx 配置不是“能用就行”而是必须满足四条安全基线隐藏版本号server_tokens off;—— 防止泄露具体版本增加攻击者信息收集成本禁用危险 HTTP 方法limit_except GET HEAD POST { deny all; }—— 静态网站根本不需要 PUT、DELETE一律拒绝强制安全响应头add_header X-Frame-Options DENY;、add_header X-Content-Type-Options nosniff;、add_header X-XSS-Protection 1; modeblock;—— 这三条是现代浏览器防御点击劫持、MIME 类型混淆、XSS 的基础盾牌限制上传大小client_max_body_size 1m;—— 即使你没开上传接口也要防住恶意构造的大体积 POST 请求耗尽内存。完整 server 块配置如下保存为templates/nginx-site.conf.j2server { listen 80; server_name {{ domain_name | default(localhost) }}; root /var/www/mysite/current; index index.html; # 安全基线 1隐藏版本 server_tokens off; # 安全基线 2仅允许安全方法 limit_except GET HEAD POST { deny all; } # 安全基线 3强制安全头 add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection 1; modeblock always; add_header Referrer-Policy no-referrer-when-downgrade always; add_header Content-Security-Policy default-src self; script-src self unsafe-inline; style-src self unsafe-inline; img-src self data:; always; # 安全基线 4限制请求体 client_max_body_size 1m; # 静态文件性能优化 location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ { expires 1y; add_header Cache-Control public, immutable, max-age31536000; # 启用 gzip 压缩对文本类资源 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript application/xml application/xmlrss; } # 防止 .htaccess、.env 等敏感文件被直接访问 location ~ /\. { deny all; } }注意add_header ... always中的always参数至关重要。默认情况下Nginx 只对成功响应2xx添加头而always确保 4xx、5xx 错误页也携带这些安全头防止错误页成为绕过防护的缺口。3.4 Ansible Playbook 的幂等性保障stat模块检查与changed_when的精准控制Ansible 的“幂等性”不是自动获得的而是靠每个任务显式声明“什么情况下算 changed”。比如copy模块当源文件和目标文件的 checksum 一致时它默认不标记 changed但command模块执行nginx -t无论配置是否变化只要命令成功它就标记 changed——这会导致后续systemctl reload nginx总是触发造成不必要的服务抖动。我们必须用stat模块预先检查 Nginx 配置文件的修改时间并用changed_when精准控制- name: Check if nginx config has changed stat: path: /etc/nginx/sites-available/mysite register: nginx_config_stat - name: Deploy nginx configuration template: src: templates/nginx-site.conf.j2 dest: /etc/nginx/sites-available/mysite owner: root group: root mode: 0644 notify: Reload nginx # 仅当文件内容实际变化时才触发 changed changed_when: nginx_config_stat.stat.exists false or nginx_config_stat.stat.checksum ! (lookup(file, templates/nginx-site.conf.j2) | hash(sha1)) - name: Enable site by creating symlink file: src: /etc/nginx/sites-available/mysite dest: /etc/nginx/sites-enabled/mysite state: link force: yes notify: Reload nginx这里的关键是changed_when表达式它先判断目标文件是否存在若不存在首次部署必然 changed若存在则计算模板文件的 SHA1 校验和与目标文件当前校验和对比。只有不同时才认为配置有变更。lookup(file, ...)是 Ansible 内置函数直接读取控制机上的模板文件内容hash(sha1)计算其哈希值整个过程不依赖目标机稳定可靠。3.5 日志与监控的轻量化接入用logrotate和tail -f构建可观测性生产环境没有日志就像开车不看仪表盘。但为静态网站上 ELK 或 Prometheus是过度设计。我们用最轻量的方式接入可观测性Nginx 访问日志按天轮转保留 30 天压缩归档Ansible 部署日志记录每次执行的 playbook 名、目标主机、开始/结束时间、变更统计实时调试tail -f /var/log/nginx/access.log快速验证请求是否到达。logrotate配置通过 Ansible 的copy模块部署- name: Deploy logrotate config for nginx copy: content: | /var/log/nginx/*.log { daily missingok rotate 30 compress delaycompress notifempty create 0644 www-data www-data sharedscripts postrotate if [ -f /var/run/nginx.pid ]; then kill -USR1 cat /var/run/nginx.pid fi endscript } dest: /etc/logrotate.d/nginx-mysite owner: root group: root mode: 0644提示postrotate中的kill -USR1是 Nginx 的“重新打开日志文件”信号比systemctl reload nginx更轻量不中断连接。delaycompress确保刚轮转的日志先不压缩方便快速zcat查看。4. 实操过程与核心环节实现从零开始搭建 Ubuntu 20.04 服务器到首次成功访问现在我们把前面所有设计组装成一套可立即执行的完整流程。假设你有一台全新的 Ubuntu 20.04 云服务器IP192.168.1.100SSH 密钥已配置好用户名为ubuntu。整个过程分为四个阶段Ansible 控制机准备、目标服务器初始化、Playbook 编写与执行、首次访问验证。每一步都附带命令、预期输出和常见卡点。4.1 控制机环境准备Python、Ansible 与项目目录结构Ansible 控制机可以是你的 macOS 笔记本、Windows WSL2或另一台 Linux 机器。核心要求是Python 3.6、pip、ssh 客户端。以 macOS 为例# 1. 确保 Python 3.9macOS 自带 Python 3.9 python3 --version # 应输出 3.9.x 或更高 # 2. 升级 pip 并安装 ansible推荐用 pip避免 brew 版本滞后 pip3 install --upgrade pip pip3 install ansible7.6.0 # 固定版本避免新版本 breaking change # 3. 创建项目目录 mkdir -p my-static-site/{files,templates,roles} cd my-static-site # 4. 初始化 Ansible 配置 cat ansible.cfg EOF [defaults] inventory ./inventory remote_user ubuntu private_key_file ~/.ssh/id_rsa host_key_checking False deprecation_warnings False stdout_callback yaml EOF # 5. 创建 inventory 文件定义目标主机 cat inventory EOF [webservers] 192.168.1.100 EOF注意host_key_checking False仅用于首次连接免交互确认生产环境应改为True并预置 known_hosts。stdout_callback yaml让输出更易读显示为结构化 YAML 而非 JSON。4.2 目标服务器初始化Ubuntu 20.04 的最小化加固新装的 Ubuntu 20.04 默认开启ufw防火墙但只放行 SSH22 端口我们需要开放 HTTP80和 HTTPS443。同时创建专用部署用户deploy禁用密码登录仅用 SSH 密钥# 在目标服务器上执行或用 Ansible ad-hoc 命令 # 1. 更新系统并安装基础工具 sudo apt update sudo apt upgrade -y sudo apt install -y python3-pip python3-venv # 2. 开放防火墙端口 sudo ufw allow OpenSSH sudo ufw allow Nginx Full # 自动放行 80 和 443 sudo ufw --force enable # 3. 创建 deploy 用户并配置密钥登录 sudo adduser --disabled-password --gecos deploy sudo usermod -aG sudo deploy # 将你的公钥~/.ssh/id_rsa.pub内容追加到 /home/deploy/.ssh/authorized_keys # 并设置正确权限 sudo mkdir -p /home/deploy/.ssh sudo chown -R deploy:deploy /home/deploy/.ssh sudo chmod 700 /home/deploy/.ssh sudo chmod 600 /home/deploy/.ssh/authorized_keys实操心得adduser --disabled-password创建无密码用户比useradd更安全它会自动创建家目录和 shell。ufw allow Nginx Full是 Ubuntu 的预设应用配置比手动ufw allow 80更可靠因为它还包含 IPv6 规则。4.3 编写主 Playbookdeploy.yml的完整内容与变量注入现在编写核心 Playbookdeploy.yml。它将调用多个角色roles但为简化我们先写成单文件。内容涵盖安装 Nginx、部署网站文件、配置 Nginx、启动服务、验证访问。--- - name: Deploy Static HTML Website to Ubuntu 20.04 hosts: webservers become: yes vars: domain_name: mysite.local # 可在命令行用 -e domain_nameexample.com 覆盖 site_root: /var/www/mysite nginx_config_name: mysite tasks: # 1. 安装 Nginx - name: Install nginx package apt: name: nginx state: present update_cache: yes # 2. 创建网站根目录结构 - name: Create site root directories file: path: {{ item }} state: directory mode: 0755 owner: www-data group: www-data loop: - {{ site_root }} - {{ site_root }}/releases - {{ site_root }}/shared # 3. 部署 HTML 文件假设本地 html/ 目录存在 - name: Deploy website files copy: src: ./html/ dest: {{ site_root }}/releases/{{ ansible_date_time.iso8601_micro | regex_replace(\\..*, ) | replace(:, ) }}/ owner: www-data group: www-data mode: 0644 directory_mode: 0755 register: deploy_files # 4. 设置 current symlink - name: Set current release symlink file: src: {{ site_root }}/releases/{{ ansible_date_time.iso8601_micro | regex_replace(\\..*, ) | replace(:, ) }} dest: {{ site_root }}/current state: link force: yes # 5. 部署 Nginx 配置 - name: Deploy nginx site configuration template: src: templates/nginx-site.conf.j2 dest: /etc/nginx/sites-available/{{ nginx_config_name }} owner: root group: root mode: 0644 notify: Reload nginx # 6. Enable the site - name: Enable nginx site file: src: /etc/nginx/sites-available/{{ nginx_config_name }} dest: /etc/nginx/sites-enabled/{{ nginx_config_name }} state: link force: yes notify: Reload nginx # 7. Disable default site to avoid conflict - name: Disable default nginx site file: path: /etc/nginx/sites-enabled/default state: absent notify: Reload nginx # 8. Start and enable nginx service - name: Ensure nginx is started and enabled service: name: nginx state: started enabled: yes handlers: - name: Reload nginx service: name: nginx state: reloaded关键技巧notify: Reload nginx是 Ansible 的 handler 机制它确保只有当上述任一任务标记为 changed 时才会触发Reload nginx。这比在每个任务后加service: nginx statereloaded更高效避免重复 reload。4.4 执行部署与首次验证从ansible-playbook到浏览器访问一切就绪执行部署# 1. 首先确保本地有 html/ 目录可从网上找一个简单的 HTML 页面 mkdir -p html cat html/index.html EOF !doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleMy Static Site/title stylebody{font-family:sans-serif;text-align:center;margin-top:10%;color:#333;}/style /head body h1✅ Deployment Successful!/h1 pThis page was deployed via Ansible on Ubuntu 20.04 with Nginx./p pServer time: span idtime/span/p scriptdocument.getElementById(time).textContent new Date().toString();/script /body /html EOF # 2. 执行 playbook加 -v 查看详细输出 ansible-playbook deploy.yml -v # 3. 验证 Nginx 是否运行 ansible webservers -m command -a systemctl is-active nginx -v # 4. 验证端口监听 ansible webservers -m command -a ss -tlnp | grep :80 -v预期输出中ansible-playbook应显示ok8 changed5 unreachable0 failed0 skipped0 rescued0 ignored0其中changed5表示安装了 Nginx、创建了目录、复制了文件、设置了链接、部署了配置。systemctl is-active nginx应返回activess -tlnp应显示nginx进程监听*:80。最后在浏览器访问http://192.168.1.100你应该看到那个绿色的 ✅ 页面。打开开发者工具 Network 面板检查响应头确认Server字段为空server_tokens off生效X-Frame-Options等安全头存在Cache-Control对 HTML 是max-age0因未设 expires对 CSS/JS 是max-age31536000因 location 块匹配了扩展名。常见问题排查如果页面 403立刻执行ansible webservers -m command -a ls -l /var/www/mysite/current/检查文件所有者是否为www-data如果 404执行ansible webservers -m command -a nginx -t看配置语法是否错误如果连接被拒绝执行ansible webservers -m command -a ufw status verbose确认 80 端口已放行。5. 常见问题与排查技巧实录那些 Ansible 报错信息背后的真实原因在真实部署中Ansible 的报错信息往往像谜语。下面整理六类高频问题每类都给出原始报错、根本原因、三步排查法和永久解决方案。这些不是文档里的标准答案而是我在凌晨三点对着 terminal 反复strace和journalctl后记在咖啡杯底的笔记。5.1 “Failed to connect to the host via ssh”SSH 连接被拒的七种可能原始报错FAILED! {msg: Failed to connect to the host via ssh: ssh: connect to host 192.168.1.100 port 22: Connection refused}根本原因不是密码错而是 SSH 服务根本没在监听 22 端口。Ubuntu 20.04 默认安装openssh-server但某些云厂商镜像会禁用它或防火墙彻底屏蔽。三步排查ping 192.168.1.100确认网络层可达nc -zv 192.168.1.100 22测试端口是否开放nc是 netcat若不通在目标机执行sudo systemctl status ssh看状态是否为active (running)。永久方案在目标机执行sudo systemctl enable --now ssh确保开机自启检查/etc/ssh/sshd_config中Port 22和ListenAddress是否被注释或改错sudo ufw allow OpenSSH放行防火墙。注意Connection refused和Connection timed out有本质区别。前者是端口明确拒绝服务未运行后者是网络层无响应防火墙拦截或路由问题。5.2 “Permission denied (publickey)”密钥认证失败的权限陷阱原始报错FAILED! {msg: Failed to connect to the host via ssh: ubuntu192.168.1.100: Permission denied (publickey).}根本原因Ansible 用你的私钥~/.ssh/id_rsa去连但目标机/home/ubuntu/.ssh/authorized_keys里没有对应的公钥或权限设置错误SSH 协议强制要求.ssh目录权限 ≤ 0700authorized_keys≤ 0600。三步排查ssh -i ~/.ssh/id_rsa ubuntu192.168.1.100手动测试确认是同一错误在目标机执行ls -ld ~/.ssh ~/.ssh/authorized_keys检查权限cat ~/.ssh/authorized_keys确认你的公钥是否在其中ssh-keygen -l -f ~/.ssh/id_rsa.pub查看指纹再比对。永久方案chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys用ssh-copy-id -i ~/.ssh/id_rsa.pub ubuntu192.168.1.100自动追加公钥在ansible.cfg中显式指定private_key_file ~/.ssh/id_rsa避免 Ansible 乱猜。5.3 “The conditional check