玩转企业常见应用与服务系列(十五):Ansible palybook 原理与实践

2023-11-30 12:44:28 浏览数 (1)

Ansible Palybook 简介

Playbooks是Ansible的配置,部署和编排语言。playbook是由一个或多个play组成的列表,play的主要功能在于将事先归并为一组的主机装扮成事先通过ansible中的task定义好的角色。从根本上来讲,所谓的task无非是调用ansible的一个module。将多个play组织在一个playbook中,即可以让它们联合起来按事先编排的机制完成某一任务。

Ansible Palybook的编写是基于YAML语言的。YAML语言,即Yet Another Markup Language,是一种能被计算机直接识别的标记语言,同时也方便人的阅读,且方便和脚本语言交互。但是,YAML的配置即为严格,在配置时必须注重空格的数量。总的来看,YAML语言特性如下:

  • 1、可读性强
  • 2、和脚本语言的交互性好
  • 3、使用实现语言的数据类型
  • 4、一致的信息模型
  • 5、易于实现
  • 6、可以基于流来处理
  • 7、可扩展性强

Ansible Playbook 组件

Ansible的Playbook有以下组件:

  • Target
    • 定义playbook的远程主机组,即控制的下游设备信息
  • Variable
    • 定义Playbook所使用的变量。
  • Task
    • 定义Playbook控制下游设备要执行的命令。
  • Handler
    • 定义在Palybook在Task执行完毕后要调用的任务。
Ansible Playbook各组件参数

Ansible的上述组件的参数如下:

Target常用参数
代码语言:javascript复制
hosts #定义Ansible用户控制的下游设备
remote_user #定义执行Ansible设置的用户
sudo #设置为yes时,执行任务时使用root权限
sudo_user #指定sudo的普通用户
connection #默认基于ssh链接客户端
gather_facts #获取远程主机facts基础信息
Variable常用参数
代码语言:javascript复制
vars  #定义变量
vars_files  #指定变量文件
vars_prompt #用户交互模式自定义变量
setup #通过远程gather_facts获取的信息
Task常用参数
代码语言:javascript复制
name #任务的名称,在Ansible运行的过程中起到提示的作用,会打印在屏幕上
action #Ansible控制下游设备的命令,通过Ansible的各个模块来进行控制
template #Ansible控制下游设备的模板
handler #定义一个调用,该调用在Handler处被定义,在所有的Task结束后被执行。

Playbook基本语法

playbook使用yaml语法格式,后缀可以是yaml,也可以是yml。

代码语言:javascript复制
[root@localhost ~]$ vim test.yaml
---      #表示文档开始
- hosts: test       # "- "表示一个块序列的节点,注意:横杠后面有空格,可以写主机名,主机组名,多个使用逗号隔开
  remote_user: root      #指定在进行远程操作时使用root用户进行操作
  tasks:       #使用tasks关键字指明要进行操作的任务列表,之后的行都属于tasks键值对中的值。
  - name: Ping  #每个任务都以"- "开头,每个任务都有自己的名字,任务名使用name关键字进行指定
    ping:         #ansible模块
  - name: make directory test  #第二个任务使用file模块,使用file模块时,指定了path参数与state参数的值。
    file:     #ansible模块
       path: /data/test   #模块的参数
       state: directory   
    
---                             #标记文件的开始
- hosts: webservers             #指定该playbook在哪个服务器上执行
  vars:                         #表示下面是定义的变量,
    http_port: 80               #变量的形式,key: value,这里http_port是变量名,80是值
    max_clients: 200
  remote_user: root             #指定远程的用户名,这里缩进和vars保持了一致,说明变量的代码块已经结束。
  tasks:                        #下面构成playbook的tasks,每个task都有 - name: 开始,name指定该任务的名称。
  - name: ensure apache is at the latest version  #指定该任务的名称。
    yum: pkg=httpd state=latest                   #yum说明要是用的模板名称,后面指定对应的参数,这两行结合起来就相当于一个shell命令。

  - name: write the apache config file            #每个task之间可以使用空行来做区分。
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf

#需要说明的是缩进的意义和python中缩进的意义是一样,是来区分代码块的。

检测语法

