抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

ansible 深入实践

[toc]

RHCA447 ansible的版本: 2.8

什么是ansible

开源的自动化管理工具
用于管理中间件(应用)

  • 必备条件
    1. 主控端需要知道所有被它管理的节点的ip
      1. inventory 主控清单
    2. 它需要有权限连接到所有的节点并能执行相应的任务
      —> 能免连上 - ssh
      —> 有权限执行相应的任务 - 授权
    3. 主控端需要能够选择特定的主机执行任务
    4. 批量执行的任务是由管理员编写并下发给主控端的
  • 当前主流的应用自动化管理工具

    1. ansible
      1. 主法简单
      2. 轻量级
      3. python 开发
      4. 无客户端 ,基于ssh连接被控端
      5. serverless,即无服务端,在主控端上,只有一条ansible指令,不需要启动任何服务
      6. 同时管理500+节点的时候会出现性能问题
    2. saltstack
      1. 语法相对复杂
      2. 重理级
      3. python开发
      4. 有客户端,但也支持无客户端的ssh连接
      5. 有服务端,被称之为salt-master, 需要去考虑master的高可用及负载均衡能力,当master挂了时,随之所有客端的地址都改。
      6. 号称可以管理上万台的节点
    3. puppet
    4. Fabric
      1. 可以整合python语句实现更匹配公司业务的自动化工具编写
      2. 无客户端无服务端,基于ssh进行近程控制
  • ansible 对被控端的要求

    1. 得有ssh
    2. 所有被控端得有一个统一的用户,可用于主控制端
    3. 被控端的这个用户,得有相应用的权限
    4. 在所有的被控端上得有python的环境

实验hosts清单

127.0.0.1 workstation
172.25.250.9 workstation
172.25.250.10 servera servera.lab.example.com
172.25.250.11 serverb serverb.lab.example.com
172.25.250.12 serverc serverc.lab.example.com
172.25.250.13 serverd serverd.lab.example.com
172.25.250.14 servere servere.lab.example.com

ansible 安装与初始化

  • 安装ansible
# yum -y install ansible # 这里以ansible 2.8的版本为例
  • 配置ssh免密

  • 配置主机清单

# vim /etc/ansible/hosts # 这里是ansible.cfg inventory 默认指定的主机清单
servera 
serverb
serverc
serverd
servere
  • 测试连通性
# ansible all -m ping
  • 命令行参数
    • 选择主机: 从主机清单当中选择特定的主机
    • 执行相应的任务模块
      • -m ping
    • 配置要执行的模块的参数,使用-a 来指定
  • 主机清单

    • -i 用于管理被控端的主机列表
    • 它就是一个文件
    • 默认情况ansible 会从 /etc/ansible/hosts 这个主机清单中读取主机
    • 可以自定义位置,如果自定义了位置,在执行asnible指令时需要使用-i参数指定
    • /etc/ansible/ansible.cfg # inventory = /etc/ansible/hosts ansible默认指定主机清单的位置

      主机选择

  • 主机选择

    • all 选择所有主机
    • “*” 选择所有主机
    • groupname 选择主机组
    • ansible -i inventory webservers,dbservers,servere --list-hosts 可以用逗号分隔来匹配多个组或者多个主机,主机名,或者组名会在主机清单中自动实别
    • 使用通配符来匹配主机
        # ansible -i inventory "*.example.com" --list-hosts
        # ansible -i inventory "172.25.*" --list-hosts
        # asnbile -i inventory "*.example.com,!*.lab.example.com" --list-hosts # 匹配所有.example.com 结尾的主机,但排除所有.lab.example.com结尾的主机
      
    • 使用正则表达式来匹配,在开头的地方使用~,用来表示这是一个正则表达式
        # ansible  -i inventory '~(s|db).*example\.com' --list-hosts
      
    • 通过—limit 在选定的组中明确指定主机 ansible -i inventory ungrouped --limit srv1.example.com --list-hosts,表示选定组后,只执行组中的指定节点
    • 通过—limit 参数,还可以指定一个文件,该文件中定义指定的主机列表
        # ansible ungrouped --limit @retry_hosts.txt --list-hosts # 指定组中,限制hosts.txt中的主机列表执行
      

      主机清单定义

ansible 当前主控端用户是谁,那么默认用当前登录用户来连接被控端主机,即被控端也需要有当前主机的用户
ansible2.8之前定义主机组

[webservers]
servera
serverb
[dbservers]
serverd
servere
[webdbservers:children]
webservers
dbservers

注意配置清单中,不确定的节点要放在清单配置文件的最上边,若放到最下边,它会认为是最后一个组中的主机
继以上原因,ansible2.8之后定义主机组

ansible.cfg配置文件中写了主机清单配置的类型,yaml形式的配置清单默认是开启的
# enable_plugins = host_list, virtualbox, yaml, constructed

而host_list格式的文件,可以用ansible-inventory -y -i ./inventory_old --list --output ./inventory 转换为yaml格式的主机配置清单

ungrouped: # 这样定义的就是未定义的主机组
  hosts:
    serverx1:
    serverx2:
    serverx3:
webservers:
  hosts:
    servera:
    serverb:
dbservers:
  hosts:
    serverd:
    servere:
webdbservers:
  children:
    dbservers:
    webservers:

ansible -i inventory all --list-hosts 列出所有的主机
ansible -i inventory webservers --list-hosts 列出webservers组所有的主机

主机清单定义的示例

all:
  children:
    webservers:
      vars:
        ansible_ssh_user: root
        ansible_ssh_pass: redhatweb
      hosts:
        servera:
          ansible_host: 192.168.0.41
        serverb:
          ansible_host: 192.168.0.241
    dbservers:
      vars:
        ansible_ssh_user: root
        ansible_ssh_pass: redhatdb
      hosts:
        serverc:
        serverd:
  • 分发密钥

    • ansible需要连接上主机
      • 主机清单
    • ansible需要通过认证
      • ansible_ssh_user # 变量
      • ansible_ssh_pass # 变量
    • 在所有被端创建一个可以被ansible远程连接的用户

      • ansible all -m group 'name=devops gid=10000'
      • ansible all -m user 'name=devops shell=/bin/bash uid=10000 group=devops'
      • ansible all -m authorized_key -a 'user=devops state=persent' key="ssha-rsa AAXX#注:这里是要分发公钥的内容"'
      • 也可以用以下两步来取代以上这一步

          # ansible all -m file -a 'path=/home/devops/.ssh state=directory owner=devops group=devops mode=0700'
          # ansible all -m copy -a '/root/.ssh/id.rsa.pub dest=/home/devops/.ssh/authorized_keys owner=devops group=devops mode=0600'
        
        # vim /etc/sudoers # 这个在workstation节点上执行,然后把配置同步到其它节点
        devops ALL=(ALL) NOPASSWD:ALL
        

        ansible all -m copy -a "src=/etc/sudoers dest=/etc/sudoers owner=root group=root mode=0440"

ansible.cfg中的host_key_checking = False 首次登录时不检测密钥,即ssh登录时自动回答yes —> ssh的known_hosts

ansible配置文件

优先级最高1:ANSIBLE_CONFIG=/tmp/ansible.cfg
优先级2:(常用)执行ansible指令所在的当前目录ansible.cfg
优先级3:~/.ansible.cfg当前用户家目录下的一隐藏文件,如果当前目录下不存在ansible.cfg配置文件,则会查找用户目录下的该隐藏文件
优先级4:/etc/ansible/ansible.cfg默认配置文件,如果上面两个路径下的ansible.cfg都不存在,则使用该文件
补充 grep -Ev "\s*#|^$" ansible.cfg 查看所有生效的配置

  • 配置项
    • inventory: 用于定义默认主机清单的路径,默认值为/etc/ansible/hosts
    • remote_tmp: 被控端临时脚本所存放的路径
    • local_tmp: 主控端临时脚本所存放的路径
    • remote_port: ansible主控端连接被控端的ssh的默认的端口号,默认是22
    • remote_user: ansible主控端连接被控端所使用的用户
    • private_key_file = /root/.ssh/id_rsa 主控端私钥在本地存放的位置
    • host_key_checking: 定义是否关闭远程hosts_key的检查
  • 配置remote_user的提权
    • privilege_escalation
      • become=True 开启提权
      • become_method=sudo 提权的方式使用sudo
      • become_user=root 提权到root用户
      • become_ask_pass=False 提权到指定用户是否需要密码

ansible ad-hoc,模块

在ansible 当中,向主控端下发控制指令的两种方式:

  • ansible ad-hoc
  • ansible playbook

