在本文中,我将分享我的团队在 BitBucket Pipelines 上设置基础设施以促进集成测试的经验。

我将讨论我们在将服务的外部依赖项编排为 docker 容器并使它们在本地和 CI 集成测试期间相互通信时所面临的挑战。

具体来说,我将概述 docker-in-docker 问题,如何在本地、本地在 docker & 在 CI 中解决它,以及如何通过 docker-compose.yml & 和一些 Bash 将它们实际放在一起。

我们的特殊经验是使用 Kafka,但广泛的想法也可以应用于其他堆栈。

背景

对于我们当前的(未开发)项目,我的团队正在开发一个应用程序来处理来自 Kafka 流的事件。为了确保团队的速度不会随着项目的推进而降低,我们希望设置一套集成测试,这些测试将在 BitBucket Pipelines 的 CI 测试阶段的每次提交上运行。

我们对如何确保尽可能多地轻松运行测试有一些具体要求——不仅在 CI 阶段,而且在本地:

  • 必须直接从我们的 IDE 运行测试而不需要太多的魔法 - PyCharm 允许您在特定测试上单击运行,这就像最好的方法 :) 拥有一个合适的调试器对我们来说是不可或缺的。

  • 在本地运行测试时,我们希望能够直接通过本机 Python 解释器执行它们,也可以从类似于将在 CI 期间运行的容器中执行它们。 PyCharm 允许您使用 docker 容器中的解释器 - 当我们想在类似 CI 的容器中运行我们的测试并且仍然有适当的调试器时,我们会这样做。对于正常的开发/测试,我们更喜欢使用原生 Python 解释器,因为开发体验更流畅。

  • 在本地和 CI 中尽可能接近生产环境。 IE。无论环境如何,它都必须易于生成外部依赖项并能够完全控制它们。

在继续之前,我将重申我们将要解决的三个环境:

  • 本地 - 使用本机 Python 解释器的开发笔记本电脑(通过pyenv+pipenv)

  • 本地 docker - 再次在开发笔记本电脑上,但 python 在 docker 中

  • CI - BitBucket Pipeline 构建,它在专用容器中运行我们的脚本

技术栈

我们的服务是一个 Python 3.7 应用程序,由 Robinhood 的Faust异步框架提供支持。我们使用 Kafka 作为消息代理。为了确保产生和消费的 Kafka 消息的格式,我们使用AvroSchemas,它们由Schema Registry服务管理。

