Lesson 0017:Ansible——管 100 台服务器像管 1 台

还记得 Lesson 0008 的 Terraform 吗?Terraform 管基础设施(服务器、网络、防火墙)的创建。这一课学 Ansible——管服务器内部(装软件、改配置、启服务)的配置。

两者互补,合在一起才是完整的 IaC:

Terraform:创建服务器 + 安全组 + 网络     ← 基础设施层
Ansible: 装 Docker + 配 Nginx + 部署应用  ← 配置管理层

Ansible 为什么特别?

你之前用 Shell 脚本管服务器(0004/0010):

ssh server1 "apt update && apt install -y nginx"
ssh server2 "apt update && apt install -y nginx"
ssh server3 "apt update && apt install -y nginx"

问题:10 台服务器、每台要装 5 个包、配置 3 个文件——脚本会变成几百行的意大利面条。而且跑第二遍时可能报错(nginx 已经装过了)。

Ansible 的三条设计原则解决了这个问题:

Agentless(无 agent)——通过 SSH 连接,目标机器不用装任何东西
Idempotent(幂等)——跑一次和跑十次结果一样,不会重复安装
Declarative(声明式 YAML)——告诉 Ansible 要什么状态,它自己算需要做什么

四个核心概念

概念是什么类比
Inventory(清单)你要管哪些机器通讯录
Playbook(剧本)在这些机器上做什么Shell 脚本,但是 YAML
Module(模块)具体执行的操作单元npm 包 / 标准库函数
Role(角色)可复用的配置包(给多台机器装同一套东西)Docker 镜像

第一部分:安装并跑通第一台机器(15 分钟)

# 在你的 Ubuntu 机器上安装 Ansible
sudo apt update && sudo apt install -y ansible
ansible --version   # 验证安装

# 最简单的 inventory:只管本机
mkdir -p /tmp/ansible-demo && cd /tmp/ansible-demo
cat > inventory.ini << 'EOF'
[webservers]
localhost ansible_connection=local
EOF

# 第一个命令:ping 所有机器
ansible all -i inventory.ini -m ping
# 输出:localhost | SUCCESS => { "ping": "pong" }

ansible all -i inventory.ini -m ping 的含义:对 inventory.ini 里的所有机器,执行 ping 模块。这就是 Ansible 的最简形式——一行命令管 1 台或 100 台。

第二部分:第一个 Playbook——装 Nginx(15 分钟)

Ad-hoc 命令适合一次性操作。重复执行的配置写成 Playbook:

cat > nginx-playbook.yml << 'EOF'
---
- name: 安装并启动 Nginx          # Playbook 名
  hosts: webservers                # 对 inventory 里的 webservers 组执行
  become: yes                      # sudo 执行

  tasks:                           # 任务列表,从上到下顺序执行
    - name: 更新 apt 缓存
      apt:
        update_cache: yes
        cache_valid_time: 3600     # 1 小时内不重复更新

    - name: 安装 Nginx
      apt:
        name: nginx
        state: present             # present = 确保已安装(幂等!)

    - name: 确保 Nginx 在运行
      service:
        name: nginx
        state: started             # 确保已启动
        enabled: yes               # 开机自启

    - name: 放一个自定义首页
      copy:
        content: "<h1>由 Ansible 管理</h1>"
        dest: /var/www/html/index.html
        mode: '0644'
EOF

# 执行 Playbook
ansible-playbook -i inventory.ini nginx-playbook.yml

关键点:幂等性。再跑一遍 ansible-playbook,输出会显示 ok=4 changed=0——四个 task 全部 skipped,因为状态已经正确了。这就是 Ansible 和 Bash 脚本的本质区别。

模块作用关键参数
apt管理 Debian/Ubuntu 软件包state: present/latest/absent
service管理 systemd 服务state: started/stopped/restarted, enabled: yes
copy复制文件到目标机器src/dest, mode
template用 Jinja2 渲染模板后复制同 copy + {{变量}}
command/shell执行任意命令(最后手段)不幂等,尽量用专用模块

第三部分:变量和模板——学会用 Jinja2(10 分钟)

不同环境用不同配置?Ansible 用 Jinja2 模板 + 变量:

# 1. 创建 Jinja2 模板
cat > nginx.conf.j2 << 'EOF'
server {
    listen {{ nginx_port }};
    server_name {{ server_name }};
    root /var/www/{{ app_name }};
}
EOF