代码语言:javascript复制
[root@localhost ~]$ ansible-playbook  --syntax-check  /path/to/playbook.yaml

测试运行

代码语言:javascript复制
[root@localhost ~]$ ansible-playbook -C /path/to/playbook.yaml

运行

代码语言:javascript复制
[root@localhost ~]$ ansible-playbook  /path/to/playbook.yaml
示例:Playbook 创建用户
代码语言:javascript复制
[root@localhost ~]$ vim sysuser.yml
---
- hosts: all
  remote_user: root

  tasks:
    - name: create mysql user
      user: name=mysql system=yes uid=36
    - name: create a group
      group: name=httpd system=yes
Playbook示例 安装nginx服务
代码语言:javascript复制
[root@localhost ~]$ vim nginx.yml
- hosts: all
  remote_user: root

  tasks:
    - name: add group nginx
      user: name=nginx state=present
    - name: add user nginx
      user: name=nginx state=present group=nginx
    - name: Install Nginx
      yum: name=nginx state=present
    - name: Start Nginx
      service: name=nginx state=started enabled=yes
tags: 添加标签

可以指定某一个任务添加一个标签,添加标签以后,想执行某个动作可以做出挑选来执行,多个动作可以使用同一个标签。

示例:httpd.yml
代码语言:javascript复制
- hosts: websrvs
  remote_user: root
  
  tasks:
    - name: Install httpd
      yum: name=httpd state=present
      tage: install 
    - name: Install configure file
      copy: src=files/httpd.conf dest=/etc/httpd/conf/
      tags: conf
    - name: start httpd service
      tags: service
      service: name=httpd state=started enabled=yes

ansible-playbook -t install,conf httpd.yml   指定执行install,conf 两个标签
handlers
handlers和notify结合使用触发条件

Handlers 实际上就是一个触发器是task列表,这些task与前述的task并没有本质上的不同,用于当关注的资源发生变化时,才会采取一定的操作。

Notify此action可用于在每个play的最后被触发

这样可避免多次有改变发生时每次都执行指定的操作,仅在所有的变化发生完成后一次性地执行指定操作。在notify中列出的操作称为handler,也即notify中调用handler中定义的操作

在系统中,我们修改了服务器的配置文件,这时候就需要重启操作服务,就可以使用到handlers。配合 notify使用。

代码语言:javascript复制
---
- hosts: control-node
  remote_user: root
  vars:
    - pkg: httpd
    - name: template configuration file
      template: src=template.j2 dest=/etc/foo.conf  #修改了配置文件然后依次启动memcached和apache服务。
        notify:                               #使用notify来声明引用handlers。
         - restart memcached
         - restart apache
    
  handlers:                               #下面定义了两个handlers
    - name: restart memcached
      service:  name=memcached state=restarted
    - name: restart apache
      service: name=apache state=restarted

在使用handlers的过程中,有以下几点需要注意:

  • handlers只有在其所在的任务被执行时,都会被运行;
  • handlers只会在Play的末尾运行一次;如果想在一个Playbook的中间运行handlers,则需要使用meta模块来实现,例如:- meta: flush_handlers。
  • 如果一个Play在运行到调用handlers的语句之前失败了,那么这个handlers将不会被执行。我们可以使用mega模块的–force-handlers选项来强制执行handlers,即使在handlers所在Play中途运行失败也能执行。
register 和when

register 用于注册一个变量,保存命令的结果(shell或command模块),这个变量可以在后面的task、when语句或模板文件中使用。

代码语言:javascript复制
- shell: /bin/pwd
  register: pwd_result
代码语言:javascript复制
debug:
    #msg: "{{ pwd_result }}"   # 输出全部信息
    #msg: "{{ pwd_result.cmd }}"   # 引用方式一
    msg: "{{ pwd_result['stdout_lines'] }}"  # 引用方式二

when 相当于shell脚本里的if 判断,when语句就是用来实现这个功能的,它是一个jinja2的语法,但是不需要双大括号,用法很简单。

代码语言:javascript复制
- name: echo date  #执行了一个 date 命令,register 关键字将 date 命令的输出存储到 date_output 变量名
  command: date 
  register: date_output 

 - name: echo date_output   #用 when 对关键字对分析后的进行判断,如果匹配,则执行这个 task,不匹配就不执行
   command: echo "30"
   when: date_output.stdout.split(' ')[2] == "30"

