原创作品:首发u3v3, 转载请保留

地址:https://www.u3v3.com/ar/1267

作者ID:noomrevils

首发日期:2017.2.28

引言

上篇文章Ansible 学习(初探) 介绍了Ansible的特性和概念, 并描述了在两台vps构建的一个私有网络上实践了环境搭建,使用一些基本模块。

实际的工作的场景不可能只靠简单条命令来解决复杂的问题,这里就需要引出本篇的主角playbook

playbook 是一系列ansible基础命令的组合和拓展,采用yaml语法,不同于ad-hoc命令一次只能执行一个模块, playbook可以将变量配置和任务执行流程灵活的组织起来,从而能够完成一些复杂的部署任务。

playbook的优势

  • 采用yaml描述任务和配置,独立于编程语言,易于学习和维护。
  • 模块化的结构鼓励片段复用,官方提供了Ansible Galaxy,社区的力量促进学习,分享和重用高质量的playbook。
  • 支持jinja2模板语法,指令状态捕获和传递,条件和循环分支,顺序/异步执行方式,这些特征使playbook有着很强的表达能力

PlayBook的组成

下面列出的nginx.yml文件是一个playbook中的一个play,它所完成的流程是在被控节点上安装并启动nginx。一个playbook可以包含多个这样针对不同节点清单的play。接下来简单介绍下组成这个play的元素

- hosts: web
  vars:
    - worker_processes: 4

  remote_user: deploy
  become: true
  become_method: sudo

  tasks:
    - name: install nginx
      yum: name=nginx state=installed update_cache=true

    - name: update nginx config
      template: 
        src: /home/deploy/ansible-playbook/nginx.conf
        dest: /etc/nginx/nginx.conf
      notify:
        - reload nginx

    - name: ensure nginx is running
      service: name=nginx state=started 

  handlers:
    - name: reload nginx
      service: name=nginx state=reloaded

主机和用户

- hosts: web
  vars:
    - worker_processes: 4

  remote_user: deploy
  become: true
  become_method: sudo
  ...

这里定义了执行目标主机清单,操作用户和对应权限,以及使用到的变量信息,其实这些内容就是命令行参数,配置文件的另一种组合表达形式,yaml的语法带来了可读性和易维护的优势

任务列表

...

  tasks:
    - name: install nginx
      yum: name=nginx state=installed update_cache=true

    - name: update nginx config
      template: 
        src: /home/deploy/ansible-playbook/nginx.conf
        dest: /etc/nginx/nginx.conf
      notify:
        - reload nginx

    - name: ensure nginx is running
      service: name=nginx state=reloaded 

  ...

任务列表是playbook中的骨架,它定义了一系列顺序执行的步骤, 将独立的模块任务逻辑的联系在一起。
每一个具体的任务,都需要一个与之对应的名字,方便执行时打印信息和引用。

这里第一个任务借助yum模块,安装nginx,这里参数的调用采用的是key=value的方式,也可以像第二个任务那样,采用类似字典的语法来指定。

第二个任务是将本地模板文件,通过jinja2模板引擎,展开变量worker_processes渲染后,传输到被控节点nginx目录下

...

user nginx;
worker_processes {{ worker_processes }};
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

...

Handlers

handler和任务(task) 十分类似, 区别在于,handler区别是被动由一个任务在自身状态发生变换的时候触发,可以通过,notify,和task的名字来指定调用handler的场景。

- name: update nginx config
      template: 
        src: /home/deploy/ansible-playbook/nginx.conf
        dest: /etc/nginx/nginx.conf
      notify:
        - reload nginx
   ...

  handlers:
    - name: reload nginx
      service: name=nginx state=reloaded

    ...

这里在更新过nginx的配置文件后,我们通过ansible告诉被控节点reload nginx服务

执行PlayBook

$ ansible-playbook nginx.yml [参数]

常用参数:

--syntax-check :检查playbook的语法

--step:以单任务分步骤运行,方便做每一步的确认工作

--check:检测模式,playbook中定义的所有任务将在每台远程主机上进行检测,但并不直正执行

--verbose(-v):显示详细输出,也可以使用-vvvv显示精确到每分钟的输出

--forks:最大并行执行playbook的服务器数量

输出示例:

$ ansible-playbook nginx.yml

PLAY [web] *********************************************************************

TASK [setup] *******************************************************************
ok: [10.99.0.11]

TASK [install nginx] ***********************************************************
ok: [10.99.0.11]

TASK [update nginx config] *****************************************************
changed: [10.99.0.11]

TASK [ensure nginx is running] *************************************************
ok: [10.99.0.11]

RUNNING HANDLER [reload nginx] ************************************************
changed: [10.99.0.11]

PLAY RECAP *********************************************************************
10.99.0.11                 : ok=5    changed=2    unreachable=0    failed=0

按Role来构建PlayBook

上面的playbook存在于一个单一文件中nginx.yml, 当流程和步骤变的更加复杂的时候,单个文件就显得混乱,不好维护了。Role(角色)的概念本质是将playbook按照变量,任务,处理程序,文件等基本元素,以不同级别的目录层次进行拆分,从而达到将相关任务和数据封装到一个连贯独立的结构中。

