Ansible与Docker实战:从零构建声明式自动化运维工作流

📅 2026/7/28 17:12:06 👁️ 阅读次数 📝 编程学习
Ansible与Docker实战:从零构建声明式自动化运维工作流

你有没有过这样的经历:刚接手几台服务器,光是装环境、配服务、同步配置就花了大半天,还生怕哪台漏了步骤;或者,团队里有人更新了某个服务的配置,结果因为手动操作,有几台机器忘了同步,半夜报警响个不停。这些看似琐碎的“运维体力活”,消耗的不仅是时间,更是稳定性和心力。

过去,我们可能会写一堆Shell脚本,用SCP来回传,靠SSH连上去手动执行。这种方法在小规模时勉强可行,一旦服务器数量上去,或者流程复杂起来,就变得异常脆弱且难以维护。自动化运维的核心,从来不是追求某个炫酷的技术,而是要把这些重复、易错的手工操作,沉淀成一套可靠、可重复、可版本化管理的“标准作业程序”。

今天要聊的Ansible和Docker,就是构建这套“标准作业程序”的两块核心积木。但很多初学者容易陷入一个误区:把Ansible仅仅看作一个“批量执行命令”的工具,把Docker仅仅看作一个“更好的虚拟机”。这种理解会严重限制它们的价值。Ansible真正的威力在于其“声明式”的状态管理能力,而Docker的价值在于提供了环境一致性的终极交付物。将它们结合,你构建的将不是一个只能跑一次的临时脚本,而是一个从代码到服务的完整、自描述的自动化流水线。

本文不会停留在简单的安装和命令罗列。我们将从一个真实的场景出发,拆解如何用Ansible+Docker,把一个手工部署Nginx的混乱过程,重构为只需一条命令就能在任意新机器上复现的自动化流程。你会看到,自动化运维的难点,往往不在工具本身,而在于如何设计一个清晰、健壮、可扩展的工作流。

1. 重新理解自动化运维:从“跑命令”到“管状态”

在深入Ansible和Docker之前,我们必须先扭转一个观念:自动化运维不等于写脚本批量执行命令。命令式脚本(apt-get install,systemctl start)关注的是“做什么”,而声明式配置管理关注的是“最终状态应该是什么样”。这中间的差别,决定了系统的可维护性和幂等性。

1.1 为什么Shell脚本不是终极答案?

假设我们要在10台服务器上安装并启动Nginx。一个典型的Shell脚本可能是这样的:

#!/bin/bash # 在每台机器上手动执行这个脚本 apt-get update apt-get install -y nginx systemctl start nginx systemctl enable nginx cp /tmp/nginx.conf /etc/nginx/nginx.conf systemctl restart nginx

这个脚本有几个致命问题:

  1. 非幂等:如果第二次运行,apt-get updateinstall会重复执行,虽然可能无害,但会输出不必要的日志。更糟糕的是,如果nginx已经启动,直接cp配置文件并restart,可能会在配置错误时导致服务中断。
  2. 脆弱:任何一步失败(如网络问题导致apt-get install失败),脚本不会自动回滚或清理,可能留下一个中间状态。
  3. 无状态跟踪:我们无法快速知道这10台机器当前的Nginx配置是否完全一致,是哪一版。
  4. 难以扩展:如果要增加对Firewall规则、日志轮转、监控探针的配置,脚本会迅速膨胀,逻辑纠缠。

1.2 Ansible的声明式哲学:描述终点,而非路径

Ansible采用了一种不同的思路。它使用YAML格式的“Playbook”来描述目标状态。对于同一个需求,Ansible Playbook可能长这样:

--- - name: 确保Nginx被安装、配置并运行 hosts: web_servers become: yes # 使用sudo权限 tasks: - name: 安装Nginx包 apt: name: nginx state: present update_cache: yes - name: 上传定制的Nginx配置文件 template: src: templates/nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: '0644' notify: # 如果配置文件改变,则触发处理程序 - 重启Nginx - name: 确保Nginx服务正在运行且开机自启 service: name: nginx state: started enabled: yes handlers: - name: 重启Nginx service: name: nginx state: restarted

