chef之cookbook入门简明手册

时间:2022-09-19 09:11:45
本手册基于markdown编写,因为论坛不能删除markdown文件,压缩后,由不方便故这里上传生成后的pdf(),令附上,markdown内容,方便大家自行修改编辑

===================================================

---
layout: post
title: "chef之cookbook绝对简明手册"
date: 2012-12-28 21:09
comments: true
categories: linux
---


### 1:楔子:

从前有个名叫chef的大厨,功夫甚为了得,通过如下几招:
<pre>
* 1:rpm-Uvh http://rbel.frameos.org/rbel6
* 2:yum -y install rubygem-chef-server
* 3:setup-chef-server.sh
</pre>
便能在一个叫做centos6.*(可能在其它地方也可以)的地方快速搭建一个内部餐厅,
然后在来招
<pre>
knife configure -i
</pre>
便能把餐厅给装修了
餐厅好了,该需要客人了,既然是内部客人,就该有自己的内部vip的凭着,于是chef大厨把“/etc/chef/validation.pem"当做内部餐厅的vip凭证,客人只要练这样一招"curl -L http://opscode.com/chef/install.sh | bash",然后在自己的"/etc/chef/client.rb",写上chef大厨餐厅的地址,自己的名字,自己需要的一些东西,类似这样:
<pre>
cat /etc/chef/client.rb
log_level :info #整个菜谱执行过程的记录级别
log_location STDOUT
chef_server_url 'http://172.58.0.61:4000' #chef大厨餐厅的地址
validation_client_name 'chef-validator'
node_name "172-58-0-64" #自己的名字
</pre>
然后在来招:
<pre>
chef-client
</pre>
就成为chef大厨餐厅的免认证,唯一VIP会员了。

这么复杂的东西,chef大厨也自写了一套秘籍:knife,来进行管理,knife种有不同的招式,来对不同的方面进行不的处理;使用起来也相当方便

比如给客人新加一道菜(客户吃什么也都是chef控制的,东西怎么做也是chef控制的,太霸道了)
<pre>
knife node  run_list add client1 nova
</pre>
在去掉一道菜:
<pre>
knife node  run_list remove client1 nova
</pre>
这套秘籍还有很多的招式,预知详情,输入knife,按回车

不过呢,chef大厨认为自己厨艺比较高,怕餐厅工作都自己搞,人多了忙不过来,就定了个规矩,上菜,只上菜谱,而且不主动给送(就是这么霸道);客户要什么菜,告诉大厨,大厨给你发过去;这样大厨就比较轻松了,客户累了一点(也就是是说真正干事的人是客户自己,客户在自己的家里按照菜谱做自己要的菜);不过这样做的好处就是可以用较低的成本,完成较多的工作;而且呢,客户也可以是一些小客户,在地球上没有独立或者固定地址的客户。

既然这样,也就是说,如果是仅仅是想到不错的菜,对菜的具体加工过程没有洁癖的话,只要去研究这个菜谱怎么搞,就好了;如果有洁癖,想吃很好的菜,那就去研究这个餐厅的工作机制吧,我想要的是吃上不错的菜,那么我就去研究这个菜谱怎么写;而我是个懒人,实现我的效果即可



### 2:菜谱(cookbook)

先看眼菜谱是什么样的

<pre>
tree openssh
openssh
├── attributes
│   └── default.rb
├── files
│   └── default
│       └── mysh
├── metadata.rb
├── recipes
│   ├── default.rb
│   └── pubkeys.rb
└── templates
    └── default
        ├── ssh_config.erb
        └── sshd_config.erb
6 directories, 7 files
</pre>


#### 1:我需要什么
* 1: 变量(attributes)
  存储那些会变,用的比较多,而且不常变的东西,也是就说这里存储的都是默认配置

  在这个文件里定义 chef之cookbook入门简明手册penssh/attributes/default.rb

  可以这样定义:
  <pre>
  default['myssh'] = 'False'
  default['openssh']['server']['port'] = "22222"
  default['openssh']['server']['protocol'] = "2"
  default['openssh']['client']['forward_x11'] = "no"
  default['openssh']['client']['gssapi_authentication'] = "no"
  </pre>
  以default(因为这里都是默认配置嘛)开头,然后可以通过已中括号括起的字符串(用引号标起来了嘛)一级级的来组织我们的变量

