VAGRANT

新的开发同事入职,总是要去帮他们重新搭建开发环境,刚开始一两个倒觉得还好,花半天时间就可以搞定;但随着新加入人数越来越多,而且还经常会碰到同事电脑上的某个服务没启动,导致项目没法跑起来,又要帮忙调试,这些无脑的体力活也变得越来越冗长乏味。消除重复,是程序猿的天生职责,工作的重复也不例外。

于是我找到了Vagrant,其实早在2014年就听说,只是当时团队还很小,而且每个人都能驾驭自己的开发环境,所以没多留意,只是知道有这么个东西。现在回想,当时真是目光短浅,早用起来就不会发上面那段牢骚了。下面进入正题。

什么是Vagrant?

Vagrant为开发环境而生,是用于创建并配置轻量的、可重现的、可移植的开发环境。

为什么要用Vagrant?

据我了解,目前团队合作的开发环境大致分为三种:

第一种:每个人的电脑都有一套独立配置的开发环境。这种方式如果在项目中需要依赖一个新的服务,那所有人的电脑都要挨个装个遍,虽然这个情况不会经常出现,一旦出现就是灾难。

第二种:有一台专供开发的服务器,服务器上每个人都有独立的空间用于托管并运行自己的代码。这种方式较为高级,成本相对也较高,但是也有一个致命的短板,如果有一个人把这台服务器搞挂了,那整个团队跟着歇菜。

第三种:使用类似Vagrant的虚拟化方式。过去你可能经常听说:喂,哥们,我代码更新了,你拉一下。使用Vagrant后,你可以:那个谁,我把开发环境更新了,你拉一下。

当然可能还有些土豪公司的做法是从第二种衍生出来,每个人有独立的开发服务器,有专职的运维同学统一配置,这里就不做讨论了。
对比以上三种方式,使用虚拟化的方便程度溢于言表。

Vagrant的工作原理

你看了上面那段可能还觉得一头雾水,你叫我拉一下,是拉什么?(呃,这位同学,不要想歪了)

如果使用传统的虚拟机搭建开发环境,大致步骤如下:

  1. 下载一个虚拟化的软件,如:VMare,VirtualBox;
  2. 然后找一个系统安装包安装;
  3. 安装好了之后,进入系统,配置开发环境;
  4. 配置代码目录共享到虚拟系统;
  5. 大功告成,现在可以本地写代码,虚拟机执行了。

Vagrant的工作原理与上面类似,只是做了抽象,让配置和迁移更简单。

  1. 首先需要安装一个Provider,这个Provider就是为Vagrant提供虚拟化服务的软件;
  2. 下载一个Box,相当于上面的安装包,进入代码目录初始化;
  3. 进入虚拟实例,配置开发环境;
  4. 退出并关闭虚拟实例,将当前环境打包成一个新的Box
  5. 将这个Box共享给其他小伙伴,然后你们就可以快乐幸福地用同样的开发环境写代码了。

Vagrant的从0到1

1 下载VirtualBox

前往官网下载:https://www.virtualbox.org/wiki/Downloads,建议把Extension Pack也装上

2 下载Vagrant

前往官网下载:https://www.vagrantup.com/downloads.html

3 为Vagrant新增Box

使用命令vagrant box add {name} {url},其中{name}是自定义的名称(可选),{url}可以是远程的文件地址或本地的文件路径,考虑到国内复杂的网络环境,尽量还是先下到本地。还有一定要从官方下载box,吸取15年xcode ghost事件的教训。

下载http://files.vagrantup.com/precise64.box~/Downloads/precise64.box

执行:

> vagrant box add my-php-dev ~/Downloads/precise64.box

4 初始化

进入到我们的项目目录~/Documents/php/project初始化,使用上一步创建的box:

> cd ~/Documents/php/project
> vagrant init my-php-dev

初始化后产生了一个Vagrantfile文件,这是vagrant对当前项目的配置文件,以下有几个配置建议

4.1 网络配置

如果你想通过一个固定的ip访问到你的项目,那给这个虚拟实例设置一个固定的私有ip,注意不要与其他ip产生冲突:

config.vm.network "private_network", ip: "192.168.100.100"

这样可以方便我们配置一个host指向这个实例,如:

> vi /etc/hosts
192.168.100.100 dev.project.com

如果你的虚拟实例还需要访问外部的一些资源,比如局域网中的数据库,那么就需要为它桥接一个外网:

config.vm.network "public_network",
  # 默认由dhcp分配ip
  use_dhcp_assigned_default_route: true,
  # 桥接的方式,可设置多个
  bridge: ["en0: Wi-Fi (AirPort)", "en1: Wi-Fi (AirPort)"]

这里直接通过wifi连到局域网

4.2 工作目录配置

Vagrant启动后会把当前的项目目录挂载到/vagrant,但可以修改:

# 将当前目录mount到/var/www/project
config.vm.synced_folder ".", "/var/www/project", 
  # 如果不存在则创建
  create: true

更多配置请参考官方文档

5 启动

> vagrant up

启动后会创建一个Vagrant的工作目录.vagrant

如果启动时报错:

Failed to mount folders in Linux guest. This is usually because
the "vboxsf" file system is not available. Please verify that
the guest additions are properly installed in the guest and
can work properly. The command attempted was:

mount -t vboxsf -o uid=`id -u vagrant`,gid=`getent group vagrant | cut -d: -f3` vagrant /vagrant
mount -t vboxsf -o uid=`id -u vagrant`,gid=`id -g vagrant` vagrant /vagrant

The error output from the last command was:

stdin: is not a tty
mount: unknown filesystem type 'vboxsf'

安装vagrant-vbguest插件

> vagrant plugin install vagrant-vbguest

如果还没解决,请参考https://github.com/mitchellh/vagrant/issues/3341

6 配置环境

登录到实例

> vagrant ssh

默认是使用vagrant用户登录。

上面安装的box是ubuntu,可参考http://www.cnblogs.com/CheeseZH/p/4694135.html安装,如果安装提示权限错误,加上sudo就可以

Vagrant的从1到n

1 打包

打包前需要将实例停止

# 停止实例
> vagrant halt
# 重新打包
> vagrant package

打包后会在当前目录生成一个package.box文件

2 分发

根据上面的步骤,Vagrant会在当前目录产生3个文件:

  1. 配置文件:Vagrantfile
  2. 工作目录:.vagrant
  3. 新的Box:package.box
2.1 初级分发

初级的分发方式很简单,就是手动分发,把Vagrantfilepackage.box拷给你的同事,假设他的项目目录是/mydev/php/project,当然在这之前他的电脑上必须预先安装好了VirtualBoxVagrant

第一次初始化开发环境:

# 将Vagrantfile拷到项目根目录
> mv Vagrantfile /mydev/php/project/
# 将package安装到vagrant,注意box的命名需与上文一致
> vagrant box add my-php-dev package.box
# 进入项目目录
> cd /mydev/php/project/
# 启动
> vagrant up

如果开发环境有变更,生成了新的package.box,重新拷贝过来,需要执行:

# 关闭并销毁当前虚拟实例
> vagrant destroy
> vagrant box remove my-php-dev

再重复上面第一次的步骤。

2.2 高级分发

上面初级分发虽然简单,但中间还是有很多人工干预的步骤,这增加了操作失误的概率,也无法实现上文提到“拉一下”的效果。

下面就说说如何实现“拉一下”的效果,需要搭建一个主机托管不同版本的Box。以下内容参考自self-hosted-vagrant-boxes-with-versioning

2.2.1 定义Box存放的目录结构
- /var/www
    `- vagrant                          # 根目录
       `- my-php-dev                    # Box文件夹
          |- boxes                      # 存放所有不同版本的Box
          |  |- my-php-dev_0.1.0.box    # 版本 0.1.0
          |  `- my-php-dev_0.1.1.box    # 版本 0.1.1
          `- my-php-dev.json            # Box的目录索引文件

根目录vagrant用来存放所有的Box集合,也许你还会建一个my-java-dev

2.2.2 配置Web服务

进入到服务器,确保服务器已安装好nginx,创建2.2.1中的目录结构:

> mkdir -p /var/www/vagrant/my-php-dev/boxes

新增配置文件:

> vi /etc/nginx/sites-enabled/vagrant

内容为:

server {
    listen   80;

    server_name vagrant.myhost.com;

    root /var/www/vagrant;

    # 访问box目录时,返回目录索引文件
    # e.g. http://vagrant.myhost.com/my-php-dev/ 解析为 /var/www/vagrant/my-php-dev/my-php-dev.json  
    location ~ ^/([^\/]+)/$ {
        index $1.json;
        try_files $uri $uri/ $1.json =404;
        autoindex off;
    }

    # 允许索引boxes目录
    location ~ ^/([^\/]+)/boxes/$ {
        try_files $uri $uri/ =404;
        autoindex on;
        autoindex_exact_size on;
        autoindex_localtime on;
    }

    # 修改json文件的header
    location ~ \.json$ {
        add_header Content-Type application/json;
    }

    # 修改json文件的header
    location ~ \.box$ {
        add_header Content-Type application/octet-stream;
    }

    # 阻止直接访问根目录
    location ~ ^/$ {
        return 403;
    }
}

重新载入nginx配置:

> service nginx reload
2.2.3 新增Box的目录索引文件

在服务器中进入box目录,添加目录索引文件:

> cd /var/www/vagrant/my-php-dev/
> vi my-php-dev.json

内容为:

{
    "name": "my-php-dev",
    "description": "my php development box",
    "versions": [{
        "version": "0.1.0",
        "providers": [{
            "name": "virtualbox",
            "url": "http://vagrant.myhost.com/my-php-dev/boxes/my-php-dev_0.1.0.box",
            "checksum_type": "sha1",
            "checksum": "d3597dccfdc6953d0a6eff4a9e1903f44f72ab94"
        }]
    },{
        "version": "0.1.1",
        "providers": [{
            "name": "virtualbox",
            "url": "http://vagrant.myhost.com/my-php-dev/boxes/my-php-dev_0.1.1.box",
            "checksum_type": "sha1",
            "checksum": "0b530d05896cfa60a3da4243d03eccb924b572e2"
        }]
    }]
}

box文件的checksum可通过openssl sha1生成:

> openssl sha1 /var/www/vagrant/my-php-dev/boxes/my-php-dev_0.1.0.box
SHA1(/var/www/vagrant/my-php-dev/boxes/my-php-dev_0.1.0.box)= d3597dccfdc6953d0a6eff4a9e1903f44f72ab94
2.2.4 上传你的Box

上面我们打包时生成的package.box,可以重命名为带版本号的文件名my-php-dev_0.1.0.box,也可以在打包时指定:

> vagrant package --output 'my-php-dev_0.1.1.box'

my-php-dev_0.1.0.boxmy-php-dev_0.1.1.box都上传到服务器的/var/www/vagrant/my-php-dev/boxes/目录中。

上传完成后,访问http://vagrant.myhost.com/vagrant/my-php-dev/boxes/应该会列出所有box文件。

大放送一个php脚本,自动生成目录索引文件:

<?php

$boxName = isset($argv[1]) ? $argv[1] : '';

if (!$boxName) {
    echo 'The box name is required' . PHP_EOL;
    exit(1);
}

$boxBaseDir = __DIR__ . '/' . $boxName;
$boxJsonFile = $boxBaseDir . '/' . $boxName . '.json';
$boxesDir = $boxBaseDir . '/boxes';

if (!file_exists($boxBaseDir) || !file_exists($boxJsonFile) || !file_exists($boxesDir)) {
    echo 'The box is not inited' . PHP_EOL;
    exit(1);
}

$meta = json_decode(file_get_contents($boxJsonFile), true);
$regVersionBox = '/_(\d+\.\d+\.\d+)\.box$/';

$boxes = array_filter(scandir($boxesDir), function($filename) use ($regVersionBox) {
    // 过滤掉不包含版本号的box文件
    return $filename != '.' && $filename != '..' && preg_match($regVersionBox, $filename);
});

$meta['versions'] = isset($meta['versions']) ? $meta['versions'] : array();
if (!isset($meta['baseurl'])) {
    echo 'Warning! baseurl is not defined in json' . PHP_EOL;
}

foreach ($boxes as $box) {
    preg_match($regVersionBox, $box, $match);
    $version = $match[1];
    $isVersionExist = false;
    foreach ($meta['versions'] as $ver) {
        if ($ver['version'] == $version) {
            $isVersionExist = true;
            break;
        }
    }
    if ($isVersionExist) {
        continue;
    }

    $checksum = sha1_file($boxesDir . '/' . $box);
    $meta['versions'][] = array(
        'version' => $version,
        'providers' => array(
            array(
                'name' => 'virtualbox',
                'url' => $meta['baseurl'] . '/' . $boxName . '/boxes/' . $box,
                'checksum_type' => 'sha1',
                'checksum' => $checksum
            )
        )
    );
}

$metaJson = json_encode($meta, JSON_PRETTY_PRINT);
file_put_contents($boxJsonFile, $metaJson);
echo $metaJson . PHP_EOL;
?>

将以上脚本保存到服务器目录/var/www/vagrant/中,存为autoversion.php,每次上传完新版本的box文件后,执行:

> cd /var/www/vagrant/
> php autoversion.php my-php-dev

就可以自动生成新的目录索引文件了。

2.2.5 修改Vagrantfile

修改本地项目中的Vagrantfile,在

config.vm.box = "my-php-dev"

之后添加:

config.vm.box_url = "http://vagrant.myhost.com/vagrant/my-php-dev/my-php-dev.json"
2.2.6 拉一下

Vagrantfile与项目代码一样放到版本控制里,注意把.vagrant目录在版本控制中忽略。

Vagrantfile中有一个配置被注释了:

# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false

就表示box_check_update默认是开启的,也就是说执行vagrant up时会自动检查Box是否有更新。

> vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Checking if box 'my-php-dev' is up to date...
==> default: A newer version of the box 'my-php-dev' is available! You currently
==> default: have version '0.1.0'. The latest is version '0.1.1'. Run
==> default: `vagrant box update` to update.
...

按照提示,执行vagrant box update就可以更新到最新的版本。

到此为止,我们的开发环境已经可以一键拉取了。

总结

使用Vagrant搭建开发环境,将大大减少重复配置的工作,是时候跟“代码在我这运行没问题”说再见了。