这里第 1 个 task 是执行了一个 date 命令,register 关键字将 date 命令的输出存储到 date_output 变量名。第 2 个 task 对输出进行分析,并使用 when 对关键字对分析后的进行判断,如果匹配,则执行这个 task,不匹配就不执行。这里要重点说下的,因为 register 获取到的输出内容都是字符串,而 ansible 又是 python 写的,你可以使用 python 字符串的方法对其做处理,比如本文中使用的 split,还可以使用 find 方法。

示例
代码语言:javascript复制
tasks:
  - name: "shutdown RedHat flavored systems"
    shell: /sbin/shutdown -h now
    when: ansible_os_family == "RedHat"  #当系统属于红帽系列,执行shell模块
循环

标准循环关键字:”with_items” ,对迭代项的引用,固定变量名为"item”,使用with_item属性给定要迭代的元素。

代码语言:javascript复制
[root@localhost ~]$ cat xh.yml 
---
- hosts: all
  gather_facts: no
  tasks:
    - name: dispaly list
      debug: msg="{{item}}"
      with_items:
        - one
        - two
        - three
        - four
代码语言:javascript复制
[root@localhost ~]$ ansible-playbook -i hosts xh.yml 
 
PLAY [all] *************************************************************************************************************
TASK [dispaly xunhuan] *************************************************************************************************
ok: [192.168.52.129] => (item=one) => {
    "changed": false, 
    "item": "one", 
    "msg": "one"
}
ok: [192.168.52.129] => (item=two) => {
    "changed": false, 
    "item": "two", 
    "msg": "two"
}
ok: [192.168.52.129] => (item=three) => {
    "changed": false, 
    "item": "three", 
    "msg": "three"
}
ok: [192.168.52.129] => (item=four) => {
    "changed": false, 
    "item": "four", 
    "msg": "four"
}

安装一堆软件包。

代码语言:javascript复制
---
    - hosts: localhost
      tasks: 
        - yum: name="{{item}}" state=installed
          with_items: 
            - pkg1
            - pkg2
            - pkg3

它会一个一个迭代到特殊变量"{{item}}"处。

loop等价于with_list,从名字上可以知道它是遍历数组(列表)的,所以在loop指令中,每个元素都以列表的方式去定义。列表有多少个元素,就循环执行file模块多少次,每轮循环中,都会将本次迭代的列表元素保存在控制变量 item中。

安装多个软件

代码语言:javascript复制
tasks:
  - name: "Install Packages"
    yum: name={{ item }}  state=latest
    loop:
      - httpd
      - mysql-server
      - php
模板templates

Jinja2语言,使用字面量,有下面形式

代码语言:javascript复制
字符串:使用单引号或双引号
数字:整数,浮点数
列表:[item1, item2, …]
元组:(item1, item2, …)
字典:{key1:value1, key2:value2, …}
布尔型:true/false
算术运算: , -, *, /, //, %, **
比较操作:==, !=, >, >=, <, <=
逻辑运算:and,or,not
流表达式:For,If,When
template 的使用

templates是ansible的一个模块,其功能是根据模板文件动态生成配置文件,templates文件必须存放于templates目录下,且命名为".j2"结尾,yaml/yml文件需要和templates目录平级,这样我们在yml文件中调用模板的时候,就不需要写模板文件的路径,否则需要描述模板文件的路径,因为template模块会自动去找templates目录下的模板文件.

代码语言:javascript复制
./  
├── temnginx.yml  
└── templates  
└── nginx.conf.j2
template示例

示例:利用template 同步nginx配置文件,准备templates/nginx.conf.j2文件。

代码语言:javascript复制
vim temnginx.yml
- hosts: websrvs
  remote_user: root
  
  tasks:
    - name: template config to remote hosts
      template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf

ansible-playbook temnginx.yml

for循环使用

代码语言:javascript复制
{% for vhost in nginx_vhosts %}
server {
listen {{ vhost.listen }}
}
{% endfor %}

if单分支选择使用

代码语言:javascript复制
{% if vhost.server_name is defined %}
    server_name {{ vhost.server_name }}
{% endif %}

