Docker 案例:为多个服务组成应用构建本地开发环境

Docker

在当今非常多的复杂 web 应用运行在容器化产品的时代,我们仍旧保持使用古董开发方法,在本地开发机器上安装 PostgresqlRedisRuby 及其它依赖控件。

将使维护开发进程变成非常困难的任务,尤其当基础系统变得异构化、扩展出大量的后台服务且依赖多种多样版本的控件时,特别是依赖的各控件有多个不同版本的变化时,将变得异常困难。

本文,将以我的开发项目 Amplifr 为例,回顾本地开发环境容器化布署方法。主要通过 docker-compose 及其内部的虚拟网络技术的帮助下完成。这种技术简单而高效。

应用开发项目的所有基础服务都进行容器化,在 Kubenetes 产品上布署管理。我们将只学习在单机本地化布署的方法,主要依据配置开发进程便利性的原则。

本地开发环境容器化的益处

  • 无需在本地主机安装所有开发依赖控件,诸如,数据库、语言解释器等,保持本地主机的相对 纯净
  • 自然支持多个开发环境,诸如,在主机上运行不同版本的 Ruby、Postgresql 数据库服务等。

项目概览

虽然 Amplifr 项目后台运行在 Rails 基础上,项目也具备复杂的前端,由独立的 Node.js 服务、Logux web-socket 服务和其它协助者服务提供,分别由 Node.js、Ruby 和 Golang 开发语言编写。

下图展示了项目基本架构逻辑组成:T

Overall Amplifr's services map

下面就整个系统架构及依赖进行简单回顾。

后端服务

后端服务是典型的 Rails 应用,完成项目的所有商业逻辑及利用 Sidekiq 完成许多后台优化性能的任务。

前端服务

前端仅仅是整个项目公共 HTTP 请求的入口。它服务于前端资源分配且代理其它请求并传输到 Rails 后端。
后端服务也与前端整合共享一些数据,诸如, 'browsers.json' 格式的文件,用于正确渲染 HTML 网页。

Frontend-backend integration

Logux 服务

Logux 是对外暴露 web-socket 端口、控制与客户端浏览器双向连接的一项服务。执行项目商业逻辑中,它在后端整合2类 HTTP 通讯。它可保证所有商业逻辑运行在 Ralis 后端服务的同时,通过 HTTP 发送来自后端服务的通知到原始的客户。

Logux-backend integration

链接地址缩短服务

链接地址缩短(link shortener) 是特有的一项 web 服务,由 Golang 开发语言编写。 它的目标是缩短一个链接地址或展开,且管理对缩短链接地址展开的统计。

Link shortener server integration with backend

「预览」服务

预览服务是公共服务,用于来自客户端浏览器对 OpenGraph 展示的渲染请求。它仅有公共 http 请求端入口。

其它控件

Shortener (链接缩短)- 是独立的服务,用于缩短 url 和保持对展开的链接地址的分析。它使用 Golang 开发语言编写。它具有公共用于展开缩短的链接地址的外部入口,及与后端运行的控制发布社交内容缩短的链接地址服务的内部入口。

一些其它的内部服务,诸如 telegram 和 facebook 自动机器人程序等,只有与后端整合连接的入口。

服务控件间的依赖关系

许多服务控件其本身就是一种复合的 web 服务,它可能依赖更底层的系统服务控件。例如,依赖 Postgres 、Redis等服务。

Internals of the backend component and how we are going to dockerize it

容器化

💡我们将利用 Docker Compose 工具整合每一个独立容器化的服务控件。Docker Compose 就是一个配置且同时运行多个 docker 容器,并将多容器整合在一起的易用工具,它只需一个启动(up)命令就可将配置好的相互依赖的多容器全部运行起来,完成整体服务:

docker-compose up

💡使所有的服务容器整合在一起利用了  docker networks(docker 虚拟网络)技术,它使得其配置的所有容器彼此间可以通讯交互。本例子项目中,简单化地将所有容器通过名为 'internal' 的 docker 虚拟网络进行联通。做为睿智的读者您,应可以为每个服务容器或互相联通的服务容器组配置不同的独立 docker 虚拟网络。

