发行版中立、无客户端式的企业级IT自动化系统

Red Hat Ansible自动化系统学习笔记

Posted by Bob Guo on September 20, 2022

对于任何一个Linux系统管理员来说,能够将一些需要定期完成的工作(例如滚更新、建立镜像备份等)进行自动化而不是让Siri设置闹钟响铃手动ssh上去执行显然能极大的降低系统管理员的负载,减少可能的故障率,也就是所谓的负载均衡。也许对于整个网络只有三五台Linux设备,其中大多数还不需要/不允许用户进行维护(例如智能手机和路由器/光猫)的大部分用户,甚至少部分不仅有Linux工作站还有家庭数据中心的重度用户来说,研究折腾自动化架构远不如今天心情好ssh上去pacman -Syu && reboot来的舒服,毕竟情绪价值也是不可忽视的价值;但一旦将需要维护的节点数量从一位数升级到两位、三位甚至更高,一个一个手动操作即使对于最魔怔的Command Line Master Race来说也是堪比炼狱的折磨,更不要说如果其中某个节点突然出现网络波动、依赖冲突等预料外因素导致系统故障带来的挫败感和继续处理问题的进一步折磨。除了日常维护,每次对节点进行更换、升级、重装等操作之后对服务的配置与部署也同样折磨,尤其是由于这种配置与部署通常需要大量重复输入,会使人更加疲倦,加大出错的可能性。总而言之,对于任何一个合格的系统管理员来说,熟练使用自动化架构是技能树上必须点掉的一环。

什么是Ansible

Ansible,正如文章的标题所言,是Red Hat开发的一个开源、发行版中立且无客户端的IT自动化系统。它基于Python开发,使用ssh操作受控节点,因此不需要在受控节点上安装任何形式的客户端(除非你把openssh算作它的客户端),换句话说就是减少了一个可能的攻击点(盯着openSSH源码修bug的人可比可能的盯着Ansible客户端的人多得多)。同时,Ansible可以通过plugin和playbook的体系模块化编写,高度自定义的同时写起来用起来还方便很多。

为什么要用Ansible

也许每年给红帽送十万甚至九万美元的企业和机构有足够的理由(以及足够的rack)利用Ansible达成IT自动化,但对于我来说,学习Ansible最大的理由就是这玩意儿RHCE考,而且我完全不会。首先,我用Arch,有一说一只要条件允许使用最新版本的软件包绝对是最优解,除非你真的能买到RHEL的企业级支持或者自有团队backport重要的patch和feature;其次,我同时操作的、需要维护的Linux终端最多三台,一台工作站一台NAS一台笔记本,开仨Konsole就够了我要个锤子IT自动化,实在不行scp一个脚本配合crontab定期执行不就完事了?
好吧,不开玩笑,认真点说。上面提到,Ansible基于Python开发,这意味着在使用时可以掺杂一些Python中常用的库来进一步拓展它的灵活性。譬如,配合Jinja生成每个节点不同的配置,这点对于一些需要根据硬件规格的不同更改配置方式的应用来说是十分重要的。当然,由于Ansible的复杂性,本文只可能cover一些最基础的技术细节,详细内容还是需要参考对应的手册与文档。

前置概念

在进一步讨论Ansible之前,我们需要先了解一些前置概念与服务,这些内容将会在接下来的技术细节中大量出现。

patterns

首先要讲的是patterns,也就是节点列表。默认情况下,Ansible会自动从/etc/Ansible/hosts读取对应的节点列表并做适配,不过使用时也可以通过指令要求Ansible使用特定文件作为节点列表。这些文件的官方用语叫Inventory,或hostfile,弄起来还是不算复杂的。关于如何添加受控节点列表,请参照下文设置受控节点一章。在Ansible里,当你运行一个指令时,你就需要使用patterns来定义目标节点。一般来说,patterns有七种输入:

  • 所有节点(all或者一个*)
  • 单独一个节点
  • 多个节点(用,或者:分隔,如果使用的是IPv6的IP地址需要使用,)
  • 单个组
  • 多个组(使用:分隔)
  • 除了某组(即仅使用在a组但不在b组的节点,使用:!分隔)
  • 交集(仅使用两个组中都存在的节点,用:&分隔)

需要注意的是,无论在何种情况下,你都需要对pattern参数做出指定。Ansible不会提供一个预设值,如果不填就会报错退出。

modules

知道了谁是目标,下一步就是整理需要的工具了。在Ansible中,模组就是这个完成任务需要的工具,Ansible本身只是一个框架,利用不同的模块完成不同的任务。这些模块可以是对应的组织或机构开发的(比如Red Hat自己会针对运维Red Hat Enterprise Linux开发对应的模块供用户使用),也可以是由第三方社区进行开发的(例如Ansible社区为Docker开发的这些模块),这些模块通常会以collection的方式提供给sysadmin以部署在Ansible上。更多的模块可以通过GitHub或Ansible Galaxy获取,本文就不再赘述。
需要注意的是,不同模组的使用方式、配置方式各不相同。在使用前,sysadmin需要通过ansible-doc命令获取对应模组的说明(当然也可以上网查)