基础常用模块

  • ping

    • 用于检测ansible的主控端是否能够正常的控制被控端,如果能够连接,但是连接所使用的用户权限不足时,ping检测不出来
  • copy

    • 用于复制本地的文件或者目录到补控端的指定目录
    • src: 原文件或者目录的路径
    • dest: 复制到被控端后的路径
    • owner: 文件被复制到被控端后,在被控端上的属主
    • group: 属组
    • mode: 在被控端上的权限
    • content: 此参数与src参数不能共存,其用于指定被控端上的文件的内容
  • file
    • path: 远程主机上的文件或目录的路径
    • state: 定义被管理的文件的状态
      • absent: 删除
      • directory: 创建为目录
      • file: 如果文件存在,返回文件的当前状态,如果文件不存在,则报错
      • hard: 创建为硬链接
      • link: 创建为软链接
      • touch: 如果存在则更新时间
      • group
      • owner
      • mode
    • owner:
    • group:
    • mode: 644
    • src: 如果文件的状态为link或者hard时,需要指定原文件的路径
  • shell
    • 用于在远程主机上执行shell指令
    • 与shell功能类似的模块
      • command: 功能比shell稍弱,不支持管道,重定向、&& 这种特殊的操作符
      • raw
      • script: 将主控端上的脚本自动复制到远程主机上执行
    • cmd: 要执行的shell指令
    • chdir: 执行指令之前要切换到的目录
    • creates: 用于判断当前指令是否应该执行
    • removes: 与creates功能一样,用法相反
      # asnible all -m shell -a 'chdir=/tmp ls # 执行结果会返回到终端上面
      # ansible all -m shell -a 'chdir=/tmp cmd=ls'  #  执行结果不返回到终端上面
      # ansible servera -m shell -a "creates=/tmp/555.txt touch /tmp/666.txt" #存在不执行touch
      # ansible serverb -m shell -a "removes=/tmp/555.txt touch /tmp/666.txt" #存在执行touch
      
    • shell 有一个缺陷,就是不能保证幂等的原则
  • authorized_key
    • 将主控端的公钥复制到远程主机指定用户.ssh目录下的authorized_keys文件当中
    • 常用参数
      • user 要把公钥放到哪个主机的哪个用户下
      • key 被推送的公钥的内容
      • path 被推送的这个公钥放到远程主机的哪个路径下,默认是.ssh/authorized_keys中
      • state 是推送公钥,还是从远程主机上删除指定的公钥
        • present
        • absent
          # asnbile all -m copy -a "src=/etc/fstab dest=/tmp/fstab"
          
          copy模块的作用: 从主控端复制文件和目录到被控端

模块帮助

ansible-doc -l可以列出所有支持模块
ansible-doc <nodule_name> 可以打印当前模块的详细用法
-s <module_name>打印当前模块的所有参以及参数的说明

yum

  • 用于安装软件包
    • 常用参数:
      • name
      • state
        • present
        • absent
        • latest
          # ansible workstation -m yum -a 'name=httpd state=present'
          

fetch

  • 将远程主机上的指定文件复制到控制节点
  • 参数
    • src: 指的是远程主机上的文件和路径
    • dest: 指的是主控端上的文件和路径
    • owner
    • group
    • mode
      # ansible workstation -m fetch -a 'src=/etc/httpd/conf/httpd.conf dest=/tmp' # 将远程主机上的httpd.conf 拷到控制节点的/tmp目录下面
      

service

  • 管理远程主机上的服务的启停
  • 参数

    • name: 指定要管理的服务的名称
    • state: 服务的启停状态操作
      • started
      • stopped
      • restarted
    • enabled: 是否设置为开机自启
      # ansible workstation -m service -a 'name=httpd state=started enabled=yes'
      

      systemd

  • 功能与service 一样,只不过它无法管理红帽6版本中的服务

  • 参数
    • name
    • state
      • started
      • stopped
      • restartd
    • enabled
    • daemon_reload

      cron

  • 定时任务计划
    • backup: 对远程主机上的原任务计划内容修改之前做备份
    • cron_file: 如果指定该参数,则用该文件替换远程主机上的cron.d目录下的用户的任务计划
    • day: 日(1-31,,/2,…)
    • hour: 小时(0-23,,/2,…)
    • minute: 分钟(0-59, ,/2,…)
    • month: 月(1-12,,/2,…)
    • weekday: 周(0-7,*,…)
    • job: 要执行的任务,依赖于state=persent
    • name: 该任务的描述
    • special_time: 指定什么时候执行,参数
      • reboot,yearly,annually,monthly,weekly,daily,hourly
    • state: 确认任该计划是创建还是删除
    • user: 以哪个用户的身份执行
# ansible workstation -m cron -a 'name="test cron module of ansible" job="echo hello" minute="30" hour="00" user=root' # 创建
# ansible workstation -m cron -a 'name="test cron moudle of ansible" job="echo hello" minute="30" hour="1" weekday="*/2"' # 基于上条做定时任务的修改

group

  • 创建用户组
    • 参数
      • name: 组名
      • gid: 组id
      • state:
        • present
        • absent
          # asnbile workstation -m group -a "name=user1 gid=10002"
          

          user

          通过ansible创建用户时,强烈建议先创建用户组,并指定用户组的id,与用户的id
          Linux实别用户是依据uid,gid来实别的,除了保证用户名一样,还要确保其uid gid一致

  • 创建用户
  • 参数
    • name: 指定创建用户的名称
    • home: 指定用户的家目录,需要与createhome配合使用
    • groups: 指定用户的附加组
    • group: 指定用户的属组
    • uid: 指定用户的uid
    • password: 指定用户的密码
    • createhome: 是否创建家目录 yes| no
    • system: 是否为系统用户
    • comment: 定义用户描述信息
    • remove: 当state=absent时, remove=yes 同表示边同家目录一起删除,等价与userdel -r
    • state: 是创建还是删除
    • shell: 指定用户的shell环境
      需要说明的是,在指定password参数时,不能使用明文密码,因为后面这一串密码会被直接传送到被管理主机的/etc/shadow文件中,所以需要先将密码字符串进行加密处理。然后将得到的字符串放到password中即可
      # ansible workstation -m user -a "name=user1 group=user1 uid=10002 createhome=yes shell=/bin/bash  password={{'redat'| password_hash('sha256')}}"
      

      get_url

  • 从互联网上下载文件到目标服务器
    • 参数
      • url: 指定url路径
      • dest: 下载至被控端的路径
      • owner:
      • group
        # ansible workstation -m get_url -a 'url="http://xxx" dest="/tmp"'
        

        unarchive

  • 解压压缩文件,将指定sre当中的文件直接解压至远程主机
  • 解压压缩文件
    • 参数
      • src: 指定要解压文件的路径,这个路径可是一个url,也可以是主控端上的一个压缩包
      • dest: 指定解压至被控端的目录

        setup

  • 用于获取变量
    # ansible all -m setup
    
    这个打印的是ansible_facts包含的所有的变量信息,打印的信息是一个打的字典

练习

  • 要求

    • 在webservers组的所有主机上安装nginx
    • 修改nginx的配置文件,使用之监听在8081端口,并且nginx的用户使用www用户
    • 修改nginx的首页文件,内容为”hello ansible”
    • 启动nginx,并设置为开机自启
    • 当nginx的配置文件变更时,自动更新reload nginx

      # 1. 安装
      # ansible webservers -m yum -a "name=nginx state=present"
      # 创建用户
      # ansible webservers -m group -a 'name=www gid=10003 system=yes'
      # ansible webservers -m user -a 'name=www group=www uid=10003 system=yes createhome=no'
      # 2. 修改配置文件
      # ansible servera -m fetch -a "src=/etc/nginx/nginx.conf dest=/tmp/"
      # 3. 修改配置文件
      # vim files/nginx.conf
      user www www;
      Listen 8081
      # 4. 修改首页文件
      # ansible webservers -m copy -a "src=files/nginx.conf dest=/etc/nginx/nginx.conf mode=0644"
      # ansible webservers -m copy -a 'content="hello ansible" dest=/usr/share/nginx/html/index.html'
      # 5. 启动nginx并设置为开机自启
      # ansible webservers -m systemd -a "name=nginx state=started daemon_reload=yes enabled=yes"
      
    • 在447的调优当中

      1. 在大规模文件复制场景当中,不要使用copy,而使用synchronize
      2. 在文件修改的场景,尽可能使用template,而不要使用lineinfile
      3. 在软件包安装的场景,尽可能在yum的name当中指定多个软件包,而不要使用循环
    • 上题使用yaml 来实现

      ```yaml

    • hosts: webservers
      tasks:
      • name: install nginx
        yum:
        name: nginx
        state: present
      • name: create group for nginx
        group:
        name: www
        state: present
        gid: 10003
        system: yes
      • name: create user for nginx
        user:
        name: www
        state: present
        system: yes
        shell: /sbin/nologin
        createhome: no
        group: www
        gid: 10003
      • name: copy nginx.conf to dest
        copy:
        src: file/nginx.conf
        dest: /etc/nginx/nginx.conf
        mode: 0644
        notify: restart nginx
      • name: generate index.html on dest
        copy:
        content: “hello ansible”
        dest: /usr/share/nginx/html/index.html
        notiofy: restart nginx
      • name start nginx
        systemd:
        name: nginx
        state: started
        daemon_reload: yes
        enabled: yes
        handlers:
      • name: restart nginx
        systemd:
        name: nginx
        state: restarted
        daemon_reload: yes
        ```
    • 在上面的示例中,一个playbook被分为了三个部分
      • target: 选取的目标
      • vars: 定义亦量
      • tasks: 执行的任务
      • handler: 触发器

yaml 语法

  • yaml语法是通过严格的缩进控制层级的,上层对下层是包含关系;缩进只能使用空格,不可以使用tab,一般是两个空格
  • yaml语法是对编程友好的,在这里我们就直接以程序语法结构来对照;
  • 执行剧本时,自上而下执行,顺序是不可以颠倒的;

    • 数据类型

      • string
      • int
      • float
      • list/array/slice

           [host1, host2, host3]
           - host1
           - host2
           - host3
        
           - name: install nginx
             yum:
               name: nginx
               state: present
           ["name":"install nginx","yum": "{"name": "nginx","state":"present"}"]
        
      • dict/map

        选取目标