Docke 化 Ruby 后端(Backend)

现在我们有了一个标准的开发栈计划:PostgresRedisRails web 服务Sidekiq 后台。所有这些我们在 'docker-compose.yaml' 配置文件中配置。

下面是关键要点:

  • 对于 PostgresRedis ,我们将配置 永久卷  存储应用运行中的数据。
  • 我们不把 Ruby 源代码拷入容器中,替代方法是 - 挂载保存 Rails 应用源代码目录到 /app 文件夹下。
  • 我们也为 bundle 和其它服务配置永久存储,加快之后的启动速度。
  • 我们将配置 amplifr_internal 网络,使所有容器可通过这个网络交互。
  • 应用项目中会需要配置一些应用使用的环境变量,我们也将在 docker-compose 文件中配置。
  • 在 docker-compose.yaml 配置文件中,我们配置了一个基础服务(app),之后,使用 YAML 文件的 锚(Anchors )别名(aliases) 语法去引用它,而不必要在后续的基于基础服务(app)的服务配置中重复书写其基类配置。

❗请记住本例项目中的配置,不同于为项目创建 docker image(映像)的方法。创建 docker 映像的方法是将所有的源代码和依赖的服务都拷贝或安装在映像里,使构建的 docker 映像(image)是一个自恰系统,而无外部依赖。

'gist with all the config ',这是在 github 上项目的全部配置内容文件,但现在让我们关注一些配置重点:

用于其它服务继承的基类服务(base-service)配置描述

services:
  app: &app
    build:
      context: .
      dockerfile: Dockerfile.dev
      args:
        PG_VERSION: '9.6'
    image: amplifr-dev:0.1.0
    volumes:
      - .:/app:cached
      - bundle:/bundle
    environment:
      # 环境变量设置(environment settings)
      - BUNDLE_PATH=/bundle
      - BUNDLE_CONFIG=/app/.bundle/config
      - RAILS_ENV=${RAILS_ENV:-development}

      - DATABASE_URL=postgresql://postgres@postgres/amplifr_${RAILS_ENV}
      - REDIS_URL=redis://redis:6379/

      # 前后端服务整合环境变量设置(service integrations)
      - FRONTEND_URL=https://frontend-server:3001/
      - LOGUX_URL=http://logux-server:31338
    depends_on:
      - postgres
      - redis
    tmpfs:
      - /tmp

基类服务(base service)容器将按 Dockerfile.dev 配置文件配置创建,其中指定了创建容器对 Postgres 数据库版本的依赖。所有其它 Ruby 类的服务映像都将继承这个基类服务的配置,下面是其继承关系图表:

Service inheritance

配置文件中也定义了挂载 /app 目录为这几个相关服务容器的 docker 卷。这防止每次启动时依赖包的重新安装。

配置中还定义了两组项目环境变量:
1) 系统(system) 级别的环境变量。如 BUNDLE_PATH , REDIS_URL  和  DATABASE_URL 。
2) 整合各依赖服务的访问地址环境变量:
FRONTEND_URL - 后端(backend)对前端(frontend)的内部入口访问地址。
LOGUX_URL - 内部访问 Logux HTTP 服务入口地址,用于从 Rails-app 到 Logux 发送操作任务。

运维(runner)服务配置说明

runner 服务容器用于执行运维指令任务,诸如,rake 任务,或做为 Rails 环境生成器。它是面向控制台,使用命令行控制的服务。因此,必须建立 stdin_opentty 配置选项,以满足当服务容器启动时通过 -i--t 命令选项保持服务容器可通过 Bash shell 进行交互的需求:

services:
  runner:
    <<: *app #原文这里为 *backend, 译者认为应是 *app
    stdin_open: true
    tty: true
    command: /bin/bash

可使用如下命令启动它并下达指令

$ docker-compose run runner bundle exec rake db:create

# 或直接运行 (run)容器并在容器中运行任何命令 
$ docker-compose run runner

名为 server 的服务容器配置