如何配置Ansible

安装服务端

要配置Ansible,首先得安装它的服务端。尽管它是RH的官方项目,但你直接去sudo dnf install Ansible大概率会报错。在RHEL8上,你需要启用EPEL或根据这篇文章安装服务端,而RHEL9就不需要这么做,可以直接安装。由于Ansible并不依赖systemd服务,理论上安装完成之后你就可以直接开始写playbook跑项目了,但是先别急,还要配置呢。

SSH登录

记得上面说的,Ansible依赖ssh工作吗?看到ssh你想到什么了?没错,SSH登录授权。众所周知,SSH登录授权问题有三种解决方式,一种无密码(无论是全局不需要还是某IP段不需要),但这种方式在安全性上显然是有很大问题的。如果任意一个节点被拿下,这些所有被控节点也等于直接暴露,因此这种方式最多只能娱乐。在playbook中定义用户名和密码进行登录也无不可,但随着用户增加配置文件的数量也会增加,造成维护和更新上的麻烦,这点在后面写playbook的时候再提。最后一种方式是使用SSH Key进行登录,由于对于服务端这台机器来说使用SSH Key登录不需要做额外的配置,同时SSH Key可以与机器进行物理隔离,这种方式是相对最为安全和便利的方式。使用

ssh-copy-id -i ~/.ssh/id_rsa.pub user@host

将已生成的SSH Key复制到对应服务器对应用户的~/.ssh/authorized_keys文件夹,或者手动进行部署即可。

设置受控节点

Ansible没有“客户端”的概念,那么怎么确定哪些节点需要受控呢?难道nmap全网扫端口然后暴力测试?这显然是不可能的。在部署前,sysadmin需要在配置文件中确定受控节点的参数,至于如何设置受控节点列表,这里就只介绍修改默认列表的情况。如果是需要使用其他列表,可以通过运行时添加-i来指定,在后面也会提到。
默认列表的地址是

/etc/ansible/hosts

使用文本编辑器打开它,就可以开始编辑。在RHEL8上,Ansible默认的hosts文件内容有注释和一些基本的指南,整理翻译如下:

这是Ansible host文件的默认版本,它应当存放在/etc/ansible/hosts。须知如下:

  1. 文档的注释部分以#开头
  2. 空白行会被默认忽视
  3. 使用[名称]定义不同目标组
  4. 可以使用系统名或IP地址
  5. 一个系统可以是多个目标组的成员
  6. 如果有多个遵循一定规律的目标地址,可以利用这样的方式定义:www[001:006].example.com

所以,以下这些hosts样例都是可以被正常执行的:

www.example.com
192.168.0.1
192.168.[0:225].1

需要注意的是,Ansible的指令-后面我们会聊到,可以选择指定某个节点运行。如果输入上文这样的地址名或IP地址来操作显然并不方便。这时目标组就可以起到很大的作用。你完全可以把每个地址都纳入一个目标组,权当命名标签使用,就像这样:

[Example]
www.example.com

这样在后面操作时只需要使用Example即可,不需要大费周章地输入www.example.com才能操作,省事多了。

跑一点简单的指令

就像Python可以直接在IDLE里跑Hello World而不需要gcc helloworld.c && ./a.out一样,Ansible也允许用户直接运行一些指令而不需要撰写playbook。这种一次性的简单也被叫做ad hoc commands,这是拉丁语,意思是“为了xx”,使用/usr/bin/Ansible来快速运行一些单次指令,语法如下:。

$ ansible [pattern] -m [module] -a [module opitions]

在pattern这里指定受控节点,在module里指定使用的模块,在module opitions里再添加其他的参数(如果有的话)。十分简单吧?
哦,这段的标题是跑一点简单的指令,那么就来一个最简单的重启指令吧。

$ ansible all -a “/sbin/reboot”

你会注意到这条指令里并没有-m [module]的存在。如果仅仅使用ansible这个命令运行ad hoc command,为了方便编写,在没有指定的情况下会有一个默认调用的模块:ansible.builtin.command,也就是Ansible自带的指令集,它的文档在这里
除了不写模块之外,还有一种是没有-a [module opitions]。模块的参数本身就是可选项,如果使用的模块功能比较单一(像ping这种)自然就可以不写-因为没啥可写的。

Playbook

诚然,经过精心调配的ad hoc指令可以copy到一个.sh文件里多次复用,但这种复用最多也只是像“嘿siri,帮我的几个Arch Linux节点更新软件”一样的简单任务。一旦任务复杂,涉及到大量不同的节点或库,折腾ad hoc就属于多少沾点的行为了。在这种情况下,我们需要的是Ansible Playbook。
Ansible Playbook可以理解为一个威力加强版的ad hoc command。它允许系统管理员将大量任务部署进一个脚本运行。而这就不得不提到Ansible十分阴间的一点:采用了YAML语言来撰写配置文件。好吧,YAML其实没什么不好,而且作为红帽的项目用红帽主推的语言是天经地义的1!5!,但YAML一个很大的特点就是对缩进与格式要求颇为严格,这就造成了如果你在撰写配置文件时不够仔细经常可能会出现写的都是对的就是跑不起来的问题,这个时候就可以去看看是不是哪里格式没写对。我的博客采用的Jekyll框架在配置时也大量依赖YAML技术,当时部署的时候把我折腾的死去活来,从此我就对YAML没啥好感了。
总而言之,跑Ansible要写配置,要写配置就得写YAML。为了方便新手入门,贴心的Red Hat还在文档里记录了YAML的语法知识。说回Playbook,一个Playbook由play组成,每个play甚至可以一环嵌一环,考虑到play一词还有戏剧的释义(并且playbook也是剧本),这个取名无疑是十分贴切的。每个play本身至少需要指明两个参数:目标节点,以及运行的指令。你可能发现了,一场戏的组成部分跟ad hoc command完全一致,所以在我们需要了解的基础知识也是通用的。 Frameworks

