我的自动化之旅:为大家服务的自动化
很久以前,当我第一次读到 程序员修炼 (The Pragmatic Programmer) 时,我读了一些让我真真难以忘怀的建议。
不要使用手工流程 (Don't Use Manual Procedures)。
这在 Ubiquitous Automation 一节。总之,他们希望你将所有的事情都自动化。
麻烦的是,我对于如何真正的让任何东东都自动化并没有太多的想法。当时,我仍困于这样一个世界,其中所有的程序都是一个使用 CLI 的庞大的 Java 程序,而我的印象是,命令行界面无非是过去时代的一个垂死残余。
我的一些假装自动化之旅充满了陷阱和自我无能的尴尬故事,而我希望与大家分享这些故事。
bash 脚本编程
与命令行系统交互的陷阱之一是,每一个复杂的交互使用一个同样复杂的命令 —— 而对于那些我可能会一次又一次运行的命令,每次我都将不得不记得精确的调用。
大多数时候,这个可以工作,虽然每次都要花不少的时间上谷歌或 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.
Hashbang
仅凭这一点是不够好到让这个脚本正常工作的,但我们没有做很多的事来帮助系统弄清楚到底如何运行这个我们传给它的神秘的脚本。
让我们想象下我们创建了一个文件:
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 打开。
Bashy Bash
所以,不久以后,我的 home 目录到处都开始充斥着名字诸如 work.sh
和 test.sh
这样的有用的小的 bash 文件。
Alias
如果你在 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 是一个构建工具!它像下面这样将一个项目的所有自动化全部安排在了一个 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 来取代使用脚本。
Fabric
为嘛不用一个也存在于 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,而让脚本为我设置这些,会比必须手动处理所有繁琐的安装步骤棒得多。
Ansible
Fabric 很棒,但有几件事它做的没那么漂亮。
在调试 Fabric 任务时,我在同一个服务器一遍又一遍的运行任务,而每一次它都会从头开始,运行整个脚本。如果有一个步骤很慢,那么它将会拖慢我的调试过程。
这意味着,当我使用它们时,我只会注释掉脚本的大块内容,测试脚本“头部”的同时留下尾部注释,这样我就可以推进我的自动化了。
有很多操作,第一次运行的时候会通过,但后续就全失败了,像“创建一个数据库”或者“创建一个目录”,这些操作第二次运行会失败,因为在我第二次运行的时候,该数据库或者目录已经存在了。
问题是啥?大多数的 Fabric 操作缺少"幂等性" - 一个五美元词,表示"每次运行同个操作总会产生相同的结果。"
另外一个问题是大量的配置文件。我发现自己经常要么编写丑陋的 Bash 操作来对大的配置文件进行小手术,要么使用字符串工具在 python 中构建配置文件,然后将它们放入到适当的位置。
最后一个问题?证书。为了让系统正常工作,证书需要放在哪里这个问题从来就还没搞得很清楚。我最终决定采取环境变量,但这意味着我建立的每个新系统,我都要马上立即提交三十几个变量到一个 .bashrc
文件中。
这仍然工作得很好,但我们可以做的更好。幸运的是,那里有一个框架,可以带这些属性运行操作,并且还让我们牢牢地处在 Python 的范畴之内。Ansible。
Ansible 提供了一种 YAML 格式,它可以用来描述一个工作中的系统,然后它将通过一个 SSH 连接构建那个系统。一旦我熬过了 YAML 中固有的讨人厌的“编码” ,这就很棒!除其他事项外,Ansible 还包含一个全功能的模板系统,用于创建配置文件,以及带双向加密的密码存储,用以隐匿重要的配置凭据。
Ansible 解决了很多问题。
Invoke
但是 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 所有的便利性。
P.S.
一个最后的牢骚:Invoke 漂亮地支持 Python 3,但是 Ansible 仍然绑在了 Python 2 的 Python 黑暗时代,所以为了同时运行 Invoke 和 Python,我们必须接受一个次等的 Python。
该死。
P.P.S.
据我所知,Javascript 没有像这样的东东。我能找到最接近的东西是 gulp
。难道我将不得不满足于 gulp
吗?Yegh.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论