tBucket Pipelines 期间的# 个同级 Docker 容器 - Kafka 用例
在本文中,我将分享我的团队在 BitBucket Pipelines 上设置基础设施以促进集成测试的经验。 我将讨论我们在将服务的外部依赖项编排为 docker 容器并使它们在本地和 CI 集成测试期间相互通信时所面临的挑战。 具体来说,我将概述 docker-in-docker 问题,如何在本地、本地在 docker & 在 CI 中解决它,以及如何通过 docker-compose.yml &
在本文中,我将分享我的团队在 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服务管理。
[](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 引擎,而不是连接到容器内的引擎。通过连接到主机引擎,将启动同级容器,而不是子容器。
[](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.yml
和docker-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 文件。
感谢您阅读这篇文章 - 我希望您在其中找到有用的信息!
如果您知道从上面解决问题的更好方法,请告诉我!我找不到任何东西,因此想将其他人从我所经历的痛苦中拯救出来:)
更多推荐
所有评论(0)