在Docker中运行更多东西

2020-12-27 08:03:27

在2020年,Docker是分发和运行大多数面向开发人员的软件的最佳媒介。 Docker非常适合为您的企业网络应用程序构建和部署工件,这一点已被广泛接受,但是在诸如开发人员工具之类的事情上,这鲜为人知。但是,与以常规方式安装和运行它们相比,在容器中运行工具具有许多好处,我们都应该开始做更多的工作。

我在个人或工作计算机上安装的东西很少。我没有安装terraform,aws,node或pip,但是我一直都在使用它们。我每个人都有一个Docker映像,并且以最小的权限在容器中运行它们。我绝对不是唯一的人,但它没有应有的受欢迎。这些工具实际上都不需要完全访问我的计算机即可完成工作,但这通常是它们的运行方式。

此时,由于Docker Inc.在该领域的投资(适用于Mac和Windows的Docker),Docker随处可见,并且您可以免费获得跨平台支持。这对于开发/分发工具的人员以及需要共享工具的团队中的人员都是有用的。您只有一个Docker映像,它将几乎在所有地方运行。操作系统软件包管理器可能很棒,但是它们并不是跨平台的。诸如pip install之类的东西有时可以跨平台运行,但是还有其他严重的缺点。

尽管每个平台都有其自己的沙箱机制,但与Docker一起运行可让您指定运行时上下文并以跨平台方式强制执行沙箱,这在您期望其他人运行与您相同的命令时非常有用。

当您运行docker时,您必须明确说明特权。默认情况下,大多数容器都处于沙盒状态且没有特权。它无权访问周围环境变量。它无权访问主机系统的磁盘。像jq这样的工具只需要读取stdin并打印到stdout。它不需要访问我的Shell的环境变量(或者,如果需要,我可以将它们明确传递给容器)。仅在工作目录上,也可能在缓存目录上运行,yarn应该可以正常运行。我不希望它可以访问我的〜/ .aws目录(出于明显的原因)。

有些工具确实需要访问事物。我希望aws CLI能够读取〜/ .aws,因此我明确地授予了它。这使得该工具运行起来更加冗长,但缺乏魔力。

在容器中运行程序与正常运行非常相似,但是用户无需跳动即可配置系统,构建和安装。图像的开发人员会跳过这些箍,并通过简单的界面生成可运行的工件。无论该工具是用Python还是Rust或C或其他语言编写,该接口都是相同的。

下载预编译的二进制文件几乎像这样,除非赔率更差。也许有适合您的架构的版本。如果它是静态链接,那么您就很高兴。否则,请使用ldd反向工程需要安装libjpeg的事实。

Docker镜像“行之有效”。它捆绑了需要运行的内容。

如果您考虑一下,执行pip install awscli很奇怪。对于最终用户而言,该工具是用Python编写的,这无关紧要,而要求他或她设置并使用Python工具则毫无意义。我并不是要特别选择pip或awscli,但这对于分发非库软件来说是一种不良的机制。剩下太多的机会了。这是一个笨拙且漏水的界面,无法分发工具。 npm install也是如此。告诉别人通过安装golang来安装工具,然后运行go build。不用了,谢谢。如果我要入侵该项目,那么一定要这样做。但是不要强加给最终用户。

合作时,人们必须运行相同版本的软件以获得一致的结果,这一点很重要。版本固定对此至关重要。固定依赖清单固然很好,但这还不够:仅涵盖使用语言包管理器进行安装的一种情况。它可能不涵盖使用相同的linter版本,或相同版本的节点,aws,ansible,terraform或在操作系统级别安装的任何库。调用docker run node:13.10.1而不是用户碰巧已作为节点安装的任何东西,通常都可以解决此问题。能够在使用时指定版本,而不是在其他安装过程中带外指定版本,这也很方便和整洁。

与Docker并存运行不同版本的工具很容易。 Docker可以比Python的virtualenv,Ruby的rvm等更广泛地解决此问题。您可以在调用该工具时指定使用哪个版本的工具,并且它不仅可以固定工具的版本,还可以固定很多上下文。对于重现性总是首选。

在最近的一种工作环境中,当我们将运行时从Python 3.6.5升级到Python 3.6.8时,测试用例开始失败。具有使用任何版本的Python轻松运行测试的能力,可以很容易地一分为二并确定3.6.7中的更改是原因。可以在没有Docker的情况下进行调试,但是对于Docker来说,这自然而又容易。

