自动化环境搭建

自动化

我们来梳理一下上面这个场景里的问题:

  • 开发自己不做部署
  • 环境的安装是手工的
  • 应用的配置信息需要手工修改

除了艺术品之外,在工业社会里,手工就意味着低效,容易犯错,且不可持续。一切可以自动化的,都应该被自动化起来。

一个软件系统往往会包含很多的组件(消息队列服务,应用服务器,数据库服务器,负载均衡器,反向代理,文件服务器等),而且每一套环境(开发环境,测试环境,UAT,Staging,生产)还有自己独立的组件。

因此一个操作系统要被配置成系统的某个组件还需要做很多工作:以Java为例,我们需要安装特定版本的JDK,设置CLASSPATH环境变量,修改操作系统的内核参数,创建特定用户(数据库用户等),修改一些目录的权限等等。这些操作如果交给人工来完成,必然会出现各种错误(想想这个过程要被在不同的环境中重复多遍,出错的机率会大大增加)。

事实上业界已经有了很多帮助开发/运维工程师进行环境安装的工具,比如

  • Chef
  • Puppet
  • Ansible

前两者我已经在《轻量级Web应用开发》有过介绍,这里我们以Ansible为例来描述。

Vagrant

Vagrant提供对虚拟机的封装,使用它可以很容易的通过配置的方式来定义一个虚拟机

使用Vagrant,你只需要定义一个文本文件Vagrantfile即可。Vagrant自带的命令行工具vagrant会尝试加载这个文件,并按照其中的配置来启动虚拟机。Vagrantfile按照ruby的语法编写,不过不用担心,你无需在其中定义函数或者类,只需要做一些配置即可。

下面是一个简单的虚拟机定义:

  1. Vagrant.configure("2") do |config|
  2. config.vm.box = "precise64"
  3. config.vm.network "private_network", :ip => "192.168.2.100"
  4. end

我们指定了虚拟机使用precise64(precise是一个ubuntu的发行版,64表示它是一个64位系统的镜像)这样一个镜像,并且给这个虚拟机分配一个私有的IP地址,这样我们就可以在宿主环境中通过这个IP来访问该虚拟机了。

定义了Vagrantfile之后,使用vagrant工具的子命令就可以启动虚拟机了

  1. $ vagrant up

你可以在VirtualBox的界面里看到正在运行的虚拟机(Vagrant在底层使用了VirtualBox的虚拟机,而不是自行开发另外一套):

virtual box

启动之后,你可以通过

  1. $ vagrant ssh

来登录到虚拟机中。

如果你不知道使用哪个镜像,不知道如何配置Vagrantfile,可以使用这个命令来从头开始:

  1. $ vagrant init hashicorp/precise64
  2. $ vagrant up

vagrant命令会自动下载镜像,并设置环境,然后启动虚拟机。

在工程实践里,Vagrantfile会checkin到代码库中,这样团队里的其他人也可以很容易的在本地重新搭建相同的环境。另外,我推荐你将box的版本尽量和生产环境一致(比如都使用ubuntu的precise64位),这样可以尽早发现一些环境相关的问题。

初始化环境

Vagrant还提供了丰富的机制来初始化环境。你可以使用简单的Shell脚本,或者全功能的AnsibleChef等来初始化环境。

设想你需要在虚拟机环境就绪后,在vagrant用户的home目录下创建一个叫workers的目录,安装一个叫wget的软件包,然后下载一个网络上的文件到workers目录。

要完成这样的动作,我们可以在当前目录(和Vagrantfile放在一起)创建一个setup.sh的脚本:

  1. #!/usr/bin/env bash
  2. mkdir -p ~/workers
  3. sudo apt-get update
  4. sudo apt-get install wget
  5. wget http://host:port/resource.zip -O ~/workers/resource.zip

然后在Vagrantfile中加入:

  1. Vagrant.configure("2") do |config|
  2. config.vm.box = "precise64"
  3. config.vm.provision :shell, path: "setup.sh"
  4. end

Vagrant在初始化虚拟机的时候,会执行setup.sh,这样我们就得到了一个经过设置的环境。设置环境可能会是一个非常复杂的过程,比如安装web服务器,定义缓存目录,安装监控服务器的客户端,为某些应用程序创建专用用户,修改权限等等,如果用shell来写,会比较复杂。

Vagrant支持很多的provision的工具,比如Ansible来完成这种复杂的操作。