定义 web 服务容器。这里的关键点是它定义了 docker 虚拟网络 internal 且加入在这个虚拟网络下的其服务容器别名 backend-server,这样它在这个网络中将被通过 backend-server 这个网络别名访问到。

services:
  server:
    <<: *app
    command: bundle exec thin start
    networks:
      default:
      internal:
        aliases:
          - backend-server
    ports:
      - '3000:3000'

networks:
  internal:

Sidekiq 服务容器配置

简单,它只是运行 sidekiq 且继承自基类服务(base service)容器配置:

services:
  sidekiq:
    <<: *app
    command: sidekiq

Redis 和 Postgres 服务容器配置

  postgres:
    image: postgres:9.6
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - 5432

  redis:
    image: redis:3.2-alpine
    volumes:
      - redis:/data
    ports:
      - 6379

volumes:
  postgres:
  redis:

这里配置的重点是挂载容器永久卷的目录路径,保证运行中数据的永久存储。

Dockerfile

我不想深入讨论 Dockefile 的编写。你可在  这里 找到完全的文件内容。只是注意它来自标准的 ruby 映像(image),并安装上依赖的软件控件,诸如, Postgressql 连接客户端软件及其它捆绑的执行程序等。

使用方法

使用方法较简单:

$ docker-compose run runner ./bin/setup # 在 docker 中执行  ./bin/setup 命令
$ docker-compose run runner bundle exec rake db:drop # 执行 rake 任务
$ docker-compose up server # 启动 web 服务g
$ docker-compose up -d # 启动所有的服务容器(web,sidekiq)
$ docker-compose up rails db # 启动 postgres 客户端

Docker Compose 的配置执行机制可以指定启动服务的依赖,即当一个服务容器启动时,先要保证其依赖的服务容器已启动。例如, Sidekiq 服务容器的运行需要 Redis 和 Postgres 服务提供服务,这就是我们在服务容器配置段中配置 depends_on 节的缘故。

下面是服务依赖关系图示,显示出这些服务容器运行次序:

Service startup sequence in case of depencencies

小结

我们已建立起开发基于 Rails 技术的 docker 类型的本地开发环境。它与在本机上布署的开发环境功能相同: 有持久的数据库数据存储,可运行 rake 任务等等。运行在容器上的rails dbrails c 等命令也可以很好地工作。

主要的优点是,我可以很容易地使用一行配置的改变,切换 Postgres 或 Ruby 的运行版本。重建 docker image 后将使开发运行在新的环境下。

容器化 Node.js (前端服务 -- frontend server)

主要关键点有这些:

  • 使用官方基础的 node docker 映像(image),不需任何调整
  • 添加名为 server 服务容器到名为  amplifr_internal 的 docker 虚拟网络下
  • 定义 BACKEND_URL 环境变量,表示接入后端服务的途径
  • 挂载 mode_modules 卷,其是 Node.js 模块的安装目录
version: '3.4'

services:
  app: &app
    image: node:11
    working_dir: /app
    environment:
      - NODE_ENV=development
      #译注:下面引用的是后端服务簇中 server 服务的别名和映射端口
      - BACKEND_URL=http://backend-server:3000
    volumes:
      - .:/app:cached
      - node_modules:/app/node_modules

  runner:
    <<: *app
    command: /bin/bash
    stdin_open: true
    tty: true

  server:
    <<: *app
    command: bash -c "yarn cache clean && yarn install && yarn start"
    networks:
      default:
      amplifr_internal:
        aliases:
          - frontend-server
    ports:
      - "3001:3001"

networks:
  amplifr_internal:
    external: true

volumes:
  node_modules:

使用方法

现在,前端(frontend)服务容器可十分简单地启动,执行如下命令:

$ docker-compose up server

但是,要启动前端(frontend)服务必须先启动后端(backend)服务,因为前端服务引用到后端的 internal 网络,它只有将后端服务启动后才有效。(译注:这里的意思是要启动前端,必须先按后端的 docker compose 配置文件启动后端,然后再启动前端的 docker compose)。

