开发微服务-Node,React和Docker

原文出处 Developing Microservices - Node, React, and Docker

在本文中,你将学习如何使用Docker快速创建可复用的开发环境,以管理许多NodeJS微服务。

微服务架构

这篇文章假定你对以下主题预先做了了解。更多主题信息请参阅提供的资源地址:

主题 资源地址
Docker Get started with Docker
Docker Compose Get started with Docker Compose
Node/Express API Testing Node and Express
React React Intro
TestCafe Functional Testing With TestCafe
Swagger Swagger and NodeJS

注意: 想了解稍微简单一点的实现,请访问我之前的文章 - 使用Docker开发和测试微服务.

目录

  1. 目标
  2. 体系结构
  3. 项目设置
  4. Users服务
  5. Web服务 - 第1部分
  6. Movies服务
  7. Web服务 - 第2部分
  8. 工作流
  9. 测试设置
  10. Swagger设置
  11. 下一步

目标

本教程结束时,你应该能够......

  1. 使用Docker和Docker Compose在本地配置和运行微服务
  2. 利用卷将代码挂载到容器中
  3. 在Docker容器中运行单元和集成测试
  4. 用功能性的端到端测试来测试整套服务
  5. 调试正在运行的Docker容器
  6. 使在不同容器中运行的服务能够相互通信
  7. 通过基于JWT的认证保护你的服务
  8. 配置Swagger与服务进行交互

体系结构

这篇文章的最终目标是将文章开头图片中的技术组织到以下容器和服务中:

名称 服务 容器 技术
Web Web web React, React-Router
Movies API Movies movies Node, Express
Movies DB Movies movies-db Postgres
Swagger Movies swagger Swagger UI
Users API Users users Node, Express
Users DB Users users-db Postgres
Functional Tests Test n/a TestCafe

让我们开始吧!

项目设置

首先clone基础项目,然后checkout到第一个标签:

$ git clone https://github.com/mjhea0/microservice-movies
$ cd microservice-movies
$ git checkout tags/v1

总体的项目结构

.
├── services
│   ├── movies
│   │   ├── src
│   │   │   └── db
│   │   └── swagger
│   ├── users
│   │   └── src
│   │       └── db
│   └── web
└── tests

在我们添加Docker之前,请务必查看一下代码,方便对所有的功能有个基本的了解。你也可以动手测试一下这些服务......

Users:

  • 导航到 “services/users”
  • npm install
  • 将package.json中的start脚本更新为"gulp --gulpfile gulpfile.js"
  • npm start
  • 在浏览器中访问http://localhost:3000/users/ping

Movies:

  • 导航到 “services/movies”
  • npm install
  • 将package.json中的start脚本更新为"gulp --gulpfile gulpfile.js"
  • npm start
  • 在浏览器中访问http://localhost:3000/movies/ping

Web:

  • 导航到 “services/web”
  • npm install
  • npm start
  • 在浏览器中访问http://localhost:3006,这时你应该可以看到登录页面。

接下来,在项目根目录添加docker-compose.yml文件。Docker Compose将调用该文件将多个服务链接在一起。使用一个命令,Docker Compose就可以启动我们所需的所有容器,使这些容器可以根据需要相互通信。

有了这个,在我们开始每一项服务时,确保可以随时进行测试......

Users 服务

我们将从数据库开始,因为API依赖于它的运行……

Database

首先,在“services/users/src/db”目录中添加 Dockerfile 文件:

FROM postgres

# run create.sql on init 
ADD create.sql /docker-entrypoint-initdb.d

在这里,我们通过在容器中的“docker-entrypoint-initdb.d”目录里添加一个SQL文件来扩展官方的Postgres镜像,该镜像将在init上执行。

然后更新 docker-compose.yml 文件:

version: '2.1'

services:

  users-db:
    container_name: users-db
    build: ./services/users/src/db
    ports:
      - '5433:5432' # expose ports - HOST:CONTAINER
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    healthcheck:
      test: exit 0

这个配置将从“services/users/src/db”中找到 Dockerfile 文件创建一个名为users-db的容器。(目录是相对于 Dockerfile 文件的)。

一旦启动,环境变量将被添加,并在成功启动和运行后发送0的退出代码。Postgres将在宿主机上使用5433端口,其他服务使用5432端口。

NOTE: 如果你只希望Postgres用于其他服务而不是主机,请使用expose, 而不是 ports:

expose:
  - "5432"

注意使用的版本-2.1。这与安装的Docker Compose版本没有直接关系;而是指定你想要使用的文件格式。

启动这个容器:

$ docker-compose up --build -d users-db

一上来,我们先来个快速的完整性检查。进入shell:

$ docker-compose run users-db bash

然后运行env确保设置了正确的环境变量。你也可以查看“docker-entrypoint-initdb.d” 目录:

# cd docker-entrypoint-initdb.d/ 
# ls 
create.sql

完成后exit

API

转向API,在“services/users”目录上添加一个 Dockerfile 文件,确保查看注释:

FROM node:latest

# set working directory
RUN mkdir /usr/src/app
WORKDIR /usr/src

# add `/usr/src/node_modules/.bin` to $PATH
ENV PATH /usr/src/node_modules/.bin:$PATH

# install and cache app dependencies
ADD package.json /usr/src/package.json
RUN npm install

# start app
CMD ["npm", "start"]

注意:确保利用Docker的分层缓存系统,通过在添加应用程序的源文件之前添加package.json和安装依赖项来加快构建时间。有关这方面的更多信息,请查看 构建有效的Dockerfiles-Node.js

然后将users-service添加到 docker-compose.yml 文件中:

users-service:
  container_name: users-service
  build: ./services/users/
  volumes:
    - './services/users:/usr/src/app'
    - './services/users/package.json:/usr/src/package.json'
  ports:
    - '3000:3000' # expose ports - HOST:CONTAINER
  environment:
    - DATABASE_URL=postgres://postgres:postgres@users-db:5432/users_dev
    - DATABASE_TEST_URL=postgres://postgres:postgres@users-db:5432/users_test
    - NODE_ENV=${NODE_ENV}
    - TOKEN_SECRET=changeme
  depends_on:
    users-db:
      condition: service_healthy
  links:
    - users-db

这里发生了什么?

  • volumes:volumes用于在容器中挂载目录,这样在对代码进行修改时无需重新生成镜像。这是本地开发环境的默认设置,因此可以快速地获得关于代码的更改反馈。
  • depends_on:depends_on指定服务启动的顺序。在这个示例中,users-service在启动之前需要等待users-db的成功启动(退出代码为0)。
  • links: 通过 links 可以链接到在其他容器中运行的服务。因此,通过这个配置,users-service中的代码能够通过users-db:5432 来访问数据库。

注意: 对于depends_onlinks的区别有疑问的话,可以查看 Stack Overflow 讨论 了解更多信息。

设置NODE_ENV环境变量:

$ export NODE_ENV=development

构建镜像并启动这个容器:

$ docker-compose up --build -d users-service

注意: 请记住,Docker Compose处理构建和运行时间。这可能会令人有些困惑。比如,看看当前的docker-compose.yml文件 - 构建时发生了什么?运行时间怎么样?你怎么知道的?

一旦启动,在项目根目录中创建一个名为 _initdb.sh 的新文件,并添加Knex迁移和种子命令:

#!/bin/sh   
docker-compose run users-service knex migrate:latest --env development --knexfile app/knexfile.js 
docker-compose run users-service knex seed:run --env development --knexfile app/knexfile.js

然后应用迁移并添加种子:

$ sh init_db.sh
Using environment: development
Batch 1 run: 1 migrations
/src/src/db/migrations/20170504191016_users.js
Using environment: development
Ran 1 seed files
/src/src/db/seeds/users.js

测试:

接口 HTTP 方法 CRUD 方法 结果
/users/ping GET READ pong
/users/register POST CREATE add a user
/users/login POST CREATE log in a user
/users/user GET READ get user info
$ http POST http://localhost:3000/users/register username=foo password=bar
$ http POST http://localhost:3000/users/login username=foo password=bar

注意: 上述命令中的httpHTTPie 库的一部分,这个库是cURL之上的包装器。

在这两种情况下,你都应该看到成功的状态和一个token,如下:

{
    "status": "success",
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
}

最后,运行单元和集成测试:

$ docker-compose run users-service npm test

运行结果如下:

routes : index
  GET /does/not/exist
    ✓ should throw an error

routes : users
  POST /users/register
    ✓ should register a new user (178ms)
  POST /users/login
    ✓ should login a user (116ms)
    ✓ should not login an unregistered user
    ✓ should not login a valid user with incorrect password (125ms)
  GET /users/user
    ✓ should return a success (114ms)
    ✓ should throw an error if a user is not logged in

auth : helpers
  comparePass()
    ✓ should return true if the password is correct (354ms)
    ✓ should return false if the password is correct (315ms)
    ✓ should return false if the password empty (305ms)

auth : local
  encodeToken()
    ✓ should return a token
  decodeToken()
    ✓ should return a payload


12 passing (4s)

查看测试规格以获得更多信息。就这样!接下来是网络服务......

Web服务 - 第1部分

随着我们的用户服务开始运行,我们可以将注意力转移到客户端,并在容器中启动React应用程序来测试身份验证。

注意: React代码是从我的两个学生 - Charlie BlackstockEvan Moore分别编写的intro-react-redux-omdbcommunikey移植而来的。