* 2: 模板(templates)
  整体确定,部分内容会有变化的文本文件;一般这里存储的都是配置文件,所以每个配置文件都是独立的模板,存放openssh/templates文件夹下的

  模板文件名字可以自定义,以.erb结尾

  比如:
  <pre>
  cat openssh/templates/default/sshd_config.erb
\<% @settings.each do |key, value| %>
\<%         if value.kind_of? Array %>
\<%                 value.each do |item| %>
\<%=                 "#{key.split("_").map { |w| w.capitalize}.join} #{item}" %>
\<%                 end %>
\<%         else %>
\<%=         "#{key.split("_").map { |w| w.capitalize}.join} #{value}"%>
\<%         end %>
\<% end %>
  Port <%= @%node['openssh']['server']['port']>
  <% if ['openssh']['server']['port'] == 2222 %>
  Protocol <%= @node['openssh']['server']['protocol'] %>
  <% else %>
  Protocol 1 %>
  <% end %>
  </pre>

  这里实际就是ruby的语法(虽然我看不懂),上面那段比较复杂,看起来是一个循环,先不看;看看后面的简单的,可以对变量进行赋值
  看看后面那2行生成的内容是什么样的(哎,由果推因,无知啊)
  <pre>
  Port 2222
  Protocol 2
  </pre>
  原来<%= @var%>,var那里写上变量名就行了,这个变量名可以直接从第一步定义的变量中导入,只需要将那个"变量"中的default,变成node就可以了. 通过这样,我们可以对配置文件做基本的操作,只需要我们将变量按我们的思路去处理,也就是在这里对变量的处理是最主要的,一会就说怎么让配置文件中的变量搞的飞起来

  还有一点,猜对了,如果"变量"中的port那项不是2222的话,protocol就是1了

* 3: 文件(files)

  100年都不变的各种文件(\*.av,\*.:,\*.bin...)
  存在这里(openssh/files/default)的文件,会原封不懂的给客户,至于放到客户的什么地方,就有后面提到的动作控制了




#### 2:我做什么

有了上面的准备工作,下面就该操刀实干了

* 操作(recipes)

  openssh/recipes 下的以.rb结尾的文件称为一个个recipe,这里记录了每一步该用什么,怎么用,做什么;从前到后依次执行

##### Where there is a shell, there is a way !

shell是如此的强大,但是如果全部用shell来实现我们的需求,那我们刚才花的那几分钟岂不是白花了,不能让他白花;看看shell之外有有那些路可以走。

首先整理下,一般情况下,我们都需要对系统做什么操作(当然,我们需要的所有东西,基本都可以通过执行一个命令来实现,我认为,从某个角度来说,这就是对shell的另一种实现)

操作上有以下几种:

模式:
<pre>
模式 “模式名” do
  。。。 #这里写你
  。。。 #要做的操
  。。。 #做
end
</pre>

模式只能使用chef定义好的,模式名,可以自定义

* 1:对文件夹进行操作

  - 1:创建一个文件夹
  <pre>
   directory "/root/script" do
     owner "root"
     group "root"
     mode  0755
     action :create
   end
  </pre>

  创建一个/root/script的文件夹,用户属于root,用户组也属于root,权限为0755

  - 2:删除一个文件夹
  <pre>
  directory "/root/script" do
     recursive true  #递归删除
     action :delete
  end
  </pre>

  不要怀疑,刚看起来,对一个文件夹操作都要这么多行代码,真麻烦

* 2:对普通文件进行操作
  - 1 创建一个文件
  <pre>
  file "/root/123.txt" do
    owner "root"
    group "root"
    mode 00755
    action :create
  end
  </pre>
  - 2 删除一个文件
  <pre>
  file "/root/123.txt" do
    action :delete
  end
  </pre>



