ansible是一个常用的运维管理工具,使用它可以避免很多重复性工作,节省大量时间。 这里是网易云课堂·IT技术快速入门学院演示视频中使用的文档,8元小课系列,可以在系列教程中找到该系列所有文章。 QQ交流群(ansible实践互助):955105412。
之前尝试制作了两期《HyperLedger Fabric》的课程,得到不少了同学的捧场。同时发现一些技术工具和学习方法,对我们这些工作了好多年的老鸟来说,早已习以为常,但是对于部分还在学校的或刚毕业的同学来说,非常陌生。
这些内容本质上又非常简单,只有“知道”与“不知道”这样一点点区别,不值得长篇大论,但结果却是没有人来点破,或者被包裹进昂贵的课程中,不可思议的价格,给在校生带来经济上的压力。
我们认为,把这些内容用“小课”的方式呈现出来,是很有价值的。一门小课,就像是公司内部的一次小小的分享会,可以把一个人的劳动所得复制给更多人,从而为听众节省大量的时间。
ansible就是把你手动ssh登录到多个目标机器上进行的一系列操作的过程自动化。
你只需要确保执行ansible命令的本地机器能够通过用户名和密码登录到目标机器上,并且在本地机器上的ansible文件中写好要在目标机器上执行的操作。
目标机器只需要支持ssh登录和python命令(一般的linux操作系统都有,ansible会将python写的任务脚本上传到目标机器上执行)。
ansible的文档首页 https://docs.ansible.com/ 对文档进行了分类,都是接到了文档内容页面。
安装文档中介绍了ansible的安装方法,作为一个很基础的工具,基本上每个操作系统,都有对应的安装方法。
官方的Getting Started介绍的太简单了,对初学者来说,看完还是一头雾水。
git clone https://github.com/lijiaocn/ansible-example.git
ansible有两个命令,一个是ansible
,一个是ansible-playbook
,前者需要每次输入要执行的命令,后者可以读取playbook
文件,一次性完成playbook文件中指定一系列操作。
playbook文件是重点,文档中有很大篇幅是介绍playbook的:playbook。
需要准备一个文件,在文件中写下目标机器的地址,这个文件默认是/etc/ansible/hosts
,但是为了管理方便,最好为每个环境单独创建一个hosts文件。
比方说创建一个名为inventories
的目录,在这个目录下,为生产环境的机器创建一个production
目录,production/hosts
中记录的是生产环境中的机器的地址,demo/hosts
中记录的是演示环境中机器的地址,这样将不同环境中的机器明确地分开了,可以减少运维事故。
$ tree inventories/
inventories/
├── production
│ └── hosts
└── demo
└── hosts
hosts
文件中可以直接是目标机器的地址,可以是IP,也可以是域名,每个地址占用一行,例如:
192.168.33.11
www.baidu.com
如果目标集群中的机器的角色相同,承担的是同样任务,这种方式一般也足够了。如果目标集群中的机器分别承担不同任务,最好将它们按照各自的角色分组,例如:
[master]
192.168.33.11
[nodes]
192.168.33.11
192.168.33.12
192.168.33.13
同一个地址,可以同时位于多个组中。
可以对分组再次分组,例如《Kubernetes1.12从零开始》中使用的hosts文件是这样的:
[etcd]
192.168.33.11
192.168.33.12
192.168.33.13
[master]
192.168.33.11
192.168.33.12
192.168.33.13
[node]
192.168.33.11
192.168.33.12
192.168.33.13
[kube-router]
192.168.33.11
192.168.33.12
192.168.33.13
############# group's group ##############
[etcd_client:children]
etcd
master
[etcd_server:children]
etcd
[etcd_peer:children]
etcd
[apiserver:children]
master
[controller:children]
master
[scheduler:children]
master
[kubelet_client:children]
master
[kubelet:children]
node
名称里有:children
的分组,是分组的分组,它的成员是前面定义的分组。
还可以在这里为每个机器
设置变量,譬如《HyperLedger Fabric手把手入门》中使用的hosts文件:
[orderer]
orderer0.member1.example.com MSPID=orderers.member1.example.com ORG_DOMAIN=member1.example.com ansible_host=192.168.33.11
[peer]
peer0.member1.example.com MSPID=peers.member1.example.com ORG_DOMAIN=member1.example.com ansible_host=192.168.33.11 STATE_DB=CouchDB COUCH_USER=admin COUCH_PASS=password
peer1.member1.example.com MSPID=peers.member1.example.com ORG_DOMAIN=member1.example.com ansible_host=192.168.33.12 STATE_DB=CouchDB COUCH_USER=admin COUCH_PASS=password
peer0.member2.example.com MSPID=peers.member2.example.com ORG_DOMAIN=member2.example.com ansible_host=192.168.33.13 STATE_DB=CouchDB COUCH_USER=admin COUCH_PASS=password
[machine]
192.168.33.11
192.168.33.12
192.168.33.13
你已经注意到了,这个hosts文件不太一样,地址后面多出了一些诸如MSPID=XXX
样式的内容,它们是为对应机器设置的变量,这些变量在可以在后面要讲的playbook文件中引用。
分组和变量的使用方法在后面演示,现在你先记得有这么一回事就行。
另外关于分组还要多说一句,ansible有两个默认的分组:all
和ungrouped
:all分组包括所有分组的中的机器,ungrouped是所有只属于all分组,不属于其它分组的机器。
在定义你自己的分组的时候,要注意分组名称不要与它们冲突。
讲述这部分内容的官方文档是:Working with Inventory
Modules是ansible的“军火库”,几乎所有的操作功能都是用module实现的。
ansible用到最后,就是在使用module。 module的数量相当多,好在常用的就那么几个,这里演示一些常用的,其它的你可以通过每个module的文档学习。
ping模块是用来测试目标机器是否可达的,用法如下:
lijiaos-mbp:example lijiao$ ansible -i inventories/demo/hosts -u root -k all -m ping
SSH password:
192.168.33.12 | SUCCESS => {
"changed": false,
"ping": "pong"
}
192.168.33.11 | SUCCESS => {
"changed": false,
"ping": "pong"
}
-i
指定hosts文件,-u
指定目标机器上的用户名,-k
指定目标机器登录密码,all
是要操作的hosts文件中的分组,前面我们说过,all是默认存在的一个分组,包括所有机器,-m
指定要使用的模块ping
。
ping模块大概是最简单的一个模块,没有参数,再来看一个复杂一点的模块shell,它的功能是在目标机器上执行shell命令:
lijiaos-mbp:example lijiao$ ansible -i inventories/demo/hosts -u root -k all -m shell -a "hostname"
SSH password:
192.168.33.11 | SUCCESS | rc=0 >>
192.168.33.11
192.168.33.12 | SUCCESS | rc=0 >>
192.168.33.12
-a
是指定传递给模块的参数。
用ansible
命令对目标机器操作时,都是在命令行指定要做的操作,一般都是一些比较简单操作,譬如查看下状态、上传下载文件等。
很多强大的功能要通过ansible-playbook
才能发挥出来。
playbooks是yml格式的文件,描述了要在哪些机器上执行哪些操作。
创建一个playbook文件,playbook-single.yml,如下:
- hosts: machines
remote_user: root
tasks:
- name: create a tmp file
shell: |
cd /tmp/
touch abcd123
这个playbook文件的意思是,在所有的machines
上,用root的身份执行,并通过shell模块创建文件/tmp/abcd123,用法如下:
lijiaos-mbp:example lijiao$ ansible-playbook -i inventories/demo/hosts -k playbook-single.yml
SSH password:
PLAY [machines] ******************************************************************************
TASK [Gathering Facts] ***********************************************************************
ok: [192.168.33.12]
ok: [192.168.33.11]
TASK [create a tmp file] *********************************************************************
changed: [192.168.33.12]
changed: [192.168.33.11]
PLAY RECAP ***********************************************************************************
192.168.33.11 : ok=2 changed=1 unreachable=0 failed=0
192.168.33.12 : ok=2 changed=1 unreachable=0 failed=0
注意这里使用ansible-playbook
命令,-i
和-k
参数含义与前面ansible命令的参数相同,这里没有使用-u
指定账号,是因为在playbook-single.yml中已经设置了使用root:
remote_user: root
操作在playbook文件的tasks
中设置,tasks是一个数组,可以添加多个任务:
tasks:
- name: create a tmp file # 自定义的操作名称
shell: | # 使用shell模块,后面的|是yaml语法,表示后面空行之前的内容都是shell模块的参数
cd /tmp/
touch abcd123
用ansible命令来看一下文件是否创建:
lijiaos-mbp:example lijiao$ ansible -i inventories/demo/hosts -u root -k all -m shell -a "ls /tmp/abc*"
SSH password:
192.168.33.11 | SUCCESS | rc=0 >>
/tmp/abcd123
192.168.33.12 | SUCCESS | rc=0 >>
/tmp/abcd123
前面给出的ansible-playbook的用法,是最初级的用法,比较完整的用法是将操作封装到 role 中。
先解释一下什么是role,为什么要有role。
在ansible看来role就是对playbook中的操作做了一次分组,把一些操作放在这个role中,另一些操作放在那个role中。 在我们看来,role是目标机器的角色之一,我们把不同的角色的操作划分到不同的目录中,一是管理方便,二是可以复用。
role要在roles
目录中定义,在roles目录中创建与role同名的目录,每个role目录中包含四个目录:
lijiaos-mbp:example lijiao$ tree roles/
roles/
└── prepare
├── files
│ └── demo.file
├── handlers
│ └── main.yml
│ └── centos.yml
├── tasks
│ └── main.yml
└── templates
└── demo.template.j2
tasks/main.yml
是这个role的操作入口,handlers/main.yml
是一些可以被触发
的操作,files
中存放可以直接被上传到目标机器的文件,templates
中存放的是可以直接上传到目标机器的模版文件,这两个的区别后面说明。
tasks/main.yml
是必须要有的,其它目录中如果没有文件,可以不创建。
上面的目录中创建了一个名为prepare
的role,我们计划将机器的初始化设置操作全部在收集在这个 role 中。prepare/task/main.yml 用到了 authorized_key、hostname、shell 和 import_tasks 四个模块。当目标机器的操作系统是 centos 的时候,import_tasks
引入了centos.yml
文件。
- name: Set authorized key
tags: ssh
authorized_key:
user: root
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
- name: Set hostname
hostname:
name: "{{ inventory_hostname }}"
- name: Set bash prompt
shell: |
echo 'export PS1="[\u@\H \W]\\$ "'>> ~/.bashrc
- name: install dependent packages
import_tasks: centos.yml
when: ansible_distribution == "CentOS"
centos.yml
文件内容如下:
- name: set time zone
file:
src: '{{ item.src }}'
dest: '{{ item.dest }}'
state: link
with_items:
- { src: "/usr/share/zoneinfo/Asia/Shanghai", dest: "/etc/localtime" }
- name: set local
shell: localedef -i zh_CN -f UTF-8 zh_CN.UTF-8
- name: install epel
yum:
name: "{{ item }}"
state: present
with_items:
- epel-release
- name: install pkgs
yum:
name: "{{ item }}"
state: present
with_items:
- yum-utils
- ipset
- iptables
- iproute
- ipvsadm
- supervisor
- ntp
- name: start basic service
systemd:
enabled: yes
name: "{{ item }}"
state: started
with_items:
- ntpd
- supervisord
这些操作的含义在后面章节逐一说明,先给出用法:
ansible-playbook -i inventories/demo/hosts -u root -k prepare.yml
这里介绍role/prepare/task/main.yml文件中的操作。
前面的操作过程中使用了-k
参数,每次都需要输入密码,一是比较烦,二是如果机器的密码不同,那就失灵了(后面会演示一下如果目标机器密码不同该怎样操作)。
最好把本地的证书传到目标机器上,实现免密码登录,prepare的task/main.yml中,有这样一段:
- name: Set authorized key
tags: ssh
authorized_key:
user: root
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
它就是用authorized_key模块将本地的证书~/.ssh/id_rsa.pub
上传到目标机器上,实现免密码登录。
注意你需要确保你本地有id_rsa.pub文件,否则用ssh-keygen
命令创建一个:
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/lijiao/.ssh/id_rsa):
- name: Set hostname
hostname:
name: "{{ inventory_hostname }}"
- name: Set bash prompt
shell: |
echo 'export PS1="[\u@\H \W]\\$ "'>> ~/.bashrc
- name: set time zone
file:
src: '{{ item.src }}'
dest: '{{ item.dest }}'
state: link
with_items:
- { src: "/usr/share/zoneinfo/Asia/Shanghai", dest: "/etc/localtime" }
- name: install epel
yum:
name: "{{ item }}"
state: present
with_items:
- epel-release
- name: install pkgs
yum:
name: "{{ item }}"
state: present
with_items:
- yum-utils
- ipset
- iptables
- iproute
- ipvsadm
- supervisor
- ntp
- name: start basic service
systemd:
enabled: yes
name: "{{ item }}"
state: started
with_items:
- ntpd
- supervisord
这里通过在目标机器上部署、设置nginx,讲解角色下面的files、templates和handlers目录的作用。
nginx
role的文件如下:
lijiaos-mbp:example lijiao$ tree roles/nginx/
roles/nginx/
├── files
│ ├── start.sh
│ └── stop.sh
├── handlers
│ └── main.yml
├── tasks
│ └── main.yml
└── templates
└── hello.com.conf.j2
nginx/tasks/main.yml
内容是:
- name: install pkgs
yum:
name: "{{ item }}"
state: present
with_items:
- nginx
- name: nginx is running
systemd:
name: nginx
state: started
daemon_reload: yes
- name: create directory
file:
path: "{{ item }}"
state: directory
with_items:
- "{{ nginx_config_path }}"
- "{{ nginx_script_path }}"
- name: upload template config
notify: reload nginx
template:
src: "{{ item }}.j2"
dest: "{{ nginx_config_path }}/{{ item }}"
with_items:
- hello.com.conf
- name: upload files
copy:
src: "{{ item }}"
dest: "{{ nginx_script_path }}/{{ item }}"
mode: u=rwx
with_items:
- start.sh
- stop.sh
这里有两个变量:nginx_config_path
和nginx_script_path
,用两个大括号包裹引用。
它们是在inventories/demo/group_vars/all
中定义的:
nginx_config_path: /etc/nginx/conf.d
nginx_script_path: /root/nginx
变量除了可以在group_vars
和host_vars
目录中定义,还可以在hosts文件中定义:
[machines]
192.168.33.11 port=8001
192.168.33.12 port=8002
以及在playbook文件中定义,回想一下我们用到的第一个playbook,里面有vars
:
$ cat playbook-single.yml
- hosts: machines
vars:
http_port: 80
max_clients: 200
remote_user: root
tasks:
- name: create a tmp file
shell: |
cd /tmp/
touch abcd123
role/nginx/templates/hello.com.conf.j2
是一个模版文件:
,模版文件中可以使用变量:
server {
listen {{ port }};
location / {
proxy_pass https://www.baidu.com ;
}
}
模版文件中可以使用变量,这里使用的变量port
是在hosts文件中定义的,可以为每个机器定义不同的端口:
[machines]
192.168.33.11 port=8001
192.168.33.12 port=8002
它们被用template模块上传,上传时会将模版文件中的变量换成变量的值,如下:
- name: upload template config
notify: reload nginx
template:
src: "{{ item }}.j2"
dest: "{{ nginx_config_path }}/{{ item }}"
with_items:
- hello.com.conf
role/nginx/files
中的文件,用COPY命令上传,文件不会被做任何改动,这一点和templates显著不同:
- name: upload files
copy:
src: "{{ item }}"
dest: "{{ nginx_script_path }}/{{ item }}"
mode: u=rwx
with_items:
- start.sh
- stop.sh
在tasks中,用notify
命令触发handler的执行:
- name: upload template config
notify: reload nginx
template:
src: "{{ item }}.j2"
dest: "{{ nginx_config_path }}/{{ item }}"
with_items:
- hello.com.conf
只有被触发的handler才会运行,并且是在所有的task之后运行。 如果有多个handler被触发,按照它们在handlers/main.yml中出现的顺序执行。
什么时候要用handler? 比如配置文件被更新以后,需要重启或者重新加载的服务,这时候就可以在更新配置文件的task中,使用notify触发handler。