这个Playbook的精髓在于:

  • 幂等性state: present确保Nginx被安装,如果已经安装,则什么都不做。state: started确保服务在运行。无论执行多少次,最终状态都是一致的。
  • 模块化:每个task使用专门的模块(apt,template,service)处理特定任务,语义清晰。
  • 变更通知template任务在配置文件内容实际发生改变时,才会触发notify,进而执行handlers中的“重启Nginx”。如果配置文件没变,服务就不会被不必要的重启。
  • 可读性:YAML结构清晰,接近于自然语言描述的需求。

Ansible的核心价值,就是通过这种声明式语法,将系统配置“代码化”。这份Playbook可以放入Git仓库,进行版本控制、代码评审和回滚。它定义的是“Web服务器应有的状态”,而不是“达到这个状态需要敲哪些命令”。

1.3 Docker的补充:将环境与配置一起打包

Ansible解决了服务器上软件配置的自动化问题。但还有一个更底层的问题:操作系统版本、库依赖、环境变量等基础环境的不一致。这就是Docker要解决的。

Docker允许你将应用及其所有依赖(库、环境变量、配置文件)打包成一个独立的“镜像”。这个镜像可以在任何安装了Docker引擎的机器上,以完全一致的方式运行起来,成为一个“容器”。

继续上面的例子,我们可以更进一步:

  1. 用Dockerfile定义一个Nginx镜像,里面包含特定版本的Nginx和我们定制的配置文件。
  2. 用Ansible Playbook来负责在目标机器上安装Docker引擎,然后拉取并运行我们这个定制好的Nginx镜像。

这样,我们交付的就不再是一堆需要在目标机器上执行的安装和配置指令,而是一个自包含、版本明确、环境一致的标准化交付物(Docker镜像),以及一套负责部署和生命周期管理(Docker引擎安装、容器启停)的自动化流程(Ansible Playbook)

两者的分工可以这样理解:Docker负责制造一个个标准化、隔离的“集装箱”(应用环境),而Ansible负责在庞大的“码头”(服务器集群)上,调度、放置和管理这些集装箱。

2. 环境搭建:避开初次使用的典型陷阱

理解了核心理念,我们开始动手。安装过程本身不难,但有几个关键选择会直接影响后续的使用体验。这里我们以最常见的CentOS 7和Ubuntu 20.04为例,但重点在于解释每个步骤背后的原因。

2.1 Ansible 控制节点安装:选对版本和源

Ansible采用无代理架构,你只需要在一台机器(称为控制节点)上安装Ansible,它就能通过SSH协议去管理其他机器(被控节点)。

对于控制节点(通常是你自己的笔记本或一台跳板机):

# Ubuntu/Debian sudo apt update sudo apt install -y software-properties-common sudo add-apt-repository --yes --update ppa:ansible/ansible sudo apt install -y ansible # CentOS/RHEL 7 (使用EPEL源) sudo yum install -y epel-release sudo yum install -y ansible # CentOS/RHEL 8 或 Rocky/AlmaLinux 8+ sudo dnf install -y epel-release sudo dnf install -y ansible

关键注意点:

  1. 版本选择:生产环境建议使用稳定版本。通过系统包管理器安装的通常是较新的稳定版。避免使用过旧的版本(如CentOS 7默认仓库里非常老的版本),某些模块可能缺失或行为不同。
  2. Python环境:Ansible本身用Python编写,控制节点需要Python 3.8+。大部分现代Linux发行版已满足。如果遇到问题,请先确认python3 --version
  3. 无需在被控节点安装Ansible:这是Ansible最大的优势之一。被控节点只需要满足:a) 能通过SSH访问;b) 有Python解释器(绝大多数Linux发行版默认都有)。对于没有Python的极特殊情况,Ansible提供了raw模块来先安装Python。