Ansible

Ansible是一个自动化配置工具,相对于ChefPuppet,它的安装和配置更加简单(无需在被配置的服务器安装额外的Agent程序)。它通过ssh将一些Ansible模块部署到远程机器上,然后执行。

使用Ansible可以同时配置,更新多个机器。目前很多企业都会使用各种各样的云产品,比如AWS的EC2,阿里云等,通过Ansible可以很容易的将这些环境配置变成自动化。在企业内部的私有云(从一台服务器划分出来的众多虚拟机)中,也可以使用Ansible来减少配置环境的时间,提高效率。

在一个冬日的下午,我和一个新手程序员结对在服务器上修改tomcat服务器的一些日志的配置,折腾了很久之后,我放弃了。我心想,要不删了webapps这个目录重新部署一下看看吧,可能是缓存问题也说不定。不过,头昏脑涨的我并没有发现敲入的命令是rm -rf /usr/share/tomcat7。新手程序员问我,rm -rf是什么意思?我一边用力的敲下回车键,一遍警告这个新手:“rm -rf是一个非常危险的操作,它表示要强力删除整个……”。等我发现我删除的是tomcat7的时候已经太晚了,我们的QA环境彻底挂了,所有人都被block住了(还好不是在其他人给客户showcase的时候)。

另一个工程师,我们姑且称之为运维工程师吧,花费了好几个小时来重新安装tomcat,以及其中的各种jvm参数。

我们在随后的几周里,引入了Ansible,这样即使头昏脑涨的程序员无意识的敲入了愚蠢而致命的命令也无所谓,我们只需要2分钟就可以配置好一个tomcat服务器,崭新的。

惯例

Ansible中的一些关键概念:

  1. role 定义一个角色,比如nginx就可以是一个角色,要完成nginx的安装需要很多小的步骤,这些步骤都包含在nginx这个role中
  2. inventory 定义一组环境,比如Web服务器需要三台做负载均衡,数据库由两台服务器组成等,这些都可以通过inventory文件来描述,inventory文件被称为清单文件
  3. playbook 定义在哪些inventory应用哪些role

Ansible中有一些惯例,遵循这些惯例有助于你快速读懂其他人写的playbook/role

  1. production # 生产环境的清单文件
  2. staging # staging环境的清单文件
  3. qa # 测试环境的清单文件
  4. site.yml # 主playbook
  5. webservers.yml # web服务器的playbook
  6. dbservers.yml # 数据库服务器的playbook
  7. roles/
  8. common/ # this hierarchy represents a "role"
  9. tasks/ #
  10. main.yml # 具体任务定义
  11. handlers/ #
  12. main.yml # 回调任务
  13. templates/ #
  14. nginx.conf.j2 # 模板文件
  15. files/ #
  16. app.conf # 需要拷贝到被配置环境中的文件
  17. vars/ #
  18. main.yml # 变量定义
  19. defaults/ #
  20. main.yml # 低优先级变量定义
  21. meta/ #
  22. main.yml # 元数据,用以表述作者信息,定义依赖等
  23. webtier/ # 另外一个role,结构和`common`一致
  24. monitoring/ # 用于监控的role,结构同上

比如一个简单的inventory文件看起来是这样的:

  1. [webservers]
  2. 10.29.2.1
  3. 10.29.2.2
  4. 10.29.2.3
  5. [dbservers]
  6. 10.29.2.4
  7. 10.29.2.5

没错,它就是一个简单的ini文件。如果你需要添加新的机器,只需要将域名/IP地址添加到对应的小节即可。

Ansible使用yml作为配置,我们来看一个playbook的例子:

  1. - name: webservers
  2. hosts: webservers
  3. roles:
  4. - roles/nginx
  5. user: robot
  6. sudo: true
  7. - name: dbservers
  8. hosts: dbservers
  9. roles:
  10. - roles/mongodb
  11. user: robot
  12. sudo: true
  13. environment:
  14. http_proxy: http://user:pass@proxy.host:8080
  15. https_proxy: http://user:pass@proxy.host:8080

这个playbook定义了两个环境的配置信息:webserversdbserverswebservers中的所有主机会被应用nginx角色(安装和配置nginx,并启动nginx服务),而dbservers中的主机会被应用mongodb的角色。

dbservers中,我们还加入了environment节,其中定义了可以用在安装过程中的一些环境变量设置。