* 3:对命令进行操作
  - 2 执行一个命令
   <pre>
   execute "upgrade script" do
     command "shutdown -h 0"
     action :run
   end
   </pre>

  command 决定了执行什么命令(里面写什么就执行什么)
  action 怎么操作做这个命令(这是个通用选项),这里写的是执行,也是每次到这里都会执行这个命令
  这个操作每执行一次,你的系统就关机一次
* 5:对软件包进行操作
  支持redhat,debian系,忽视yum与apt,这个可以写出2种系统都兼容的命令
  - 1 安装一个软件
  <pre>
  package "dstat" do
      action :install
  end
  </pre>

  在模块名出写上,要操作的包的名字

  action 决定怎么操作做个包
    * install:安装
    * upgrade:升级
    * remove:卸载(centos系列种,相当于yum remove,删除了软件包,但没有删除配置文件)
    * purge:卸载包,删除配置文件
* 6:对模板进行操作

  个人认为:linux种对各种服务的配置与管理,基本上就是对配置文件的管理了,这里要多花精力
  <pre>
  address = '192.168.0.'+node['ipaddress'].split('.')[2]
  port = node['mysqld']['port']
  template "/etc/my.cnf" do
    source "my.cnf.erb"
    owner "mysql"
    group "mysql"
    mode 0755
    variables\(
    'bind-ipaddress' => address,
      …… #此处省略
      …… #若干行
    'port' => port
    \)
  end
  </pre>
  在template开始之前,可以对变量做各种处理,以达到我们想要的结果(处理的语法就是ruby的语法),在variables种可以放多个变量;这里我们也可以使用客户的一些信息,比如客户IP(node['ipaddress'])等,通过对定义的变量和客户的一些信息,在加上一些处理,基本就能满足大部分需求了。
  这个template会去"openssh/templates"文件下招my.cnf.erb这个文件,而且这个文件可能是这样的
  <pre>
  \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
  bind-address = <%= @address %>
  \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
  \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
  port = <%= @port %>
  </pre>

  而最终系统的/etc/my.cnf就会是这样:
  <pre>
  \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
  bind-address = 192.168.0.45
  \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
  \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
  port = 3306
  </pre>

* 7:对文件(1小节种提到的文件)进行操作
  这个主要是文件上传的功能,上传制定文件到客户的制定位置,并给与制定的权限
  <pre>
  cookbook_file "/root/script/src/mysql-5.1.56.tar.gz" do
    source "mysql-5.1.56.tar.gz"
    mode '0700'
    owner 'root'
    group 'root'
  end
  </pre>
  这个会去openssh/files/default目录下找名字为mysql-5.1.56.tar.gz的文件,然后上传到客户的/root/script/src/mysql-5.1.56.tar.gz,并切用户和用户组都是root,权限是0700

逻辑上有以下几种:

* 1:执行一个简单的操作

  通过上面的各种模式,就可以定义一个简单的操作了
* 2:执行操作甲后,执行另一个操作乙

  首先相信这样的需求,如果让我设计,我会怎么实现他:

  - 1:首先,操作甲执行完后,要通知乙下,告诉乙做什么,怎么做;这样乙才知道什么执行;
  - 2:而且操作乙平常是不执行的,否则等甲的通知也每什么意义了(当然有一种蛋都疼的情况就是,操作乙平常需要执行,操作甲执行后也需要执行)

  看看chef的实现方式:
  <pre>
  template "/etc/nginx/nginx.conf" do
    notifies :run, "execute[nginx_reload]", :immediately
  end
  execute "nginx_reload" do
    command "nginx -s reload"
    action :nothing
  end
  </pre>
  notifies:标记了,什么时候通知什么操作,做什么动作

  官方文档是这样写的:
  <pre>
  notifies :action, "resource_type[resource_name]", :notification_timing
  </pre>
  那么“notifies :run, "execute[nginx_reload]", :immediately”,这句话翻译成人话,就是这样的,马上运行名字为”nginx_reload”的命令;在结合场景翻译下:在对/etc/nginx/nginx.conf配置完成后,马上运行名字为”nginx_reload”的命令。

  action: 这里的nothing,标记了该操作,平时的不作为,如果吧这里的nothing改为run,那么就满足了上面说到的蛋蛋都疼的需求

