对于大部分开发者而言,第一次部署 Web 应用几乎都是痛苦的回忆。
当你在本地的开发服务器上欣赏完刚刚写完的项目时,你才完成了万里长征的第一步。首先你需要购买一台价格和性能合适的云主机,接着 SSH 远程登录到你的远程主机配置项目环境(例如 LAMP),然后把项目代码拷贝到远程主机,最后运行你的项目。
让我们来动手实践吧。本文所有的代码都可以在我的 GitHub 仓库 里面找到。
由于购买云主机有一定的经济成本,所以这里我使用 DevOps 界非常流行的 Vagrant 工具(点击这里下载)。Vagrant 是一个简单易用的虚拟机管理工具(在本文中,有时“虚拟机”会被称为“主机”),能够通过命令行轻松地配置、开启、关闭、登录虚拟机。这里我们用 Vagrant 搭建一个内网中的主机,并将一个静态网页(HTML文件)部署到这台机器上。
配置 Vagrant
首先,确保你的 Vagrant 已经安装完毕:
$ vagrant --version
如果下面显示了版本信息,则说明安装成功啦。
然后我们新建一个目录,所有的活动将在这个目录里面进行。
$ mkdir deploy-my-first-project
$ cd deploy-my-first-project
然后我们创建 Ubuntu 虚拟机:
$ vagrant init ubuntu/trusty64
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.
然后发现当前目录里面多了个 Vagrantfile 文件!这个文件用于配置虚拟机,我们要想通过内网(Private Network)访问这台主机,就需要进行如下的配置(用下面的代码替换掉原来 Vagrantfile 里面的内容):
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.network "private_network", ip: "192.168.33.10"
end
配置改好了,接下来启动虚拟机:
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/trusty64'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'ubuntu/trusty64' is up to date...
...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
default: The guest additions on this VM do not match the installed version of
default: VirtualBox! In most cases this is fine, but in rare cases it can
default: prevent things such as shared folders from working properly. If you see
default: shared folder errors, please make sure the guest additions within the
default: virtual machine match the version of VirtualBox you have installed on
default: your host and reload your VM.
default:
default: Guest Additions Version: 4.3.36
default: VirtualBox Version: 5.1
==> default: Configuring and enabling network interfaces...
==> default: Mounting shared folders...
default: /vagrant => /Users/mRc/Desktop/deploy-my-first-project
初次启动可能会比较久,因为需要下载 Ubuntu 镜像。下载过程中,你可以选择读接下来的内容,也可以喝杯咖啡放松一下。启动成功后,我们就可以 SSH 登录我们的虚拟机啦:
$ vagrant ssh
Welcome to Ubuntu 14.04.5 LTS (GNU/Linux 3.13.0-145-generic x86_64)
* Documentation: https://help.ubuntu.com/
System information as of Thu May 3 02:12:01 UTC 2018
System load: 0.91 Processes: 82
Usage of /: 3.6% of 39.34GB Users logged in: 0
Memory usage: 29% IP address for eth0: 10.0.2.15
Swap usage: 0%
Graph this data and manage this system at:
https://landscape.canonical.com/
Get cloud support with Ubuntu Advantage Cloud Guest:
http://www.ubuntu.com/business/services/cloud
0 packages can be updated.
0 updates are security updates.
New release '16.04.4 LTS' available.
Run 'do-release-upgrade' to upgrade to it.
vagrant@vagrant-ubuntu-trusty-64:~$
对于没有登录过远程主机的同学来说可能会有点惊讶。没错,我们已经进入了另一个系统,命令行提示符已经发生了变化。在这里你可以输入各种命令进行探索(Vagrant 虚拟机确实也很适合学习 Linux 操作系统),但是不要皮到 rm -rf /*
(从删库到跑路的惨痛经历历历在目)。
(在全新的世界里面畅游了许久……)
喔!别忘了我们登进这个系统的是为了部署我们的网站!赶紧回到正题。
“手动”部署网站
Nginx 是一款极其流行的高性能 HTTP 和反向代理服务器,这里我们就用它来架设我们的网站。首先需要在主机中安装 Nginx(Vagrant 虚拟机 root 密码默认是 vagrant):
vagrant@vagrant-ubuntu-trusty-64:~$ sudo apt-get update
vagrant@vagrant-ubuntu-trusty-64:~$ sudo apt-get install -y nginx
装好之后,我们陷入了沉思:怎么把我们的项目代码从自己的电脑搬到需要部署的主机上?需要注意的是,Vagrant 提供了本地文件系统和虚拟机文件系统的“同步绑定”(行话叫 mount,详情请查看 Vagrant 文档中关于 Syncing Folders 的描述),但是实际部署到主机时通常采用如下四种方法:
- 直接使用 vim 等工具将项目代码拷贝进远程主机
- 使用 GitHub、GitLab 或 Bitbucket 等代码托管服务
- 使用 Chef、Puppet、Ansible 等自动化部署工具
- 使用容器技术(Docker, Kubernetes等)
很显然第一种方法非常愚蠢,但是我们这里偏偏用第一种方法,因为我们这次只需要部署一个静态 HTML 文件。以后我会写使用容器技术部署的教程,敬请期待。
下面是我们的网页源码(index.html),用它替换掉 /usr/share/nginx/html/index.html 里面的文件(记得要 sudo
喔):
<html>
<head>
<title>My Project</title>
</head>
<body>
<h1>I Made It!</h1>
<p>I Have Successfully Deployed My First Project!</p>
</body>
</html>
然后我们重启 Nginx 服务:
vagrant@vagrant-ubuntu-trusty-64:~$ sudo service nginx restart
* Restarting nginx nginx [ OK ]
按 Ctrl + D 或输入 logout
,退出虚拟机。
vagrant@vagrant-ubuntu-trusty-64:~$ logout
Connection to 127.0.0.1 closed.
打开我们的浏览器,输入设定的内网 IP:http://192.168.33.10/。
哇塞,我们帅气的网页部署成功了!
以为这样就完了?并没有。时代在变化,你的项目代码也需要不断的修改,每次修改后如果想要让用户看到改变,都需要 SSH 登录到远程主机重复上述的部署流程。
如果你觉得这点劳动量还是不算什么,那么请设想一下业务扩展需要,需要 n 台主机来适应更大的用户访问量,并且我们的项目进化成了由多个微服务、数据库、消息队列、任务队列、负载均衡组成的庞然大物……更可怕的是,我们追求敏捷开发和快速迭代的产品经理要求每天都能部署……
总结一下,用传统的“手工”部署的方法存在以下问题:
- 过程机械而重复,十分枯燥乏味,而且很可能容易出错
- 大规模部署时效率非常低,无法适应持续部署/交付(CI/CD)的需求
科技界有个很流行的说法:偷懒是进步的动力。为了能够从繁琐的手动部署中解放出来,我们需要更先进的技术——没错,就是本文的主角 Ansible。
Ansible 介绍和安装
Ansible 是一款革命性的 IT 自动化工具,拥有平滑的学习曲线却又不失强大而又灵活的功能。和竞争对手 Chef、Puppet 或 Fabric 相比,拥有如下优势:
- 无需在部署主机上安装任何代理(Chef 和 Puppet 都需要安装)
- 无需学习特定的语言(Chef 使用的是 Ruby,Fabric 则是纯 Python 代码)
- 使用 YAML 记录执行流程,声明式语法,可读性好
- 基于模块,具有高度的复用性
- 能够和当前主流云计算平台、容器工具完美结合
首先安装 Ansible,可以查看官方的安装说明。通常我们使用 Python 的包管理工具 pip 进行安装:
$ sudo pip install ansible
检验我们是否安装成功了:
$ ansible --version
如果有版本提示信息,那么就安装成功了!
光说不练假把式,我们来感受 Ansible 的力量吧!
用 Ansible 部署单台主机
首先,我们要从零开始部署,把之前的虚拟机删掉:
$ vagrant destroy --force
==> default: Forcing shutdown of VM...
==> default: Destroying VM and associated drives...
重新访问 http://192.168.33.10/,我们发现之前部署成功的网站已经不能访问了。然后重新创建虚拟机:
$ vagrant up
由于我们之前已经下载过 Ubuntu 镜像,所以这次会快很多。
接下来就是进行 Ansible 的配置。有两种配置方式:第一种是全局配置,在 /etc/ansible 里面编辑配置文件;第二种是局部配置,在当前项目里面编辑配置文件。如果检测到了局部配置文件,那么就会忽略全局配置。通常建议使用局部配置,并将配置文件和 Playbook (可以理解为 Ansible 脚本)加入源代码管理系统中。
在当前 deploy-my-first-project 目录中,生成以下文件:
$ tree
.
├── Vagrantfile
├── ansible.cfg
├── deploy.yml
├── hosts
└── index.html
Vagrantfile 之前在手动部署时已经存在了。ansible.cfg 是 Ansible 的配置文件,内容如下:
[defaults]
inventory = hosts
remote_user = vagrant
private_key_file = .vagrant/machines/default/virtualbox/private_key
host_key_checking = False
- inventory 是主机清单,里面是我们需要部署的主机,这里我们指定为 hosts 文件(接下来会创建)
- remote_user 是远程登录的用户名,Vagrant 为我们提供了一个叫 vagrant 的用户用于登录
- private_key_file 指定了用于 SSH 登录的私钥文件,就相当于访问远程主机的一把钥匙(这里就不详细讲述 SSH 身份验证了,有兴趣可以自己了解一下)
- host_key_checking 是用于检查所登录主机的 key 是否与 .ssh/known_hosts 里面对应主机的 key 相同,在这里我们把这个功能关闭掉(设置为
False
),实际生产时不要关闭这个功能。
然后是 hosts 文件的内容,只有一条 IP 记录,对应我们需要部署的主机:
192.168.33.10
一切就绪!
使用 Ansible 通常有两种方式:
- Ad-Hoc 模式,又称为临时命令模式,输入一条命令执行一个操作。通常用于执行一些非常简单的操作,例如检查是否能与主机连接、获取主机正常运行时间(uptime)等等。
- Playbook 模式,执行一个 YAML 文件里面的全部指令,类似于执行脚本。Ansible 的功能强大之所在。
我们用一条 Ad-Hoc 指令检查是否能 SSH 登录到我们的虚拟机(192.168.33.10)。
$ ansible all -m ping
192.168.33.10 | SUCCESS => {
"changed": false,
"failed": false,
"ping": "pong"
}
成功了!
我们来理解一下 ansible all -m ping
这条命令。all
是指 inventory 中的所有主机,-m
是用于指定执行模块(module),这里选择 ping
模块。
需要注意的是,Ansible 执行所有指令都是基于模块的,然后在实际执行时将我们所选择的模块生成并执行对应的 Python 脚本,因此一次部署流程就对应一系列模块的执行。Ansible 的强大正是由小巧的内核和 1600 多个模块构成,几乎你能想到的操作流程都要对应的模块。例如, git
模块能够管理 Git 仓库,yum
模块能够操作 yum 包管理工具,docker
模块能够轻松管理 Docker 镜像和容器。
如果要查看某个模块怎么使用,只需要使用 ansible-doc
工具:
$ ansible-doc git
接下来我们就来写 Playbook 来指定我们的部署流程。下面是 deploy.yml 的内容:
---
- name: Deploy Static Site with Nginx
hosts: all
become: True
tasks:
- name: Install Nginx
apt: name=nginx update_cache=yes
- name: Copy index.html
copy: src=index.html dest=/usr/share/nginx/html/index.html mode=0644
- name: Restart Nginx
service: name=nginx state=restarted
Playbook 是熟练使用 Ansible 最重要的知识点。首先,你需要理解 YAML 这种格式的语法。如果不熟悉,建议读一下阮一峰的 YAML 语言教程。如果你对 JSON 比较熟悉的话,上面的 Playbook 等价于下面的 JSON:
[
{
"name": "Deploy Static Site with Nginx",
"hosts": "all",
"become": true,
"tasks": [
{
"name": "Install Nginx",
"apt": "name=nginx update_cache=yes"
}, {
"name": "Copy index.html",
"copy": "src=index.html dest=/usr/share/nginx/html/index.html mode=0644"
}, {
"name": "Restart Nginx",
"service": "name=nginx state=restarted"
}
]
}
]
我们来看一下 Playbook 的构成。
一个 Playbook 由多个 Play 组成,每个 Play 由多个 Task 组成。其中 Play 必须要有 hosts(目标主机)和 tasks(至少一个 Task)组成,每个 Task 必须要指定执行模块及其参数。我们还注意到每个 Play 和 Task 都有一个 name
属性,这个属性并不是必须的,但它能极大地增加 Playbook 的可读性,并且它的功能不只是“注释”——有些功能支持以 name
为单位进行操作,例如 ansible-playbook
支持一个参数叫 —start-at-task
,能够选择从哪个 Task 开始执行。
回到我们的 deploy.yml,里面只有一个名为 Deploy Static Site with Nginx
的 Play,指定执行主机为 all
,become
属性是可选的,用于执行 root 权限,然后 tasks
中包含三个任务: Install Nginx
、Copy index.html
和 Restart Nginx
,分别调用了 apt
、copy
和 service
模块。
执行我们的 Playbook:
$ ansible-playbook deploy.yml
PLAY [Deploy Static Site with Nginx] *****************************************
TASK [Gathering Facts] *******************************************************
ok: [192.168.33.10]
TASK [Install Nginx] *********************************************************
changed: [192.168.33.10]
TASK [Copy index.html] *******************************************************
changed: [192.168.33.10]
TASK [Restart Nginx] *********************************************************
changed: [192.168.33.10]
PLAY RECAP *******************************************************************
192.168.33.10 : ok=4 changed=3 unreachable=0 failed=0
我们在浏览器输入 192.168.33.10 试一下,果真部署成功了。等一下!我还想在网站里面加点东西。下面是新的网站代码(index.html),加了一行 I Added Something Interesting.
:
<html>
<head>
<title>My Project</title>
</head>
<body>
<h1>I Made It!</h1>
<p>I Have Successfully Deployed My First Project!</p>
<p>I Added Something Interesting.</p>
</body>
</html>
修改完成之后,我们重新部署:
$ ansible-playbook deploy.yml
...
PLAY RECAP *******************************************************************
192.168.33.10 : ok=4 changed=2 unreachable=0 failed=0
打开浏览器访问 http://192.168.33.10/,看到了我们刚才的改动:
没错!每次修改之后只需一行命令即可将改动立刻部署到远程主机!
用 Ansible 部署多台主机
现在我们的网站一炮走红,全国各地的网友争相访问!于是领导决定在北京、上海、广州三地分别架设主机,我们现在要将网站同时部署到三个主机上。
我们还是用 Vagrant 来模拟多台主机。先删掉之前的主机:
$ vagrant destroy --force
==> default: Forcing shutdown of VM...
==> default: Destroying VM and associated drives...
然后将 Vagrantfile 改为如下代码:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
# Use the same key for each machine
config.ssh.insert_key = false
config.vm.define "beijing" do |beijing|
beijing.vm.box = "ubuntu/trusty64"
beijing.vm.network "private_network", ip: "192.168.33.10"
end
config.vm.define "shanghai" do |shanghai|
shanghai.vm.box = "ubuntu/trusty64"
shanghai.vm.network "private_network", ip: "192.168.33.11"
end
config.vm.define "guangzhou" do |guangzhou|
guangzhou.vm.box = "ubuntu/trusty64"
guangzhou.vm.network "private_network", ip: "192.168.33.12"
end
end
然后启动我们的“集群”!
$ vagrant up
来看一下当前我们的“集群”是否真的在运行:
$ vagrant status
Current machine states:
beijing running (virtualbox)
shanghai running (virtualbox)
guangzhou running (virtualbox)
This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run `vagrant status NAME`.
很好!北京节点、上海节点和广州节点都运行正常。
接下来开始部署到这三台主机。听起来主机数量是之前的三倍,是不是工作量也应该是三倍?完全不是!实际上只需要修改两个文件即可:ansible.cfg 和 hosts,而且都只需要改一两行代码哦!
先修改 ansible.cfg:
[defaults]
inventory = hosts
remote_user = vagrant
private_key_file = ~/.vagrant.d/insecure_private_key
host_key_checking = False
这里只修改了 private_key_file
,由于三个主机都采用了同一个私钥(在 Vagrantfile 中配置的)。
然后在主机清单 hosts 里面加上三台主机:
192.168.33.10
192.168.33.11
192.168.33.12
接着还是执行熟悉的 Playbook:
$ ansible-playbook deploy.yml
PLAY [Deploy Static Site with Nginx] *****************************************
TASK [Gathering Facts] *******************************************************
ok: [192.168.33.11]
ok: [192.168.33.12]
ok: [192.168.33.10]
TASK [Install Nginx] *********************************************************
changed: [192.168.33.10]
changed: [192.168.33.12]
changed: [192.168.33.11]
TASK [Copy index.html] *******************************************************
changed: [192.168.33.12]
changed: [192.168.33.10]
changed: [192.168.33.11]
TASK [Restart Nginx] *********************************************************
changed: [192.168.33.11]
changed: [192.168.33.12]
changed: [192.168.33.10]
PLAY RECAP *******************************************************************
192.168.33.10 : ok=4 changed=3 unreachable=0 failed=0
192.168.33.11 : ok=4 changed=3 unreachable=0 failed=0
192.168.33.12 : ok=4 changed=3 unreachable=0 failed=0
最后打开浏览器分别访问三台主机,确认我们已经部署成功了。如果以后还想修改代码,还是一行命令执行 Playbook 即可。
总结
正如标题所言,一行命令真的可以操纵“千军万马”——只需执行特定的 Playbook,就可以让千万台主机自动化运行你想要的任务,而不再需要 SSH 远程登录手动执行繁琐的配置操作。有趣的是,Ansible 这个名字也是取自一本名为 Ender's Game 的科幻小说,在书中 Ansible 是一种能够同时控制一大群宇宙飞船的设备。
相关学习资源
- Ansible 官方文档:http://docs.ansible.com/
- Ansible 中文权威指南:http://ansible-tran.readthedocs.io/en/latest/index.html
- Ansible: Up and Running by Lorin Hochstein (O’Reilly)
- 未完待续...