[Alt](https://res.cloudinary.com/practicaldev/image/fetch/s--ae8xGF2A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/i/4aixk4ys9rnej285cl9x.png)

项目结构

这是项目结构,受这个方便的浮士德骨架项目的影响:

./project-name
  scripts/
    ...
    run-integration-tests.sh
  kafka/ # external services setup
    docker-compose.yml
    docker-compose.ci.override.yml
  project-name/
    src/...
    tests/...
  bitbucket-pipelines.yml 
  docker-compose.test.yml
  ...

kafka/docker-compose.yml定义了我们主应用程序的所有必要的外部依赖项。如果通过cd kafka && docker-compose up -d在本地运行,容器将被启动并且它们将在localhost:<service port>上可用 - 然后我们可以通过我们的原生 Python 运行应用程序并针对容器化服务进行测试。

 # kafka/docker-compose.yml
 version: '3.5'
services:
  zookeeper:
    image: "confluentinc/cp-zookeeper"
    hostname: zookeeper
    ports:
      - 32181:32181
    environment:
      - ZOOKEEPER_CLIENT_PORT=32181
  kafka:
    image: confluentinc/cp-kafka
    restart: "on-failure"
    hostname: kafka
    healthcheck:
      test: ["CMD", "kafka-topics", "--list", "--zookeeper", "zookeeper:32181"]
      interval: 1s
      timeout: 4s
      retries: 2
      start_period: 10s
    container_name: kafka
    ports:
      - 9092:9092 # used by other containers launched from this file
      - 29092:29092 # use outside of the docker-compose network
    depends_on:
      - zookeeper
    environment:
      - KAFKA_ZOOKEEPER_CONNECT=zookeeper:32181
      - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
      - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT_HOST://localhost:29092,PLAINTEXT://kafka:9092
      - KAFKA_BROKER_ID=1

  schema-registry:
    image: confluentinc/cp-schema-registry
    hostname: schema-registry
    container_name: schema-registry
    depends_on:
      - kafka
      - zookeeper
    ports:
      - "8081:8081"
    environment:
      - SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL=zookeeper:32181
      - SCHEMA_REGISTRY_HOST_NAME=schema-registry
      - SCHEMA_REGISTRY_DEBUG=true
      - SCHEMA_REGISTRY_LISTENERS=http://0.0.0.0:8081

配置 kafka 容器时的一个特点是KAFKA_ADVERTISED_LISTENERS- 对于给定的端口,您可以指定单个侦听器名称(例如端口 29092 的 localhost)。如果客户端使用kafka://localhost:29092作为 URI(加上kafka://kafka:9092,对于上面的示例),Kafka 将接受此端口上的连接。设置 0.0.0.0 而不是 localhost 会导致 kafka 在启动期间返回错误 - 因此您需要指定实际的侦听器主机名。我们将在下面看到为什么这很重要。

CI BitBucket 管道和同级 Docker 容器

根据Bitbucket 文档,可以在管道执行期间将外部依赖项作为容器启动。起初这听起来很吸引人,但这意味着在本地设置测试基础架构时会遇到困难,因为bitbucket-pipelines.yml语法是自定义的。由于我们需要能够轻松地在本地拥有整个堆栈并对其进行控制,因此我们采用了使用 docker-compose 来启动 dockerised 服务的方法。

要配置 CI,我们有一个bitbucket-pipelines.yml,其中包括一个测试阶段,该阶段在python:3.7容器中执行:

definitions:
  steps:
    - step: &step-tests
        name: Tests
        image: python:3.7-slim-buster
        script:
          # ...
          - scripts/run-integration-tests.sh # launches external services + runs tests. 
        ...

当通过 docker(本地和 CI)运行测试时,run-integration-tests.sh脚本将在python:3.7容器内执行 - 从这个脚本中,我们通过 docker-compose 启动 kafka 和 Friends 容器,然后开始我们的集成测试。

但是,直接这样做意味着我们将遇到“docker in docker”的情况,这被认为是痛苦的。 “直接”是指从容器内启动容器。

解决方案避免“docker in docker”是在启动容器内的其他容器时连接到主机的 docker 引擎,而不是连接到容器内的引擎。通过连接到主机引擎,将启动同级容器,而不是子容器。

[Alt](https://res.cloudinary.com/practicaldev/image/fetch/s--GHoYNiT1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/i/tgrpm0uzsqfdb9zcbpcp.png)

好消息是,当我们使用 BitBucket 管道时,这似乎是开箱即用的!因此,当我们在 docker 本地运行时,这是一个问题。

疼痛和补救措施

有两个主要问题:

  • 如何使用容器内的主机 Docker 引擎而不是容器内的引擎

  • 如何使两个同级容器相互通信

  • 如何配置我们的测试和服务,以便在我们的所有三个环境中都可以进行通信

使用主机Docker引擎

在我们项目的根目录中,我们有一个docker-compose-test.yml,它只是启动一个 python 容器并执行scripts/run-integration-tests.sh

version: '3.5'
services:
  app:
    tty: true
    build:
      context: .
      dockerfile: ./Dockerfile-local # installs docker, copies application code, etc. nothing fancy
    entrypoint: bash scripts/run-integration-tests.sh
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock # That's the gotcha

当上述容器启动时,它将安装 docker,并且我们运行的 docker 命令将被定向到我们主机的 docker 引擎 - 因为我们在容器中安装了主机的套接字文件。

要启动这个测试容器,我们实际上结合了kafka/docker-compose.ymldocker-compose-test.yml- 通过docker-compose --project-directory=. -f local_kafka/docker-compose.yml -f ./docker-compose-test.yml -f ... <build|up|....。这只是用所有 kafka 服务和附加服务扩展 compose 文件的简单方法。我们在 Makefile 中有这个来避免输入太多。例如。使用下面的 Makefile 片段,我们可以执行make build-test来构建测试容器。

command=docker-compose --project-directory=.  -f local_kafka/docker-compose.yml -f ./docker-compose-test.yml
build-test:
    $(command) build --no-cache

同级容器间通信

在本地运行容器时

很酷,我们可以避免 docker-in-docker 的情况,但是我们需要解决一个“问题” :)

如何从一个同级容器内通信到另一个容器?

或者在我们的特殊情况下,如何通过我们的测试使 python 容器连接到 kafka 容器——我们不能简单地在 python 容器中使用kafka://localhost:29092,因为 localhost 将引用,嗯..,python 容器主机。

我们找到的解决方案是通过主机,然后使用它暴露给主机的 kafka 容器的端口。

在较新版本的 Docker 中,从容器中,host.docker.internal将指向启动容器的主机。这很方便,因为如果我们将 kafka 作为容器在主机上运行并暴露其 29092 端口,那么从我们的 python 测试容器中,我们可以使用host.docker.internal:29092连接到带有 kafka 的兄弟容器!好东西!

所以本质上:

  • 从主机的本机 python 运行测试时,我们可以定位localhost:29092

  • ,当从容器中运行测试时,我们需要将它们配置为使用host.docker.internal:29092

在 BitBucket 上运行容器时

正如人们所预料的那样,现在还有另一个问题。当我们在 CI 中并启动容器时,容器内的host.docker.internal不起作用。如果是这样就好了,对吧...好消息是我们可以使用一个地址来连接到主机(即 bitbucket “主机”)。

我在检查传递给我们构建的环境变量时偶然发现了它 -$BITBUCKET_DOCKER_HOST_INTERNAL环境变量保留了我们可以用来连接到 BitBucket“主机”的地址。

现在以上实际上是我想分享的重要发现。

在下一节中,我将讨论如何实际将文件/脚本放在一起,以便它们在我们所有的三个环境中粘合在一起。

将它们粘在一起

鉴于上述解释,在我看来,最棘手的部分是如何在所有三个环境中配置 kafka 侦听器以及如何配置测试以使用正确的 kafka 地址(localhost/host.docker.internal/$BITBUCKET_DOCKER _主机_内部)。我们需要相应地配置 kafka 监听器,因为我们的测试 kafka 客户端会使用不同的地址连接到 kafka,因此 kafka 容器应该能够在相应的地址上进行监听。

为了处理 kafka 监听器部分,我们使用了 docker-compose 文件扩展功能。我们有一个“主”kafka/docker-compose.yml文件,它协调所有容器并使用设置配置它们,这些设置在通过 localhost 从本地 python 使用容器时可以正常工作。我们还添加了一个简单的docker-compose.ci.override.yml,内容如下:

version: '3.5'
services:
  kafka:
    environment:
      - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT_HOST://${BITBUCKET_DOCKER_HOST_INTERNAL:-host.docker.internal}:29092,PLAINTEXT://kafka:9092

在本地 docker 和 CI 中运行测试时启动 kafka 和朋友时将使用此文件 - 如果设置了$BITBUCKET_DOCKER_HOST_INTERNAL,则将使用其值 - 然后我们在 CI 中。如果未设置,则使用默认的 host.docker.internal - 这意味着我们在本地 docker env 中。


我们的测试/应用程序是通过 env vars 配置的。我们的run-integration-tests.sh看起来像这样

pip install --no-cache-dir docker-compose && docker-compose -v
docker-compose -f local_kafka/docker-compose.yml -f docker-compose-test-ci.override.yml down
docker-compose -f local_kafka/docker-compose.yml -f docker-compose-test-ci.override.yml up -d

# install app dependancies

# wait for kafka to start
sleep_max=15
sleep_for=1
slept=0
until [ "$(docker inspect -f {{.State.Health.Status}} kafka)" == "healthy" ]; do
  if [ ${slept} -gt $sleep_max ]; then
    echo "Waited for kafka to be up for ${sleep_max}s. Quitting now."
    exit 1
  fi
  sleep $sleep_for
  slept=$(($sleep_for + $slept))
  echo "waiting for kafka..."
done
# !! figure out in which environment we are running and configure our tests accordingly
if [ -z "$CI" ]; then
  if [ -z "$DOCKER_HOST_ADDRESS" ]; then
    echo "running locally natively"
  else
    echo "running locally within docker"
    export KAFKA_BOOTSTRAP_SERVER=host.docker.internal:29092
    export SCHEMA_REGISTRY_SERVER=http://host.docker.internal:8081
  fi
else
  echo "running in CI environment"
  export KAFKA_BOOTSTRAP_SERVER=$BITBUCKET_DOCKER_HOST_INTERNAL:29092
  export SCHEMA_REGISTRY_SERVER=http://$BITBUCKET_DOCKER_HOST_INTERNAL:8081
fi
pytest src/tests/integration_tests

该脚本可以在我们所有的三个环境中使用——它将相应地设置 KAFKA_BOOTSTRAP_SERVER(我们的测试配置)环境变量。

仅供参考 在前几段的 Makefile 示例中,为简洁起见,我省略了 ci.override.yml 文件。

感谢您阅读这篇文章 - 我希望您在其中找到有用的信息!

如果您知道从上面解决问题的更好方法,请告诉我!我找不到任何东西,因此想将其他人从我所经历的痛苦中拯救出来:)

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