cloudfoundry dev setup 分析以及chef学习

cf通过chef可以部署单节点实例。只需要按照http://support.cloudfoundry.com/entries/20407923-single-multi-node-vcap-deployment-using-dev-setup的介绍即可安装。本文就尝试着分析一下整个部署的流程,同时也对chef进行一些学习。

vcap_dev_setup

好了,首先从最外部的脚本进入,就是bin/vcap_dev_setup。这是shell脚本,前面的内容全部忽略,到run_cmd apt-get $APT_CONFIG install -qym wget这一行开始,这里在安装wget,然后check下网络,关系不大。

然后开始安装chef,这里需要拿到一个key,如果失败,可以参看http://wiki.opscode.com/display/chef/Installing+Chef+Client+on+Ubuntu+or+Debian获取key。得到key之后update apt-get然后安装chef。

接下来会安装git,然后去github把最新版本的代码全部update下来。我们可以看代码:

 

[plain] view plain copy print ?
  1. cd $CLOUDFOUNDRY_HOME && git clone $VCAP_REPO && cd vcap && git submodule update --init && git checkout $VCAP_REPO_BRANCH  
cd $CLOUDFOUNDRY_HOME && git clone $VCAP_REPO && cd vcap && git submodule update --init && git checkout $VCAP_REPO_BRANCH

就是这句,如果想看所有源码,就可以通过这些命令获得源码,如果单纯只是git clone的话,有些代码是没有的,比如services、uaa等都是没有的。当然在update之前,脚本会查看目标目录是否已经有vcap这个目录,如果有了,就不会再update了,所以如果有人已经安装完毕了,就可以直接把vcap目录拷贝过来,这样可以节省不少时间。

 

然后使用gem安装了rake,根据其comments里面所讲:  Our deployment code needs this gem. Using bundler seems like an overkill for the deployment code. So for now just manually install the required gem。也不是太懂,意会好了。

然后就调用chefsolo_launch.rb执行接下来的操作了。这里遇到过一个permission denied,因为我们代码从svn check下来,结果没有x权限,这个自己加上去就好了

chefsolo_launch.rb
 

这个文件前面大部分代码都在处理文件路径,这里列几个重要路径的default设置,其实自己都可以更改的:
  • cloudfoundry_home: /root/cloudfoundry
     
  • cloudfoundry_domain: vcap.me
     
  • deployment_spec: /root/cloudfoundry/.deployment/devbox/devbox.yml
     
  • deployment_config_path: /root/cloudfoundry/.deployment/devbox/config
     
然后各种参数,包括文件路径都被传入了一个叫做spec的hash(map)。同时JobManager通过job_dependencies.rake文件来得出每个job的依赖(job就是一个组建,比如cloud controller、health manager等),这些信息也被放入了spec。最终这些spec信息全部被写入了/tmp/solo.json文件,通过-j参数传给chef,我们可以看一下这个文件的内容:
[plain] view plain copy print ?
  1. {  
  2.   "cloudfoundry":{  
  3.     "home":"/root/cloudfoundry"  
  4.   },  
  5.   "deployment":{  
  6.     "user":"root",  
  7.     "name":"devbox",  
  8.     "domain":"vcap.me",  
  9.     "group":0  
  10.   },  
  11.   "run_list":[  
  12.     "role[nats_server]",  
  13.     "role[cloudfoundry]",  
  14.     "role[router]",  
  15.     "role[ccdb]",  
  16.     "role[cloud_controller]",  
  17.     "role[health_manager]",  
  18.     "role[dea]",  
  19.     "role[uaa]",  
  20.     "role[uaadb]",  
  21.     "role[redis_node]",  
  22.     "role[mysql_node]",  
  23.     "role[mongodb_node]",  
  24.     "role[neo4j_node]",  
  25.     "role[rabbitmq_node]",  
  26.     "role[memcached_node]",  
  27.     "role[redis_gateway]",  
  28.     "role[mysql_gateway]",  
  29.     "role[mongodb_gateway]",  
  30.     "role[neo4j_gateway]",  
  31.     "role[rabbitmq_gateway]",  
  32.     "role[memcached_gateway]"  
  33.   ],  
  34.   "jobs":{  
  35.     "installed":null,  
  36.     "install":{  
  37.       "all":null  
  38.     }  
  39.   }  
  40. }  
{
  "cloudfoundry":{
    "home":"/root/cloudfoundry"
  },
  "deployment":{
    "user":"root",
    "name":"devbox",
    "domain":"vcap.me",
    "group":0
  },
  "run_list":[
    "role[nats_server]",
    "role[cloudfoundry]",
    "role[router]",
    "role[ccdb]",
    "role[cloud_controller]",
    "role[health_manager]",
    "role[dea]",
    "role[uaa]",
    "role[uaadb]",
    "role[redis_node]",
    "role[mysql_node]",
    "role[mongodb_node]",
    "role[neo4j_node]",
    "role[rabbitmq_node]",
    "role[memcached_node]",
    "role[redis_gateway]",
    "role[mysql_gateway]",
    "role[mongodb_gateway]",
    "role[neo4j_gateway]",
    "role[rabbitmq_gateway]",
    "role[memcached_gateway]"
  ],
  "jobs":{
    "installed":null,
    "install":{
      "all":null
    }
  }
}
这个文件也比较简单,通俗易懂。唯一值得一提的是这个run list,它包含了所有需要安装的role(这个在下面的chef solo中会介绍,目前就认为是一个job即可),同时是根据依赖规则排列的,也就是被依赖的在前面。所以chef安装的时候会从上到下依次执行。比如先安装nats,然后是cloudfoundry。
 