- hosts: webservers 
- hosts: dbservers
- hosts: all
- hosts: webservers,dbservers
- hosts: 's*'
- hosts: '*.example.com,!*.lab.example.com'

tasks

  • tasks中定义具体要执行的任务,可以有多个,默认情况下,自上而依次执行;如果其间一个任务执行失败,则后惯任不再执行,整个play会自动退出
- name: install nginx
  yum:
    name: nginx
    state: present
- name: install package
  yum:
    name:
      - httpd
      - vim 
      - socat
      - "@Developmen Tools"
    state: preset
- name: copy nginx.conf to  remote hosts
  copy:
    src: file/nginx.conf
    dest: /etc/nginx/nginx.conf
    mode: 0644
    owner: root
    group: root

handlers

  • 要想使用handlers,需要在需要使用handlers的task上配置notify;当这个task被执行并且返回的状态为change时,handler即会被调用;
  • handler会在所有的task执行完毕后,才会执行;
  • 如果有多个任务调用了相同的handler,而这个handler只会在最后执行一次
  • 示例

      tasks:
        - name: template configuration file
          template:
            src: template.j2
            dest: /etc/foo.conf
          notify:
            - restart memecached
            - restart apache
        - name: start memcached
          service:
            name: memcached
            state: started
        - name: start apache
          service:
            name: httpd
            state: started
      handlers:
        - name: restart memcached
          service:
            name: memcached
            state: restarted
        - name: restart apache
          service:
            name: httpd
            state: restarted
        - name: check server
          check:  ## 通知器检查
            name: httpd
            state: restarted
    
  • listen 示例

当tasks任务重多,而调用handlers,这样会频繁的在剧本中写,在2.8之后提供了listen
以下示例中同时定义了多个listen 以供调用

- hosts: webservers
  tasks:
    - name: install memcache and apache
      yum:
        name:
          - httpd
          - memcached
        state: present
    - name: copy configuration file
      copy:
        src: files/nginx.conf
        dest: /etc/foo.conf
      notify: restart service
    - name: test
      copy:
        src: files/nginx.conf
        dest: /tmp/xxx.conf
      notify: test
    - name: restart memcached
      service
        name: memcached
        state: started
    - name: start apache
      service:
        name: httpd
        state: started
  handlers:
    - name: restart memcached
      service:
        name: memcached
        state: restarted
    - name: restart apache
      service:
        name: httpd
        state: restarted
      listen:
        - restart service
        - test

变量

  • 变量定义的几个位置
    • 在ansible-playbook的命令执行当中通过-e参数传递
    • inventory 当中
    • playbook 当中的vars当中
    • playbook 当中的vars_files当中
    • 可以定义在任务当中
    • ansible-playbook的命令行传参
    • yaml执行的当前目录host_vars当中为主机定义的变量,host_vars的子目录为inventory_hostname 即主机名,再下级目录定义变量文件
    • yaml执行的当前目录group_vars当中为主机组定义的变量 ,group_vars的子目录为组名,再下级目录定义亦是文件
    • 通过host_vars 和group_vars目录来定义
    • 通过register关键字实现变量的注册
    • 通过ansible的facts来定义以及获取变量
    • 在ansible的roles当中的defaults或者vars目录当中定义
      在ansible当中,调用变量,使用"{{ varname }}"

内置变量

内置变量一般定义的inventory中,或者playbook的vars和vars_files中

  • 一般连接
    ansible_host # 用于指定被管理的主机的真实IP
    ansible_port # 用于指定连接到管理主机的ssh端口号,默认为22
    ansible_user # 用于指定连接时默认使用的用户名
  • 特定ssh连接
    ansible_connection # SSH连接的类: local,ssh,paramiko,在ansible 1.2之前默认是paramiko,后来智能选择,优先使用基于ControlPersist的 ssh(如果支持的话)
    ansible_ssh_pass # ssh 连接时的密码
    asnible_ssh_private_key_file # 密钥文件路径,如果不想使用ssh-agent管理密钥时可以使用此选项
  • 特权升级
    ansible_become # 相当于ansible_sudo或者ansible_su,允许强制特权升级
    ansible_become_user # 通过特权升级到的用户,相当于asnible_sudo_user或者ansible_su_user
    ansible_become_pass # 提供特权时,如需要密码的话,可以通过该变量指定,相当于ansible_sudo_pass 或者ansible_su_pass
  • 远程主机环境参数
    ansible_shell_executable # 设置目标主机上使用的shell,默认为/bin/sh
    ansible_python_interpreter # 用来指定python解释器的路径,默认为/usr/bin/python同样可以指定ruby、perl的路径
    ansible_*_interpreter # 其他解释器路径,用法与ansible_python_interpreter类似,这里”*”可以是ruby或者perl等其他语言
    inventory 中定义的主机清单的中的主机名也称之为inventory_hostname

    • 示例

        - hosts: all 
          tasks:
            - name: set hostname for remote_hsots
              hostname:
                name: "{{ inventory_hostname }}"
      

      这样就可以依据inventory中的主机名变量来批量配置主机名

      在默认情况下,ansible配置文件当中的所有配置项,在其前面加上ansible_则可将其申明为ansible的内置变量进行调用

ansible.cfg 中没有配置提权的情况下

- hosts: serverb 
  tasks:
  - name: task1
    debug: "task1"
  - name: task2
    debug: "task2"
  - name: task3
    vars:
      ansible_become: True
      ansible_become_user: root
      ansible_become_method: sudo
      ansible_become_ask_pass: False
    yum:
      name: redis
      state: persent

自定义变量

注意

  • 在playbook当中定义的变量,只对当前的playbook有效,其的playbook无法使用此变量;
  • 在task当中定义的变量,只对当前task有效,其他的task无法使用此变量;
  • 在inventory当中为主机组定义的变量,只对当前主机组有效;
  • 在host_vars目录当中定义的变量,只对指定的主机有效;
  • 在group_vars当中定义的变量,只对指定的主机有效;
# mkdir host_vars/server{a,b,d,e}
# vim host_vars/servera/comman.yaml
hobby: aa
# vim host_vars/serverb/comman.yaml
hobb: bb
# vim host_vars/serverc/comman.yaml
hobb: cc
# vim host_vars/serverd/comman.yaml
hobb: dd
# mkdir group_vars/webservers
# vim group_vars/webservers/comman.yaml
aa: webserversaa
# vim group_vars/dbservers/comman.yaml
aa: dbserversaa
# mkdir group_vars/dbservers
- hosts: all
  tasks:
  - name: print hobby for remote hosts
    debug:
      msg: "{{ inventory_hostname }}'s hobby is {{ hobby }}"
  - name: print aa for remote host groups
    debug:
      msg: "{{ inventory_hostname }}'s aa is {{ aa }}"

注册变量

  • 将一个任执行之后的输出结果,注册为变量,以便在后续的任务当中使用
- hosts: webservers
  tasks:
  - name: print whoami 
    shell: 
      cmd: whoami
    register: user
  - name: print user
    debug:
      var: user

执行时命令传递变量ansible-playbook 9-vars.yaml -e '{"user":"natasha","xx":"yy"}'

ansible中的变量类型

  • 在ansible当中,常用的变量类型:
    • string: 字符串
    • number: 包含整型和浮点型
    • list: 列表类型
    • dict: 字典类型
    • null: 未被定义的变量

447考试的核心是过滤器与变量占比70%

ansbile_facts

- hosts: workstation
  gether_facts: # 收变量,默认不写也会进行收集
  # gether_facacts: no # 不收集变量
  tasks:
  - name: fetch ipv4 address
    debug:
      msg: "{{ ansible_all_ipv4_address }}" # ansible_facts是可以直接省略的

变量定义实例

users:
- name: natasha
  id: 666
  shell: /bin/bash
  system: no
  createhome: yes
  home: /home/natasha
  hobby:
  - Music
  - Tea
- name: bob
  id: 6666
  shell: /sbin/nologin
  system: yes
  createhome: no
  hobby:
  - Film
  - Basketball
- name: tom
  id : 5555
  shell: /bin/bash
  system: no
  createhome: yes
  home: /home/tom
  hobby:
  - Music
  - Film
users1:
  natasha:
    id: 666
    shell: /bin/bash
    system: no
    createhome: yes
    home: /home/natasha
    hobby:
    - Music
    - Tea
  bob:
    id: 6666
    shell: /sbin/nologin
    system: yes
    createhome: no
    home: /home/bob
    hobby:
    - Film
    - Basketball
  tom:
    id: 5555
    shell: /bin/bash
    system: no
    createhome: yes
    home: /home/tom
    hobby:
    - Music
    - Film

引用

- hosts: workstation
  vars_files:
  - users.yaml
  tasks
  - name: get tom's home
    debug:
      msg: "users_tome: {{ users[2]['home'] }} users1_tom: {{ users1['tom']['home'] }}"
  - name: get bob's hobby
    debug:
      msg: "users_bob_hobby: {{ users[1]['hobby'][1]}} users1_bob_hobby: {{ users1['bob']['hobby']}}"