通常一个Role下会有如下几类文件夹

  • tasks 任务列表路径
  • templates 模版存放路径
  • handlers handlers存放路径
  • vars roles内变量存放路径
  • files 存放文件和脚本,copy模块文件搜索路径
  • meta 依赖文件路径
  • defaults 默认寻找路径

接下来结合一个具体的列子,来说明下一个基于Role的playbook的构建过程,通过这个playbook将在被控节点上部署一个 Nginx + uWSGI + Bottle 的 demo 服务。

bottle-nginx
├── hosts # 目标主机清单
├── roles
│   └── webserver
│       ├── handlers
│       │   └── main.yml
│       ├── tasks
│       │   └── main.yml
│       ├── templates
│       │   ├── bottle-nginx.conf
│       │   └── bottle-uwsgi.ini
│       └── vars
│           └── main.yml
└── site.yml # 主入口

hosts文件定义了目标主机清单,优先级高于/etc/ansible/hosts, 但是低于命令行指定

[web]
10.99.0.11

site.yml是整个playbook的主入口,指名需要执行的roles, 和一些登录参数。

- hosts: web
  remote_user: deploy
  become: true
  become_method: sudo

  roles:
    - webserver

在目录下一个定义webserver这个角色,vars目录下 main.yml放置了部署需要一些常量信息,
项目根目录,仓库地址等。

project_root: /var/www/bottle-nginx
project_name: bottle-nginx
project_repo: git@git.coding.net:usrname/ansible-bottle-demo.git
branch: master

再来看../task/main.yml定义的主流程任务

- name: install nginx
  yum: name=nginx state=installed update_cache=true

- name: ensure nginx is running
  service: name=nginx state=started

- name: install dependent packages 
  yum: name={{ item }} state=installed
  with_items:
    - git
    - uwsgi-plugin-python
    - python-pip
    - python-wheel

- name: pull source code
  git:
    dest: "{{ project_root }}"
    version: "{{ branch }}"
    accept_hostkey: yes
    key_file: /home/deploy/.ssh/id_rsa

- name: pip install requirements
  pip: requirements={{ project_root }}/requirements.txt

- name: update nginx config
  template:
    src: bottle-nginx.conf
    dest: /etc/nginx/conf.d/bottle-nginx.conf
  notify:
    - reload nginx

- name: update uwsgi config
  template:
    src: bottle-uwsgi.ini
    dest: /etc/uwsgi.d/bottle-uwsgi.ini
  notify:
    - reload uwsgi

在确保nginx正常运行后,我们安装了项目的依赖包, 这里因为使用的是yum,安装uWSGI需要指定含有python插件版本(uwsgi-plugin-python)的,这区别于直接使用pip安装,因为后面我们使用service模块来维护uWSGI。这里有个小坑,默认/etc/uwsgi.ini中需要改动参数, 才能使服务正常启动

gid = nginx
...
#emperor-tyrant = true

接下来使用ansible中git module来拉取源代码, accept_hostkeys是为仓库对应的url添加hostkey,key_file是指定被控节点上私钥作为拉取代码的key,默认采用管理节点上的私钥

然后使用pip模块安装项目python依赖, 项目本身非常简单,Bottle是python的一个微框架,单文件,简洁明了,整个项目只有一个单文件,app.py

import bottle
from bottle import route, run

@route('/')
def index():
    return 'Hello world!'

if __name__ == '__main__':
    run(host='0.0.0.0', port=8000)
else:
    app = application = bottle.default_app()

最后我们使用变量拓展nginx, uWSGI的配置文件,通知handler刷新服务。

#### bottle-uwsgi.ini

[uwsgi]
socket = 127.0.0.1:3030
chdir = {{ project_root }}/
master = true
processes = 1
threads = 1
module = app:app (filename: bottle_instance)
plugins = python (使用对应plugin)

#### bottle-nginx.conf
server{
    listen       80;
    server_name servername(ip);
    location / {
        include uwsgi_params;
        uwsgi_pass 127.0.0.1:3030;
        uwsgi_read_timeout 300;
    }
}

贴出执行后的完整输出

$ ansible-playbook site.yml

PLAY [web] *********************************************************************

TASK [setup] *******************************************************************
ok: [10.99.0.11]

TASK [webserver : install nginx] ***********************************************
ok: [10.99.0.11]

TASK [webserver : ensure nginx is running] *************************************
ok: [10.99.0.11]

TASK [webserver : install dependent packages] **********************************
ok: [10.99.0.11] => (item=[u'git', u'uwsgi', u'python-pip', u'python-wheel'])

TASK [webserver : pull source code] ********************************************
ok: [10.99.0.11]

TASK [webserver : pip install requirements] ************************************
ok: [10.99.0.11]

TASK [webserver : update nginx config] *****************************************
ok: [10.99.0.11]

TASK [webserver : update uwsgi config] *****************************************
changed: [10.99.0.11]

RUNNING HANDLER [webserver : reload uwsgi] *************************************
changed: [10.99.0.11]

PLAY RECAP *********************************************************************
10.99.0.11                 : ok=9    changed=2    unreachable=0    failed=0
right