# 2. 在 Playbook 里用 template 模块 + 变量
cat > deploy-app.yml << 'EOF'
---
- name: 部署 Web 应用
  hosts: webservers
  become: yes

  vars:                            # 变量定义
    nginx_port: 8080
    server_name: "{{ ansible_hostname }}"
    app_name: myapp

  tasks:
    - name: 渲染 Nginx 配置
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/{{ app_name }}
      notify: reload nginx         # 配置变了才触发 handler

  handlers:                        # handler = 被 notify 触发的 task
    - name: reload nginx
      service:
        name: nginx
        state: reloaded
EOF

ansible-playbook -i inventory.ini deploy-app.yml
handler 是什么?一种特殊 task,只有在被 notify 时才执行。如果模板没变化,handler 不触发——Nginx 不重启。这避免了每次跑 Playbook 都重启服务。

第四部分:实战——用 Ansible 管理你的 VPS

把学习环境从本机扩展到 VPS,这才是 DevOps 日常:

# 1. 更新 inventory.ini,加上 VPS
cat > inventory.ini << 'EOF'
[webservers]
localhost ansible_connection=local
vps ansible_host=YOUR_VPS_IP ansible_user=root
EOF

# 2. 验证连接(用 SSH key,免密码)
ansible vps -i inventory.ini -m ping

# 3. 写一个完整的 VPS 初始化 Playbook
cat > init-vps.yml << 'EOF'
---
- name: 初始化 VPS —— DevOps 基线配置
  hosts: vps
  become: yes

  tasks:
    - name: 安装基础包
      apt:
        name:
          - htop
          - vim
          - curl
          - git
          - ufw
          - fail2ban
        state: present

    - name: 开启 UFW 防火墙
      ufw:
        rule: allow
        port: "{{ item }}"
      loop: [22, 80, 443]           # loop = 对列表每个元素执行一次

    - name: 启用 UFW
      ufw:
        state: enabled

    - name: 配置时区
      timezone:
        name: Asia/Shanghai

    - name: 确保 fail2ban 运行
      service:
        name: fail2ban
        state: started
        enabled: yes
EOF

ansible-playbook -i inventory.ini init-vps.yml
Ansible 有 3000+ 内置模块——aptufwservicecopytemplatedocker_containergitcron……覆盖 DevOps 日常 95% 的操作。碰到不知道怎么做的,先查 ansible-doc -l | grep 关键词,大概率有现成的模块。

动手练习:用 Ansible 部署你的 Docker 应用

任务:写一个 Playbook,在 VPS 上自动完成:装 Docker → 拉你的应用镜像 → 用 Docker Compose 启动。

cat > deploy-docker-app.yml << 'EOF'
---
- name: Docker 化部署
  hosts: vps
  become: yes

  tasks:
    - name: 安装 Docker(官方脚本)
      shell: curl -fsSL https://get.docker.com | sh
      args:
        creates: /usr/bin/docker      # 如果 docker 已存在就跳过

    - name: 启动 Docker daemon
      service:
        name: docker
        state: started
        enabled: yes

    - name: 把用户加入 docker 组
      user:
        name: "{{ ansible_user }}"
        groups: docker
        append: yes

    - name: 安装 Docker Compose 插件
      apt:
        name: docker-compose-plugin
        state: present

    - name: 创建应用目录
      file:
        path: /opt/myapp
        state: directory
        mode: '0755'

    - name: 上传 docker-compose.yml
      copy:
        src: ./docker-compose.yml
        dest: /opt/myapp/docker-compose.yml

    - name: 启动应用
      docker_compose:
        project_src: /opt/myapp
        state: present                 # present = 确保在运行
EOF

# 先创建一个测试用的 docker-compose.yml
cat > docker-compose.yml << 'EOF'
services:
  web:
    image: nginx:alpine
    ports: ["8080:80"]
EOF

ansible-playbook -i inventory.ini deploy-docker-app.yml
你的任务:跑通上面的 Playbook 后,改成部署你自己的项目(比如之前 0005 课里的 Node.js 应用)。
加分题:把 Nginx 端口从 8080 改成变量,通过 ansible-playbook -e "app_port=9090" 传入。

小测验

Q1:Ansible 和 Terraform 的关系是什么?
Q2:Ansible 的"幂等性"是什么意思?
Q3:Handler 和普通 Task 的区别是什么?

推荐资源

下一步

下一课学习日志系统(0018)——弥补监控(0009)的另外半条腿。学完你就能回答「监控看到 CPU 飙升,怎么查日志找到根因」。

Ansible 最怕的就是拼命写 shell 模块——那你只是在用 YAML 包装 Bash 脚本。先查 ansible-doc -l 找专用模块,99% 的情况都有现成的。
← Lesson 0016 · 术语表 · Mission