2.2 Docker引擎安装:区分Docker Engine与Docker Desktop

这是新手最容易混淆的地方。Docker有两个主要产品:

  • Docker Engine (CE/EE):开源的核心容器运行时和引擎,用于Linux服务器。
  • Docker Desktop:一个面向Mac和Windows开发者的桌面应用,它内部集成了一个Linux虚拟机来运行Docker Engine,并提供了图形界面。

对于Linux服务器(我们的被控节点),我们安装的是 Docker Engine。

在Linux上安装Docker Engine(以Ubuntu为例):

# 1. 卸载旧版本(如果是全新安装可跳过) sudo apt-get remove docker docker-engine docker.io containerd runc # 2. 安装依赖工具 sudo apt-get update sudo apt-get install -y \ ca-certificates \ curl \ gnupg \ lsb-release # 3. 添加Docker官方GPG密钥和稳定版仓库 sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # 4. 安装Docker Engine sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # 5. 启动Docker并设置开机自启 sudo systemctl start docker sudo systemctl enable docker # 6. (可选但推荐)将当前用户加入docker组,避免每次使用sudo sudo usermod -aG docker $USER # 注意:需要退出当前终端重新登录,此更改才会生效

关键注意点:

  1. 使用官方源:不要使用发行版自带的陈旧版本。使用官方源能确保获得最新的安全更新和功能。
  2. 用户组权限sudo usermod -aG docker $USER这一步非常重要。它让你可以直接运行docker命令,而不需要每次都加sudo但务必理解其安全含义:加入docker组的用户实际上获得了root权限。在生产环境中,需要严格管控。
  3. 镜像加速:在国内,从Docker Hub拉取镜像可能很慢。需要配置国内镜像加速器(如阿里云、腾讯云、中科大等提供的加速器)。通常是在/etc/docker/daemon.json中配置(如果文件不存在则创建):
    { "registry-mirrors": [ "https://your-mirror.mirror.aliyuncs.com" ] }
    配置后需要重启Docker服务:sudo systemctl restart docker

对于Windows/Mac开发者:如果你在本地学习,可以安装Docker Desktop。它会处理所有底层虚拟化细节。安装后,你可以在终端(Windows PowerShell或Mac Terminal)中直接使用dockerdocker-compose命令,体验与Linux服务器基本一致。

2.3 配置SSH免密登录:Ansible的通行证

Ansible通过SSH连接被控节点。为了让过程自动化,我们需要配置控制节点到所有被控节点的SSH免密登录(基于密钥认证)。

  1. 在控制节点生成密钥对(如果已有~/.ssh/id_rsa~/.ssh/id_rsa.pub则可跳过):

    ssh-keygen -t rsa -b 4096 -C "ansible-control-node" # 一直按回车,使用默认路径和空密码
  2. 将公钥分发到被控节点

    ssh-copy-id user@remote_server_ip

    输入被控节点的用户密码。成功后,控制节点就能无需密码SSH到该被控节点。

  3. 验证连接

    ssh user@remote_server_ip

    如果能直接登录,说明配置成功。

这是Ansible自动化基石。没有它,Ansible执行每个任务时都会卡在输入密码的环节。请确保对所有需要管理的被控节点完成此操作。

3. 从零构建一个完整的自动化部署流程

现在,我们用一个实战案例,将Ansible和Docker串联起来。目标是:使用Ansible在远程服务器上部署一个带有自定义首页的Nginx容器。

3.1 项目结构设计

清晰的目录结构是维护性的开端。创建一个项目目录:

my_ansible_docker_project/ ├── ansible.cfg # Ansible配置文件 ├── inventory.ini # 服务器清单文件 ├── site.yml # 主Playbook ├── roles/ # 角色目录 │ └── nginx_docker/ # Nginx Docker角色 │ ├── tasks/ │ │ └── main.yml # 角色任务主文件 │ ├── handlers/ │ │ └── main.yml # 角色处理程序 │ ├── templates/ │ │ └── index.html.j2 # 首页模板 │ └── files/ │ └── nginx.conf # 静态Nginx配置文件(可选) └── docker/ # Docker构建相关 └── nginx/ ├── Dockerfile └── nginx.conf # 用于构建镜像的配置

3.2 第一步:编写Dockerfile,定义应用镜像

我们先在docker/nginx/Dockerfile中定义我们的Nginx镜像。这确保了应用环境的一致性。

# 使用官方Nginx Alpine镜像作为基础,体积小 FROM nginx:alpine # 删除默认的欢迎页面 RUN rm /etc/nginx/conf.d/default.conf # 将我们自定义的Nginx配置文件复制到容器内 COPY nginx.conf /etc/nginx/nginx.conf # 将我们的网站文件复制到容器内(稍后由Ansible动态生成并挂载,这里可先留空或放默认文件) # COPY html /usr/share/nginx/html # 暴露80端口 EXPOSE 80 # 使用nginx官方镜像的默认启动命令 CMD ["nginx", "-g", "daemon off;"]

对应的docker/nginx/nginx.conf可以是一个简单的自定义配置,例如调整了worker_processes等参数。

关键点:我们选择将网站文件(如index.html)通过数据卷(Volume)挂载,而不是直接打包进镜像。这样,更新网站内容时,只需要替换宿主机上的文件并重启容器,无需重新构建和推送镜像,更灵活。

3.3 第二步:编写Ansible Playbook,定义部署逻辑

现在,我们编写Ansible代码来编排整个部署过程。

1. 清单文件 (inventory.ini):告诉Ansible要管理哪些服务器。

[web] web-server-1 ansible_host=192.168.1.101 ansible_user=ubuntu web-server-2 ansible_host=192.168.1.102 ansible_user=ubuntu [web:vars] # 组变量,对此组内所有主机生效 ansible_python_interpreter=/usr/bin/python3

2. 主Playbook (site.yml):调用角色,定义在哪些主机上执行。

--- - name: 部署Nginx Docker容器到Web服务器 hosts: web # 对应inventory中的[web]组 gather_facts: yes # 收集主机信息,如IP、OS等,后续任务可能用到 become: yes # 以sudo权限执行 roles: - role: nginx_docker vars: container_name: "my_nginx" host_port: 8080 # 将容器的80端口映射到宿主机的8080端口 docker_image: "my-custom-nginx:latest"

3. 角色任务 (roles/nginx_docker/tasks/main.yml):这是核心,按顺序定义具体任务。

--- - name: 安装Docker依赖包 apt: name: "{{ item }}" state: present update_cache: yes loop: - apt-transport-https - ca-certificates - curl - software-properties-common - gnupg - lsb-release when: ansible_os_family == "Debian" # 根据系统家族判断 - name: 添加Docker官方GPG密钥 apt_key: url: https://download.docker.com/linux/ubuntu/gpg state: present when: ansible_os_family == "Debian" - name: 添加Docker稳定版仓库 apt_repository: repo: "deb [arch={{ ansible_architecture }}] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" state: present update_cache: yes when: ansible_os_family == "Debian" - name: 安装Docker Engine apt: name: - docker-ce - docker-ce-cli - containerd.io - docker-compose-plugin state: present when: ansible_os_family == "Debian" notify: 重启Docker服务 - name: 确保Docker服务正在运行 service: name: docker state: started enabled: yes - name: 将当前Ansible用户加入docker组 user: name: "{{ ansible_user }}" groups: docker append: yes notify: 重新加载用户组 - name: 创建网站内容目录 file: path: "/var/www/{{ container_name }}" state: directory owner: "{{ ansible_user }}" group: "{{ ansible_user }}" mode: '0755' - name: 生成动态首页文件 template: src: "index.html.j2" dest: "/var/www/{{ container_name }}/index.html" owner: "{{ ansible_user }}" group: "{{ ansible_user }}" mode: '0644' - name: 从Dockerfile构建镜像(或在本地构建后推送到仓库) # 这里演示两种方式: # 方式一:直接在目标服务器构建(适合开发测试,生产环境不推荐) # docker_image: # name: "{{ docker_image }}" # build: # path: "{{ playbook_dir }}/docker/nginx" # source: build # state: present # 方式二:从镜像仓库拉取(生产环境推荐) docker_image: name: "{{ docker_image }}" source: pull state: present # 假设我们已经提前构建好镜像并推送到仓库(如Docker Hub、私有Harbor) - name: 确保Nginx容器正在运行 docker_container: name: "{{ container_name }}" image: "{{ docker_image }}" state: started restart_policy: unless-stopped ports: - "{{ host_port }}:80" volumes: - "/var/www/{{ container_name }}:/usr/share/nginx/html:ro" env: TZ: "Asia/Shanghai"