如何写play

在Ansible的文档中,play被定义为“Ansible任务的主要内容”,负责定义在各受控节点上运行的任务。它包含变量、角色以及可重复运行任务的有序列表。(The main context for Ansible execution, this playbook object maps managed nodes (hosts) to tasks. The Play contains variables, roles and an ordered lists of tasks and can be run repeatedly. )简单来说,你可以把play理解成一个用固定格式写的ad hoc command。变量和任务列表自然挺好理解,但这个role可能会造成一些疑惑。在我的理解里,role可以从某种程度被理解为封装好的play,让sysadmin可以更方便地维护这套playbook,并且通过各种嵌套和调用实现更华丽的效果。
不管怎么说,我们先来看看play的标准格式。前面不是还有一条重启的ad hoc command嘛,就拿它来做例子。如果要把它写成一个play,那格式会是这样的:

- name: Rebooting device
  hosts: all
  tasks:
          - name: reboot
            ansible.builtin.command:
              cmd: /sbin/reboot

这条play被命名为Rebooting device,作用于所有设备;通过调用ansible.builtin.command模块的cmd函数运行/sbin/reboot进行重启。当然了,重启这种高频使用的功能大概率是有自己的模块的,就像这样:

- name: Rebooting device
  hosts: all
  tasks:
          - name: reboot
            reboot

它唯一的任务就是调用reboot这个模块,用一行代码解决一切,不仅更易读还方便debug。
众所周知,Linux下实现某种功能的方式不止一种,自然在Ansible中实现的方式也不止一种。如果你发现有的功能没有人做模块,你当然可以参考官方文档自己写,这里就不再赘述了。

如何运行playbook

就像使用ad hoc command一样,Ansible有一个专门的命令行指令来运行playbook:

ansible-playbook -i /path/to/my_inventory_file -u my_connection_user -k -f 3 -T 30 -t my_tag -m /path/to/my_modules -b -K my_playbook.yml

-i在前面已经讲过了,是用来自定义节点列表的指令,不加就会从默认的/etc/ansible/hosts里读取,但其他几个指令都是第一次见。虽然你可以用man ansible-playbook去翻看但都写到这里了为了水字数也要简单介绍一下。当然,这些参数都需要按需使用,并且可以通过直接写进playbook作为变量,理论上最小指令可以只有: ansible-playbook my_playbook.yml

  • -u的作用是指定登陆用的账户。前面已经说过,Ansible是基于ssh运作的,这就意味着默认情况下使用的账户名就是你现在登陆的账户名,而这就又意味着可能出现因为用户名对不上而无法正常工作的情况。使用-u手动指定就不会出现这种问题。
  • -k的用途是指定登录时使用的密码。当然,前面已经改成用ssh key登录的话就不需要加这个参数。
  • -f控制的是并行数量,即同时在多少台受控节点上运行。默认情况下是同时同步在所有节点上运行,但在这里被改成了3。理论上,数量越高运行的速度也越高。当然,如果有特殊需求,还可以参考这篇文档进行细调.
  • -T设置的是超时参数,以秒为单位。在这个时间节点内没有收到回复就会报错。由于ssh是一个相对更上层的工具,Ansible对系统底层状态是没有感知的,因此也无法分辨是系统还在加载还是出现故障无法启动。尤其是对于自检时间较长的平台(例如早期AM5)来说,在配置时确定超时时长是很重要的。
  • 小写的-t设置的是tag,你可以在一个playbook中写入很多不同的play,将它们用tag分类,然后根据情况调用。
  • -m定义需要用到的本地模组的位置
  • -b-K需要放在一起说,因为它们都是用于become的。挂上-b就代表使用become操作,而-K则是需要的密码。

简单说一下become,当你部署的服务需要使用一些特殊的用户权限时,就需要使用become。打个比方:如果安装完httpd需要让它启动(即systemctl start httpd),写成play就会是这样:

- name: Ensure the httpd service is running
  service:
    name: httpd
    state: started
  become: yes

这个时候它就会使用root权限去执行这一指令。当然,如果你前面就用root启动的playbook就不需要这么操作。另外一种使用方式是这样:

- name: Run a command as the apache user
  command: somecommand
  become: yes
  become_user: apache

这个时候你的指令就会以apache这个用户的名义执行,根据具体场景它可能很有用。