使用docker run调用工具时,应指定在其他位置可重复运行该工具所需的所有内容。它正在运行该工具的某些特定版本?好的。需要我的AWS凭证吗?好的。是否需要一些特定的环境变量组合设置?好的。

当我看到Makefile或构建说明说要运行yarn或terraform或go时,我会感到畏缩。什么版本的?关于我的环境有什么假设?也许这在18个月前就已经在您独特的机器雪花上起作用了,但是现在祝您好运。 (我的笔记本电脑也是独一无二的雪花。每个人都是,直到我们都知道如何使用NixOS为止。)

在Docker中运行工具时,除了安装Docker之外,对运行时环境几乎没有期望。所有其他要求应在docker run命令中明确提出。您在本地运行的命令在同事的计算机上以及配置最少(或没有配置)的任何CI中都可以使用。这绝对至关重要,尤其是在团队合作中。这比期望(要求)任何人的系统或CI从属设备“建立”要强得多。

除了基本操作系统外,我的主机系统上还安装了很少的东西。设置新计算机时无需记住,升级过程中出错的地方更少,共享库发生冲突的机会也更少。

我对所有一直运行的工具都有bash别名。这些只是为了我自己的方便。对于与他人共享的任何内容,我将使用项目的Makefile(请参见下文)。

别名aws =' docker run --rm -v〜/ .aws:/。aws -v" $(pwd)":" $(pwd)" -w" $(pwd)" -u 1000:1000 -e AWS_PROFILE mikesir87 / aws-cli:1.18.11 aws' alias jq =' docker run -i --rm jess / jq jq&alias terraform =' docker运行-it --rm -v〜/ .aws:/。aws -v" $(pwd)":" $(pwd)" -w" $(pwd)" -u 1000:1000 hashicorp / terraform:0.12.23'

使用这些别名,我可以AWS_PROFILE = ... aws sts get-caller-identity | jq -r .Arn,就像“确实”安装了它们一样。

别名zoom =' xhost + local:docker \&& docker运行-it --rm -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY \ --device / dev / video0 --device / dev / snd:/ dev / snd --device / dev / dri -v / dev / shm:/ dev / shm \ -v〜/ .config / zoom / .zoom:/root/.zoom -v〜/ .config / zoom / .config / zoomus.conf:/ root / .config / zoomus.conf \ jess / zoom-us'

请注意,除非我们明确允许容器在主机上声明它的端口,否则端口19421仍将顽固关闭。

我也用其他东西做。这是Snes9x(您可以想象安装吗?):

别名snes9x =' docker run -it --rm -u 1000:1000 -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY \ -v / run / dbus:/ run / dbus -v / dev / shm:/ dev / shm \ --device / dev / snd --device / dev / dri --device / dev / input / js0 \ -e PULSE_SERVER = unix:$ XDG_RUNTIME_DIR / pulse / native- v $ XDG_RUNTIME_DIR / pulse / native:$ XDG_RUNTIME_DIR / pulse / native \ --group-add $(获取的组音频| cut -d:-f3)\ -v〜/ .config / snes9x:/。snes9x / -v〜 / Games / SNES:/ SNES -v〜/ .local / share:/。local / share \ danniel / snes9x'

对于特定于项目或在团队环境中的事情,应将所有有用的命令编入Makefile之类的文件中。这包装了docker run指令的复杂性和冗长性,可以轻松共享它们,并使其符合人体工程学。

在为该网站撰写文章时,我运行make hugo-watch并在网络浏览器中加载http:// localhost:1313:

雨果=码头工人运行--rm -u $$(id -u):$$(id -g)-v" $$(pwd)":/ src -v" $$ (pwd)" /输出:/目标$(2)klakegg / hugo:0.54.0-ext-alpine $(1)hugo-watch:mkdir -p output $(call hugo,server,-it -p 1313:1313)

更漂亮= docker run -i --rm -v" $$(pwd)":" $$(pwd)" -w" $$(pwd)" elnebuloso /更漂亮:1.19.1 $(1)" src / ** / *。js" format:$(调用更漂亮)format-check:$(调用更漂亮,--check)

