这篇文章讨论了如何在我们的环境中安装和配置软件,这个任务通常被称为服务器配置(Server Provisioning)。

服务器配置

在开始介绍现代化的工具之前,我们来看看最基本且经过实战考验的服务器配置工具:shell脚本。在Chef、Ansible或Puppet出现之前,很多运营团队使用Bash来配置服务器(在Windows上则使用PowerShell脚本)。

例如,如果想在运行Ubuntu的Amazon EC2实例上安装Nginx,可以使用以下脚本(install-nginx.sh):

#!/bin/sh
ssh -t ubuntu@$1 sudo apt-get upgrade
ssh -t ubuntu@$1 sudo apt-get -y install nginx

我们可以使用shell脚本来配置服务器上的所有东西。据我所知,所有主流的配置工具都使用了基于安全传输层(如SSH)的shell命令或PowerShell(Chef可能是个例外)。即使你使用了配置工具,在某些时候也需要用到脚本。因此,当你开始使用配置工具(如Chef或Ansible)时,学习如何使用基本的shell脚本也会为你带来很多好处。

你可能会问自己,为什么在shell脚本已经可以完成所有工作的同时还要学习配置工具?很多环境已经使用shell脚本进行服务器配置,那么为什么要使用配置工具代替它们?

首先,shell脚本通常使用的是声明性语法。shell脚本通过运行命令序列来安装软件,而配置工具只需要指定服务器应该安装哪些软件,这样就可以使用相同的代码在不同的操作系统上、使用不同的包管理器以及指定不同的版本来安装和配置相同的软件。

其次,配置工具通常会提供用于组织基础设施的方式。虽然使用shell脚本也可以做到这一点,但配置工具通常会提供更简洁明了的方案。因为是行业标准,开发人员可以更轻松地找出QA环境中哪些服务器运行RabbitMQ。

第三,每个主要的配置工具都有一个蓬勃发展的社区,他们构建可复用的模块来安装大多数开源软件。你可以直接在模块配置中指定内存限制,而不需要记住Postgres配置文件在哪里,这样可以节省很多时间。

当然,原因还有很多,这里就不一一例举了。尽管学习曲线有点陡峭,但学习配置工具仍然是值得的。与shell脚本相比,配置工具更容易使用,便于思考,也更容易维护

关于命名

学习使用Chef(服务器配置工具)的前几周给我留下了深刻的印象。入门指南展示了如何创建一个“recipe”,其中包含安装或配置软件的说明,我能够理解这种比喻背后的含义。recipe必须存在于“cookbook”中,这是有道理的。然后你在“kitchen”里测试cookbook,但我开始有点怀疑了。

这种比喻有点令人感到困惑,于是我决定去看一下其他工具,如Ansible。Ansible文档的第一页介绍了“playbook”的概念,而playbook包含一系列“play”。

那么,这些问题很重要吗?当然很重要了,因为在学习配置工具之前,你应该知道,它们很有可能会引入大量令人费解的术语。即使是为了完成基本的任务,你也必须重新学习很多术语。如果你是刚开始学习配置工具,我强烈建议你随时写下这些术语定义,你还有很多东西要学。

每个软件开发人员都会为现有的单词创建不同的含义,他们甚至还会发明一些单词,比如“uninitialize”和“unregister”。这已经成为软件开发的一部分。

我会尽量用大家熟悉的术语来解释这些工具。

配置管理

你决定使用花哨的配置工具在远程服务器上安装Nginx。在开始设置数据库备份节点前,一切都很顺利。你已经编写了MySQL主服务器的配置文件,但是你不太确定如何配置MySQL从服务器的内部DNS地址。这个时候配置管理就派上用场了。

在设置服务器时,最好可以将应用程序视为由两部分组成:不可变部分(通常是代码或编译的二进制文件)和可变部分(通常是配置文件或环境变量)。大部分由社区创建的模块默认情况下会安装二进制文件,并提供尽可能合理的配置,而且会为我们暴露出一些属性,方便对其进行覆盖。

这些属性通常包含特定于用户环境的值。大多数配置工具都为用户提供了一种机制,通过模板将特定于环境的值插入到配置文件中,或直接插入到环境变量中。

你可以使用配置工具提供的配置管理来配置MySQL主服务器的配置文件,然后在其中配置从服务器。

Secret管理

这样就可以解决上述的问题,但后来发现,你必须上传AWS凭证才能让MySQL从服务器访问S3。你知道不能直接将这些凭证提交到代码库中,因此这些凭证只能存在于你的机器和NSA服务器上。