定义好之后,你可以通过下列命令来执行这个playbook

  1. ansible-playbook -i qa playbook.yml

命令

Ansible内置了很多常用的命令来简化配置的工作,比如安装软件包,拷贝文件,使用模板,创建用户,创建目录等。

安装软件包

  1. - name: install package
  2. apt: name=nginx state=present

apt命令可以用于安装一个软件包,它相当于在主机上执行apt-get install nginx -y。如果你需要安装多个软件包,可以采用with_items子命令:

  1. - name: install packages
  2. apt: name={{ item }} state=present
  3. with_items:
  4. - nginx
  5. - python
  6. - git

创建目录

  1. - name: create directory
  2. file: path=/home/vagrant/workers state=directory

拷贝文件

  1. - name: copy file to workers folder
  2. copy: src=resource.zip dest=/home/vagrant/resource.zip

如果你要使用的命令,正好Ansible没有内置,你还可以使用command命令来执行:

  1. - name: universal command
  2. command: ls -alt /home/vagrant

如果要完成Vagrant小节中的例子,我们的配置看起来就是这样的:

  1. - name: create directory
  2. file: path=/home/vagrant/workers state=directory
  3. - name: install package
  4. apt: name=wget state=present
  5. - name: download file
  6. command: wget http://host:port/resource.zip -O ~/workers/resource.zip

当然,根据预定,我们会把变量放在vars目录的main.yml中,比如上面apt例子中的多个包的安装,我们会在vars/main.yml中定义变量

  1. packages:
  2. - nginx
  3. - python
  4. - git

然后在tasks/main.yml中引用:

  1. - name: install packages
  2. apt: name={{ item }} state=present
  3. with_items: '{{ packages }}'

独立使用

通常我们会在一个Linux环境安装Ansible,这个环境专门做环境初始化的工作。如果你使用ubuntu环境,可以通过预编译的二进制包来安装:

  1. $ sudo apt-get install software-properties-common
  2. $ sudo apt-add-repository ppa:ansible/ansible
  3. $ sudo apt-get update
  4. $ sudo apt-get install ansible

当然,你也可以通过源码来安装:

  1. $ git clone git://github.com/ansible/ansible.git --recursive
  2. $ cd ./ansible
  3. $ source ./hacking/env-setup
  4. $ sudo make install #安装到系统路径,其他用户也可以使用

安装完成之后,你会得到ansible命令和ansible-playbook命令。

在执行ansible命令之前,我们需要定义ansible对应的远程机器列表配置。你需要在/etc/ansible/hosts文件中添加所有需要配置的机器IP地址或者域名(如果没有这个目录和文件,可以直接创建)。

文件内容就是每个地址一行的形式:

  1. 10.29.2.1
  2. 10.29.2.2
  3. 10.29.2.3

ansible命令可以用来向inventory执行shell命令,比如:

  1. $ ansible all -m ping -u robot -b --become-user root

上面的命令会向主机all/etc/ansible/hosts文件中指定的所有地址),执行一个Ansible模块ping(-m ping),使用用户robot(-u robot),并且以另一个用户身份root(-b —become-user root)。

如果没有设置过ssh的私钥,还需要指定--ask-pass选项,否则ansible会尝试用ssh登陆,然后失败。

  1. $ ansible all -m ping -u robot -b --become-user root --ask-pass

除了使用内置模块之外,你开可以使用其他任何shell命令:

  1. $ ansible all -a "/bin/echo hello" -u robot --ask-pass

Ansible会直接在远程机器上执行对应的shell命令。

在Vagrant中使用

Vagrant可以很容易的和Ansible集成在一起,只需要指定config.vm.provisionansible即可:

  1. Vagrant.configure("2") do |config|
  2. config.vm.provision :ansible do |ansible|
  3. ansible.playbook = "playbook.yml"
  4. end
  5. end

当然,你可以定义多个虚拟机,然后并发的来自动化配置,比如像这样:

  1. (1..5).each do |machine_id|
  2. config.vm.define "machine#{machine_id}" do |machine|
  3. machine.vm.hostname = "machine#{machine_id}"
  4. machine.vm.network "private_network", ip: "192.168.2.#{20+machine_id}"
  5. if machine_id == N
  6. machine.vm.provision :ansible do |ansible|
  7. ansible.limit = "all"
  8. ansible.playbook = "playbook.yml"
  9. end
  10. end
  11. end
  12. end