我们将运行make format设置代码格式,并执行format-check检查样式。它运行在我的Linux机器上,运行在我的同事的Mac上,并且运行在任何配备Docker的CI中。这些机器都不需要安装节点,npm或漂亮的机器。我们完全没有考虑版本控制和环境同步的问题:版本仅在Makefile中的此处指定一次,并且在任何地方都遵循。

在像Python这样的语言中,库被迫为控制传递性依赖版本而死,将black或flake8之类的工具从项目的require.txt中移出,并放入一个自包含的Docker映像中,可能会很大简化。

run_container = docker run -i --rm -u $$(id -u):$$(id -g)-v" $$(pwd)":" $$(pwd )" -w" $$(pwd)" $(3)$(1)$(2)go = $(调用run_container,golang:1.14.0-buster,$(1),-e GOCACHE = / tmp / .cache -v" $$( pwd)" / build / go:/ go)格式:$(call go,gofmt)test:$(call go,go test)编译:$(call go,go build -o build / out)

我在Go上工作不多,但是这些存根提供了如何运行的想法。

我喜欢将Python项目的范围限制在其自己的目录中,我可以通过设置PYTHONUSERBASE并运行pip install --user来实现。它可能看起来像这样:

run_container = docker run -i --rm -u $$(id -u):$$(id -g)-v" $$(pwd)":" $$(pwd )" -w" $$(pwd)" $(3)$(1)$(2)python = $(调用run_container,python:3.8.2-alpine3.11,$(1),-e PYTHONUSERBASE =" $$(pwd)&#34 ; /供应商$(2))依赖关系:$(调用python,pip install --user -r requirements.txt,-e XDG_CACHE_HOME = $(user_cache_dir)-v" $(user_cache_dir)":& #34; $(user_cache_dir)")repl:$(call python,python)运行:$(call python,python -m app.main)

上面,我尝试引用其他人维护的公开图片,以简化示例。在敏感的用例中,最好保留自己信任的一组映像,无论它们是在Docker Hub上的命名空间,还是使用您自己付费或运行的注册表来命名。

在工作目录中进行卷操作时,通常会重用目录结构($(pwd):$(pwd)),只是因为它看起来像是“自然的”选择(从数学意义上来说)。很多人将其工作目录添加到/ app之类的文件,这也很好。记住要引用目录路径,以防其中包含空格。例如,在詹金斯(Jenkins)工作中,名称中带有空格的情况有时会出现这种情况。本着“应随处可见”的精神,通常最好做引号。

我总是尝试与非root用户一起运行,主要是因为我希望写入绑定绑定的内容归我的用户所有。我经常使用-u $ {id -u):$(id -g)来提高灵活性,但是在我自己的bash别名中,我谨慎地对待了硬编码1000。我对使用用户名称空间做了一些研究,但是这似乎比我的用例值得的投入更大。

在适用于Mac的Docker上,许可权限似乎并不是问题:即使是通过根目录写入容器的绑定文件最终也会在用户拥有的主机系统上挂载。我不知道在适用于Windows的Docker中是否也是如此(从未尝试过)。

提供-u ...选项时,无需将该用户添加到Docker映像中。在大多数情况下,我正在运行没有用户的图像。这会造成一些奇怪的情况。您的HOME为空白,因此有许多工具想要写入/.cache或/.config,但它们不能:没有写入权限。有时设置-e HOME = / tmp就足够了,只需为工具提供可写位置即可。有时您会希望在该位置挂载一个主机目录,以便在容器运行期间持久化缓存/配置/所有内容。有时甚至值得在映像或容器中放置一个小/ etc / passwd文件,定义用户并为其提供一个主目录(我认为唯一要做的是在terraform映像中放置git / OpenSSH )。

如果您指定-e AWS_PROFILE之类的值,但不带任何值,则它将通过主机环境中的值传递(如果有的话)。这对于显示环境变量被接受,支持或必需,同时让用户自行提供,很有用。

古老的(令人困惑的)它。为了使容器能够从stdin中读取(例如,通过管道传递到它),需要使用-i参数运行它。如果您希望能够在容器中进行交互工作,请传递-t。如果您想要彩色输出之类的东西,通常会需要-t,但是-t在CI中可能会出错。