- hosts: workstation
  gether_facts: yes
  tasks:
  - debug:
    msg: "{{ ansible_interfaces }}"
- name: fetch ipv4 address
  debug:
    msg: "{{ ansible_eth0['ipv4']['address'] }}"
- name: fetch ansible_distribution
  debug:
    msg: "{{ ansible_distribution }} -- {{ ansible_distribution_major_version }}"
  • 自定义fact
# vim files/custom-fact.ini
[general]
package = httpd
service = httpd 
state = started
- hosts: all
  tasks:
  - name: ensure directory /etc/ansible/facts.d is exists
    file:
      path: /etc/ansible/facts.d
      state: directory
  - name: copy custom-fact.ini to dest
    copy:
      src: files/custom-fact.ini
      dest: /etc/ansible/facts.d/custom.fact
"ansible_local":{
    "custom":{
        "general":{
            "package": "httpd"
            "service": "httpd"
            "state": "started"
        }
    }
}
- hosts: workstation
  gether_facts: yes
  tasks:
  - name: fetch custom fact
    debug:
      msg: "{{ ansible_local['custom']['general']['state'] }}"
  • set_fact 用于设置变量
- hosts: workstation
  gather_facts: yes
  tasks:
  - name: set fact example
    set_fact:
      AA: bb
  - name: print AA
    debug:
      msg: "{{ AA }}"
  • fact 缓存

将fact 缓存写在数据库当中

[defaults]
gathering = smart
fact_caching_timeout = 86400
fact_caching = redis
fact_caching_connection = 127.0.0.1:6379:0

缓存到文件中去

gethering = smart
# 缓存时间,单位为秒
fact_caching_timeout = 86400
fact_caching = jsonfile
# 指定ansible包含fact的json文件位置,如果目录不存在,会自动创建
fact_cachinga_connection = /tmp/ansible_fact_cache

魔法变量

- hosts: webservers
  tasks:
  - name: set hostname for remote hosts 
    hostname:
    name: "{{ inventory_hostname }}"

hostvars

作用: 通过hostvars可以在当前执行任务的主机上获取其他主机的fact

inventory

loadbalancers:
  hosts:
    workstation:
webservers:
  hosts:
    servera:
    serverb:

要求: 在loadbalancers的组中主机部署nginx反向代理,将自身的80端口请求代理至webservers组的所有主机的80端口

要实现以上需求:

  1. loadbalancers 组的主机在执行部署的时候,必须要拿到webservers组里的所有的主机列表
    grups 魔法变量
  2. loadbalancers 组的主机在执行部署的时候,还要要拿到webservers组里的所有的主机的ip地址
    hostvars 魔法变量

groups

作用: groups自动获取inventory当中定义的所有组

# 可以这样两种方式来写 groups.webservers --> groups['webservers']
groups:
  webservers:
  - servera
  - serverb
  lbservers:
  - workstation

nginx 模板配置

upstream web {
{% for host in gorups['webservers'] %}
    server {{ hostvars[host]['ansible_eth0']['ipv4']['address']}};
{% endfor %}
}
server {
   listen 80 default_server;
   server_name _;
   location / {
       proxy_pass http://web;
   }
}
- hosts: webservers
  tasks:
  - name: install httpd
    yum:
      name: httpd
      state: present
  - name: copy index.html to dest
    copy:
      content: "This is a test for {{ ansible_eth0.ipv4.address }}"
      dest: /var/www/html/index.html 
    notify: restart httpd
  - name: start httpd
    systemd:
      name: httpd
      state: started
  handlers:
  - name: restart httpd
    systemd:
      name: httpd
      state: restarted
      daemon_reload: yes
- hosts: lbservers
  tasks:
  - name: install nginx 
    yum:
      name: nginx
      state: present
  - name: copy config to dest
    template: files/nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  - name: start nginx
    systemd:
      name: nginx
      state: started
    notify: restart nginx
  handlers:
  - name: restart nginx 
    systemd: nginx
    state: restarted
    daemon_reload: yes

paly_hosts

  • 当前playbook会在哪些hosts上运行

inventory_dir

  • 主机清单所在目录

iventory_file

  • 主机清单文件

group_names

  • 对于当前执行节点,属于哪个组
  • 例: servera 属于 lbservers webservers 这两个组,servrerb属于 webservers这一个组

变量优先级

  1. extra vars(命令中-e)最优先
  2. inventory 主机清单中连接变量(ansible_ssh_user等)
  3. play 中vars、vars_file等
  4. host_vars 和group_vars定义的变量
  5. 剩余的在inventory中定义的变量
  6. 系统的facts变量
  7. 角色定义的默认变量(roles/rolesname/defaults/main.yaml)

lookup 与query

lookup: 负责调用插件从特定的存储当中读取数据充作变量
query: 和lookup 功能是一样的,但是它返回的是一个列表

  • file
    • 将一个文件当中的的内容读出来存储为一个变量
    • 文件总是来自主控端
{{ lookup('file', '/root/.ssh/id_rsa.pub ')}}
- hosts: workstation
  tasks:
  - name: lookup file
    set_fact:
      public_key: "{{ lookup('file', '/root/.ssh/id_rsa.pub')}}"
  - name: print public_key
    debug:
      msg: "public_key's value is : {{ public_key }}"

可以通过如下命令查询ansible的 lookup支持所有的插件

ansbile-doc -t lookup
  • pipe

pipe: 将一个命令的执行结果保存为一个变量,它和register不一样的是,不需要一个专门的任务来执行这个命令lookup的变量是可以直接定义到template当中去的,而register注册的变量不可以

- hosts: workstation
  tasks:
    - name: Flamingo | Get release version
      set_fact:
        flamingo_release_version: "{{ lookup('pipe','date +%Y%m%d%H%M%Z') }}"
    - name: print flamingo release version
      debug:
        msg: "{{ flamingo_release_version }}"
    - name: hostname
      set_fact:
        hostname: "{{ query('pipe','hostname')}}"
    - name: print hostname
      debug:
        msg: "{{ hostname }}"
  • url 获取页面内容提交给主控端
- hosts: webservers
  tasks:
  - name: url
    set_fact:
      text: "{{ query('url','http://139.9.234.235')}}"
  - name:
    debug:
      msg: "{{ text }}"
  • redis_kv

redis_kv lookup可以直接从redis存储中来获取一个key的value,key必须是一个字符串,如同Redis GET指令一样,需要注意的是,要使用redis_kv lookup,需要在主控端安装python 的redis客户端,在CentOS上,软件包为python-redis。
下面是一个在playbook中调用redis lookup的task, 从本地的redis中取一个key 为weather的值

- name: lookup value in redis
  debug: msg="{{ lookup('redis_kv','redis://localhost:6379,weather' )}}"
  • etcd
    etcd 是一个分布式的key-value存储,通常被用于保存配置信息或者用于实现服务发现,可以使用etc lookup来从etcd中获取指定key的value
    curl -L http://127.0.0.1:2379/v2/keys/weather -XPUT -d value=sunny
    
  • 定义一个调用etcd插件的task
- name: look up value in etcd
  debug: msg="{{ lookup('etcd','http://127.0.0.1:2379',weather)}}"

默认情况下,etcd lookup会在http://127.0.0.1:2379上查找etcd服务器。但我们在执行playbook之前可以通过设置ANSIBLE_ETCD_URL环境变量来修改这个设置

  • password
    password lookup会随机生成一个密码,并将这个密码写入到参数指定的文件中。如下示例。创建一个名为bob的mysql用户,并随机生成该用户的密码,将密码写入到主控端的bob-password.txt中
- name: create deploy mysql user
  mysql_user:
    name=bob
    password={{ lookup('password','bob-password.txt')}}
    priv=*.*:ALL
    state=present
  • dnstxt
    dnstxt lookup用于获取指定域名的TXT记录。需要在主控端安装python-dns
    使用方法如下
- name: lookup TXT record
  debug: msg="{{ lookup('dnstxt','aliyun.com')}}"

如果某一个主机有多个相关的TXT记录,那么模块会把他们连在一起,并且每次调用时连接顺序可能不同

template jinjia2模板

set_fact: redismem="{{ (ansible_memtotal_mb /2) | int}}M"

jinjia2 条件语句

  • 判断
{% if ansible_bond0 is defined %}
bind {{ ansible_bond0.ipv4.address }} 127.0.0.1
{% elif ansible_eth0 is defined %}
bind {{ ansible_eth0.ipv4.address }} 127.0.0.1
{% else %}
bind 0.0.0.0
{% endif %}

## 主从配置
{% if 'redis-slave' in group_name %}
slaveof {{ hostvars[groups['redis-master'][0]]['ansible_eth0']['ipv4']['address'] }} {{ port | default(6379) }}
{% if requirepass is defined %}
masterauth {{ requirepass }}
{% endif %}
{% endif %}
## 以上为嵌套if
{% if requirepass is defined %}
requirepass {{ requirepass }}
{% endif %}
redis:
  children:
    redis-master:
      hosts:
        servera:
          ansible_ssh_host: 192.168.0.41
    redis-slave:
      hosts:
        serverb:
          ansible_ssh_host: 192.168.0.241
  • for 循环

for循环只能用于循环列表,不可以循环字典,如果是字典,必须将其转换为列表才能循环