* 3:这个操作,只在某种条件下执行

  其实这里应该有2个,该操作在某种条件下执行或者不执行:
  - 1:只有在什么情况下才做这个操作
  - 2:在这种情况下就不做这个操作

  如果将上面2个需求翻译成英语就是
  - 1 chef之cookbook入门简明手册nly if
  - 2:not if

  翻译成chef的语法就是:(会英语就是好啊,学技术都这么快,语法都能猜出来了)
  - 1 chef之cookbook入门简明手册nly_if
  - 2:not_if

  only_if和not_if的意义虽然不一样,但是用法是一样的

  这种情况下chef的操作代码就像是这样:
  <pre>
  execute 'restart_mysql' do
    command '/etc/init.d/mysqld restart'
    not_if 'netstat -ln |  grep 3306'
  end
  </pre>
  这个翻译成人话就是执行"netstat -ln|grep 3306",如果有输出,就不执行这个命令来,否则执行

* 8:必杀技之执行shell脚本

  既然有shell,就能实现我们的功能,如果上面的那些还满足不了,或者就喜欢写shell脚本,那么这个就是最好也是唯一的选择了

  <pre>
  script "install_nginx" do
    interpreter "bash"
    user "root"
    cwd "/usr/local/src"
    code <<-EOH
    wget 'http://nginx.org/download/nginx-1.2.1.tar.gz'
    tar -xzvf nginx-1.2.1.tar.gz -C /usr/local/src/
    git clone https://github.com/yaoweibin/nginx_tcp_proxy_module.git \
    /usr/local/src/nginx_tcp_proxy_module
    cd /usr/local/src/nginx-1.2.1/
    patch -p1 <  /usr/local/src/nginx_tcp_proxy_module/tcp.patch
    ./configure --add-module=/usr/local/src/nginx_tcp_proxy_module
    make
    make install
    EOH
    not_if 'ls /usr/local/src | grep nginx'
  end
</pre>

interpreter:这里指定了bash,既然需要制定,就说明这里的选择不仅仅一个,还应该有其它,还真猜对了,这里可以写 bash csh perl ruby python(当然应该是bash用的多些吧,反正如果让我执行py的话我是不会这样写的)

user:指定了在什么目录下运行,当然在脚本的第一行,cd 到某个目录也是一样的效果

code:指定了结尾的logo,这里,从当前行开始,一直到,EOH中间的都是要执行的脚本

not_if 在这里加了个判断,只要在/usr/local/src目录下没有名字nginx的文件时,才执行这个脚本

