大家好!
我想谈谈过去六个月来我一直在从事的项目。 我在业余时间从事该项目,但是创建它的动机来自于主要工作中的观察。
在一个正在运行的项目中,我们使用微服务的体系结构,随着时间的流逝,越来越多的这些主要问题之一逐渐显现出来,那就是测试。 当某个服务依赖于五到七个其他服务,再加上其他一些(或什至几个)数据库来启动时,那么以“实时”形式进行测试非常不便。 您必须在各个方面都紧紧套上摩卡,甚至无法自己完成测试。 好吧,或者以某种方式组织一个可以真正启动所有依赖项的测试环境。
实际上,为了方便第二种选择,我只是坐下来写xenvman 。 简而言之,这类似于docker-compose和test容器的混合体,只是没有绑定到Java(或任何其他语言),并且具有通过HTTP API动态创建和配置环境的能力。
xenvman
用Go编写,并实现为简单的HTTP服务器,它使您可以使用可使用该协议的任何语言的所有可用功能。
xenvman可以做的主要事情是:
- 使用简单的JavaScript脚本灵活地描述环境内容
- 即时创建图像
- 创建正确数量的容器并将它们组合成一个隔离的网络
- 将环境的内部端口转发到外部,以便测试甚至可以从其他主机获得必要的服务
- 在旅途中动态更改环境的组成(停止,启动和添加新容器),而无需停止工作环境。
环境环境
xenvman的主要特征是环境。 这是一个孤立的气泡,其中启动了服务的所有必需依赖项(打包在Docker容器中)。