upstream web{
{% for host in groups['webservers'] %}
    {% if hostvars[host]['ansible_bond0']['ipv4']['address'] is defined %}
    server {{ hostvars[host]['ansible_bond0']['ipv4']['address']}};
    {% elif hostvars[host]['ansible_eth0']['ipv4']['address'] is defined %}
    server {{ hostvars[host]['ansible_eth0']['ipv4']['address']}};
{% endif %}
}
  • 在template 文件中定义变量
{% if 'authorztivenames' in group_names %}
{% set zone_type = 'master' %}
{% set zone_dir = 'data' %}
{% else %}
{% set zone_type = 'slave' %}
{% set zone_dir = 'slaves' %}
{% endif %}

zone "internal.example.com" IN {
type {{ zone_type }};
file "{{ zone_dir }}/internal.example.com";
{% if 'authorativename' not in group_names %}
master {192.168.2.2;};
{% endif %}
};

过滤器

  • default

作用于对变量的进一步的处理,当变量不存在时,用于设定默认值

"Host:" "{{ db_host | default('localhost')}}"

过滤器
dict2items
items2dict

示例,基于以下变量,实现一个playbook 创建以上用户

users:
  natasha:
    shell:/bin/bash
    createhome: yes
    id: 666
    home: /tmp/natasha
  bob:
    shell: /bin/bash
    id: 66666
    createhome: no
  tom:
    shell: /bin/bash
    createhome: yes
    id: 11111

参考

- name: create users
  group:
    name: {{ item.key }}
    gid: {{ item.value.id }}
  loop: {{ users | dict2items }}

mode: {{ item.mode | default(omit) }} ##这句的话意思是item.mode 不存在,则不进配置

  • mandatory

判断一个变量是否被定义,如果未定义,则主动失败,主动报错为变量没有定义

- hosts: localhost
  gether_facts: no
   tasks:
  - name: print my_value
    debug:
      msg: "{{ my_value | mandatory}}"
  • upper

用于将所有字符串转换成为大写
{{ teststr | upper }}

  • lower
    将所有的字符串转换成为小写
    {{ teststr | lower}}
  • capitalize
    将所有的字符串首字母大写,其他字母小写
    {{ teststr | capitalize }}
  • reverse
    将字符串倒序排列
    {{ teststr | reverse }}
  • first
    返回字符串的第一个字符
    {{ teststr | first }}
  • last
    返回字符串的最后一个字符
    {{ teststr | last }}
  • trim
    将字符串开头和结尾的空格去掉
    {{ teststr | trim }}
  • center
    将字符串放在中间

    按30个字符的长度,将字符串居中,两边以空格填弃

{{ teststr2 | center(30)}}

  • length
    返回字符串的长度,与count 等价
    {{ teststr2 | length }}

  • list
    返回字符串换为列表
    {{ teststr3 | list }}

  • shuffle
    list将字符串转换为列表,但是顺序排列,shuffle会将字符串转换为列表,但是会随机打乱字符串顺序
    {{ teststr3 | shuffle }}
  • split
    将字符串分割为列表
    {{ teststr4.split() }}
  • json
    将列表按指定的值连接为字符串
    {{ teststr4.split() | json('.') }}
  • hash
    将给定的字符串做hash算法

  • password_hash(‘sha256’)
    一般用于新建系统用户
    {{ 'redat' | password_hash('sha256') }}

  • replace
    修改字符串

  • regex_replace

  • regex_search
    正则匹配与搜索,搜索到第一个退出
    示例
- name: regex_search
  debug:
    msg: "{{ 'marvin, arthur, arthur'|regex_search('ar\\S*r') }}"
- name: regex_replace
  debug:
    msg: "{{ 'marvin, arthur, arthur' | regex_repliace('ar(\\S*r)', '\\1mb') }}"
  • b64encode
    base64加密与echo redhat | base64一样
  • b64decode
    base64 -d 一样

数字相关的过滤器

  • 两个number相加
  • 两个number相减
    / 浮点数除法运算
    // 整数的除法运算
    % 整数的除法运算
  • 两个numbers相乘
    ** 乘方

示例

{{ (ansible_facts['ansible_date_time']['hour'] | int) + 1 }}

操作数字相关的过滤器

  • int
    • 将对应的值转换为整数
      "{{ 8+('8'| int)}}"

      默认情况下,如果无法完成数字转换则返回0;这里指定如果无法完成数字转换则返回6

"{{ 'a' | int(default=6) }}"

  • float
    • 将对应的值转换为浮点数
      "{{ '8' | float }}"
      "{{ 'a' | float(8.88)}}"
  • abs

    • 获取绝对值
      "{{ '-8'| abs }}"
  • round

    • 小数点四舍五入

"{{ 3.1415926 | round(5) }}"

  • random
    • 从一个给定的范围中获取随机值
      "{{ 100 | random }}" ## 从0到100中随机获取一个随机数
      "{{ 10 | random(start=5)}}" ## 从5到10中随机返回一个数字
  • 生产中常用的过滤器的场景
    1. 提取特定的字符串,将其转换为列表
    2. 提取字典当中的特定的值,其其转换为列表
    3. 80% 的操作都是为循环

列表操作相关的过滤器

  • length: 返回列表长度
  • first: 返回列表的第一个值
  • last: 返回列表的最后一个值
  • main: 返回列表中最小的值
  • max: 返回列表中最大的值
  • sort: 重新排列列表,默认为升序排列, sort(reverse=true)为降序
  • sum: 返回纯属数字嵌套列表中所有数字的和
  • flatten: 如果列表中包含列表,则flatten可拉平嵌套 的列表,levels参数可用于指定被拉平的层级
  • join: 将列表中的元素合并为一个字符串
  • random: 从列表中随机返回一个元素
  • shuffle: 将列表中的元素随机重排
  • upper: 将列表中的字符串元素转换为大写
  • lower: 将列表中的字符串元素转换为小写
  • union: 将两个列表合并,如果元素有重复,则只留下一个
  • intersect: 获取两个列表的交集
  • difference: 获取存在于第一个列表中,但不存在于第二个列表中的元素
  • symmetric_difference: 取出两个列表中各自独立元素,如果重复只留一个

应用于字典变量的过滤器

  • combine: 将两个字典合成一个
  • dict2items: 将字典转换成列表
  • items2dict: 将列表转换为字典
  • josn_query: 将变量转抱成json,并查询子元素
  • to_json/to_yaml/to_nice_json/to_nice_yaml
  • from_json

ipaddr 过滤器

  • 想要使用ipaddr过滤器,需要在主控端安装python-netaddr

yum install -y python-netaddr

- host: localhost
  vars:
    my_ipaddr: ["192.168.0.1/24"]
  tasks:
  - name: ipaddr
    debug:
      msg: "{{ my_ip_addr | ipaddr }}"
  - name: ipaddr(network)
    debug:
      msg: "{{ my_ip_addr | ipaddr('network') }}"
  - name: ipaddr(host)
    debug:
      msg: "{{ my_ip_addr | ipaddr('host') }}"
  - name: ipaddr(netmask)
    debug:
       msg: "{{ my_ip_addr | ipaddr('netmask') }}"
  - name: ipaddr(size)
    debug:
      msg: "{{ my_ip_addr | ipaddr('size') }}"
  - name: ipaddr(subnet)
    debug:
      msg: "{{ my_ip_addr | ipaddr('subnet') }}"
  - name: ipaddr(ipv4)
    debug:
      msg: "{{ my_ip_addr | ipaddr('ipv4') }}"
  - name: ipaddr(broadcast)
    debug:
      msg: "{{ my_ip_addr | ipaddr('broadcast') }}"

url分割过滤器

- hosts: localhost
  gather_facts: no 
  vars:
    url: 'http://user:password@www.example.com:8080/xxx/index.html?query=132'
  tasks:
    - name: "get url hostname"
      debug:
        msg: "{{ url | urlsplit('hostname') }}" # ==> "msg": "www.example.com"
    - name: "get url netloc"
      debug:
        msg: "{{ url | urlsplit('netloc') }}" # ==> "user:password@www.example.com:8080"
    - name: "get url path"
      debug: 
        msg: "{{ url | urlsplit('path') }}" # "msg": "/xxx/index.html" 
    - name: "get url port"
      debug:
        msg: "{{ url | urlsplit('port') }}" # ==> "msg": "8080"
    - name: "get url scheme"
      debug:
        msg: "{{ url | urlsplit('scheme') }}" # ==> "msg": "http"
    - name: "get url query"
      debug:
        msg: "{{ url | urlsplit('query' )}}" # ==> "msg": "query=132"

应用于文件路径的过滤器

  • basename
    • 返回文件路径中的文件名部分
  • dirname
    • 返回文件路径中的目录部分
  • expanduer
    • 将文件路径中的~替换为用户目录
  • realpath
    • 处理符号链接后的文件实际路径
- name: test hostname
  hosts: test
  vars:
    homepage: /usr/share/nginx/html/index.html
  tasks:
    - name: copy homepage
      copy:
        src: files/index.html
        dest: {{ homepage }}

可以通过basename改写成如下方式

- name: test basename
  hosts: test
  vars:
    homepage: /usr/share/nginx/html/index.html
  tasks:
    - name: copy homepage
      copy:
        src: files/{{ homepage | basename }}
        dest: {{ homepage }}

