内部应用程序的容器化可能很复杂; Docker的Dockerfile最佳实践列表是指导该过程的重要资源。
将应用程序包装在容器中很容易。 将您的应用程序提取到多个组件中并以能够带来容器化所有好处的方式部署这些组件会稍微复杂一些。 使用内部应用程序,可以很容易地遵循最佳实践,例如使用卷来装载数据库存储或将容器限制为单个进程。 在某些情况下,这可能涉及重新设计应用程序的一部分,这可以通过内部代码库实现。
当您试图运行一个自以为是的第三方应用程序时,进行这些更改并不容易。 在许多情况下,用户对数据存储和应用程序配置以及流程协调和执行的控制有限。 某些应用程序对执行环境有强烈的意见,或者对端口或其他资源做出假设。 这可能使得难以遵循容器化最佳实践或协调节点HA或故障转移。
这篇文章将提供一些基本的容器化最佳实践,重点放在有思想的第三方应用程序上。 有关本指南的背景知识,请查看我在Dockercon EU 2014上的演讲 。
无文件配置
许多应用程序需要通过文件加载配置。 但是,通过环境配置容器要简单得多。
对于内部应用程序来说,将配置从YAML这样的文件格式切换到环境通常是很简单的,尤其是使用像Isomer这样的抽象层时。 第三方应用程序通常不那么灵活,因此要通过环境配置您的容器,必须使用配置中介。
康德
Confd采用一组提供的自定义模板,并根据环境或密钥库内容编写配置文件。 这是弥合容器和应用程序级配置之间差距的好工具。
使用confd涉及通过密钥库服务器(etcd / Consul)或直接在容器中作为环境变量公开配置数据,然后在运行主应用程序之前将confd作为容器启动的一部分来执行。 这是将复杂的配置方法强制转换为容器基础架构中易于自动化的最简单的方法。 有关此配置方法的示例,请参见我的GMod服务器存储库 。
通过安装或添加进行配置
虽然可以包括静态逻辑以从环境或密钥库服务器配置应用程序,但是从另一个容器装入配置具有其优点。 通过运行临时容器来写入所需的任何配置文件并安装该容器公开的卷,您最终将获得更加复杂和动态的部署。
使用此方法的好处是,您的主应用程序容器与通过基础结构拉出和写入配置信息的逻辑隔离开来。 它完全专注于应用程序的执行。 由裸露的应用程序组件组成的单用途容器是理想的部署。
这种配置方法并不只限于使用etcd或Consul 。 而是,它简单地抽象了执行正在使用的任何配置集成的逻辑,例如etcd,Consul,Chef或从远程服务器提取的简单文件。 这种方法的一个微妙的好处是,您可以通过从本地文件夹挂载预先生成的配置来测试容器。 在生产环境中,可以用指定的容器替换它。
要查看配置安装的示例,请检查所有容器存储库中安装卷或添加配置文件,例如tutum / mysql 。 这使用一组简单的文件ADD命令。
# Add MySQL configuration
ADD my.cnf /etc/mysql/conf.d/my.cnf ADD mysqld_charset.cnf /etc/mysql/conf.d/mysqld_charset.cnf
任务分离
对于第三方应用程序,很难像Dockerfile最佳实践所建议的那样分离出不同的进程。 在这种情况下,我们受到应用程序入口点的严重限制。
我们能做的最好的事情就是分离出通常针对应用程序运行的命令。 除了将它们运行在主应用程序容器中之外,我们还可以将逻辑移动到通过卷安装或通过容器端口代理本地套接字链接的一组长寿命和短寿命容器中。
我们要避免的主要事情是让单个整体容器运行整个应用程序,并通过SSH运行管理员注释。 在这种情况下,容器充当了应用程序的虚拟机,我们错过了容器提供的许多隔离和分发收益。
一个容易犯的错误是在主应用程序容器中嵌入一个进程,以从运行的应用程序中提取数据。 我之前曾使用过此方法来提取自动生成的凭据,监视状态并跟踪错误。 这种方法的问题在于它增加了不必要的复杂性。 要么跟踪进程要么主应用程序进程都必须守护,并且协调这些进程的艰巨工作就取决于容器内的逻辑。
一种更清洁的解决方案是将所有跟踪,监视或交互活动推送到外部容器,并通过卷安装或套接字代理将其链接到主应用程序容器。 过去我这样做是为了解析应用程序日志,以自动方式提取生成的身份验证凭据。 查看我的Teamspeak watcher存储库以获取示例。
由于我们现在有了一个单一用途的主容器,因此我们可以将所有协调推到容器编排层,并围绕主应用程序容器构建所有应用程序组件的堆栈。 这样,我们可以让业务流程层以标准方式控制我们定义的任何组件。
并非所有应用程序都使用标准套接字或卷进行交互,在这种情况下,最简单的解决方案通常是最佳的。
假设您需要定期在应用程序控制台上触发命令。 在非容器化环境中,这将涉及cron触发控制台命令,该命令只能连接到应用程序的本地实例。 如果无法映射本地套接字以允许远程控制台连接,请尝试仅将支持性基础结构添加到您的主容器中,并将业务逻辑保留到您的外部容器中。
实现此目的的一种方法是保持一个简单的Web服务器来代理在您的主容器中运行的远程命令,而该远程服务器没有关于存在哪些命令或可能执行哪些命令的上下文。 然后,辅助容器可以将请求发送到远程Web服务器而不是本地控制台。 这不是理想的情况,因为更复杂的组件正在主容器中运行。 但是,某些应用程序自以为是的性质成为了容器化的障碍。
您的最终部署可能如下所示:
# start a primary app container exposing volumes for /opt/logs and exposing /opt/tmp/app.socket via a port
docker run -v /opt/data:/opt/data --name=myapp1 foo/myapp
# start a monitor container, polling /opt/logs for events
docker run --volumes-from myapp1 --name=myapp1-monitor foo/mypp-monitor
# start a temporary container to use an exposed port from myapp-1 to interact with the running application instance, and execute a create user command
docker run -it --rm --link myapp-1:myapp --name=myapp1-controller foo/myapp-controller create user test
查看bfosberry / teamspeak以及相关的观察器和控制器作为示例。
分离所有数据存储
另一个很明显的一点是,所有持久性数据存储都需要分开。 对于有意见的应用程序,这可能很简单,例如在启动时编写配置文件并提供应用程序应使用的外部数据库,或者可能很复杂,例如将目录中的文件子集链接到已安装的卷。
为了确定需要提取应用程序的哪些组件,可能需要进行一些发现。 但是,为此目的,存储卷往往是通用兼容的。 考虑使用Postgres容器通过卷挂载来持久化数据。
VOLUME ["/var/lib/postgresql"]
VOLUME ["/run/postgresql"]
不要守护
一个容易犯的错误是编写一个像这样的简单Dockerfile:
FROM ubuntu
RUN apt-get install -y mysql-server
CMD start.sh
start.sh看起来像这样:
/etc/init.d mysql start && tail -f /var/log/mysql.log
这会将应用程序作为容器中的守护程序启动,然后拖尾应用程序日志。 问题在于,没有直接的过程控制。 从业务流程层发送的任何信号都会进入尾声处理。
通过在前台运行应用程序进程,我们可以维护信号管道。 业务流程层发送到容器的任何信号都直接进入主应用程序进程。
这是从tutum / apache-php在前台运行Apache的示例。
source /etc/apache2/envvars
tail -F /var/log/apache2/* &
exec apache2 -D FOREGROUND
登录基础架构
一个很明显的观点是从应用程序中聚合和分离日志。 在大多数情况下,日志将被写入应用程序或系统日志目录内的文件夹中。 确保将它们挂载到日志记录卷或将它们打印到stdout,以便它们包含在标准日志记录基础结构中。
如果无法以适合自己需要的方式配置日志记录位置,则始终可以启动后台重定向以将日志推送到stdout或其他位置。 理想情况下,这甚至都不需要考虑,因为您的应用程序应该在前台运行。 但是,根据应用程序,容器中运行的其他组件以及您的日志记录基础结构,很可能需要通过安装来提取日志。 为了在使用自定义日志接收器时使主容器保持简单,最好的方法是记录到stdout并通过一组容器收集日志。 例如,请查看Deis记录器 。
遵循CMD最佳做法
设计您的容器集时,一个相当简单的胜利是遵守[围绕ENTRYPOINT和CMD使用的最佳实践。
通过为所有容器使用通用入口点,并根据使用情况使用不同的命令,您可以在应用程序部署的各个部分中重复使用相同的容器映像。 这将减少您的存储库和图像占用空间。 在我的主应用程序很大(4 GB和更大)的情况下,我倾向于避免这样做,以减少使用较大图像的容器的数量。 有关此示例,请查看mysql / mysql-server 。
有意见的应用程序的容器化障碍
大多数应用程序从未打算在容器中运行,只能在踢和尖叫时进行容器化。 不要让那阻止您尝试-毕竟,他们的理由不是他们的理由,而是容器化。 在下一节中,我概述了您可能遇到的一些问题以及一些解决方法。
自我发现可能不支持动态端口
某些应用程序,特别是许多游戏服务器,会将其绑定的IP和端口报告给主服务器列表以进行发现。 尽管可以通过动态端口映射直接访问游戏服务器,但是通过搜索提供给玩家的端口将是内部端口,而外部则无法访问。
解决此问题的最简单方法是在基础结构上分配外部端口,并使用直接静态映射到应用程序容器中。 在这种情况下,需要通过一些配置参数将您的应用程序配置为使用实质上是动态内部端口的端口。 您可以在我的GMod Server存储库中看到一个示例。
某些端口可能无法配置
可能无法更改某些应用程序端口。 在其他情况下,该应用程序可以在单个端口上绑定到TCP和UDP。 当前尚无法通过单个配置指令告诉Docker 在特定端口上同时绑定UDP和TCP,以确保主机和容器之间的凝聚力。
为12345和12345 / udp请求动态绑定可能会提供具有两个不同内部端口号的映射。 静态映射端口是解决此问题的最简单方法。
如果端口不可配置,则始终可以使用iptables将流量从一个本地端口转发到另一个本地端口。 但是,这增加了容器的复杂性,并且可能与您的应用程序具有自我发现功能的情况不兼容。
某些应用程序不适合动态部署
对于许多游戏服务器,典型的发现过程始于用户搜索具有匹配特定条件的空闲插槽的服务器。
假设此自我发现过程正常运行,则用户将能够连接并且可以为服务器添加书签或收藏夹。 通常,此书签由纯IP和端口组成。 借助动态基础架构,如果移动游戏服务器,则端口和IP最有可能发生变化。 在大多数游戏服务器流量为UDP的情况下,可能难以合并书签并确保一致的体验。
一种解决方案是在旧容器的位置保留一个“修补程序”,以将流量从旧主机转发到新主机,并动态映射端口。 使用一些可用于容器部署的更高级的SDN解决方案,可以使用可路由IP解决此问题。 这使我们可以将许多静态部署的应用程序视为高可用的动态资源。
例子
在GSCS模式项目中,您可以找到许多这些概念的示例。
翻译自: https://www.javacodegeeks.com/2015/06/containerizing-opinionated-applications.html