这个时候你需要的是Secret管理。

与自动化领域的所有东西一样,你也有很多管理秘钥的可选项。谷歌提供了一项名为KMS的服务,AWS也提供了一项名为Secret Manager的服务,Chef提供了加密数据包,Hashicorp提供了一款名为Vault的产品,Ansible也有一款名为Vault的产品。除了KMS会对字符串进行加密之外,所有这些工具都提供了相同的功能:保护对加密秘钥的访问(这些秘钥被用在配置管理中)。

有好几次,我不小心将秘钥提交到了代码库。这类事情一直在发生,而且非常危险。

切勿以明文形式存储API密钥或凭证

可以使用Secret管理解决方案来存储这些数据,然后将其绑定到配置工具中

一个简单的例子:Chef

首先需要安装Chef Development Kit(ChefDK)。

如前所述,我们需要一个recipe来安装Nginx。出于教学的目的,我们将从头开始创建它,而不是从社区的cookbook中捞一个出来。

我们需要创建一个cookbook。cookbook通常存在于`cookbooks`目录中,在项目的根目录运行以下命令:

mkdir cookbooks

现在让我们创建一个cookbook,用于放置我们的新recipe:

chef generate cookbook cookbooks/application

这个命令在`cookbooks/application`目录中创建了很多文件,我们关心的是`cookbooks/application/recipes/default.rb`这个文件。这个文件包含了默认的recipe,我们将安装Nginx的命令放到这个文件中。

apt_update

package 'nginx'

cookbook_file '/var/www/html/index.html' do
  source 'index.html'
  owner 'www-data'
  group 'www-data'
  mode '0755'
  action :create
end

这个文件中的前两个命令将执行你期望的操作:

  • `apt_update`更新你的aptitude包。
  • `package ‘nginx’`使用操作系统默认包管理器安装`nginx`包(在这个示例中,它使用的是aptitude)。

最后一个命令将`cookbooks/application/files/index.html`拷贝成远程服务器上的`/var/www/html/index.html`,并设置文件的权限,让Nginx服务器可以访问它。

这个文件还不存在,所以需要创建它。首先要创建`文件`目录:

mkdir cookbooks/application/files

然后创建文件`cookbooks/application/files/index.html`,其中包含以下内容:

<html lang="en-us">
  <head>
    <title>Hello, World!</title>
  </head>
  <body>
    Chef has landed.
  </body>
</html>

更新`packer.json`,加入Chef相关配置:

{
  "builders": [{
    "type": "amazon-ebs",
    "region": "us-east-1",
    "source_ami": "ami-04169656fea786776",
    "instance_type": "t2.small",
    "ssh_username": "ubuntu",
    "ami_name": "Ubuntu 16.04 Nginx - {{timestamp}}",
    "tags": {
      "Image": "application"
    }
  }],
  "provisioners": [{
    "type": "chef-solo",
    "cookbook_paths": ["cookbooks"],
    "run_list": ["recipe[application]"]
  }]
}

我们对之前的`packer.json`进行了两处更改。

首先,我们为AMI添加了一个`Image`标签。我们之前从Packer的输出中复制AMI ID,并粘贴到Terraform代码中。这不是一个可维护的解决方案,因为AMI ID会经常发生变化,而且我们不应该在每次发生变化时都要将更改推送到存储库中。相反,我们使用Terraform的`data`资源来动态读取AMI ID(使用`Image=application`查询最新的AMI)。

其次,我们使用`chef-solo`替换了`shell`。我们告诉它在哪里可以找到cookbooks目录,以及要运行哪个recipe。默认情况下,`run_list`中的`recipe[COOKBOOK]`条目将执行`recipes/default.rb`。我们也可以显式指定explicity:`recipe [COOKBOOK::RECIPE]`来覆盖默认行为。由于我们的recipe保存在`recipes/default.rb`中,所以将使用默认行为。

现在开始构建我们的AMI:

packer build packer.json

我们的新AMI有一个`Image`标签,现在修改`terraform.tf`中硬编码的AMI,让它通过标签来查找AMI。

将以下内容添加到`terraform.tf`中:

data "aws_ami" "web" {
  most_recent = true
  owners = ["self"]
  filter {                       
    name = "tag:Image"     
    values = ["application"]
  }                              
}

现在使用`aws_ami.web resource`输出的ID替换`aws_instance.web1`和`aws_instance.web2 `resource中的AMI ID:

resource "aws_instance" "web1" {
  ami                    = "${data.aws_ami.web.id}"
  availability_zone      = "us-east-1a"
  instance_type          = "t2.small"
  vpc_security_group_ids = ["${aws_security_group.application.id}"]
  subnet_id              = "${aws_subnet.private1.id}"
}

resource "aws_instance" "web2" {
  ami                    = "${data.aws_ami.web.id}"
  availability_zone      = "us-east-1b"
  instance_type          = "t2.small"
  vpc_security_group_ids = ["${aws_security_group.application.id}"]
  subnet_id              = "${aws_subnet.private2.id}"
}

运行下面的命令创建Chef配置的服务器,然后启动浏览器,打开地址为负载均衡器的域名:

terraform plan -out terraform.plan
terraform apply "terraform.plan"
open "http://$(terraform output dns)"

你应该能够在打开的浏览器页面上看到:Chef has landed!

一个简单的例子:Ansible

让我们使用Ansible来构建这个相同的示例。首先需要安装Ansible

Ansible将安装和配置说明组织到`tasks`中,然后将`tasks`组织到`playbook`中。让我们为playbook创建一个目录结构。

mkdir playbook
mkdir playbook/files

这并不是组织Ansible playbook的最佳实践。因为我们的用例很简单,所以使用了简化版本。如果你对Ansible感兴趣,应该根据官方提供的建议来构建playbook

在`playbook/application.yml`中创建playbook,内容如下:

---
- hosts: all
  gather_facts: False
  become: yes
  pre_tasks:
  - name: Install Python 2.7
    raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal)
- hosts: applications
  become: yes
  tasks:
  - name: Install Nginx
    apt:
      name: nginx
      state: present
      update_cache: yes
  - name: Update contents of index.html
    copy:
      src: index.html
      dest: /var/www/html/index.html
      owner: www-data
      group: www-data
      mode: 0755

这个playbook文件包含配置我们的服务器所需的所有信息。现在让我们来讨论一下它的结构。

每个playbook包含一个“play”列表,每个play包含一个“tasks”列表,task用于安装和配置软件。我们的playbook包含两个play。第一个play在Ubuntu上安装Python 2.7(用于运行Ansible)。第二个play安装和配置Nginx。

我们在每个play的根节点配置了两个参数:`hosts`和`become`。`hosts`参数告诉Ansible应该在哪台机器上运行playbook(“all”表示在所有机器上运行)。`become:yes`表示Ansible将通过sudo运行所有命令,否则将会出现很多权限错误。

play的第一个task负责安装和配置Nginx,它将更新aptitude缓存,并确保`nginx`包存在。如果已经安装了`nginx`包,这个命令将不执行任何操作。

第二个task将`files/index.html`拷贝到远程服务器上,并为其分配正确的权限。

这个文件还不存在,所以让我们创建它。将以下内容加入到`playbook/files/index.html`中:

<html lang="en-us">
  <head>
    <title>Hello, World!</title>
  </head>
  <body>
    Ansible has landed.
  </body>
</html>

这就是我们配置Ansible所需的全部内容。现在让Packer使用这个配置。使用以下内容更新`packer.json`:

{
  "builders": [{
    "type": "amazon-ebs",
    "region": "us-east-1",
    "source_ami": "ami-04169656fea786776",
    "instance_type": "t2.small",
    "ssh_username": "ubuntu",
    "ami_name": "Ubuntu 16.04 Nginx - {{timestamp}}",
    "tags": {
      "Image": "application"
    }
  }],
  "provisioners": [{
    "type": "ansible",
    "playbook_file": "./playbook/application.yml",
    "host_alias": "applications"
  }]
}

我们只修改了使用Ansible作为配置器,需要提供一个指向playbook文件的路径,我们将其设置为`./playbook/application.yml`。我们可以看到用于安装Nginx的play顶部有一行:`hosts: applications`。这是我们用来告诉Ansible需要安装应用程序的主机别名。我们需要告诉Packer我们正在为其中一个主机构建映像,所以我们将`host_alias`属性设置为`applications`。

运行下面的命令来创建Ansible配置的服务器,然后启动浏览器,打开地址为负载均衡器的域名:

packer build packer.json
terraform plan -out terraform.plan
terraform apply "terraform.plan"
open "http://$(terraform output dns)"

你应该可以在打开的浏览器页面上看到:Ansible has landed!

英文原文:http://stephenmann.io/post/a-brief-introduction-to-provisioning/

感谢张婵对本文的审校。

Comments are closed.