loop循环

在编写playbook的时候,不可避免的要执行一些重复性操作,比如安装软件包,批量创建用户,操作某个目录 下的所有文件等,
ansible作为一门简单的自动化语言,流程控制,循环语句这些编程语言的基本元素它同样都具备
在ansible2.5以前,playbook 通过不同的循环语句来实现循环,这些语句使用with_作用,在2.5以后,推荐使用loop关键字来实现循环

示例

tasks:
  - name: postfix and httpd are running
    service:
      name: "{{ item }}"
      state: present
    loop:
      - postfix
      - httpd
users:
  natasha:
    shell:/bin/bash
    createhome: yes
    id: 666
    home: /tmp/natasha
  bob:
    shell: /bin/bash
    id: 66666
    createhome: no
  tom:
    shell: /bin/bash
    createhome: yes
    id: 11111
- hosts: workstation
  vars_files:
  - vars/users.yaml
  tasks:
  - name: create group
    group:
      name: "{{ item['key'] }}"
      gid "{{ item['value']['id'] }}"
    loop: "{{ users | dict2items }}"
  - name: create user
    user:
      name: "{{ item.key }}"
      group: "{{ item.key }}"
      uid: "{{ item['value']['id'] }}"
      createhome: "{{ item['value']['createhome'] }}"
      home: "{{ item.value.home | default(omit) }}"
      shell: "{{ item.value.shell }}"
    loop: "{{ users | dict2items }}"

loop_control

loop_control用于循环时,获取列表的索引

- hosts: workstation
  gether_facts: no
  vars:
    testlist:
      - a
      - [b,c,[e,f]]
      - d
  tasks:
    - debug:
        msg: "{{ index }}: {{ item }}"
      loop: "{{ testlist | flatten(levels=1)}}"
      loop_control:
        index_var: index

zip_longest

  • 使用zip_longest过滤器将两个列表中的元素对齐合并,与python语法中的zip函数类似
- hosts: localhost
  gather_fact: no
  vars:
    testlist1: [a,b]
    testlist2: [1,2,3]
    testlist3: [A,B,C,D]
  tasks:
    - debug:
      msg: "['{{ item.0}}','{{item.1}}','{{item.2}}']"
    loop: "{{ testlist1 | zip_longest(list1,list2,list3)| list}}"

product

  • 使用product过滤器实现嵌套循环
- hosts: localhost
  gether_fact: no
  vars:
    testlist1:
    - alice
    - bob
    testlist2:
    - a
    - b
    - c
  tasks:
  - name: debug loops
    debug: msg="name is {{ item[0] }} values is {{ item[1] }}" ## alice a alice b alice c bob a bob b bob c
    loop: "{{ testlist1 | product(testlist2) | list }}"

看一个示例

mkdir /data/{a,b}/{1,2,3}

- hosts: localhost
  gether_facts: false
  vars:
    dir1:
    - a
    - b
    dir2:
    - 1
    - 2
    - 3
  tasks:
    - file:
      state: directory
      path: "/data/{{item[0]}}/{{item[1]}}"
    loop: "{{ dir1 | product(dir1) | list }}"

range

  • 通过range过滤器实现对数字序列的循环
- hosts: test
  gather_fact: no
  tasks:
    - debug:
        msg: "{{ item }}"
      loop: "{{ range(0,6,2) | list }}"

subelements

  • 使用subelements 过滤器实现循环复杂变量的元素
    需要说明的是,loop只能循环列表,所以这个元素需要是一个列表,或者可以被转换成为列表
- hosts: workstation
  gether_facts: no
  vars:
    users:
      - name: bobo
        gender: mable
        hobby:
          - skateboard
          - videogame
      - name: alice
        gender: female
        hobby:
          - music
          - skateboard
  tasks:
    - debug:
        msg: "{{ item.0.name }}'s hobby is {{ item.1 }}"
      with_subelements
        - "{{ users }}"
        - hobby
    - debug:
        msg: "{{ item.0.name }}'s hobby is {{ item.1 }}"
      loop: "{{ users | subelements('hobby')}}"

补充 运维组件的统计

https://landscape.cncf.io/?zoom=200

ansible 条件语句

基于上一个任务的执行结果,来判断一条任务要不要执行
在不同的操作系统上使用不同的安装工具来安装

- hosts: workstation
  tasks:
  - name: yum install httpd
    yum:
      name: httpd
      state: present
    when: ansible_describution == "CentOS"
  - name: apt install httpd
    yum:
      name: apache
      state: present
    when: ansible_describution == "Ubuntu"
  • 条件判断的关键词是when

在执行playbook 时,有时play的结果依赖于变量,fact 或者是前一个任务的执行结果,或者基于上一个task执行返回的结果而决定如何执行后续的task。此时就需要用到条件判断。
条件语句在Ansible中的使用场景

  • 在目标主机上定了义一个硬限制,比如目标主机的最小内存必须达到多少,才能执行该task
  • 捕获一个命令的输出。根据命令输出结果的不同以触发不同的task
  • 根据不同目标主机的facts,以定义不同的task
  • 根据目标机的cpu的大小,以调优相关应用情能
  • 用于判断某个服务的配置文件是否发生变更,以确定是否需要重启服务
- name: install vim
  host: all
  tasks:
    - name: Install VIM via in yum
      yum:
        name: vim-enhanced
        state: installed
      when: ansible_os_family == "RedHat"
    - name: Install VIM via in opt
      apt:
        name: vim
        state: installed
      when: ansible_os_family == "Debian"
    - name: Unexpected OS family
      debug:
        msg: "OS Family {{ ansible_os_family }} is not supported"
      when: not (ansible_os_family == "RedHat" or ansible_os_family == "Debian")

比较运算符

在上面的示例当中,我们使用了 ‘==’的比较运算符,在ansible中,还支持如下比较运算符

== 比较两个对象是否相等,相等,则返回真,用于比较字符串和数字
!= 比较两个对象是否不等,不等则为真
> 比较两个对象的大小,左边的值大于右边的值,则为真
< 比较两个对象的大小,右边的值大于右边的值,则为真
>= 比较两个对象的大小,左边的值大于等于右边的值,则为真
\<= 比较两个对象的大小,左边的值小于等于右边的值,则为真

when: ansible_mechine == 'x86_64'
when: max_memory <= 512

逻辑运算符

在Ansible当中,除了比较运算符,还有逻辑运算符

and 逻辑与,当左边和右边两个表达式相同时为真,则返回真
or 逻辑或,当左边和右边两个表达式任意一个为真,则返回真
not 逻辑否,对表达式取反
() 当一组表达式组合在一起,形成一个更大的表达式,组合内的表达式都是逻辑与关系

## 逻辑或
when: ansible_distribution == "RedHat" or ansible_distribution == "Fedora" or ansible_destribution == "CentOS"
## 逻辑与
when: ansible_destribution_version == "7.5" and ansible_kernel == "3.10.0-327.el7.x86_64" and ansible_distribution == "CentOS"
### 逻辑与第二种形式
when:
  - ansible_distribution_version == "7.5"
  - ansible_kernel == "3.10.0-327.el7.x86_64"
