如何在基于Docker的微服务中pipe理每个环境的数据?

在微服务体系结构中,我很难掌握如何pipe理环境特定的configuration(例如数据库或消息代理的IP地址和凭证)。

假设您有三个微服务(“A”,“B”和“C”),每个微服务由不同的团队拥有和维护。 每个团队都需要一个团队集成环境…他们在哪里使用他们的微服务的最新快照,以及所有依赖微服务的稳定版本。 当然,你也需要QA /分期/生产环境。 大图的简化视图如下所示:

“微服务A”团队环境

  • 微服务A( SNAPSHOT
  • 微服务B(STABLE)
  • 微服务C(STABLE)

“微服务B”团队环境

  • 微服务A(STABLE)
  • 微服务B( SNAPSHOT
  • 微服务C(STABLE)

“微服务C”团队环境

  • 微服务A(STABLE)
  • 微服务B(STABLE)
  • 微服务C( SNAPSHOT

质量保证/分期/生产

  • 微服务A(稳定,发布等)
  • 微服务B(稳定,发布等)
  • 微服务C(STABLE,RELEASE等)

这是很多的部署,但是这个问题可以通过持续集成服务器来解决,也许可以像Chef / Puppet /等等。 真正困难的部分是每个微服务都需要一些特定的环境数据给每个被部署的地方。

例如,在“A”团队环境中,“A”需要一个地址和一组证书来与“B”交互。 但是,在“B”团队环境中,“A”的部署需要不同的地址和凭证来与“B”的部署进行交互。

而且,当你接近生产时,像这样的环境configuration信息可能需要安全限制(即只有某些人可以修改甚至查看它)。

那么,在微服务体系结构中,如何维护特定于环境的configuration信息并将其提供给应用程序呢? 一些方法浮现在脑海中,尽pipe它们都有问题:

  • 让构build服务器在构build时把它们烧到应用程序中 – 我想你可以创build一个每个环境属性文件或脚本的回购,并且为每个微服务的构build过程伸出并拉入适当的脚本(你也可以有一个单独的,限制访问回购生产的东西)。 不过,你需要大量的脚本。 对于微服务可以部署的每个地方的每一个微服务,基本上都是单独的。
  • 将它们烘焙到每个环境的基础Docker镜像中 – 如果构build服务器将微服务应用程序放入Docker容器中作为构build过程的最后一步,则可以为每个环境创build自定义基础镜像。 基本映像将包含一个shell脚本,用于设置所有您需要的环境variables。 您的Dockerfile将被设置为在启动您的应用程序之前调用此脚本。 这与之前的要点也有类似的挑战,因为现在你正在pipe理大量的Docker镜像。
  • 在运行时从某种registry中提取环境信息 – 最后,可以将你的每个环境configuration存储在Apache ZooKeeper(甚至只是一个普通的数据库)中,然后让你的应用程序代码在运行时将其拉入它启动。 每个微服务应用程序都需要知道它在哪个环境(例如启动参数),以便知道从registry中获取哪一组variables。 这种方法的优点是,现在您可以使用完全相同的构build工件(即应用程序或Docker容器)从团队环境一直到生产。 另一方面,您现在将拥有另一个运行时依赖项,您仍然必须pipe理registry中的所有数据。

人们通常如何在微服务架构中解决这个问题? 这听起来似乎是一件很平常的事情。

概观

龙岗!

  • ENTRYPOINT是你的朋友
  • Sam Newman 构build微服务非常棒
  • 服务间安全提示:双向TLS可能会工作,但可能会出现延迟问题
  • 我将从我的团队中得到一个真实的例子。 我们不能使用configuration服务器,事情变得很有趣。 现在可以pipe理。 但由于公司有更多的服务,可能无法扩展。
  • configuration服务器似乎是一个更好的主意

更新 :差不多两年后,我们可能会转移到Kubernetes ,并开始使用它随附的etcd-powered ConfigMapsfunction。 我将在configuration服务器部分再次提及。 如果您对这些主题感兴趣,这篇文章还是值得一读的。 我们仍然使用ENTRYPOINT和一些相同的概念,只是一些不同的工具。

ENTRYPOINT

我build议ENTRYPOINT是pipe理Docker容器环境特定configuration的关键。

简而言之:在开始之前创build一个脚本来引导您的服务,并使用ENTRYPOINT来执行这个脚本。

我将详细描述这一点,并解释如何在没有configuration服务器的情况下做到这一点。 它有点深,但不是难以pipe理的。 然后,我将详细介绍configuration服务器,这是许多团队的更好的解决scheme。

构build微服务

你说得对,这些都是常见的问题,但是这并不是一成不变的解决scheme。 最一般的解决scheme是configuration服务器。 ( 普通的,但仍然不是一成不变的)。但是也许你不能使用其中的一种:我们被安全团队禁止使用configuration服务器。

如果你还没有,我强烈推荐阅读Sam Newman的“ Building Microservices” 。 它考察了所有常见的挑战,并讨论了许多可能的解决scheme,同时也从一个经验丰富的build筑师给予有益的观点。 (注意:不要担心你的configurationpipe理有一个完美的解决scheme,从一个“足够好”的解决scheme开始,你可以迭代和改进,所以你应该尝试获得有用的软件给你客户尽快 ,然后在后续版本中改进。)

警戒的故事?

再次重读这个…我多less需要充分解释这一点。 从Python的禅 :

 If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. 

我不满意我们的解决scheme。 然而这是一个可行的解决scheme,因为我们不能使用configuration服务器。 这也是一个真实世界的例子。

如果你读了它,想:“哦,不,我为什么要这么做! 那么你知道,你需要看看configuration服务器。

服务间安全

你似乎也关心不同的微服务如何相互authentication。

对于与此身份validation相关的工件和configuration…将它们视为其他任何configuration工件。

您对服务间安全有什么要求? 在你的文章中,这听起来像你正在描述应用层,用户名/密码authentication。 也许这对你想要的服务是有意义的。 但是你也应该考虑双向TLS :“这个configuration要求客户端提供他们的证书给服务器,除了服务器提供给客户端。” 生成和pipe理这些证书可能会变得复杂……但是,如果您select这样做,则会像其他任何configuration/工件一样,在configuration/构件周围进行洗牌。

请注意,双向TLS可能会导致大量的延迟问题。 我们还没有。 我们正在使用除双向TLS以外的其他措施,并且一旦经过validation,我们可能会抛弃双向TLS。


来自我的团队的真实世界的例子

我目前的团队正在做一些结合你提到的两种方法的东西(释义):

  • 在构build时烘烤configuration
  • 在运行时拉取configuration

我的团队正在使用Spring Boot 。 Spring Boot具有非常复杂的外部configuration和“configuration文件”系统。 Spring Boot的configuration处理非常复杂而且function强大,所有的优点和缺点(在这里都没有涉及)。

虽然这是Spring Boot的开箱即用,但这些想法是一般的。 我更喜欢用于Java微服务的Dropwizard ,或Python中的Flask ; 在这两种情况下,你都可以做类似的事情,Spring Boot正在进行…你只需要自己做更多的事情。 好与坏:这些灵活的小框架比Spring更灵活,但是当你编写更多的代码并进行更多的集成时,你对QA有更多的责任,并testing你的复杂/灵活的configuration支持。

由于第一手的经验,我将继续Spring Boot的例子,但不是因为我推荐它! 使用适合你的团队的东西。

在Spring Boot的情况下,您可以一次激活多个configuration文件。 这意味着您可以拥有基本configuration,然后使用更具体的configuration进行覆盖。 我们在src/main/resources保留一个基本configuration, application.yml 。 所以,这个configuration与可装运的JAR打包在一起,当JAR执行时,这个configuration总是被拾取。 因此,我们在该文件中包含所有默认设置(对所有环境都是通用的)。 例如:“embedded式Tomcat”的configuration块始终使用启用了这些密码套件的TLS。 ( server.ssl.ciphers

当一个或两个variables需要被某个环境覆盖时,我们利用Spring Boot的支持从环境variables中获取configuration 。 示例:我们使用环境variables将URL设置为我们的服务发现。 这将覆盖已发货/拉取的configuration文件中的任何默认值。 另一个例子:我们使用一个环境variablesSPRING_PROFILES_ACTIVE来指定哪些configuration文件是活动的。

我们也希望确保master包含一个经过testing的,适用于开发环境的configuration。 src/main/resources/application.yml有相同的默认值。 另外,我们把config/application-dev.yml dev-onlyconfiguration文件放在config/application-dev.yml ,检查config目录是否容易拾取,但是不能在JAR中 config/application-dev.yml 。 很好的function。 开发人员知道(从README和其他文档),在开发环境中,我们所有的Spring Boot微服务都需要激活devconfiguration文件。

对于dev之外的环境,你可能已经可以看到一些选项了…这些选项中的任何一个都可以(几乎)完成你所需要的一切。 你可以根据需要混合搭配。 这些选项与您在原始文章中提到的一些想法重叠。

  1. 维护特定于环境的configuration文件,比如application-stage.ymlapplication-prod.yml等,这些configuration文件覆盖偏离默认值的设置(在一个非常严重的git仓库中)
  2. 维护模块化的供应商特定的configuration文件,如application-aws.ymlapplication-mycloudvendor.yml (您存储的位置取决于它是否包含秘密)。 这些可能包含切入阶段,产品等的值。
  3. 在运行时使用环境variables覆盖任何相关的设置; 包括从1和2中挑选configuration文件
  4. 在构build或部署时使用自动化来烘焙硬编码的值(模板)(输出到某种types的高度locking的存储库,可能与(1)的存储库不同)

(1),(2)和(3)一起工作。 我们很高兴地做这三件事情,事实上很容易logging,推理和维护(在得到最初的结果之后)。

你说 …

我想你可以创build每个环境属性文件或脚本的回购[…]但是,您将需要大量的脚本。

它可以被pipe理。 拉或烘烤configuration的脚本:这些脚本在所有服务中可以是统一的。 也许脚本被复制时,有人克隆你的微服务模板(顺便说一句:你应该有一个官方的微服务模板!)。 或者,也许这是一个内部PyPI服务器上的Python脚本。 在我们谈论Docker之后,我们再来进一步讨论。

由于Spring Boot对(3)有很好的支持,并且支持在YML文件中使用defaults / templating,所以你可能不需要(4)。 但是,这里的事情变得非常特定于你的组织。 我们团队的安全工程师希望我们使用(4)来为超出dev环境提供一些特定的值:密码。 这个工程师不希望密码在环境variables中“浮动”,主要是因为那么 – 谁来设置它们? Docker调用者? AWS ECS任务定义(可通过AWS Web UI查看) ? 在这种情况下,密码可能会暴露给自动化工程师,他们不一定有权访问包含application-prod.yml的“locking的git存储库”。 (4)可能不需要,如果你这样做(1); 您可以将密码硬编码保存在严格控制的存储库中。 但是也许在部署自动化的时候有一些秘密,你不需要在(1)的同一个仓库中。 这是我们的情况。

更多关于(2):我们使用awsconfiguration文件和Spring Boot的“configuration作为代码”进行启动时调用以获取AWS元数据,并基于此重写一些configuration。 我们的AWS ECS任务定义激活awsconfiguration文件。 Spring Cloud Netflix文档给出了一个这样的例子:

 @Bean @Profile("aws") public EurekaInstanceConfigBean eurekaInstanceConfig() { EurekaInstanceConfigBean b = new EurekaInstanceConfigBean(); AmazonInfo info = AmazonInfo.Builder.newBuilder().autoBuild("eureka"); b.setDataCenterInfo(info); return b; } 

接下来,Docker。 环境variables是在Docker中传递configuration参数的好方法。 我们不使用任何命令行或位置参数,因为我们遇到了ENTRYPOINT的一些ENTRYPOINT 。 无论是从命令行,还是从诸如AWS ECS或Apache Mesosphere / Marathon的主pipe/调度程序传递--env SPRING_PROFILES_ACTIVE=dev--env SPRING_PROFILES_ACTIVE=aws,prod--env SPRING_PROFILES_ACTIVE=aws,prod 。 我们的entrypoint.sh也有助于传递与Spring无关的JVM标志:我们使用通用的JAVA_OPTS约定来实现这一点。

(哦,我应该提到…我们也使用Gradle来构build我们的构build,目前…我们用Gradle任务Dockerfile docker builddocker runDockerfile docker push ,我们的Dockerfile是模板化的,上面我们有像@agentJar@这样的variables,在编译的时候被覆盖, 我真的不喜欢这个,我认为这可以用普通的旧configuration( -Dagent.jar.property.whatever )来处理,这可能会但是我只是提到它的完整性,我对此感到高兴:在构build, Dockerfileentrypoint.sh脚本中没有做任何事情,它与特定的部署上下文(比如AWS)紧密结合,所有这些工作都在开发环境和部署环境中进行,所以我们不需要部署Docker镜像来testing它:它是可移植的,因为它应该是。

我们有一个包含Dockerfileentrypoint.sh的文件夹src/main/docker Dockerfile (这个脚本被ENTRYPOINT ;这个DockerfileDockerfile )。 我们的Dockerfileentrypoint.sh在所有微服务中几乎完全一致。 当你克隆我们的微服务模板时,这些是重复的。 不幸的是,有时你必须复制/粘贴更新。 我们还没有find一个好办法,但这并不是非常痛苦。

Dockerfile执行以下操作(构build时间):

  1. 派生自我们的“黄金”基础Dockerfile for Java应用程序
  2. 抓住我们的工具拉动configuration。 (从内部服务器抓取任何开发者或Jenkins机器的构build版本)(您也可以使用像wget这样的Linux工具以及DNS /基于约定的命名来获取它,也可以使用AWS S3和基于惯例的命名)。
  3. 将一些东西复制到Dockerfile ,比如JAR, entrypoint.sh
  4. ENTRYPOINT exec /app/entrypoint.sh

entrypoint.sh执行以下操作(运行时):

  1. 使用我们的工具来拉取configuration。 (一些逻辑理解,如果awsconfiguration文件不活跃, awsconfiguration文件不是预期的。)如果有任何问题立即大声地死亡。
  2. exec java $JAVA_OPTS -jar /app/app.jar (提取所有属性文件,环境variables等)

所以我们在应用程序启动的时候已经介绍过,configuration是从某个地方拉出来的,但是在哪里呢? 从以前的观点来看,他们可能在一个git仓库中。 你可以拉下所有的configuration文件,然后使用SPRING_PROFILES_ACTIVE来说哪个是活动的; 但是你可能会把application-prod.yml到舞台机器上(不好)。 相反,你可以看看SPRING_PROFILES_ACTIVE (在你的configuration逻辑中),并且只提取需要的东西。

如果您正在使用AWS,则可以使用S3存储库而不是git存储库。 这可能允许更好的访问控制。 而不是生活在相同的repo / bucket中的application-prod.ymlapplication-stage.yml ,你可以这样做,使得application-envspecific.yml总是具有所需的configuration,在S3​​桶中由给定的常规名字AWS账户。 即“从s3://ecs_config/$ENV_NAME/application-envspecific.yml获取configuration”(其中$ENV_NAME来自entrypoint.sh脚本或ECS任务定义)。

我提到Dockerfile是可移植的,并没有耦合到某些部署上下文。 这是因为entrypoint.sh被定义为以灵活的方式检查configuration文件; 它只是想要configuration文件。 所以,如果你使用Docker的--volume选项来安装一个带有config的文件夹,那么脚本会很高兴,并且不会试图从外部服务器上获取任何东西。

我不会进入部署自动化很多…但只是很快提到,我们使用terraform,boto3和一些自定义的Python包装代码。 jinja2用于模板(烘烤那些需要烘烤的值)。

这是这种方法的严重限制:微服务进程必须被终止/重新启动以重新下载并重新加载configuration。 现在,对于无状态服务集群,这不一定代表停机时间(考虑到客户端负载平衡,configuration为重试的function区以及水平范围等因素,因此某些实例始终在池中运行)。 到目前为止,它正在努力,但微服务仍然有相当低的负载。 增长即将到来。 我们将会看到。

解决这些挑战的方法还有很多。 希望这个练习让你思考什么会为你的团队工作。 试着去做一些事情 快速原型,你会摆脱细节,你走。

也许更好:configuration服务器

我认为这是一个更常见的解决scheme: configuration服务器。 你提到了ZooKeeper 。 还有领事 。 ZooKeeper和Consul都提供configurationpipe理服务发现。 还有etcd 。

在我们的例子中,安全小组对于集中式configurationpipe理服务器并不熟悉。 我们决定使用NetflixOSS的Eureka进行服务发现,但是暂停在configuration服务器上。 如果我们不喜欢上面的方法,我们可能会切换到Archaius进行configurationpipe理。 Spring Cloud Netflix旨在使Spring Boot用户轻松实现这些集成。 虽然我认为它希望你使用Spring Cloud Config(服务器/客户端)而不是Archaius。 还没有尝试过。

configuration服务器似乎更容易解释和思考。 如果可以的话,你应该从configuration服务器开始。

 If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. 

configuration服务器的比较

如果你决定尝试一个configuration服务器,你需要做一个研究高峰。 这里有一些很好的资源来启动你:

  • 服务发现:Zookeeper vs etcd与领事
  • Aphyr在etcd和Consul的一致性方面的书面报告 (已经确定的特定问题已经得到解决,但值得一读和思考)

如果你尝试领事,你应该看这个演讲,“作为一个早期采用者的经营领事” 。 即使你除了领事之外还尝试其他的东西,这个话题也会为你提供build议和洞察。

16/05/11编辑: ThoughtWorks技术雷达现在已经把领事移入“采用”类别 ( 他们的评估的历史在这里 )。

17/06/01编辑:我们正在考虑搬到Kubernetes有多个原因。 如果我们这样做,我们将利用K8S附带的etcd-powered ConfigMapsfunction。 这就是这个问题:-)

更多资源

  • 我最喜欢的微服务一站式商店: Martin Fowler的“微服务资源指南”
  • 如果你不想购买/阅读所有的build筑微服务 ,那么通过NGINX有几个章节的PDF 。 从第86页开始,它进入“dynamic服务注册”,涵盖了ZooKeeper,领事,尤里卡等等…纽曼涵盖了这些话题比我更好。