然后呢,又用Dir.mktmpdir方式创建了一个solo.rb文件,这个文件是用-c参数传给chef-solo的,里面就配置了cookbook和role的路径,也就是dev_setup文件夹下面的两个同名文件夹。
最后,chefsolo_launch.rb将所有的任务交给chef了,自己向他传了几个参数,最终要的就是前面提到的solo.json以及solo.rb两个文件了。脚本就是
[ruby] view plain copy print ?
  1. exec("sudo env #{proxy_env.join(" ")} chef-solo -c #{File.join(tmpdir, "solo.rb")} -j #{json_attribs} -l #{chef_log_level}")  
exec("sudo env #{proxy_env.join(" ")} chef-solo -c #{File.join(tmpdir, "solo.rb")} -j #{json_attribs} -l #{chef_log_level}")
那么接下来所有的任务就是chef-solo了

Chef solo

首先chef是什么:按照我的理解,chef就是一个无视环境的部署工具。安装脚本也就是recipes不需要写上具体如何操作(How),而只需要写明需要什么(What)。chef就可以在目标机器上面获取环境信息,然后根据脚本的需求自动安装配置部署。因此chef是一个非常强大的工具,同时chef有自己的server,上面有许多已经存在的部署脚本(叫cookbook),可以多加利用。
而chef分为两种使用方式,一种是和chef server交互,一种就是chef solo。第一种就是server可以提供脚本配置属性等数据。而solo就是完全自己配置。cf就是采用了solo方式。
Chef solo的主要使用方式是:建个cookbook,然后,然后就可以让chef干活了,很简单的样子。一个cookbook就是一个部署脚本,里面可以包含许多内容,在CF中一般包含了recipes、attributes、templates、provider以及resources。
那么recipes就是脚本了,也就是chef需要执行的一些脚本,也就是本cookbook所有需要做的行为都在recipe里面要写明。
attributes顾名思义就是一些属性定义,我们可以在deployment的cookbook的attributes中看到这样的代码:default[:deployment][:log_path] = File.join(deployment[:home], "log"),这个属性在recipe里面引用就会使用node[:deployment][:log_path]。这里有一个变量:ENV["HOME"],表示的是~目录,也就是HOME目录。
templates是一个比较有意思的东西,它里面是一些erb文件,我们先不看这些erb文件,因为会看不懂的。我们直接看使用的地方,也就是recipe里面的template块。举个例子:
[ruby] view plain copy print ?
  1. template "nats_server" do  
  2.   path File.join("""etc""init.d""nats_server")  
  3.   source "nats_server.erb"  
  4.   owner node[:deployment][:user]  
  5.   mode 0755  
  6.   notifies :restart"service[nats_server]"  
  7. end  
  template "nats_server" do
    path File.join("", "etc", "init.d", "nats_server")
    source "nats_server.erb"
    owner node[:deployment][:user]
    mode 0755
    notifies :restart, "service[nats_server]"
  end
这个里的意思是这个template名字叫做nats_server,这个名字其实随便叫的,然后有一个path,是/etc/init.d/nats_server,你会发现:这不就是服务的文件吗?对的!然后看到source就是一个nats_server.erb文件,在template文件夹下面。后面的大致也能看懂,notifies应该就是消息通知,但是我暂时没找到说明文档,不懂。好了,现在再看erb文件,就可以发现,它很像server文件,其实它就是init.d下面的nats_server文件,只不过,里面某些参数使用了chef中的attributes来表示罢了。其他的template基本也如此,也就是说template是用来表示一个文件内容的,专业点讲叫做render。
provider:个人感觉cf中的使用和官方文档的说法不太一样,这里似乎就是提供了一些函数,给recipes调用而已。
resources:chef官网的说法就是work的basic unit,也是为recipe所用的

在CF中,chef会根据上文提到的run list的顺序依次安装执行。但是run list中提到的是role,而不是recipe,role其实是许多recipes的合集,一个role就是一个抽象的概念,每个node(就认为是一台机器好了)可以有多个role。其实挺像Java的interface的,每个接口定义了一个抽象的功能,一个类可以有多个接口。知道了role之后再看role的相关文件在哪里,因为总得有地方说明role要做些什么行为的。这个文件就在dev_setup\roles目录下。可以看到许多的json文件,以nats_server.json为例:
[plain] view plain copy print ?
  1. {  
  2.   "name": "nats_server",  
  3.   "default_attributes": {},  
  4.   "override_attributes": {},  
  5.   "json_class": "Chef::Role",  
  6.   "description": "NATS message bus server",  
  7.   "chef_type": "role",  
  8.   "run_list" : [ "recipe[deployment]",  
  9.                  "recipe[essentials]",  
  10.                  "recipe[ruby]",  
  11.                  "recipe[nats_server]" ]  
  12. }  
{
  "name": "nats_server",
  "default_attributes": {},
  "override_attributes": {},
  "json_class": "Chef::Role",
  "description": "NATS message bus server",
  "chef_type": "role",
  "run_list" : [ "recipe[deployment]",
                 "recipe[essentials]",
                 "recipe[ruby]",
                 "recipe[nats_server]" ]
}
上面一些参数关系也不大,主要是run_list,我们可以看到它需要四个recipe依次执行。也就是四个对应cookbook下的recipe文件。
下面来看看recipe的实例吧,首先可以看看deployment的recipe:
[ruby] view plain copy print ?
  1. node[:nats_server][:host] ||= cf_local_ip  
  2. node[:ccdb][:host] ||= cf_local_ip  
  3. node[:acmdb][:host] ||= cf_local_ip  
  4. node[:uaadb][:host] ||= cf_local_ip  
  5. node[:postgresql][:host] ||= cf_local_ip  
  6.   
  7. [  
  8.   node[:deployment][:home], File.join(node[:deployment][:home], "deploy"),  
  9.   node[:deployment][:log_path], File.join(node[:deployment][:home], "sys""log"),  
  10.   node[:deployment][:config_path],  
  11.   File.join(node[:deployment][:config_path], "staging"),  
  12.   node[:deployment][:setup_cache],  
  13. ].each do |dir|  
  14.   directory dir do  
  15.     owner node[:deployment][:user]  
  16.     group node[:deployment][:group]  
  17.     mode "0755"  
  18.     recursive true  
  19.     action :create  
  20.   end  
  21. end  
  22.   
  23. var_vcap = File.join("""var""vcap")  
  24. [var_vcap, File.join(var_vcap, "sys"), File.join(var_vcap, "db"), File.join(var_vcap, "services"),  
  25.  File.join(var_vcap, "data"), File.join(var_vcap, "data""cloud_controller"),  
  26.  File.join(var_vcap, "sys""log"), File.join(var_vcap, "sys""run"), File.join(var_vcap, "data""cloud_controller""tmp"),  
  27.  File.join(var_vcap, "data""cloud_controller""staging"),  
  28.  File.join(var_vcap, "data""db"), File.join("""var""vcap.local"),  
  29.  File.join("""var""vcap.local""staging")].each do |dir|  
  30.   directory dir do  
  31.     owner node[:deployment][:user]  
  32.     group node[:deployment][:group]  
  33.     mode "0755"  
  34.     recursive true  
  35.     action :create  
  36.   end  
  37. end  
  38.   
  39. template node[:deployment][:info_filedo  
  40.   path node[:deployment][:info_file]  
  41.   source "deployment_info.json.erb"  
  42.   owner node[:deployment][:user]  
  43.   mode 0644  
  44.   variables({  
  45.     :name => node[:deployment][:name],  
  46.     :ruby_bin_dir => File.join(node[:ruby][:path], "bin"),  
  47.     :maven_bin_dir => File.join(node[:maven][:path], "bin"),  
  48.     :cloudfoundry_path => node[:cloudfoundry][:path],  
  49.     :deployment_log_path => node[:deployment][:log_path]  
  50.   })  
  51. end  
  52.   
  53. file node[:deployment][:local_run_profiledo  
  54.   owner node[:deployment][:user]  
  55.   group node[:deployment][:group]  
  56.   content <<-EOH  
  57.     export PATH=#{node[:ruby][:path]}/bin:`#{node[:ruby][:path]}/bin/gem env gempath`/bin:#{node[:maven][:path]}/bin:$PATH   
  58.     export CLOUD_FOUNDRY_CONFIG_PATH=#{node[:deployment][:config_path]}   
  59.   EOH  
  60. end  
node[:nats_server][:host] ||= cf_local_ip
node[:ccdb][:host] ||= cf_local_ip
node[:acmdb][:host] ||= cf_local_ip
node[:uaadb][:host] ||= cf_local_ip
node[:postgresql][:host] ||= cf_local_ip

[
  node[:deployment][:home], File.join(node[:deployment][:home], "deploy"),
  node[:deployment][:log_path], File.join(node[:deployment][:home], "sys", "log"),
  node[:deployment][:config_path],
  File.join(node[:deployment][:config_path], "staging"),
  node[:deployment][:setup_cache],
].each do |dir|
  directory dir do
    owner node[:deployment][:user]
    group node[:deployment][:group]
    mode "0755"
    recursive true
    action :create
  end
end

var_vcap = File.join("", "var", "vcap")
[var_vcap, File.join(var_vcap, "sys"), File.join(var_vcap, "db"), File.join(var_vcap, "services"),
 File.join(var_vcap, "data"), File.join(var_vcap, "data", "cloud_controller"),
 File.join(var_vcap, "sys", "log"), File.join(var_vcap, "sys", "run"), File.join(var_vcap, "data", "cloud_controller", "tmp"),
 File.join(var_vcap, "data", "cloud_controller", "staging"),
 File.join(var_vcap, "data", "db"), File.join("", "var", "vcap.local"),
 File.join("", "var", "vcap.local", "staging")].each do |dir|
  directory dir do
    owner node[:deployment][:user]
    group node[:deployment][:group]
    mode "0755"
    recursive true
    action :create
  end
end

template node[:deployment][:info_file] do
  path node[:deployment][:info_file]
  source "deployment_info.json.erb"
  owner node[:deployment][:user]
  mode 0644
  variables({
    :name => node[:deployment][:name],
    :ruby_bin_dir => File.join(node[:ruby][:path], "bin"),
    :maven_bin_dir => File.join(node[:maven][:path], "bin"),
    :cloudfoundry_path => node[:cloudfoundry][:path],
    :deployment_log_path => node[:deployment][:log_path]
  })
end

file node[:deployment][:local_run_profile] do
  owner node[:deployment][:user]
  group node[:deployment][:group]
  content <<-EOH
    export PATH=#{node[:ruby][:path]}/bin:`#{node[:ruby][:path]}/bin/gem env gempath`/bin:#{node[:maven][:path]}/bin:$PATH
    export CLOUD_FOUNDRY_CONFIG_PATH=#{node[:deployment][:config_path]}
  EOH
end
最上面在赋一些参数,不理他,然后创建了deployment所需要许多许多目录,然后可以看到一个template,我们看名字可以知道是要给一个文件写一些值进去,因为source的文件就是一个简单的json文件。通过查找,可以找到这个文件就是:....../.deployment/devbox/config/deployment_info.json。这个路径定义在attributes里面。最后是写一个file,里面的content就是两句export。这个文件的路径也可以查到,是:/root/.cloudfoundry_deployment_local
再看一个recipe,这个是essential的recipe
[ruby] view plain copy print ?
  1. %w{apt-utils build-essential libssl-dev  
  2.    libxml2 libxml2-dev libxslt1.1 libxslt1-dev git-core sqlite3 libsqlite3-ruby  
  3.    libsqlite3-dev unzip zip ruby-dev libmysql-ruby libmysqlclient-dev libcurl4-openssl-dev libpq-dev}.each do |p|  
  4.   package p do  
  5.     action [:install]  
  6.   end  
  7. end  
  8.   
  9. if node[:deployment][:profile]  
  10.   file node[:deployment][:profiledo  
  11.     owner node[:deployment][:user]  
  12.     group node[:deployment][:group]  
  13.     content "export PATH=#{node[:ruby][:path]}/bin:`#{node[:ruby][:path]}/bin/gem env gempath`/bin:$PATH"  
  14.   end  
  15. end  
%w{apt-utils build-essential libssl-dev
   libxml2 libxml2-dev libxslt1.1 libxslt1-dev git-core sqlite3 libsqlite3-ruby
   libsqlite3-dev unzip zip ruby-dev libmysql-ruby libmysqlclient-dev libcurl4-openssl-dev libpq-dev}.each do |p|
  package p do
    action [:install]
  end
end

if node[:deployment][:profile]
  file node[:deployment][:profile] do
    owner node[:deployment][:user]
    group node[:deployment][:group]
    content "export PATH=#{node[:ruby][:path]}/bin:`#{node[:ruby][:path]}/bin/gem env gempath`/bin:$PATH"
  end
end
这个和上一个有点不同了,第一部分在做install,我们可以看到许多的pkg需要安装,这些pkg其实可以在dev setup的时候看到他们的身影。第二部分不难,就是写了一个profile。
最后看一个实际组件的recipe,就选择cloudcontroller好了:
[ruby] view plain copy print ?
  1. template node[:cloud_controller][:config_filedo  
  2.   path File.join(node[:deployment][:config_path], node[:cloud_controller][:config_file])  
  3.   source "cloud_controller.yml.erb"  
  4.   owner node[:deployment][:user]  
  5.   mode 0644  
  6.   
  7.   builtin_services = []  
  8.   case node[:cloud_controller][:builtin_services]  
  9.   when Array  
  10.     builtin_services = node[:cloud_controller][:builtin_services]  
  11.   when Hash  
  12.     builtin_services = node[:cloud_controller][:builtin_services].keys  
  13.   when String  
  14.     builtin_services = node[:cloud_controller][:builtin_services].split(" ")  
  15.   else  
  16.     Chef::Log.info("Input error: Please specify cloud_controller builtin_services as a list, it has an unsupported type #{node[:cloud_controller][:builtin_services].class}")  
  17.     exit 1  
  18.   end  
  19.   variables({  
  20.     :builtin_services => builtin_services  
  21.   })  
  22. end  
  23. cf_bundle_install(File.expand_path(File.join(node["cloudfoundry"]["path"], "cloud_controller")))  
  24.   
  25. staging_dir = File.join(node[:deployment][:config_path], "staging")  
  26. node[:cloud_controller][:staging].each_pair do |framework, config|  
  27.   template config do  
  28.     path File.join(staging_dir, config)  
  29.     source "#{config}.erb"  
  30.     owner node[:deployment][:user]  
  31.     mode 0644  
  32.   end  
  33. end  
template node[:cloud_controller][:config_file] do
  path File.join(node[:deployment][:config_path], node[:cloud_controller][:config_file])
  source "cloud_controller.yml.erb"
  owner node[:deployment][:user]
  mode 0644

  builtin_services = []
  case node[:cloud_controller][:builtin_services]
  when Array
    builtin_services = node[:cloud_controller][:builtin_services]
  when Hash
    builtin_services = node[:cloud_controller][:builtin_services].keys
  when String
    builtin_services = node[:cloud_controller][:builtin_services].split(" ")
  else
    Chef::Log.info("Input error: Please specify cloud_controller builtin_services as a list, it has an unsupported type #{node[:cloud_controller][:builtin_services].class}")
    exit 1
  end
  variables({
    :builtin_services => builtin_services
  })
end
cf_bundle_install(File.expand_path(File.join(node["cloudfoundry"]["path"], "cloud_controller")))

staging_dir = File.join(node[:deployment][:config_path], "staging")
node[:cloud_controller][:staging].each_pair do |framework, config|
  template config do
    path File.join(staging_dir, config)
    source "#{config}.erb"
    owner node[:deployment][:user]
    mode 0644
  end
end
首先向config文件写下一个cc的配置文件cloud_controller.yml,然后呢有一个cf_bundle_install函数,这个函数在哪里呢?我们可以去cloudfoundry的cookbook的libraries里面找到它,因为cloudfoundry的cookbook的recipe会在cc的recipe之前执行,所以这个函数是找得到的。这个函数的代码是:
[ruby] view plain copy print ?
  1. def cf_bundle_install(path)  
  2.   bash "Bundle install for #{path}" do  
  3.     cwd path  
  4.     user node[:deployment][:user]  
  5.     code "#{File.join(node[:ruby][:path], "bin", "bundle")} install"  
  6.     only_if { ::File.exist?(File.join(path, 'Gemfile')) }  
  7.   end  
  8. end  
  def cf_bundle_install(path)
    bash "Bundle install for #{path}" do
      cwd path
      user node[:deployment][:user]
      code "#{File.join(node[:ruby][:path], "bin", "bundle")} install"
      only_if { ::File.exist?(File.join(path, 'Gemfile')) }
    end
  end
可以看到就是去vcap目录下面找cloud controller然后,使用bundle安装那个Ruby on Rails系统即可。

好了,至此已经把dev setup所有的工作都研究清楚了,中间还有一些细节需要深入一下,不过大致的概念已经比较清楚了。
 

你可能感兴趣的:(云计算,PaaS,CloudFoundry)