很久以前,当我第一次读到“程序员修炼(The Pragmatic Programmer)”时,我读了一些让我真真难以忘怀的建议。
"不要使用手工流程(Don't Use Manual Procedures)"。
这在Ubiquitous Automation一节。总之,他们希望你将所有的事情都自动化。
麻烦的是,我对于如何真正的让任何东东都自动化并没有太多的想法。当时,我仍困于这样一个世界,其中所有的程序都是一个使用CLI的庞大的Java程序,而我的印象是,命令行界面无非是过去时代的一个垂死残余。
我的一些假装自动化之旅充满了陷阱和自我无能的尴尬故事,而我希望与大家分享这些故事。
与命令行系统交互的陷阱之一是,每一个复杂的交互使用一个同样复杂的命令 —— 而对于那些我可能会一次又一次运行的命令,每次我都将不得不记得精确的调用。
大多数时候,这个可以工作,虽然每次都要花不少的时间上谷歌或StackOverflow搜索。
关于幻想小说的一个持久的事情是,你总能看到追随者使用魔杖,但没有魔法书。这是因为他们都是愚蠢的。奇才携带魔法书。
所以,我开始写下我的命令,这样我就不会忘记它们。而不是将这些命令写到便利贴,或笔记本上,我将它们写到我的home目录下。
$ cat work
ssh classam@workcomputer.blorf -P 8888 -p=hunter2
起初,我只想cat这个文件,然后将该命令复制回命令行。
$ cat work
ssh classam@workcomputer.blorf -P 8888 -p=hunter2
$ ssh classam@workcomputer.blorf -P 8888 -p=hunter2
Welcome to Work Computer. Blorf.
我和你说了,我会分享我尴尬无能的故事。
我很快意识到,没有真的有打算,我写的是bash脚本 - 我只是没有明智地执行它们。如果我只是告诉系统我要像执行程序一样执行这个文件,那么它应该足够明智地工作。
$ chmod u+x work
$ ./work
Welcome to Work Computer. Blorf.
仅凭这一点是不够好到让这个脚本正常工作的,但我们没有做很多的事来帮助系统弄清楚到底如何运行这个我们传给它的神秘的脚本。
让我们想象下我们创建了一个文件:
print("hello")
这在Python 3中是有效的,但在Python 2中无效。如果我们将这个文件保存为'hello',那么我们基本不知道怎样运行它。而如果我们尝试运行,计算机将会进行猜测,然后它会猜错。
$ ./hello
idfk, man, that doesn't look right to me
如果我们将文件命名为hello.py,给我们自己一个方便的线索以便提示自己这个是个怎么样的文件,那么我们可以使用Python解释器来执行它。
# python hello.py
that's python 3, man, I'm python 2. Whateva.
# python3 hello.py
hello
好吧,它工作了,但对于程序调用来说压力山大。七十年代的一个Unix创新可以消除这个问题:hashbang。在我们脚本的开头,我们可以准确的声明想使用哪个解释器。
#!python3
print("hello")
#!bash
echo "hello"
这里唯一的规则是,编译器必须存在于系统的$PATH变量中。当你在本地运行该脚本时,是挺棒的,但如果你远程运行脚本而不使用一个伪终端会话集时,你或许并没有一个$PATH变量,这将导致你的hashbang声明失败。
解决方法?hashbang可以包含你打算用于程序的解释器的完整路径。
#!/bin/python3
print("hello")
#!/bin/bash
echo "hello"
在很多现代编程中,你没有看到这个令人兴奋的技术,因为它并不是引人注目的可便携的 —— 你用来执行一个python程序的解释器的位置,甚至是名字,往往系统与系统之间并不相同。
虽然,bash通常位于/bin/bash,这就是为嘛sun下的每个bash程序都用#!/bin/bash打开。
所以,不久以后,我的home目录到处都开始充斥着名字诸如work.sh
和test.sh
这样的有用的小的bash文件。
如果你在Django中编程,你可能还记得,你可以在Django环境中调用的每个命令都是通过直接引用程序manage.py
开始的。
我也许像这样启动Django开发服务器:
$ cd ~/code/django_project
$ ./manage.py runserver 0:8000
这很简单,但是,一天内我必须启动那个Django开发服务器无数次!
所有一切仅需对我的.bashrc
文件稍微做个调整:
alias dj="/home/classam/code/django_project/manage.py"
突然之间,无论我当前的工作目录是什么,都可以用下面这个简单的命令启动服务器
$ dj runserver 0:8000
这是一种全新的令人兴奋的感觉。alias工具会工作良好。
随着我构建了越来越多微小的自动化步骤到我的编程环境,为每个我想要执行的命令使用一个单独的文件开始变得不方便。
来到Make工具。
现在,这完全不是什么化妆工具 - Make是一个构建工具!它像下面这样将一个项目的所有自动化全部安排在了一个make文件中,从而满足了我的需求:
run:
manage.py runserver 0:8000
shell:
manage.py shell
test:
nosetests /home/vagrant/code/things
lint:
pyflakes /home/vagrant/code/things --no-bitching-about-long-lines
然后,我可以运行我那一群杂七杂八的任务,无需非得与一组分离的shell文件交互,像这样:
$ make lint
归根结底,这不比shell文件稳定或便利得多,并且它为一堆的项目引入了一个奇怪的Make依赖,而一个Make依赖并不具备任何实际意义。
但它确实将我引入了我的旅途中的下一站:尝试使用一个task-runner来取代使用脚本。
为嘛不用一个也存在于Python世界中的依赖来取代一个Make依赖呢?欢迎来到Fabric,一个优秀的Python命令运行器。如果比之Python,你对Ruby更加了解,那么你可能会记得Fabric邪恶的对手,Capistrano。
Fabric允许我像这样结构化我的测试运行:
from fabric.api import run
def go():
"""
Documentation documentation documentation.
"""
run('manage.py runserver 0:8000')
def test():
run('nosetests /home/vagrant/code/things')
def lint():
run('pyflakes /home/vagrant/code/things --no-bitching-about-long-lines')
然后,我可以这样调用:
$ fab go
我们开始吧,我们所有的工具脚本都在同一个便利的地方。
这相当漂亮,而我开始使用它来做任何事。甚至是部署远程服务器这种棘手的问题 —— Fabric支持得相当好。
只要告诉它SSH到一个远程服务器,或者一堆远程服务器一次,然后fabric能够在所有它们上运行我的脚本。
我开始用Fabric来搭建服务器。新的服务器会需要PostgreSQL和nginx,而让脚本为我设置这些,会比必须手动处理所有繁琐的安装步骤棒得多。
Fabric很棒,但有几件事它做的没那么漂亮。
在调试Fabric任务时,我在同一个服务器一遍又一遍的运行任务,而每一次它都会从头开始,运行整个脚本。如果有一个步骤很慢,那么它将会拖慢我的调试过程。
这意味着,当我使用它们时,我只会注释掉脚本的大块内容,测试脚本“头部”的同时留下尾部注释,这样我就可以推进我的自动化了。
有很多操作,第一次运行的时候会通过,但后续就全失败了,像“创建一个数据库”或者“创建一个目录”,这些操作第二次运行会失败,因为在我第二次运行的时候,该数据库或者目录已经存在了。
问题是啥?大多数的Fabric操作缺少"幂等性" - 一个五美元词,表示"每次运行同个操作总会产生相同的结果。"
另外一个问题是大量的配置文件。我发现自己经常要么编写丑陋的Bash操作来对大的配置文件进行小手术,要么使用字符串工具在python中构建配置文件,然后将它们放入到适当的位置。
最后一个问题?证书。为了让系统正常工作,证书需要放在哪里这个问题从来就还没搞得很清楚。我最终决定采取环境变量,但这意味着我建立的每个新系统,我都要马上立即提交三十几个变量到一个.bashrc
文件中。
这仍然工作得很好,但我们可以做的更好。幸运的是,那里有一个框架,可以带这些属性运行操作,并且还让我们牢牢地处在Python的范畴之内。Ansible。
Ansible提供了一种YAML格式,它可以用来描述一个工作中的系统,然后它将通过一个SSH连接构建那个系统。一旦我熬过了YAML中固有的讨人厌的“编码”,这就很棒!除其他事项外,Ansible还包含一个全功能的模板系统,用于创建配置文件,以及带双向加密的密码存储,用以隐匿重要的配置凭据。
Ansible解决了很多问题。
但是Ansible没有解决的一个问题是啥?它不是一个很好的任务运行器。
事实上,Ansible命令可以彻头彻尾的神秘运行。
在一组服务器上运行一个playbook看起来可能像这样:
ansible-playbook -i inventory.yml mongo -u root --vault-password-file ~/.vaultpw mongo.yml
我不了解你,但这不是那种我可以轻松交给肌肉记忆的事情。
所以,再一次,我们需要一个任务运行器。我们可以回到Fabric,但是Fabric的维护者基本上已经放弃了它,转而支持一个正显示出一些第二系统效应的明确迹象的非常野心勃勃的2.0版本 —— 最明显的征兆是,它已经开发4年了,但还没有看到光明的一天。
所以Fabric现在不予考虑 —— 但同时Fabric 2.0在夹缝中生存,Fabric 2.0一个半完成的块已经出现了。虽然在功能上有所限制,它,额……比Fabric得多。
它称为Invoke,并且它只提供Fabric一半的"任务运行器(task runner)",而不提供任何一点"ssh"或者"deployment"。但这就是我们想要的!如此完美!
所以我们可以像这样封装我们的ansible部署:
from invoke import task, run
@task
def provision_mongo(ctx):
ctx.run('ansible-playbook -i inventory.yml mongo -u root --vault-password-file ~/.vaultpw mongo.yml')
然后像这样运行它:
$ inv provision_mongo
我们可以用我们用来运行剩余应用的实用脚本来包含它。
@task
def go(ctx):
ctx.run('manage.py runserver 0:8000')
@task
def test(ctx):
ctx.run('nosetests /home/vagrant/code/things')
Invoke具有更多的功能,但是涵盖所有将会超出这个已经过长的博文的范围了。
显然的,我的可重复、有用的项目自动化之旅还有很长的路要走,但是我已经在invoke
和ansible
的交界处明确地发现了一些非常有用工具。
我们获得了Python所有的可组合性,Ansible所有的实用性,以及一个task runner所有的便利性。
一个最后的牢骚:Invoke漂亮地支持Python 3,但是Ansible仍然绑在了Python 2的Python黑暗时代,所以为了同时运行Invoke和Python,我们必须接受一个次等的Python。
该死。
据我所知,Javascript没有像这样的东东。我能找到最接近的东西是gulp
。难道我将不得不满足于gulp
吗?Yegh.