还记得 Lesson 0008 的 Terraform 吗?Terraform 管基础设施(服务器、网络、防火墙)的创建。这一课学 Ansible——管服务器内部(装软件、改配置、启服务)的配置。
两者互补,合在一起才是完整的 IaC:
Terraform:创建服务器 + 安全组 + 网络 ← 基础设施层
Ansible: 装 Docker + 配 Nginx + 部署应用 ← 配置管理层
你之前用 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 镜像 |
# 在你的 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 台。
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 | 执行任意命令(最后手段) | 不幂等,尽量用专用模块 |
不同环境用不同配置?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
notify 时才执行。如果模板没变化,handler 不触发——Nginx 不重启。这避免了每次跑 Playbook 都重启服务。
把学习环境从本机扩展到 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
apt、ufw、service、copy、template、docker_container、git、cron……覆盖 DevOps 日常 95% 的操作。碰到不知道怎么做的,先查 ansible-doc -l | grep 关键词,大概率有现成的模块。
任务:写一个 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
ansible-playbook -e "app_port=9090" 传入。
下一课学习日志系统(0018)——弥补监控(0009)的另外半条腿。学完你就能回答「监控看到 CPU 飙升,怎么查日志找到根因」。
shell 模块——那你只是在用 YAML 包装 Bash 脚本。先查 ansible-doc -l 找专用模块,99% 的情况都有现成的。