在“services/web”中增加一个 Dockerfile 文件:

FROM node:latest

# set working directory
RUN mkdir /usr/src/app
WORKDIR /usr/src/app

# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH

# install and cache app dependencies
ADD package.json /usr/src/app/package.json
RUN npm install
RUN npm install react-scripts@0.9.5 -g

# start app
CMD ["npm", "start"]

截至2017年5月10日,OMDb API是私有的,所以您必须捐赠至少1美元才能获得访问权限。获得API密钥后,更 services/web/src/App.jsx 中的API_URL

const API_URL = 'http://www.omdbapi.com/?apikey=addyourkey&s='

然后更新 docker-compose.yml 文件,如下所示:

web-service:
  container_name: web-service
  build: ./services/web/
  volumes:
    - './services/web:/usr/src/app'
    - '/usr/src/app/node_modules'
  ports:
    - '3007:3006' # expose ports - HOST:CONTAINER
  environment:
    - NODE_ENV=${NODE_ENV}
  depends_on:
    users-service:
      condition: service_started
  links:
    - users-service

为了防止卷 - /usr/src/app - 覆盖package.json,我们使用了一个数据卷 - _/usr/src/app/nodemodules 。这可能有必要,也可能没有必要,具体取决于你设置镜像和容器的顺序。查看使用docker-compose安装npm包了解更多。

构建镜像并启动容器:

$ docker-compose up --build -d web-service

注意:为了避免处理太多的配置(babel和webpack),React应用程序使用Create React App

打开浏览器并导航到http://localhost:3007。你可以看到登录页面:

login page

使用下面的用户名和密码登录:

  • username:foo
  • password:bar

登录成功后,你可以看到下面的页面:

search page

services/web/src/App.jsx 中,让我们快速浏览一下loginUser()方法中的AJAX请求:

loginUser (userData, callback) {
  /*
    why? http://localhost:3000/users/login
    why not? http://users-service:3000/users/login
   */
  return axios.post('http://localhost:3000/users/login', userData)
  .then((res) => {
    window.localStorage.setItem('authToken', res.data.token)
    window.localStorage.setItem('user', res.data.user)
    this.setState({ isAuthenticated: true })
    this.createFlashMessage('You successfully logged in! Welcome!')
    this.props.history.push('/')
    this.getMovies()
  })
  .catch((error) => {
    callback('Something went wrong')
  })
}

为什么我们使用localhost而不是容器的名称,users-service?因为这个请求来自容器外部,在宿主机上。请记住,如果这个请求源于容器内部,那么我们将需要使用容器名称而不是localhost,因为在这种情况下,localhost会返回到容器本身。

确保您可以注销并注册。

接下来,让我们启动电影服务,以便终端用户可以将电影保存到集合中......

Movies 服务

Movies服务的设置与Users服务的设置几乎相同。亲自尝试来检查一下你的理解:

  1. Database

    • 添加 Dockerfile 文件
    • 更新 docker-compose.yml
    • 启动这个容器
    • 测试
  2. API

    • 添加 Dockerfile 文件
    • 更新 docker-compose.yml (请确保该服务与数据库和用户服务链接,并更新暴露端口 - api为 3001 ,db为 5434)
    • 启动这个容器
    • 应用迁移和种子
    • 测试

注意: 如果需要帮助,可以从microservices-moviesv2标签中获取代码。

与用户数据库相比,电影数据库镜像构建所需时间要少的多。为什么?

随着容器的启动,我们来测试一下接口......

接口 HTTP 方法 CRUD 方法 结果
/movies/ping GET READ pong
/movies/user GET READ get all movies by user
/movies POST CREATE add a single movie

首先在浏览器中访问http://localhost:3001/movies/ping,页面的内容为pong!打开http://localhost:3001/movies/user,内容为:

{
 "status": "Please log in" 
}

由于你需要通过身份验证来访问其他路由,我们通过运行集成测试来测试它们:

$ docker-compose run movies-service npm test

结果为:

routes : index
  GET /does/not/exist
    ✓ should throw an error

Movies API Routes
  GET /movies/ping
    ✓ should return "pong"
  GET /movies/user
    ✓ should return saved movies
  POST /movies
    ✓ should create a new movie


4 passing (818ms)

检查测试规格了解更多信息。

Web服务 - 第2部分

转向 docker-compose.yml 文件。更新web-servicelinksdepends_on键:

depends_on:
  users-service:
    condition: service_started
  movies-service:
    condition: service_started
links:
  - users-service
  - movies-service

为什么?

接下来,更新容器:

$ docker-compose up -d web-service

让我们在浏览器中测试一下!打开http://localhost:3007/。注册一个新用户,然后添加一些电影到收藏。

一定要查看收藏:

collection page

打开 _services/movies/src/routes/helpers.js 并记下 ensureAuthenticated() 方法:

let ensureAuthenticated = (req, res, next) => {
  if (!(req.headers && req.headers.authorization)) {
    return res.status(400).json({ status: 'Please log in' });
  }
  const options = {
    method: 'GET',
    uri: 'http://users-service:3000/users/user',
    json: true,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${req.headers.authorization.split(' ')[1]}`,
    },
  };
  return request(options)
  .then((response) => {
    req.user = response.user;
    return next();
  })
  .catch((err) => { return next(err); });
};

uri为什么指向users-service而不是localhost

工作流

首先检查使用Docker开发和测试微服务的工作流部分。在代码更改时进行实时重新加载,并使用console.log调试正在运行的容器。

在集合页中添加标题:

带有标题的集合页

运行日志 - docker-compose logs -f web-service - 然后对其中一个中断编译的组件进行更改:

web-service       | Compiling...
web-service       | Failed to compile.
web-service       |
web-service       | Error in ./src/components/SavedMovies.jsx
web-service       |
web-service       | /usr/src/app/src/components/SavedMovies.jsx
web-service       |   10:13  error  'Link' is not defined  react/jsx-no-undef
web-service       |
web-service       | ✖ 1 problem (1 error, 0 warnings)
web-service       |
web-service       |

更正错误:

web-service       |
web-service       | Compiling...
web-service       | Compiled successfully!

继续尝试添加和更新React应用程序,直到在容器中使用它为止感到舒服为止。

测试设置

到目前为止,我们只用单元和集成测试测试了每个微服务。让我们把注意力转向功能性的端到端的测试,来测试整个系统。为此,我们将使用TestCafe

注意:不想使用TestCafe?请查看使用Mocha,Chai,Request和Cheerio(全部在容器内)进行测试的代码

我们偷个懒,在全局安装TestCafe:

$ npm install testcafe@0.15.0 -g

然后运行测试:

$ testcafe firefox tests/**/*.js

测试结果为:

testcafe firefox tests/**/*.js
 Running tests in:
 - Firefox 53.0.0 / Mac OS X 10.11.0

 /login
 ✓ users should be able to log in and out


 1 passed (3s)

注意: 对容器内的运行测试感兴趣,可以查看官方的TestCafe文档,了解更多有关在Docker上使用TestCafe的信息。

为了简化测试工作流程,请将 test.sh 文件添加到项目根目录中:

#!/bin/bash

fails=''

inspect() {
  if [ $1 -ne 0 ] ; then
    fails="${fails} $2"
  fi
}

docker-compose run users-service npm test
inspect $? users-service

docker-compose run movies-service npm test
inspect $? movies-service

testcafe firefox tests/**/*.js
inspect $? e2e

if [ -n "${fails}" ];
  then
    echo "Tests failed: ${fails}"
    exit 1
  else
    echo "Tests passed!"
    exit 0
fi

运行测试:

$ sh test.sh

Swagger设置

在“services/movies/swagger”目录中添加 Dockerfile 文件:

FROM node:latest

# set working directory
RUN mkdir /usr/src/app
WORKDIR /usr/src/app

# add `/usr/src/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH

# install and cache app dependencies
ADD package.json /usr/src/app/package.json
RUN npm install

# start app
CMD ["npm", "start"]

更新 docker-compose.yml

swagger:
  container_name: swagger
  build: ./services/movies/swagger/
  volumes:
    - './services/movies/swagger:/usr/src/app'
    - '/usr/src/app/node_modules'
  ports:
    - '3003:3001' # expose ports - HOST:CONTAINER
  environment:
    - NODE_ENV=${NODE_ENV}
  depends_on:
    users-service:
      condition: service_started
    movies-service:
      condition: service_started
  links:
    - users-service
    - movies-service

启动:

$ docker-compose up -d --build swagger

导航到http://localhost:3003/docs并对其进行测试:

swagger 文档

现在,你只需要支持基于JWT的身份验证并添加剩余的接口即可!

下一步

接下来呢?

  1. React 应用 - React应用需要用心来完成。 添加样式,解决bug。更新flash消息,这样每次只显示一个。 编写测试。建立新的特性。添加Redux。天空的极限。如果你愿意,请联系我!
  2. Swagger - 添加基于JWT的身份验证,并从电影服务中添加额外的接口。.
  3. Dockerfiles - 阅读Docker团队的编写Dockerfiles的最佳实践,并根据需要进行重构
  4. 生产环境 - 想在AWS上部署吗?查看On-Demand Environments With Docker and AWS ECS的博客文章。

microservice-movies的v2标签中获取最终代码。请在下面添加问题和评论。还有幻灯片!如果有兴趣,可以在这里查看