### 组合 => 这里指的是换行
when: =>
  (ansible_distribution == "RedHat" and ansible_distribution_major_version == "7")
  or
  (ansible_distribution == "Fedora" and ansible_distribution_major_version == 28")
  • 判断register 注册变量的返回结果
- name: restart httpd if postfix is running
  hosts: workstation
  tasks:
    - name: get postfix server status
      command: /usr/bin/systemctl is-active  postfix
      ignore_errors: yes
      register: result
    - name: restart apache httpd based on postfix status
      service:
        name: httpd
        state: restarted
      when: result.rc == 0

条件判断与tests

在shell当中,我们可使用test命令来进行一些常用的判断操作,如下

  • 判断/test文件是否存在 test -e /test
  • 判断/testdir 是否为一个目录 test -d /testdir

事实上,在ansible中也有类似的用法,只不过ansible 没有使用linux的test命令

- hosts: test
  vars:
    testpath: /testdir
  tasks:
    - debug:
        msg: "file exist"
      when: testpath is exists ##  这里判断不存在时可以 when: not testpath is exists

判断变量

  • defined: 判断变量是否已定义,已定义则返回真
  • undefined: 判断变量是否未定义,未定义则返回真
  • none: 判断变量的值是否为空,如果变量已定义且值为空,则返回真
- hosts: test
  gether_facts: no
  vars:
    testvar: "test"
    testvar1:
  tasks:
    - debug:
        msg: "testvar is defined"
      when: testvar is defined
    - debug:
        msg: "testvar2 is undefined"
      when: testvar2 is undefined
    - debug:
        msg: "testvar1 is none"
      when: testvar1 is none

判断执行的结果

  • success 或者 succeeded: 通过任务执行结果返回的信息判断任务的执行状态,任务执行成功则返回true
  • failure 或者 failed: 任务执行失败则返回true
  • changed 或者 change: 任务执行状态为changed 则返回 true
  • skip 或者skipped: 任务被跳过则返回 true
- hosts: test
  gether_facts: no
  vars:
    doshell: true
  tasks:
    - shell: 'cat/testdir/aaa'
      when: doshell
      ignore_errors: true
    - debug:
        msg: "success"
      when: result is success

    - debug:
        msg: "failed"
      when: result is failure
    - debug:
        msg: "changed"
      when: result is changed
    - debug:
        msg: "skip"
      when: result is skip

判断路径

  • file: 判断是否是一个文件
  • directory: 判断是否是一个目录
  • link: 判断指定路径是否为一个软链接
  • mount: 判断指定路径是否为一个挂载点
  • exists: 判断指定的路径是否存在

判断字符串

  • lower: 判断字符串中的所有字母是否都是小写字母
  • upper: 判断字符串中的所有字母是否都是大写字母

block 任务分块

条件相同的所有任务可以放到一个block块中
我们在前面使用when做条件判断时,如果条件成立则执行对应的任务,但这就面监一个问题,当我们一个条件判断执行多个任务的时候,就意味着我们要在某一个任务下面都写一个when语句,而且判断条件完全一样,这种方式不仅麻烦而且显得很low。ansible提供了一种更好的方式来解决这个问题,即block
在ansible中,使用block将多个任务进行组合,当作一个整体。我们可以对这个整体做条件判断,当条件成立时,则执行块中所有的任务

- hosts: workstation
  tasks:
    - debug: 
        msg: "tasks1 not in block"
    - block:
        - debug:
            msg: "tasks2 in block1"
        - debug:
            msg: "task3 in block1"
      when: 2 > 1

随之有 rescue 求援,block 执行错误时,则执行rescue中的内容
always 不管block resuce 执行成功与否, always 中的内容一定会执行

任务委托

- name: enable alerts for web servers
  hosts: webservers
  tasks:
    - name: task1
      debug: 
        msg: "task1"
    - name: task2
      debug:
        msg: "task2"
    - name: enable alerts
      shell:
        cmd: "cat /root/ansible/ansible.cfg"
      register: result
      delegate_to: workstation
    - name: print result.stdout
      debug:
        msg: "{{ result.stdout }}"
      when: result.rc == 0

任务暂停

有些情况下,一些任务的运行需要等待一些状态的恢复,比如某一台主机或者应用刚刚重启,我们需要等待它上面的某个端口开启,此时就需要将正在运行的任务暂停,直到其状态满足要求
ansilbe提供了wait_for模块以实现任务暂停的需求

  • wait_for模块常用参数
    • connect_timeout: 在下一个任务执行之前等待的连接超时时间
    • delay: 等待一个端口或者文件或者连接到指定的状态时,默认超时时间300秒,在这等待的300s的时间里,wait_for模块会一直轮询指定的对象是否到达指定的状态,delay即为多长时间轮询一次状态
    • host: wait_for模块等待主机的地址,默认为127.0.0.1
    • port: wait_for模块等待的主机的端口
    • path: 文件路径,只有当这个文件存在时,下一个任务才开始执行,即等待该文件创建完成
    • state: 等待的状态,即等待的文件或者连接状态达到指定的状态时,下一个文件任务开始执行。当等的对象为端口时,状态有started, stoped,即端口已经监听或者端口已经关闭;当等待的对象为文件时,状态有persent 或者started,absent,文件已创建或者删除;当等待的对象为一个连接时,状态有drained即连接已建立。默认为started
    • timeout: wait_for 的等待的超时时间,默认为300秒
wait_for:
  port: 8080
  state: started
  connect_timeout: 1
  timeout: 300
  delay: 10

滚动执行

ansible.cfg 中的forks: 20 当配置为20时,是指 ,比如有100台主机,那么20台主机为一组并发先把第一个任务执行完, 再执行下一个任务,在剧本中配置serial: 1是指,一台主机把所有的任务执行完,再把下一台主机所有的任务执行完

生产中的平滑滚动更新

  1. 需要执行 delegate_to: lbservers摘除要更新的web节点
  2. 通过serial: 1 控制更新当前节点
  3. 通过任务暂停等待tomcat启动完成
  4. 业务探活
  5. 更新完成,
  6. 通过delegate_to: lbservers将该节点重新加回去
  7. max_fail_percetage: 25 即最大的失败比例

以上五步会带来一个新的问题如果任务暂停等待tomcat启动时,发现tomcat启动失败了ansible会怎么处理 ? 如果ansible不去处理它,那么滚动更新完之后,所有节点都失败了 因此,可以使用第六步来实现,即最大的失败比例来进行一个控制

- hosts: all
  serial: 1
  ma_fail_percentage: 25
  tasks:
  - name: debug1
    debug:
      msg: "tasks1"
  - name: debug2
    debug:
      msg: "task2"

只执行一次

  • run_once
- hosts: all
  tasks:
    - name: run the task only once
      debug:
        msg: "run this  task only once"
      run_once: yes

比如在初始化webserver 的时候,webserver上会初始化数据库,而数据库只需要初始化一次,当webserver有多个节点的时候,我们就要用到run_once只在一个节点上执行。

设置环境变量

tasks:
  - name: set environment
    shell:
      cmd: "/data/bin/foo --version"
    environment:
      PATH="$PATH:/data/bin"
- hosts: all 
  remote_user: root
  vars:
    proxy_env:
      http_proxy: http://proxy.example.com:8080
      https_proxy: https://proxy.bos.example.com:8080
  tasks:
    - apt:
        name: cobbler
        state: installed
      environment: proxy_env

任务标签

- name: install nginx
  yum:
    name: nginx
    state: present
  tags:
  - always  # 内置标签,不管指不指定,都要执行
- name: copy nginx.conf to  dest
  template:
    src: templates/nginx.conf
    dest: /etc/nginx/nginx.conf
    mode: 0644
  tags:
  - update-conf
  - update-code-or-conf
  notify: restart nginx
- name: generate index.html on dest
  copy:
    content: "hello ansible"
    dest: /usr/share/nginx/html/index.html
  tags:
  - update-code
  - update-code-or-conf
# ansible-playbook nginx.yaml --tags update-code # 指定标签 
# ansible-playbook nginx.yaml --tags update-conf 
# ansible-palybook nginx.yaml --tags update-conf,update-code # 多个标签 
# ansible-palybook nginx.yaml --tags update-code-or-conf # 标签组
# ansible-playbook nginx.yaml --skip-tags update-code-or-conf # 标签取反
# ansible-palybook nginx.yaml --tags tagged # 执行所有标签

roles 使用

# ansible-galaxy role init nginx # 可以初始化一个role 的规范化目录结构
nginx/
├── defaults  # 一般定义可变更的变量
│   └── main.yml
├── files  # 传输的文件存放位置
├── handlers # 触发器的存放位置 
│   └── main.yml
├── meta # 依赖相关的roles 的调用关系
│   └── main.yml
├── README.md
├── tasks # tasks 任务 
│   └── main.yml
├── templates # 配置文件模板定义的位置 
├── tests # 测试流程
│   ├── inventory
│   └── test.yml
└── vars # 一般存放固定不变的变量
    └── main.yml

tasks文件调用,生产中不建议直接在man.yaml 中写tasks

# cat main.yaml
- name: manage nginx
  include_tasks:
    file: nginx.yaml

折分成多个,放在tasks 目录下的nginx目录中

- name: manage nginx
  include_tasks:
    file: nginx/install.yaml
- include_tasks: nginx/mange_users.yaml
- include_tasks:
    file: nginx/nginx.yaml

vars 目录中的main.yaml是无法拆分的

而实际引用变量文件是在tasks中

- hosts: all
  vars_files:
  - vars.yaml
  tasks:
    - name: include_vars
      include_vars:
      - vars.yaml

引入handlers 要使用import_tasks

注意事项

  1. include_tasks 和 import_tasks 都可以引入task,但尽可能使用 include_tasks
  2. include_tasks 无法引入handlers,但import_tasks 可以
  3. 在role当中变量无法使用include方式引入

使用role

- hosts: webservers
  roles:
    - comman
    - webserver
- hosts: webservers
  roles:
    - role: nginx
    - role: mysql

可以在使用role的时候指定变量,优先级要比role vars 目录下定义的变量的优先级要高

- hosts: webservers
  roles:
    - common
    - { role: foo_app_instance, dir:'/opt/a', port: 5000 }
    - { role: foo_app_instance, dir:'/opt/b' port: 5001 }

下面也是一个带入变量的示例

- hosts: webservers
  roles:
    - roles: database
      vars:
        database_name: "{{ db_name }}"
        database_user: "{{ db_pass }}"
    - roles: webserver
      vars:
        live_hostname: web1
        domains:
          - example.com
          - www.example.com

role的执行优先级默认永远高于tasks的优先级

pre_tasks —> role —> tasks —> post_tasks

- hosts: webservers
  post_tasks:
  - name: post_tasks
    debug:
      msg: "post_tasks"
  tasks:
  - name: debug
    debug:
      msg: "hellow tasks"
  pre_tasks:
  - name: pre_tasks
    debug:
      msg: "pre_tasks"
  roles:
  - role: nginx
  • 使用pre_task 和post_tasks 控制任务的执行顺序

  • playbook里任务执行顺序总结

    • 优先执行使用pre_tasks定义的任务
    • 然后执行role中的任务
    • 再执行tasks中的任务
    • 最后执行post_tasks中定义的任务
  • 定义role的依赖

- hosts: webservers
  roles:
    - systeminit
    - nginx
  • 在meta目录中定义一个role 依赖另一个role
dependencies:
  - role: systeminit

galaxy

ansible 提供专用的roles 模板的网站

galaxy

在早期的时候,galaxy社区用于分享roles
从ansible2.8开始,ansible社区推出了一个新的概念,叫collection,它被称为之为集合

  • 模块
  • role
  • plugin: inventory支持init和yaml两种模式
  • inventory
  • ansible.cfg

  • namespace

    • commuity
# ansible-galaxy role install acandid.mysql -p ./
# ansible-galaxy collection install alikins.collection_inspect

性能调优

打开监控

ansible.cfg

callback_whitelist = timer, mail, profile_tasks, profile_roles, cgroup_perf_recap
# timer 会去监测每一个task
# cgroup_perf_recap  # 监测任性能的消耗
[callback_cgroup_perf_recap]
control_group = ansible_profile  ## 执行的时候会自动创建cgroup
# yum -y install libcgroup-tools
# cgcreate -a devops:devops -t devops:devops -g cpuacct,memory,pids:ansible_profile # 这里要对应配置文件中的名字
``


对比两个包的安装方式的性能

```yaml
- hosts: webservers
  tasks:
  - name: install package no use loop
    yum:
      name:
      - httpd
      - mod_wsgi
      - mod_ssl
      - targetcli
      state: latest
    when: ansible_fqdn == "servera"
  - name: install package use loop
    yum:
      name: "{{ item }}"
      state: latest
    loop:
    - httpd
    - mod_wsgi
    - mod_ssl
    - targetcli
    when: ansible_fqdn == "severb"

监控执行过程所消耗的时间,可以明显的对比出,使用loop循环占消耗的时间更高

- hosts: webservers
  tasks:
  - name: copy files to target
    copy:
      src: test_files
      dest: /mnt
    when: ansible_fqdn == "servera"
  - name: copy files to target
    synchronize:
      src: test_files
      dest: /mnt
    when: ansible_fqdn == "serverb"

监控以上执行过程所消耗的时间,可以明显对比出,拷贝一个目录下的所有文件,copy 消耗的时间要高出许多

命令行调试

  • 交互式执行每一步
# ansible-playbook nginx.yaml --step
  • 从指定哪一步开始执行
# ansible-playbook playbook.yaml --start-at-tasks="install packages"
  • 打印详细信息
# ansible-playbook xxx.yaml -v
# ansible-playbook xxx.yaml -vv
# ansible-playbook xxx.yaml -vvv
  • 检查相比上次发现了哪些改变
# ansible-playbook foo.yml --check --diff --limit foo.example.com

异步任务

  • 异步单个任务,即把不强依赖的较慢的任务,放到后台去执行
- name: copy files to target
  copy:
    src: test_files
    dest:/mnt
    async: 30 # 30秒超时
    poll:2 # 每两秒轮询一次

配置调优

  • ssh_connection

  • 优化ssh连接

    • 默认情况下,ansible其于ssh连按被控端,当ansible在运行playbook的时候,会建立很多的ssh连接来执行相应的task,而每个task都会创建一个新的ssh连接,
    • ssh连接的建立无疑问需要额外tcp建立连接的开销。
    • openssh支持一个优化,叫做ssh nultiplexing, 也被称作ControiPersist。 当启用了该特性,则多个连按到相同主机的ssh会五格会共享相同的tcp连接。这样就只有第一次连接的时候两要进行TCP三次握手。
    • 当启用Multiplexing后:
      • 第一次ssh连按到主机的时候,openssh会创建一个主连接
      • 紧接着openssh会创建一个控制套接字,通过去连接与远程去机关联
      • 当有新的ssh尝试连接到主机时,openssh格使用控制套接字与远程主机通信,井不会创建新的tcp连接
        开启该配置项:
        # cat ansible.cfg
        [ssh_connection]
        ssh_args = -C -o  ControlMaster=auto -o ControlPersist=60s
        
  • 配罝项说明:
    • ControlMaster:启用ssh multiplexing,允许多个同时与远程主机连接的ssh会话使用单一网络连接。auto用于告诉ssh在主连按和控制套接字不存在的情况下,自动创建它们,第一个ssh会话建立,则与同一主机连接的其他会话重复利用此连接,从而绕过较慢的初始过程,ssh在最后一个会话关闭后,立即销毁共享的连接
    • ControiPersist:使连接在后台保持打开,而不是在上一次会话后销毁连接。其配置空闲的连接保持打开的时间长度,每个新会话将重置此空闲计时器。
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=60s
  • pipelining
    • 在说明pipelinling之前,需要先了解一下ansible是如何执行一个tasks的
    • 基于playbook中的task生成一个python脚本
    • 将生成的python脚本复制到被控主机上
    • 在被控主机上运行这个脚本
      上面三个步骤中,后面两个步骤会产生两个ssh会话,而在pipline模式下,ansible执行python脚本时并不会复制它,而是通过管道传递给ssh会话,这样一来就会让原本需要的两个ssh会话减少成为一个,以降低开销,节省时间
      [defaults]
      pipelining = True
      
# vim /etc/sudoers.d/develop
develop ALL=(ALL) NOPASSWD:ALL
Defaults:develop !requiretty
  • git
    • git基本使用
    • git的版本管理及回退
    • git的分支
    • git的tag
    • git的远程仓库
  • tower—> awx
    • 安装—> 使用docker安装
    • 在awx的最新版本当中,已经推荐直接通过awx-operator将awx安装进kubernetes
  • awx的使用
    • 用户和组
    • 凭据管理
    • 仓库管理
    • inventory管理
    • template管理

git 管理

版本控制系统 SCM
SVN 版本控制系统依赖中央存储仓库,而git本地和中央仓库上都会有版本存放

安装使用

# yum -y install git
# mkdir studygit
# git init
# git status
# git add mysql # 将目录提交到暂存区
# git commit -m "first commit"
# 当文件再次发生变更时可以使用# git status # 看到modified 哪些文件发生了变更

WSL2 vagrant ohmyzsh vscode packer工具 类似linux git 环境的工具

  • 要让代码被git接管
    • 初始化代码仓库
    • 编写代码文件
    • 提交到代码仓库
# git status
# git checkout -- mysql/tasks/main.yml # 回退单个文件
  • 未被git接管的文件
  • 暂存区
  • 仓库

版本回退

  • 回退
    • 从workdir目录回退,此时代码未提交到暂存区 git checkout -- file 或者 git checkout
    • 从暂从区回退,此时代码被提交到了暂存区,但还未被提交至仓库git reset HEAD file 再执行 git checkout -- file 进行版本回退
    • 从仓库回退,此时代码已经被提交至仓库git log 查看要回退版本的commit号 git reset --hard commitid
      • 当回退错了,再次回退时
        git reflog
        git reset --hard commitid
        
      • 查看所有的历史版本,可以使用
        git log --onelie --graph
        
HEAD^ 代表的是上一个版本
HEAD^^ 代表的是前两个版本
HEAD^^^ 代表的是前三个版本依此类推

分支管理

  • 但看当前所有的分支
# git branch
# git branch -a
  • 创建分支
    # git checkout -b br1 # 创建分支
    # git branch br1 # 创建并合并分支
    
  • 删除分支
    # git branch -d br1 # 删除分支
    
  • 切换分支
    # git checkout master
    # git checkout br1
    
  • 合并分支
    # 在master上合并br1分支
    # git checkout master
    # git merage br1
    

版本标签(tag)

  • 创建 tag
# git tag v1.0
# git tag v0.9 f52633 # 打标签到指定的commit上面
# git commit -am "add tag v1"
  • 列出tag
# git tag
  • 查看tag详情

    # git show v1.0
    
  • 删除tag

# git tag -d v1.0

远端操作

  • 获取远端代码,从无到有的获取
# git clone
  • 推送代码至远端
# git push origin <branch>
  • 从远端列新代码到本地
# git pull
  • 查看关联的远端库
# git remote
  • 从远端获取代码,跟git pull 不同的是,git fetch会更新所有分支的代码到本地,而git pull只更新当前分支的代码
# git fetch

awx

组织—> 团队 —> 成员

  • 组织 —> 团队 —> 成员

  • 创建

    • 删除
    • 信息变更
    • 管理团队
      • 添加团队
      • 删除团队
      • 变更团队的信息
    • 管理用户
      • 添加用户
      • 删除用户
      • 变更用户的信息
    • 角色变更

      • 角色一般可以分配给团队或者用户
      • 一个用户在组织当中可以有多个角色
        • mormal user —普通用户
        • system auditor - 系统审核,只读,可以读取这个组织当中的所有资源
          • 用户
          • 团队
          • 主机清单
          • 主机
          • 主机组
          • 项目
          • 任务模板
        • system admin
    • 角色

      • 用户组可以操作哪些权限
    • 用户属于组织

      • 用户在组织当中有三种角色
        • system auditor
        • system administrator
        • normal user
    • 用户可以属于一个团队,也可以不属于任何团队
    • 用户可以管理团队,无论这个用户是否属于这个团队

    • 用户管理团,有三种角色

      • admin 这个用户可以不属于这个团队,但是对这个团队及其所有资源拥有所有权限
      • read 这个用户可以不属于这个团队,但是这个团队及其所有有相关资源有只读权限
      • member 用户属于这个团队,属于这个团队的用户,可以继承这个团队当中所有的权限

评论