与此相关的是,如果您希望能够使用^ C中止该容器,则需要传递-t,尽管这可能还不够。如果您的进程不知道如何处理信号(例如,来自^ C的SIGINT),则此操作将无效。在这种情况下,另外传递--init可能会有所帮助。

小容器优先于大容器。我已经看到了一种为项目构建“一切皆有厨房但沉没”(“开发环境”)Docker映像的模式。最终,您将获得一个很大的印象,其中包含许多工具(可能还有应用程序代码-请勿在开发时使用,而是使用绑定挂载卷)。与图片外的工具(例如文本编辑器)进行交互可能会很困难。然后,您可以在容器中运行,运行Shell等。这具有本文所述的某些相同好处,但使用方式不同。它与UNIX哲学的本质背道而驰。如果要引入用golang编写的负载测试工具,则必须在添加了所有golang行李的情况下重建映像。大容器类似于运行VM。这肯定比直接在主机系统上安装所有内容要好,但是可以更有效地使用容器。

在容器中运行单个工具更符合UNIX理念:单用途容器运行单用途工具,做得很好。因此,它更加灵活,可组合且功能强大。

在Docker中运行任何命令都会有少许启动延迟。如果您要运行很多小命令,那么这会加起来。每次调用docker run时,创建名称空间都会给您带来大约1s的启动罚款。自己尝试:时间docker运行--rm hello-world。注意,例如,与--net host一起运行可节省几百毫秒的时间。使用主机网络通常适合运行开发人员工具,但是您是否想使用此选项和其他潜在的晦涩选项乱丢每个docker运行程序,以使命令速度更快一些?我不。有一种简单的方式说“我只想打扰mnt命名空间”,并节省一些时间,这将是很好的。据我所知,没有。

在Mac上,每当您在绑定安装中进行磁盘IO时(即将主机系统的目录卷入容器),都会对性能造成重大影响。在没有装订架的情况下进行工作极为困难。以我的经验,缓存和委派的选项不会显着提高性能(如果您或您的同事使用Mac,仍然值得打开)。我不知道故障是出在macOS还是Docker for Mac,但这确实会使使用Docker变得不愉快。如果您在Mac上使用Docker,但从未在Linux上试用过,则应归功于自己在Linux上进行尝试。

命令非常冗长。将它们包装为别名或Makefiles或类似文件。最好有能力在内部进行查看,并准确查看为该工具提供了什么上下文,而不是隐式泄漏所有上下文而没有检查或限制它的能力。

运行make args = ...而不是yarn ...感觉很陌生。这更丑陋,更尴尬。但是,对于已经使用Makefile(或类似文件)来组织其维护命令的项目而言,并没有什么不同。

拥有大量Docker映像会占用大量磁盘空间。我必须训练自己停止分配较小的根分区,因为Docker映像会占用大量空间。这不理想,但也很不错。磁盘空间非常便宜。我很乐意做出这样的权衡。结论:下载大量图像需要大量网络流量。

图像变旧/未修补。这是一个有效的关注点,但是在我描述的用例中,我并不认为这很关键。显然:不要拍摄陈旧的图像并使用它来投放网络流量。但是,如果我的flake8 Docker映像运行的是安全补丁中落后的Alpine或Debian版本,我不会受到任何伤害。话虽如此:在所有其他条件都相同的情况下,最好使图像保持补丁状态并保持最新。

图像本质上是黑匣子。从Docker Hub下载任何内容都可能很危险,但它本身并不比curl更危险| bash,从GitHub发布页面下载二进制文件,甚至从源代码构建任何您不熟悉的项目。请注意,Docker标签是可变的,因此,如果您不控制图像,则可能应基于内容哈希值进行固定。不过,您应该可以轻松构建自己的图像。我不介意以官方操作系统映像为基础构建映像,但是根据您对风险的偏执狂偏好程度,您可能会从头开始构建。

Docker是一个了不起的工具。从我所看到的情况来看,这仍然是一个未被充分认识的用例。我对这种方法的唯一真正保留是,Docker for Mac文件系统的性能是如此糟糕,以至于“跨平台”原则上是正确的,但没有实际应用中的那么好。

我一直在寻找比Docker更简单或更轻巧的东西,但是却带来了类似的好处,尤其是沙箱和跨平台方面。也许尼克斯?如果您有意见,请告诉我。