if多分支选择使用

代码语言:javascript复制
{%if vhost.port is undefined %}
    http_port=80
{%elif vhost.port == 81%}
    http_port=81
{%else%}
    http_port = 83
{%endif%}

单行注释

代码语言:javascript复制
{#% for i in list %#}
roles
Roles介绍

ansible自1.2版本引入的新特性,用于层次性、结构化地组织playbook。roles能够根据层次型结构自动装载变量文件、tasks以及handlers等。要使用roles只需要在playbook中使用include指令引入即可。简单来讲,roles就是通过分别将变量、文件、任务、模板及处理器放置于单独的目录中,并可以便捷的include它们的一种机制。角色一般用于基于主机构建服务的场景中,但也可以是用于构建守护进程等场景中。主要使用场景代码复用度较高的情况下。

Roles目录结构

各目录含义解释

代码语言:javascript复制
roles:          <--所有的角色必须放在roles目录下,这个目录可以自定义位置,默认的位置在/etc/ansible/roles
  project:      <---具体的角色项目名称,比如nginx、tomcat、php
    files:     <--用来存放由copy模块或script模块调用的文件。
    templates: <--用来存放jinjia2模板,template模块会自动在此目录中寻找jinjia2模板文件。
    tasks:     <--此目录应当包含一个main.yml文件,用于定义此角色的任务列表,此文件可以使用include包含其它的位于此目录的task文件。
      main.yml
    handlers:  <--此目录应当包含一个main.yml文件,用于定义此角色中触发条件时执行的动作。
      main.yml
    vars:      <--此目录应当包含一个main.yml文件,用于定义此角色用到的变量。
      main.yml
    defaults:  <--此目录应当包含一个main.yml文件,用于为当前角色设定默认变量。
      main.yml
    meta:      <--此目录应当包含一个main.yml文件,用于定义此角色的特殊设定及其依赖关系。
      main.yml

roles/example_role/files/           #所有文件,都将可存放在这里
roles/example_role/templates/       #所有模板都存放在这里
roles/example_role/tasks/main.yml   #主函数,包括在其中的所有任务将被执行
roles/example_role/handlers/main.yml  #所有包括其中的 handlers 将被执行
roles/example_role/vars/main.yml    #所有包括在其中的变量将在roles中生效
roles/example_role/meta/main.yml    #roles所有依赖将被正常登入
Roles示例

通过ansible roles安装配置httpd服务,此处的roles使用默认的路径/etc/ansible/roles

代码语言:javascript复制
#在ansible目录下面,建立roles目录
#修改配置文件,使系统能够读取roles目录
[root@ansible ~]$ cat /etc/ansible/ansible.cfg | grep roles
# additional paths to search for roles in, colon separated
#roles_path    = /etc/ansible/roles  #roles默认路径
# by default, variables from roles will be visible in the global variable

创建role的步骤:

  • (1) 创建以roles命名的目录。
  • (2) 在roles目录中分别创建以各角色名称命名的目录,如webservers等。
  • (3) 在每个角色命名的目录中分别创建files、handlers、meta、tasks、templates和vars目录;用不到的目录可以创建为空目录,也可以不创建 。
  • (4) 在playbook文件中,调用各角色。

实验: 创建httpd角色

代码语言:javascript复制
#创建roles目录
mkdir roles/{httpd,mysql,redis}/tasks -pv
mkdir  roles/httpd/{handlers,files}
#查看目录结构
tree roles/
    roles/
    ├── httpd
    │   ├── files
    │   ├── handlers
    │   └── tasks
    ├── mysql
    │   └── tasks
    └── redis
        └── tasks
#创建目标文件
cd roles/httpd/tasks/
touch install.yml config.yml service.yml
#vim install.yml
   - name: install httpd package
     yum: name=httpd
     
   vim config.yml
   - name: config file  
     copy: src=httpd.conf dest=/etc/httpd/conf/ backup=yes 
   
   vim service.yml
   - name: start service 
     service: name=httpd state=started enabled=yes
     
#创建main.yml主控文件,调用以上单独的yml文件,
   main.yml定义了谁先执行谁后执行的顺序
   vim main.yml
   - include: install.yml
   - include: config.yml
   - include: service.yml
   
#准备httpd.conf文件,放到httpd单独的文件目录下
   cp /app/ansible/flies/httpd.conf ../files/
   
#创建一个网页
   vim flies/index.html
   <h1> welcome to weixiaodong home <h1>

#创建网页的yml文件
   vim tasks/index.yml
   - name: index.html
     copy: src=index.html dest=/var/www/html 

#将网页的yml文件写进mian.yml文件中
   vim mian.yml
   - include: install.yml
   - include: config.yml
   - include: index.yml
   - include: service.yml
   
#在handlers目录下创建handler文件mian.yml
vim handlers/main.yml
- name: restart service httpd
  service: name=httpd state=restarted

#创建文件调用httpd角色
cd /app/ansidle/roles
vim role_httpd.yml
---
# httpd role
- hosts: appsrvs
  remote_user: root 
  roles:       #调用角色
    - role: httpd  
    
#查看目录结构
tree 
.
httpd
├── files
│   ├── httpd.conf
│   └── index.html
├── handlers
│   └── main.yml
└── tasks
    ├── config.yml
    ├── index.yml
    ├── install.yml
    ├── main.yml
        └── service.yml

ansible-playbook role_httpd.yml

创建目录

代码语言:javascript复制
[root@ansible ~]$ cd /etc/ansible/roles/
# 创建需要用到的目录
[root@ansible roles]$ mkdir -p httpd/{handlers,tasks,templates,vars}
[root@ansible roles]$ cd httpd/
[root@ansible httpd]$ tree .
.
├── handlers
├── tasks
├── templates
└── vars

4 directories, 0 file

变量文件准备vars/main.yml

代码语言:javascript复制
[root@ansible httpd]$ vim vars/main.yml
PORT: 8088        #指定httpd监听的端口
USERNAME: www     #指定httpd运行用户
GROUPNAME: www    #指定httpd运行组

配置文件模板准备templates/httpd.conf.j2

代码语言:javascript复制
# copy一个本地的配置文件放在templates/下并已j2为后缀
[root@ansible httpd]$ cp /etc/httpd/conf/httpd.conf templates/httpd.conf.j2

# 进行一些修改,调用上面定义的变量
[root@ansible httpd]$ vim templates/httpd.conf.j2
Listen {{ PORT }} 
User {{ USERNAME }}
Group {{ GROUPNAME }}

任务剧本编写,创建用户、创建组、安装软件、配置、启动等

代码语言:javascript复制
# 创建组的task
[root@ansible httpd]$ vim tasks/group.yml
- name: Create a Startup Group
  group: name=www gid=60 system=yes

# 创建用户的task
[root@ansible httpd]$ vim tasks/user.yml
- name: Create Startup Users
  user: name=www uid=60 system=yes shell=/sbin/nologin

# 安装软件的task
[root@ansible httpd]$ vim tasks/install.yml
- name: Install Package Httpd
  yum: name=httpd state=installed

# 配置软件的task
[root@ansible httpd]$ vim tasks/config.yml
- name: Copy Httpd Template File
  template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
  notify: Restart Httpd

# 启动软件的task
[root@ansible httpd]$ vim tasks/start.yml
- name: Start Httpd Service
  service: name=httpd state=started enabled=yes

# 编写main.yml,将上面的这些task引入进来
[root@ansible httpd]$ vim tasks/main.yml
- include: group.yml
- include: user.yml
- include: install.yml
- include: config.yml
- include: start.ym

编写重启httpd的handlers,handlers/main.yml

代码语言:javascript复制
[root@ansible httpd]$ vim handlers/main.yml
# 这里的名字需要和task中的notify保持一致
- name: Restart Httpd
  service: name=httpd state=restarted

编写主的httpd_roles.yml文件调用httpd角色

代码语言:javascript复制
[root@ansible httpd]$ cd ..
[root@ansible roles]$ vim httpd_roles.yml
---
- hosts: all
  remote_user: root
  roles:
    - role: httpd        #指定角色名称

整体的一个目录结构查看

代码语言:javascript复制
[root@ansible roles]$ tree .
.
├── httpd
│   ├── handlers
│   │   └── main.yml
│   ├── tasks
│   │   ├── config.yml
│   │   ├── group.yml
│   │   ├── install.yml
│   │   ├── main.yml
│   │   ├── start.yml
│   │   └── user.yml
│   ├── templates
│   │   └── httpd.conf.j2
│   └── vars
│       └── main.yml
└── httpd_roles.yml

5 directories, 10 files

测试playbook语法是否正确

代码语言:javascript复制
[root@ansible roles]$ ansible-playbook -C httpd_roles.ym

上面的测试没有问题,正式执行playbook

代码语言:javascript复制
[root@ansible roles]$ ansible-playbook httpd_roles.ym

Ansible性能调优

相比于其他的自动化配置工具,Ansible的一个突出特性就是它是基于SSH链接对下游设备进行控制的,这样做的突出好处就是方便,下游设备不需要安装客户端软件。但是这也不可避免的带来一个问题,即Ansible的执行速度较慢。并且,随着Ansible控制设备的增多,Ansible的执行速度会越来越慢。

关于Ansible执行速度的问题,尽管是Ansible的硬伤,但是我们还是可以对其进行部分的优化,尽量的加快Ansible的执行速度。对Ansible的优化可以有两个思路,一个是优化SSH链接,使得SSH的传输速度变快。

另一个如下图所示:

每次Ansible Playbook在执行时,都会收集下游设备的信息,这个过程通常要耗费较长的时间。因此,我们可以考虑使用Redis对这些信息进行缓存,从而加快收集信息的速度,如果业务环境允许,我们也可以直接控制Ansible设备跳过该步骤。

Ansible SSH链接调优
SSH关闭密钥检测

在默认情况下,以SSH登录远程设备时,该设备会检查远程主机的公钥,并且将该公钥记录在~/.ssh/known_hosts文件中,当下次该主机访问时,OpenSSH会核对公钥。如果公钥不同,则OpenSSH会发出警告,如果公钥相同,则OpenSSH则会提示输入密码。

SSH对主机公钥的检查是根据StrictHostKeyChecking变量来设定的,StrictHostKeyChecking的检查级别包括:no(不检查),ask(是否检查要询问),yes(每次都检查),False(关闭检查)。

我们可以在Ansible的配置文件中defaults模块下加入如下代码:

代码语言:javascript复制
host_key_checking = False

加入后,配置文件如下所示:

这样,Ansible就可以关闭密钥检测了。

OpenSSH链接优化

在使用OpenSSH服务时,默认情况下服务器端会根据客户端的IP地址进行DNS反向解析,得到客户端的主机名,然后根据获取到的主机名再次进行DNS查询得到IP地址,比较这两个IP地址是否一样。这样做可以在一定程度上提高安全性,但是却会浪费时间,因此,我们可以通过关闭这一特性,来实现加速SSH链接速度的目的。 关闭该特新需要进入到/etc/ssh/sshd_config目录下,找到UseDNS的参数,将其修改为no,修改后的配置文件如下所示:

之后,重启SSHD服务即可生效。

SSH pipelining链接加速

SSH的pipelining是另一个加速Ansible执行速度的方法。在Ansible的设置中,SSH的pipelining功能时默认关闭的,这是为了兼容不同的sudo配置,主要是requieretty选项。因此,如果我们不需要在Ansible的控制中使用sudo选项,可以关闭这一选项以加快SSH链接速度。

如果要关闭这一项,可以打开Ansible的配置文件/etc/ansible/ansbile.cfg,将pipelining = False改为True即可,修改后的配置文件如下所示:

Ansible Facts调优
关闭Gather Facts

为了减少Ansible在收集客户端信息时的时间,我们首先想到的就是直接删除这一选项。要删除这一步骤,我们可以在palybook文件中添加一行:

代码语言:javascript复制
gather_facts: no

添加后的Playbook文件如下所示:

这样,我们在执行该Playbook时,就不会再次进行gather_facts的步骤了,结果如下所示:

将Facts信息存入Redis缓存

除了删除这一步之外,我们还可以考虑将客户端信息写入内存,以加快信息查询速度。

参考链接:https://blog.csdn.net/weixin_40228200 /article/details/123486710 https://blog.csdn.net /w918589859/article/details/111467805

0 人点赞