4. 角色处理程序 (roles/nginx_docker/handlers/main.yml):由notify触发,通常用于重启服务。

--- - name: 重启Docker服务 service: name: docker state: restarted - name: 重新加载用户组 shell: "newgrp docker" # 注意:这个改变在本次Ansible运行中可能不会立即生效,通常需要新开会话。 # 更稳妥的做法是在任务中直接使用`docker`模块,它会自动处理权限。

5. 模板文件 (roles/nginx_docker/templates/index.html.j2):用于动态生成内容。

<!DOCTYPE html> <html> <head> <title>Welcome from Ansible & Docker</title> </head> <body> <h1>Hello, World!</h1> <p>This Nginx container is deployed by Ansible on host <strong>{{ ansible_hostname }}</strong>.</p> <p>Current time is: {{ ansible_date_time.iso8601 }}</p> </body> </html>

3.4 第三步:执行与验证

  1. 测试连接:在控制节点,进入项目目录,测试Ansible能否连接到被控节点。

    ansible -i inventory.ini web -m ping

    看到每个主机返回"pong"即表示成功。

  2. 执行Playbook

    ansible-playbook -i inventory.ini site.yml

    Ansible会输出详细的执行过程,绿色表示成功或未更改,黄色表示更改,红色表示失败。

  3. 验证部署

    • 在控制节点,使用curl检查服务:
      curl http://192.168.1.101:8080
    • 或者登录到被控节点检查:
      sudo docker ps # 查看容器是否运行 curl localhost:8080 # 在宿主机上访问

4. 从“能用”到“好用”:进阶实践与避坑指南

一个能跑通的Playbook只是起点。要让这个流程真正可靠、可维护,还需要考虑以下方面。

4.1 变量管理与分离

不要把像docker_imagehost_port这样的变量硬编码在Playbook或角色里。应该使用变量文件或Ansible Vault进行管理。

  • 组变量/主机变量:在inventory.ini同目录或group_vars/host_vars/目录下定义YAML文件。
  • 角色默认变量:在roles/nginx_docker/defaults/main.yml中定义默认值,可以被更高级别的变量覆盖。
  • Ansible Vault:用于加密敏感信息,如密码、密钥。
    # 加密一个变量文件 ansible-vault encrypt vars/secrets.yml # 运行Playbook时使用加密文件 ansible-playbook -i inventory.ini site.yml --ask-vault-pass

4.2 错误处理与幂等性强化

  • failed_whenchanged_when:精确控制任务的成功/失败和变更状态。
  • blockrescue:实现类似try-catch的错误处理。
    - block: - name: 尝试拉取镜像 docker_image: name: "{{ docker_image }}" source: pull rescue: - name: 拉取失败,记录错误并执行备用方案 debug: msg: "镜像拉取失败,尝试从备用仓库拉取或本地构建" - name: 从备用仓库拉取 docker_image: name: "my-registry.local/{{ docker_image }}" source: pull
  • registeruntil循环:捕获任务输出,并基于输出进行重试,直到满足条件。
    - name: 等待容器健康检查通过 docker_container_info: name: "{{ container_name }}" register: container_info until: container_info.container.State.Health.Status == "healthy" retries: 10 delay: 3