容器化 Logux 服务

这总体上较简单,Logux 服务可以参考前端(frontend)服务的配置。不同点是 Logux 服务有自己的环境变量设置,以建立与整体各服务的交互。

docker-compose up server # 启动名为 server 的 docker compose 服务容器

容器化 Golang (链接地址缩短 web 服务)

主旨相同:

  • 使用建好的 Golang 映像,并挂载程序源代码目录,运行 go run 做为 Golang 语言解释器。
  • 通过 docker 虚拟网络与 Ruby 后端服务整合。

我们项目中的这个 web 服务依赖 Postgres 和 Redis 服务。下面是对其 Dockerfile 文件的配置一些说明,完整的配置例子,参见  这里

FROM golang:1.11

ARG MIGRATE_VERSION=4.0.2

# 为开发环境安装 postgres 客户端连接软件
RUN apt-get update && apt-get install -y postgresql-client

# 安装 dep 工具保证 Golang 依赖关系
RUN go get -u github.com/golang/dep/cmd/dep

# 安装迁移命令行环境,保证可进行数据库迁移任务
ADD https://github.com/golang-migrate/migrate/releases/download/v${MIGRATE_VERSION}/migrate.linux-amd64.tar.gz /tmp
RUN tar -xzf /tmp/migrate.linux-amd64.tar.gz -C /usr/local/bin && mv /usr/local/bin/migrate.linux-amd64 /usr/local/bin/migrate

ENV APP ${GOPATH}/src/github.com/evilmartians/ampgs
WORKDIR ${APP}

下面是一些可能感兴趣的配置说明:

  • 在开发环境 docker image(映像) 中,安装了 postgres 客户端(postgres-client )。当使用类似 docker-compose run runner "psql $DATABASE_URL" 的命令连接 postgres 数据库执行 psql 命令时,其提供支持。相同方法可用于 Ruby 服务容器。
  • 我们安装了 dep 工具检查并安装所有的依赖包,如使用命令: docker-compose run runner dep ensure
  • 我们在 docker 映像中安装了数据库迁移工具,可将服务容器中的数据库进行迁移,如使用命令:  docker-compose run runner "migrate -source file://migrations/ -database ${DATABASE_URL} up"

‼️ 我们不需要组成 docker 开发环境映像中的大多数工具,因为它仅包含编译后的二进制文件。

我们将使用与 Ruby 服务相同的方式将 Golang 服务容器化:

  • 提取所有服务容器共性构建名为 app 的基类容器,在此基础上构建名为 runner 的服务容器执行运维任务
  • 加入对具有永久数据存储能力的 Postgres 和 Redis 服务的依赖

下面是docker-compose.yml配置文件中的重要部分:

services:
  # 基类服务(base service)配置
  app: &app
    image: ampgs:0.3.1-development
    build:
      context: .
      dockerfile: docker/development/Dockerfile
    environment:
      REDIS_URL: redis://redis:6379/6
      DATABASE_URL: postgres://postgres:postgres@postgres:5432/ampgs
    volumes:
      - .:/go/src/github.com/evilmartians/ampgs
    depends_on:
      - redis
      - postgres

  runner:
    <<: *app

  web:
    <<: *app
    command: "go run ampgs.go"
    ports:
      - '8000:8000'
    networks:
      default:
      amplifr_internal:
        aliases:
          - ampgs-server

总结

Docker-compose是简化多服务管理复杂性的强大工具。

让我们回顾一下,使用docker compose 布署构建本地的 docker 化的开发环境的一些主要原则:

  • 挂载程序或工具源代码目录到容器中,消除 docker 启动时对这些的重建工作,这使 docker 的重启节约大量的时间。
  • 使用 docker 虚拟网络来保证服务之间的通信。它有助于整体测试所有服务,但各服务保持其独立性。
  • 通过在 docker-compose 中配置环境变量 (environments variables)使 各服务可彼此感知

就这些了。感谢您的参阅!

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://dev.to/amplifr/dockerize-the-mul...

译文地址:https://learnku.com/server/t/41902

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!