有了上面这些东西,就能够编写基本的cookbook的编写(我就仅仅知道这么多),但有时候,我们需要,对某一个更深入些,那么就去:[ http://wiki.opscode.com/display/chef/Resources#Resources-Script],就会得到你需要的了


### 3:杂项

* 1:变量

  - 1:针对一个用户的变量

    在attributes种可以定义默认的变量,但是有些时候,在莫个变量上,有些用户希望是自己独有的,这种情况,我们可以针对改用户做修改
   每个用户的情况,在chef餐厅里都有存储,可以通过"knife node"对用户进行操作:

    <pre>
   \# knife node
   FATAL: Cannot find sub command for: 'node'
   Available node subcommands: (for details, knife SUB-COMMAND --help)
   \*\* NODE COMMANDS \*\*
   knife node create NODE (options)
   knife node run_list add [NODE] [ENTRY[,ENTRY]] (options)
   knife node list (options)
   knife node edit NODE (options)
   knife node bulk delete REGEX (options)
   knife node show NODE (options)
   knife node from file FILE (options)
   knife node delete NODE (options)
   knife node run_list remove [NODE] [ENTRIES] (options)
   </pre>
  比如要对莫个客户做调整:
  <pre>
  \#knife node edit client_name
  </pre>
  会打开一个编辑器,显示当前用户的配置文件(json格式,要注意里面的东西一定要是json的否则,如果你些了半天,有个标点符号错,然后保存了,恭喜,你刚才些的白写了),
  <pre>
  {
    "name": "client_name",
    "run_list": [
      "recipe[os_init]",
      "recipe[openssh]"
    ],
    "normal": {
      "openssh": {
        "server": {
        },
        "client": {
        }
      },
      "manage": {
        "extra": [
          "user1",
          "user2"
        ]
      },
      "tags": [
      ]
    },
    "chef_environment": "_default"
  }
  </pre>
  在normal的下面有这样一段:
  <pre>
  "manage": {
    "extra": [
      "user1",
      "user2"
    ]
  },
  </pre>
  在recipe或者template中,如何使用这些信息呢?
  通过 node['manage']['default'] 这个变量就可以获取到一个内容为有2个元素('user1','user2')


  - 2:让一批用户都可以用的变量

    在attribute中的变量,所有用户都可以使用,在针对客户定义的变量,只有这个用才可以使用,但是如果有一批用户都需要这个变量呢?给这批用户一个个加多累啊,在说要改这个变量的时候,在一个个改更累,我们需要一个环境级别的变量,用户需要的时候,导入这个环境,即可。

        在chef中和cookbook并级的,还以一个environments的文件夹,这里就时存储各种变量的配置文件的
    <pre>
    \# knife environment
    FATAL: Cannot find sub command for: 'environment'
    Available environment subcommands: (for details, knife SUB-COMMAND --help)
    \*\* ENVIRONMENT COMMANDS \*\*
    knife environment list (options)
    knife environment create ENVIRONMENT (options)
    knife environment edit ENVIRONMENT (options)
    knife environment show ENVIRONMENT (options)
    knife environment delete ENVIRONMENT (options)
    knife environment from file FILE (options)
    </pre>

    随便写个存储环境变量的文件:
    <pre>
    vim environments/new.json
    </pre>
    这个也会打开一个编辑器,然活可以在里面写json格式的数据
    <pre>
    {
      "name": "openstack",
      "description": "",
      "cookbook_versions": {
      },
      "json_class": "Chef::Environment",
      "chef_type": "environment",
      "default_attributes": {
        "mysql": {
          "allow_remote_root": true,
          "root_network_acl": "%"
        }
      },
      "override_attributes": {
        "developer_mode": false,
        "enable_monit": true,
        "manage": {
          "extra": [
            "user3",
            "user4"
         ]
      }
      }
    }
    </pre>
    这个文件里面需要注意的有3块:name default_attirbute override_attribute

    name:当前环境的名字
    default_attribute:该环境中的默认属性
    override_attribute:该环境中的会覆盖到默认属性的属性

    环境变量定义好后,需要导入
    <pre>
    \# knife environment from file environments/new.json
    Updated Environment openstack
    </pre>
    环境变量导入后,在chef中就存在了,客户在要用的时候,还需要做一个操作:修改客户的配置:
      <pre>
      #knife node edit client_name
      {
        "name": "client_name",
         \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*
        "chef_environment": "openstack"
      }
      </pre>
    将chef_environment对应的值改成刚刚定义的变量名,在然后,在recipe或者template中,通过 node['manage']['default'] 这个变量就可以获取到一个内容为有2个元素('user3','user3')

* 2:针对特定类型的客户做不同处理

    比如我们需要对cenotos和ubuntu的系统分别上传不同的文件,或者不同的模板。当然我们可以在recipes种通过系统的类型来判断,做不同的操作。chef还给我们提供了一种方法。

    我们注意到在files和template些都有个default的文件夹,也是时默认从这里找我们需要的模板和文件;是不是centos的客户会首先重centos的文件夹下找呢?对就是这样的

    我们需要往不同的系统上传不同的文件或者模板时,只需要创建相应的文件夹,然后在文件夹种方式和default种同名的文件,不同的系统的客户,就会先来该系统的目录下查找,找不到,在去default目录下查找的