4.3 镜像管理策略:构建、推送与拉取

在生产环境中,绝不应该在目标服务器上构建镜像(如我们示例中注释掉的部分)。这会导致构建环境不一致、速度慢、占用生产服务器资源。

标准CI/CD流程应该是:

  1. 构建:在独立的构建服务器(如Jenkins、GitLab CI Runner)上,根据代码变更触发Docker镜像构建。
  2. 测试:对构建出的镜像进行安全扫描和功能测试。
  3. 推送:将测试通过的镜像推送到私有镜像仓库(如Harbor、Nexus、ECR)。
  4. 拉取与部署:Ansible Playbook的任务,仅仅是从私有仓库拉取指定版本的镜像,然后创建或更新容器。

这样,Ansible Playbook就变成了一个纯粹的部署编排器,职责清晰。

4.4 常见问题排查链路

当Playbook执行失败时,不要慌张,按顺序排查:

  1. SSH连接问题ansible -i inventory.ini web -m ping通吗?检查网络、防火墙、密钥认证。
  2. 权限问题:任务是否需要become: yes?用户是否在docker组?执行docker命令是否需要sudo
  3. 模块执行失败:看Ansible的错误输出。通常是:
    • 包名不对:Ubuntu和CentOS的包名不同,用ansible_os_family判断。
    • 服务未启动service模块失败,先手动去目标机器检查服务状态和日志。
    • Docker命令失败:手动在目标机器上执行docker pulldocker run看具体报错。常见问题有:镜像不存在、端口冲突、卷挂载路径权限不足、磁盘空间不足。
  4. 变量未定义:检查变量名是否拼写错误,变量文件是否被正确包含。
  5. 语法错误:使用ansible-playbook --syntax-check site.yml检查YAML语法。
  6. 使用调试模块:在关键任务前后插入debug模块,打印变量值。
    - name: 调试变量 debug: var: docker_image

4.5 与Shell脚本或传统运维工具的对比

你可能会问,有了Ansible,还需要写Shell脚本吗?答案是:需要,但分工不同。

  • Ansible:负责跨节点的、声明式的状态管理。适合做配置标准化、服务部署、文件分发、系统初始化等“确保状态”的工作。它的优势在于幂等性、可读性和跨平台兼容性。
  • Shell脚本:适合在单机上执行复杂的过程性任务,或者封装一些需要精细控制流程的操作。可以作为Ansible的一个shellcommand模块任务来调用,但应尽量短小、专注。

Ansible vs. 其他自动化工具(如SaltStack, Chef, Puppet)

  • Ansible:无代理,基于SSH,上手快,YAML语法易读,适合中小规模及起步阶段。
  • SaltStack:性能更高,实时性强,适合大规模集群,但有Agent(Minion)需要维护。
  • Chef/Puppet:更强调“配置即代码”,有强大的模型和社区,但学习曲线更陡峭,更适合有专职运维团队的大型企业。

对于从零开始的团队或个人,Ansible因其简单性和无代理架构,通常是自动化入门的最佳选择。它能让你快速看到自动化带来的收益,并随着需求复杂,再逐步引入更专业的模块、角色和最佳实践。

回到我们最初的问题:如何从零开始构建自动化运维能力?答案不是急于掌握所有Ansible模块和Docker命令,而是先选择一个最小的、真实的痛点场景(比如部署一个Web应用),用Ansible和Docker将其从头到尾自动化。在这个过程中,你会自然遇到变量管理、错误处理、镜像构建、网络配置等问题,逐个解决它们,你的自动化体系就逐渐生长出来了。记住,完美的自动化是迭代出来的,而不是设计出来的。