上图显示了xenvman服务器以及运行不同服务和数据库的活动环境。 每个环境都是直接从集成测试代码创建的,完成后将被删除。
模式
环境的一部分直接由模板决定,模板是JS中的小脚本。 xenvman具有此语言的内置解释器,并且在收到创建新环境的请求时,它仅执行指定的模板,每个模板将一个或多个容器添加到列表中以供执行。
选择JavaScript是为了允许动态更改/添加模板而无需重建服务器。 此外,模板通常仅使用语言的基本功能和数据类型(很好的老式ES5,没有DOM,React和其他魔术),因此即使对于完全了解JS的人,使用模板也不会造成特殊的困难。
模板是可参数化的,也就是说,我们可以通过在HTTP请求中传递某些参数来完全控制模板的逻辑。
即时创建图像
我认为,xenvman最方便的功能之一是在配置环境的过程中直接创建Docker映像。 为什么这有必要?
好吧,例如,在我们的项目中,为了获取服务的映像,您需要将更改提交到单独的分支,启动并等待直到Gitlab CI收集并填充映像。
如果仅一项服务已更改,则可能需要3-5分钟。
而且,如果我们正在积极地看到服务中的新功能,或者试图了解为什么它不起作用,来回添加旧的fmt.Printf
,或者经常以某种方式更改代码,那么即使延迟5分钟也可以消除性能(我们作为代码编写者)。 相反,我们可以简单地将所有必需的调试添加到代码中,在本地进行编译,然后将完成的二进制文件附加到HTTP请求。
获得此类批准后,模板将使用此二进制文件并在旅途中创建一个临时图像,我们可以从中启动容器,就好像什么都没发生一样。
例如,在我们的项目中,在服务的主模板中,我们检查参数中是否存在二进制信息,如果存在,则我们将在旅途中收集图像,否则,我们将下载latest
版本的dev
分支。 这两个选项的创建容器的进一步代码是相同的。
一个小例子
为了清楚起见,让我们看一下微型示例。
假设我们编写了某种奇迹服务器(我们称其为wut
),它需要一个数据库来存储所有内容。 好吧,作为基础,我们选择了MongoDB。 因此,为了进行全面测试,我们需要一个工作正常的Mongo服务器。 当然,您可以在本地安装和运行它,但是出于示例的简单性和可见性,我们假设出于某种原因很难做到这一点(对于实际系统中的其他更复杂的配置,这更像是事实)。
因此,我们将尝试使用xenvman来创建一个运行Mongo和wut
服务器的环境。
首先,我们需要创建一个基本目录 ,其中将存储所有模板:
$ mkdir xenv-templates && cd xenv-templates
接下来,创建两个模板,一个用于Mongo,另一个用于我们的服务器:
$ touch mongo.tpl.js wut.tpl.js
mongo.tpl.js
打开mongo.tpl.js
并在其中写入以下内容:
function execute(tpl, params) { var img = tpl.FetchImage(fmt("mongo:%s", params.tag)); var cont = img.NewContainer("mongo"); cont.SetLabel("mongo", "true"); cont.SetPorts(27017); cont.AddReadinessCheck("net", { "protocol": "tcp", "address": '{{.ExternalAddress}}:{{.Self.ExposedPort 27017}}' }); }
模板必须具有带有两个参数的execute()函数。
第一个是通过其配置环境的tpl对象的实例。 第二个参数(params)只是一个JSON对象,我们将使用它对模板进行参数化。
排队
var img = tpl.FetchImage(fmt("mongo:%s", params.tag));
我们要求xenvman下载docker mongo:<tag>
映像mongo:<tag>
,其中<tag>
是我们要使用的映像的版本。 原则上,这行代码等效于docker pull mongo:<tag>
,唯一的区别是tpl
对象的所有功能本质上都是声明性的,也就是说,只有在xenvman执行了环境配置中指定的所有模板之后,才会实际下载映像。
获得图像后,我们可以创建一个容器:
var cont = img.NewContainer("mongo");
同样,容器不会在这个地方立即创建,可以这么说,我们只是声明了创建它的意图。
接下来,我们在容器上贴一个标签:
cont.SetLabel("mongo", "true");
使用快捷方式可以使容器在环境中相互找到对方,例如在配置文件中输入IP地址或主机名。
现在我们需要将内部Mongo端口(27017)挂出。 这样很容易做到:
cont.SetPorts(27017);
在xenvman报告成功创建环境之前,最好确保所有服务都不仅在运行,而且准备接受请求。 Xenvman已为此做好了准备检查 。
为我们的mongo容器添加一个这样的:
cont.AddReadinessCheck("net", { "protocol": "tcp", "address": '{{.ExternalAddress}}:{{.Self.ExposedPort 27017}}' });
我们可以看到,在地址栏中有一些存根,可以在启动容器之前动态替换必要的值。
代替{{.ExternalAddress}}
将替换运行xenvman的主机{{.ExternalAddress}}
外部地址,并且代替{{.Self.ExposedPort 27017}}
将替换转发到内部27017 {{.Self.ExposedPort 27017}}
外部端口。
在此处阅读有关插值的更多信息。
所有这些的结果是,我们可以连接到在环境中运行的Mongo,例如从外部运行测试的主机。
wut.tpl.js
因此,c处理完monga之后,我们将为wut
服务器编写另一个模板。
由于我们要随时随地收集图像,因此模板会略有不同:
function execute(tpl, params) { var img = tpl.BuildImage("wut-image"); img.CopyDataToWorkspace("Dockerfile");
由于我们BuildImage()
此处BuildImage()
图像,因此我们使用BuildImage()
而不是FetchImage()
:
var img = tpl.BuildImage("wut-image");
为了组装图像,我们将需要几个文件:
Dockerfile-有关如何组装映像的实际说明
config.toml-我们的wut
服务器的配置文件
使用img.CopyDataToWorkspace("Dockerfile");
方法img.CopyDataToWorkspace("Dockerfile");
我们将Dockerfile从模板数据目录复制到临时工作目录 。
数据目录是一个目录,我们可以在其中存储模板工作所需的所有文件。
在临时工作目录中,我们复制进入映像的文件(使用img.CopyDataToWorkspace())。
以下内容:
我们直接以编码形式(base64)的形式将服务器的二进制信息传递给参数。 在模板中,我们只需对其进行解码,然后将结果字符串保存为工作目录下的文件,名称为wut
。
然后创建一个容器并将配置文件装入其中:
var cont = img.NewContainer("wut"); cont.MountData("config.toml", "/config.toml", {"interpolate": true});
调用MountData()
意味着来自数据目录的config.toml
文件将以/config.toml
的名称安装在容器内。 interpolate
标志告诉xenvman服务器在装入文件之前,应替换那里的所有存根。
配置如下所示:
{{with .ContainerWithLabel "mongo" "" -}} mongo = "{{.Hostname}}/wut" {{- end}}
在这里,我们寻找带有mongo
标签的容器,并替换其主机名,无论该环境如何。
替换后,该文件可能如下所示:
mongo = “mongo.0.mongo.xenv/wut”
接下来,我们再次发布端口并开始准备检查,这次是HTTP:
cont.SetPorts(params.port); cont.AddReadinessCheck("http", { "url": fmt('http://{{.ExternalAddress}}:{{.Self.ExposedPort %v}}/', params.port), "codes": [200] });
我们的模板已准备就绪,可以在集成测试代码中使用它们:
import "github.com/syhpoon/xenvman/pkg/client" import "github.com/syhpoon/xenvman/pkg/def"
似乎编写模板将花费太多时间。
但是,通过正确的设计,这是一项一次性的任务,然后可以通过传递某些参数来对其进行微调,从而使相同的模板可以越来越多地重用(甚至用于不同的语言!)。 如您在上面的示例中看到的,由于我们将所有环境都放在模板中,因此测试代码本身非常简单。
这个小例子并没有显示xenvman的所有功能,此处提供了更详细的分步指南。
客户群
当前有两种语言的客户:
去吧
巨蟒
但是添加新的并不困难,因为提供的API非常非常简单。
网页界面
在2.0.0版中,添加了一个简单的Web界面,您可以使用该界面管理环境并查看可用的模板。



xenvman与docker-compose有何不同?
当然,有很多相似之处,但是与文件中的静态配置相比,xenvman在我看来似乎稍微更灵活,更动态。
我认为这是主要的区别特征:
- 绝对所有的控制都是通过HTTP API进行的,因此我们可以使用能理解HTTP的任何语言的代码来创建环境
- 由于xenvman可以在其他主机上运行,因此我们甚至可以在未安装docker的主机上使用其所有功能。
- 动态生成图像的能力
- 在操作过程中更改环境组成(添加/停止容器)的能力
- 通过使用可参数化的模板,减少了样板代码,改善了结构,并具有重用配置代码的能力
参考文献
Github项目页面
详细的分步示例,英语。
结论
仅此而已。 我计划在不久的将来增加机会
调用模板中的模板,从而使您可以更高效地组合它们。
我将尽力回答任何问题,如果有人发现该项目有用,我将感到高兴。