配置管理工具 SaltStack 和 Ansible
系统管理员经常陷入一系列重复的任务中,如新服务器初始化、升级软件包、远程执行、配置变更、维护定时任务、应用部署、重启服务、上线、回滚等。在配置工具没有出现之前,我们会维护大量用于不同目的的脚本,甚至组合出不同的流程。在服务器规模比较小时,配置管理的重要性并不突出,但是当要维护成千上万的服务器时,就会发现如下几点问题:
1.无法保证快速上线。如果没有自动化批量部署方案,对全部服务器的部署过程耗时很长,工作量也是巨大的。
2.跨平台支持有限。往往不同的平台重新实现一遍操作代码。
3.可维护性差。随着需要维护的系统架构越来越复杂,如果没有使用良好的模块化设计,会造成任务程序越来越复杂,而且常常由于没有社区的贡献,架构灵活度也很差,这不可避免地让维护者选择重新造轮子。
配置管理工具中有两个 Python 的实现:Saltstack 和 Ansible,配置管理的特点就是通过配置而不是脚本来管理远程服务器,无论是单台服务器还是上万台服务器,通过配置来管理都很方便。
SaltStack
SaltStack 和 Puppet 一样采用 C/S 架构,但相对 Puppet 而言,它更轻量,配置语法使用 YMAL,其发布订阅系统是基于 ZeroMQ 的,以广播的形式通知客户端(官方称为 Minion)。它封装了常用的命令执行模块,包括管理软件包、管理用户、传输文件、数据库管理等等。在功能上,SaltStack 大概相当于 Fabric+Puppet。
我们先在 Minion/Server 端安装对应的软件包:
> curl-L https://bootstrap.saltstack.com-o install_salt.sh # Server 端使用如下命令: > sudo sh install_salt.sh-P-M # Minion 端使用如下命令: > sudo sh install_salt.sh-P
首先修改 Master 端的配置,把/etc/salt/master 里面的 hash_type 改成“sha256”,重启 Master:
> sudo/etc/init.d/salt-master restart
查看 Master 的识别指纹(Fingerprint):
> sudo salt-key-F master|grep master.pub
master.pub:67:34:6c:aa:43:ff:36:ca:41:b5:fa:ee:37:11:cc:44:71:49:ce:3f:8b:27:1d:06:9e:
a5:9d:aa:b6:bf:45:11
然后对每个 Minion 端修改配置,可以把/etc/salt/minion 里面的如下代码行改掉:
master:192.168.0.175 # Master 的 IP,注意冒号之后要有一个空格
master_finger:'67:34:6c:aa:43:ff:36:ca:41:b5:fa:ee:37:11:cc:44:71:49:ce:3f:8b:27:1d
:06:9e:a5:9d:aa:b6:bf:45:11'
id:130 #设定客户端的 Minion id 标识,Salt 默认使用 socket.getfqdn() 的结果作为标识。
不同的 Minion 端需要使用不同的 id
重启 Minion 端:
> sudo/etc/init.d/salt-minion restart
这个时候在 Master 上可以看到这个 Minion 端的 key:
> sudo salt-key-L Accepted Keys: Denied Keys: Unaccepted Keys: 130 Rejected Keys:
接受它:
> sudo salt-key-a 130 # 可以使用-A 接受全部
通过 ping 测试连通性:
> sudo salt '*' test.ping # 支持通配符,星号表示所有客户端 130: True
现在对 Minion 端进行一系列操作:
> sudo salt 130 cmd.run 'hostname' #获取主机名
> sudo salt 130 cmd.run"ifconfig|grep-1 eth1|grep-Eo 'inet (addr:)?([0-9]*\.)
{3}[0-9]*'|cut-d ':'-f 2"#获取 IP 地址
> sudo salt 130 disk.usage #使用内建的方法获得硬盘使用情况。全部的内建模块可以
在 https://docs.saltstack.com/en/latest/ref/modules/all/index.html 找到,不同的模块
可能还需要安装对应的依赖,否则不能使用
> sudo salt '*' pip.install markdown,psutil #给客户端统一通过 pip 安装 markdown 和 psutil
> sudo salt '*' pip.install pyOpenSSL upgrade=True #通过参数的方式实现 pip install-
U
> sudo salt 130 ps.kill_pid 111 signal=9 # ps 模块是 psutil 模块的缩写
> sudo salt 130 ps.top 5 10
如果执行过程中没有符合预期的结果,可以使用“sudo salt-minion-l debug”或者“sudo salt-master-l debug”的方式启动,就可以通过详细的 Debug 输出了解到问题出在什么地方了。
使用 Grains
上面我们是通过匹配 Minion id(也包含通配符、正则匹配等方式)选择目标服务器的,还可以通过 Salt 的 Grains 特性来选择。Grains 是每一个 Minion 端自身的静态属性,以 Python 字典的形式存放在 Minion 端:
> sudo salt 130 grains.items|grep-A 1 'os:' #静态属性非常多,我们只过滤操作系统
这一项。只选择 os 也可以使用 sudo salt '*' grains.item os
os:
Ubuntu # 130 是一个 Ubuntu 服务器
可以通过操作系统来选择目标服务器:
> sudo salt-G 'os:Ubuntu' test.ping
全部的 Grains 名字可以通过如下命令获取:
> sudo salt '*' grains.ls
除此之外,我们还可以选择使用复合匹配(Compound matchers):
> sudo salt-C 'E@13*and G@os:Ubuntu or webserv*' test.ping
其中 E 表示正则匹配,G 表示 Grains。可以用布尔操作符连接多个目标条件来获取目录服务器。
Salt 支持批量执行,在某些工作中非常有用,如重启负载均衡集群中的 Web 服务:
> sudo salt '*'-b 10 test.ping #同一时间只有 10 台机器运行此命令,当
有 Minion 返回执行结果时,再让下一个 Minion 执行,直到所有目标机器执行完成
> sudo salt-G 'os:Ubuntu'-b 10%test.ping #同一时间只有 10%的服务器运行
配置状态
和 Puppet 一样,在 Salt 中可以描述系统的目标状态。Salt 将其称为一个 State,Salt 的 State 模块文件用 YAML 写成,以.sls 结尾(SaLt State 的大写字母的缩写)。它们从功能上等同于 Puppet 模块的 manifest 文件,只是后者用 Puppet DSL 写成,以.pp 结尾。
/etc/salt/master 中的 file_roots 指定了定义 Salt 状态的文件的目录,我们把它修改成如下内容:
file_roots:
base:# base 是必须存在的,其他的子项都是可选存在的
-/srv/salt
dev:#开发环境的目录,我通常习惯先放在 dev 目录下,在测试环境试验问题后再合并到 prod 中
-/srv/salt/dev
prod:#生产环境的目录
-/srv/salt/prod
将本节全部源文件拷贝到/srv 下:
> sudo chown ubuntu:ubuntu/srv-R > cp chapter7/section3/*/srv-r
一般而言,对某个软件进行配置管理应该创建一个同名的目录,在目录下创建 init.sls 文件。如果配置很简单,也可以直接使用同名的.sls 文件。现在演示对 Redis 进行配置管理,添加/srv/salt/dev/redis/init.sls 作为入口,入口文件一般没有具体的逻辑,只是通过 include 包含一些状态文件:
> cat/srv/salt/dev/redis/init.sls include: -redis.server
当同时存在 redis.sls 和 redis/init.sls 时,将指向 redis.sls,忽略 redis/init.sls。
再看看目录下的其他文件。
1.common.sls:安装 Redis。
{%from"./redis/map.jinja"import redis_settings with context%}# Jinja2 语法,引用其他 Jinja2 文件
install-redis:
pkg.installed:#pkg 是自带的状态模块。全部状态模块可以在 https://docs.
saltstack.com/en/latest/ref/states/all/找到。installed 是确保指定的包被安装。
-name:{{redis_settings.pkg_name}}
{%if redis_settings.version is defined%}
-version:{{redis_settings.version}}
{%endif%}
2.server.sls:配置 Redis。
include:
-redis.common
{%from"redis/map.jinja"import redis_settings with context%}
{%set cfg_version=redis_settings.cfg_version-%}
{%set cfg_name=redis_settings.cfg_name-%}
{%set pkg_name=redis_settings.pkg_name-%}
redis_config:
file.managed:# file 也是一个自带的状态模块,managed 表示可以从 Master 端下载文件,这个文件可能需要先使用对应的模板渲染。
-name:{{cfg_name}}
-template:jinja
-source:salt://redis/files/redis-{{cfg_version}}.conf.jinja
-require:
-pkg:{{pkg_name}}
redis_service:
service.{{redis_settings.svc_state}}:#默认是 service.running,也就是验证服务在运行中
-name:{{redis_settings.svc_name}}
-enable:{{redis_settings.svc_onboot}}
-watch:#表示里面任何一个状态变化就触发
-file:{{cfg_name}}
-require:#在执行这一步之前需要满足的东西都列在下面;不满足就不执行
-pkg:{{pkg_name}}
3.files/redis-3.0.conf.jinja:Redis 3.0 配置的模板,files 目录下还包含 redis-2.8.conf.jinja,对应 Redis 2.8 的配置。
{%from"redis/map.jinja"import redis_settings with context%}
daemonize{{redis_settings.daemonize}}
pidfile{{redis_settings.pidfile}}
port{{redis_settings.port}}
...
4.map.jinja:包含 Redis 的公用默认设置以及不同操作系统下的配置的映射。
在部署的时候 Salt 会先通过 Jinja2 的模板替换生成 Redis 配置文件,再复制文件到目标服务器。
使用 Pillar
Pillar 用来存放键值对变量,变量存放在 Master 端,由 Master 编译好后下发给 Minion 端。它通常用来存放敏感数据(如 ssh key、加密证书等)和一些通用的、有平台差异性的变量。定义 Pillar 变量的文件目录是通过/etc/salt/master 中的 pillar_roots 指定的,我们把它修改成如下内容:
> grep-A 3"pillar_roots:"/etc/salt/master|grep-v"^#"
pillar_roots:
base:
-/srv/pillar
与状态文件相似,Pillar 也有 top.sls,使用相同的匹配方式将数据应用到 Minion 端上:
base:
'*':
-packages #所有 Minion 端都要加载
'13*':# 13 开头的 Minion 端需要加载 redis 这个配置
-redis
不同系统之间的包的名字不同,可以在 packages.sls 里面统一配置:
{% if grains['os']=='RedHat'%}
python-dev:python-devel
git:git
{% elif grains['os'] in ('Debian', 'Ubuntu')%}
python-dev:python-dev
git:git-core
{% endif%}
Redis 的基本设置存放在 redis.sls 中:
redis:
root_dir:/var/lib/redis
user:redis
port:6379
bind:127.0.0.1
snapshots:
-'900 1'
-'300 10'
-'60 10000'
lookup:
svc_state:running
cfg_name:/etc/redis.conf
pkg_name:redis-server
svc_name:redis-server
现在可以通过 Pillar 的键过滤目标服务器了:
> sudo salt-I 'redis:port:6379' test.ping
Salt 模板
现在 Salt 支持 Jinja2、Mako、Wempy 三种模板语言和 YAML、JSON 两种格式的搭配,本节选用的是 Jinja+YAML。如果使用非默认的组合,可以在开头添加一个 Shebang,比如“#!jinja|mako|yaml”表示使用 Jinja2+Mako+YAML 的组合。
现在看一下之前提到的模板文件 map.jinja:
> cat/srv/salt/dev/redis/map.jinja
{% set default_settings={
'redis':{
'bind':'127.0.0.1',
'database_count':16,
'root_dir':'/var/lib/redis',
'daemonize':'yes',
... #为节省篇幅,省略大部分默认的设置
'user':'redis'
}
}%}
{% set redis_settings=salt['pillar.get']('redis', default=default_settings.redis,
merge=True)%}
redis_settings 最后会从 Pillar 里获得对应的字段值做替换。
现在就可以安装 Redis 了:
> sudo salt '*' state.highstate
当 Master 运行 state.highstate,Minion 端会先下载 top.sls,如果发现有符合它的匹配,就下载相应的文件,编译并运行。运行完成后,Minion 端会返回结果的汇总。
SSH 模式
使用 SSH 模式不需要安装 Minion 端就可以实现远程部署,我们需要先安装 salt-ssh:
> sudo apt-get install salt-ssh-y
现在给 192.168.0.132 添加 SSH 信任:
> sudo ssh-copy-id-i/etc/salt/pki/master/ssh/salt-ssh.rsa.pub vagrant@192.168.0.132
定义文件:
> cat/etc/salt/roster
web1:
host:192.168.0.132
user:ubuntu
web2:
host:192.168.0.133
user:ubuntu
web3:
host:192.168.0.221 user:vagrant
passwd:vagrant #不加 SSH,用密码也可以
sudo:True #是否需要 sudo,默认是 False
之前的/srv/salt/top.sls 使用 IP 作为标识,现在使用以 web 开头的标识。需要修改一下:
> cat/srv/salt/top.sls
dev:
'web*':
-redis
现在就可以一样地使用命令了:
> sudo salt-ssh '*' test.ping > sudo salt-ssh web3 state.highstate #为了演示,web1 和 web2 没有设置 sudo 权限,不能操作/etc 下的 Redis 配置。所以只能部署 web3
Ansible
Ansible 也是配置管理和应用部署工具,由 Michael DeHaan 创建,他同时也是知名软件 Cobbler 和 Func 的作者。Ansible 默认通过 SSH 协议管理服务器,不需要安装客户端。
我们先安装它:
> pip install ansible > python-c 'import ansible;print ansible.__version__' 2.1.0.0 # 2.0 的版本做了一次较大的重构,并重新组织了代码
定义服务器的清单列表文件:
> sudo mkdir/etc/ansible > cat/etc/ansible/hosts #当然也可以使用其他名字,通过-i 指定的方式使用 [webservers] 192.168.0.132 192.168.0.174 192.168.0.101
webservers 这个组有三个 IP。还可以通过如下方式表示复杂的服务器类型:
[dbservers] db[01:20].dongwm.com #也就是 db01 到 db20 这 20 个服务器 c.dongwm.com:5501 #使用 5501 作为 ssh 端口 db21 ansible_ssh_port=5555 ansible_ssh_host=192.168.0.100 #设置别名和端口 localhost ansible_connection=local #设置本地的连接类型 192.168.0.201 ansible_connection=ssh ansible_ssh_user=dongwm
现在执行远程命令(省略实现 SSH 信任的步骤):
> ansible webservers-a"whoami"
> ansible all-m ping # ping 是它内置的模块。全部的模块可以在 http://docs.ansible.com/ansible/list_of_all_modules.html 找到
> ansible webservers-m service-a"name=nginx state=restarted"--become #重启 Nginx 服务,sudo 命令未来会被废弃,使用 become 替代
> ansible webservers-a"hostname"-f 3 #-f 表示同时执行,提高并发效率
> ansible webservers-m file-a"dest=/tmp/file mode=600 owner=root group=root"--
become #可以修改目标文件权限、用户组等
> echo 1 >> /srv/ansible/file
> ansible webservers-m copy-a"src=/srv/ansible/file dest=/tmp/file"#把文件复制到目标服务器的/tmp/目录下
> ansible webservers-m apt-a"name=nginx state=present"--become #确保安装,但是不更新。如果使用“state=latest”表示确保安装到最新版;“state=absent”表示确保没有安装。不同的系统安装包的模块不同,apt 模块仅限 Debian/Ubuntu 系统
> ansible 192.168.0.132-m git-a"repo=https://github.com/Icinga/icinga2.git dest=/opt/icinga version=HEAD"#使用 Git 克隆最新的 Icinga2 代码到 192.168.0.132 的/opt/icinga 目录
Playbooks
剧本(Playbooks)是 Ansible 的配置、部署、编排语言。它们可以被描述为一个需要远程主机执行的命令集合。
本节我们使用剧本实现一个 Redis 部署的功能。Playbooks 使用 YAML 语法,看一下主文件
main.yml:
---
-hosts:webservers
remote_user:ubuntu
become:yes
become_method:sudo
vars_files:#用到的变量单独存放在 YAML 文件中
-vars.yml
tasks:#任务列表,按顺序执行
-name:ensure packages installed
apt:pkg={{item}}state=latest
with_items:
-make
-build-essential
-tcl8.5
-name:ensure vm.overcommit_memory equal 1 #通常应该在新服务器上架的初始化过程中设置内核参数
sysctl:name="vm.overcommit_memory"value=1 sysctl_set=yes state=present reload=
yes
-name:download latest stable redis
get_url:url=http://download.redis.io/redis-stable.tar.gz dest=/tmp/redis-stable.
tar.gz
-name:untar redis
unarchive:src=/tmp/redis-stable.tar.gz dest=/tmp
-name:build redis
command:make-C/tmp/redis-stable
-name:create redis group
group:name=redis state=present system=yes
-name:create redis user
user:name=redis group=redis createhome=no shell=/bin/false system=yes state=
present
-name:make sure that/etc/redis exists
file:path=/etc/redis state=directory mode=0755
-name:make sure that/var/db/redis exists
file:path=/var/db/redis state=directory mode=0755 group=redis owner=redis
-name:copy service file
copy:src=templates/redis-server.service dest=/lib/systemd/system/redis-server.
service #可以被 systemd 识别,就能执行“sudo systemctl start redis-server”了
-name:installing redis binaries
command:cp/tmp/redis-stable/src/{{item}}/usr/local/bin
with_items:#这个列表会依次执行上面的命令,item 就是每次要执行命令的文件名字
-redis-server
-redis-cli
-redis-check-aof
-name:copy redis.conf file
template:src=templates/redis.conf.j2 dest=/etc/redis/redis.conf group=redis
owner=redis #需要使用 template 这个模块进行 Jinja2 渲染
notify:
-restart redis #通知触发的命令在 handlers 中需要定义
-name:cleaning up build files
command:rm-rf/tmp/{{item}}
with_items:
-redis-stable
-redis-stable.tar.gz
handlers:# handlers 中定义了上面 notify 中触发的命令具体内容
-name:restart redis
service:name=redis state=restarted # service 指 service 模块
变量集中在 vars.yml 文件中,和之前的 Jinja2 中的默认设置值一样,这里也只展示一小部分:
> cat vars.yml --- redis_settings: appendfilename:appendonly.aof appendonly:'no' # no 和 yes 需要使用加引号的字符串,否则会被当成 True/False appendfsync:everysec auto_aof_rewrite_min_size:64mb auto_aof_rewrite_percentage:100 ...
Ubuntu 16.04 LTS 已经使用 systemd 作为系统和服务管理器,redis-server.service 可以作为 Ubuntu 的系统服务的配置文件。不使用 systemd 的系统可以使用 upstart,首先把 upstart 脚本拷贝到/etc/init 目录下:
> sudo cp templates/upstart.conf/etc/init
然后启动服务:
> sudo service redis-server start
Redis 配置文件由前面介绍 Salt 的那一节里用到 redis-3.0.conf.jinja 模板生成,为了符合 Ansible 的习惯,模板改用了 j2 后缀。由于没有用到 map.jinja 文件,所以没有开始的 import 语句:
> cat/srv/ansible/dev/redis/templates/redis.conf.j2
daemonize{{redis_settings.daemonize}}
pidfile{{redis_settings.pidfile}}
port{{redis_settings.port}}
...
现在就可以给 webservers 集群安装 Redis 了:
> ansible-playbook/srv/ansible/dev/redis/main.yml
更多的剧本示例可以参看 ansible-examples(https://github.com/ansible/ansible-examples )。
Role 和 Galaxy
需要部署的内容越来越多,剧本文件也会越来越多,这个时候势必出现很多重复的代码,最好的解决办法是使用 Role(角色)重新组织这些剧本文件,把它们分层,结构化地使用。一个大型的部署剧本目录结构如下(假设服务的名字叫作 xx):
> tree/srv/ansible/dev/xx ├──defaults #默认变量 | └──main.yml #存放成目录的方式后,main.yml 就是主入口 ├──handlers #把事件处理独立成目录 | └──main.yml ├──meta #存放元数据和依赖的其他角色的信息 | └──main.yml ├──files #需要拷贝的文件 ├──tasks #把执行的任务独立成目录 | ├──install.yml #安装相关的设置 | ├──local_facts.yml | ├──main.yml #通过 main.yaml 导入其他 yaml 的设置 | └──configuration.yaml #配置相关的设置 ├──templates #模板 | ├──Debian #不同平台的模板存放的目录不同 | | ├──xx.conf.j2 #配置可能同时存在多个版本(一般是业务需要)。比如之前看到的 redis-3.8 和 redis-3.0 | ├──default | | ├──xx.conf.j2 | ├──RedHat | | ├──xx.conf.j2 | ├──upstart.conf.j2 | └──redis-server.service.j2 └──vars #把变量存储在独立的目录中 └──main.yml
tasks/main.yaml 中使用如下方法导入其他 YAML 配置:
--- -include:install.yml -include:configuration.yaml
假设部署 webservers 的时候需要安装 xx 服务,可以这样编写剧本:
---
-hosts:webservers
roles:
-common #假设/srv/ansible/dev/common 有与 xx 同样的目录结构,它存放了相对通用的剧本
-xx
在部署 webservers 的时候,会对 roles 中定义的每个角色(假如叫作 a)做如下的处理:
- 如果 roles/a/tasks/main.yml 存在,将其加入到剧本。
- 如果 roles/a/handlers/main.ym 存在,将其加入到剧本。
- 如果 roles/a/vars/main.ym 存在,将其加入到剧本。
- 如果 roles/a/meta/main.ym 存在,将其加入到剧本。
Ansible Galaxy(https://galaxy.ansible.com/ )是官方提供的一个免费的搜索、查询、下载社区提供的角色(Role)服务。在上面可以找到非常多的剧本例子,甚至可以直接在自己的项目中使用。现在通过 Galaxy 安装 Jenkins(一个持续集成工具)的方法。
首先在主控端安装角色:
> cat/etc/ansible/ansible.cfg #默认的角色会存放在/etc/ansible/roles 目录下,由于我们使用虚拟环境,没有/etc 目录下的写权限,需要自定义角色配置的存放目录 [defaults] roles_path=/srv/ansible/roles > ansible-galaxy install dongweiming.jenkins
配置角色:
> mkdir/srv/ansible/dev/jenkins/host_vars-p > cat/srv/ansible/dev/jenkins/jenkins.host [jenkins] 192.168.0.132 > cat/srv/ansible/dev/jenkins/host_vars/192.168.0.132 #当目标服务器的 IP 是 192.168.0.132 时,将使用这个文件定义的变量,同样的还有 group_vars。这种方式更灵活 --- plugins: -'ldap' -'github' -'translation' -'preSCMbuildstep' port:9000 prefix:'/build' email: smtp_host:'mail.dongwm.com' smtp_ssl:'true' default_email_suffix:'@dongwm.com' java_version:"openjdk-9" > cat/srv/ansible/dev/jenkins/jenkins.yml --- -hosts:jenkins become:yes roles: -dongweiming.jenkins
现在就可以给 192.168.0.132 这台服务器安装 Jenkins 了:
> cd/srv/ansible/dev/jenkins > ansible-playbook-i jenkins.host jenkins.yml
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论