跳转至

容器化 React 应用

这节课让我们来容器化上面完成的 React 应用。不过在开始前,我们先重构下我们的项目吧。

重构

在项目根目录下面新增一个叫services的文件夹,然后将nginx文件夹移动到 services 目录下面,将client文件夹移动到 services 目录下面,然后在 services 目录下面新建一个 users文件夹,将根目录下面的project文件夹、Dockerfile、manage.py、requirements.txt 文件都移动到services/users目录下面,操作完成后,你看到的我们的代码结构应该是这样的:

$ flask-microservices-users [master] ⚡ ls -la
total 88
drwxr-xr-x  12 ych  staff    384  1 26 12:12 .
drwxr-xr-x   4 ych  staff    128 12 28 16:08 ..
drwxr-xr-x  14 ych  staff    448  1 26 12:27 .git
-rw-r--r--   1 ych  staff     23  1  5 17:56 .gitignore
-rw-r--r--   1 ych  staff      5 12 28 15:19 .python-version
-rw-r--r--   1 ych  staff    645  1  5 18:13 .travis.yml
-rw-r--r--   1 ych  staff  11357 12 29 10:44 LICENSE
-rw-r--r--   1 ych  staff    206  1  5 18:23 README.md
-rw-r--r--   1 ych  staff    922  1 26 12:11 docker-compose-prod.yml
-rw-r--r--   1 ych  staff    755  1 26 12:15 docker-compose.yml
drwxr-xr-x   6 ych  staff    192  1 26 12:09 services
$ flask-microservices-users [master] ⚡ ls -la services
total 16
drwxr-xr-x   6 ych  staff   192  1 26 12:09 .
drwxr-xr-x  12 ych  staff   384  1 26 12:12 ..
drwxr-xr-x  10 ych  staff   320  1 16 17:37 client
drwxr-xr-x   4 ych  staff   128 12 30 15:15 nginx
drwxr-xr-x   7 ych  staff   224  1 26 12:14 users
$ flask-microservices-users [master] ⚡ ls -la services/users
total 40
drwxr-xr-x  7 ych  staff   224  1 26 12:14 .
drwxr-xr-x  6 ych  staff   192  1 26 12:09 ..
-rw-r--r--  1 ych  staff   329 12 29 10:47 Dockerfile
-rw-r--r--  1 ych  staff  1366  1  5 17:46 manage.py
drwxr-xr-x  9 ych  staff   288 12 31 10:56 project
-rw-r--r--  1 ych  staff   159  1 13 14:28 requirements.txt

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

version: '3.3'

services:
  users-db:
    container_name: users-db
    build:
      context: ./services/users/project/db
      dockerfile: Dockerfile
    ports:
      - 3307:3306
    environment:
      - MYSQL_ROOT_PASSWORD=root321

  users-service:
    container_name: users-service
    build:
      context: ./services/users
      dockerfile: Dockerfile-dev
    volumes:
      - './services/users:/usr/src/app'
    ports:
      - 5001:5000 # 暴露端口 - 主机:容器
    environment:
      - APP_SETTINGS=project.config.DevelopmentConfig
      - DATABASE_URL=mysql+pymysql://root:root321@users-db:3306/users_dev
      - DATABASE_TEST_URL=mysql+pymysql://root:root321@users-db:3306/users_test
    depends_on:
      - users-db
    links:
      - users-db

同样的,更新docker-compose-prod.yml文件:

version: '3.3'

services:
  users-db:
    container_name: users-db
    build:
      context: ./services/users/project/db
      dockerfile: Dockerfile
    ports:
      - 3307:3306
    environment:
      - MYSQL_ROOT_PASSWORD=root321

  users-service:
    container_name: users-service
    build:
      context: ./services/users
      dockerfile: Dockerfile-prod
    expose:
      - '5000' # 只暴露容器端口
    environment:
      - APP_SETTINGS=project.config.ProductionConfig
      - DATABASE_URL=mysql+pymysql://root:root321@users-db:3306/users_prod
      - DATABASE_TEST_URL=mysql+pymysql://root:root321@users-db:3306/users_test
    command: gunicorn -b 0.0.0.0:5000 manage:app
    depends_on:
      - users-db
    links:
      - users-db

  nginx:
    container_name: nginx
    build: ./services/nginx
    restart: always
    ports:
      - 8080:80
    depends_on:
      - users-service
    links:
      - users-service

注意上面的docker-compose文件我们更新到3.3版本,注意看和之前的2.1版本是有部分区别的。然后重新构建下我们的容器:

$ docker-compose -f docker-compose.yml up -d --build

确保上面的构建过程是正确的,我们可以通过查看users-service服务的日志来确认:

$ docker-compose logs -f users-service
Attaching to users-service
users-service    |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
users-service    |  * Restarting with stat
users-service    |  * Debugger is active!
users-service    |  * Debugger PIN: 170-528-240
users-service    | 172.18.0.1 - - [26/Jan/2018 04:22:08] "GET /ping HTTP/1.1" 200 -

然后我们可以跑一遍我们的测试用例,确保的业务逻辑也是符合我们的预期的:

$ docker-compose -f docker-compose.yml run users-service python manage.py test

如果一切正常,上面的测试也会正常通过的。

本地开发环境

services/client目录下面添加Dockerfile-dev文件:

FROM node:9.4
MAINTAINER "qikqiak@gmail.com"

# 设置工作目录
RUN mkdir /usr/src/app
WORKDIR /usr/src/app

# 添加node_modules/.bin目录到 $PATH 环境下
ENV PATH /usr/src/app/node_modules/.bin:$PATH

# 安装并缓存依赖包(用silent选项可以减少一些不必要的错误)
ADD package.json /usr/src/app/package.json
RUN npm install --silent
RUN npm install react-scripts@1.0.17 -g --silent

# 启动应用
CMD ["npm", "start"]

然后我们在docker-compose.yml中新增 React 应用的服务:

client:
  container_name: client
  build:
    context: ./services/client
    dockerfile: Dockerfile-dev
  volumes:
    - './services/client:/usr/src/app'
  ports:
    - '3007:3000'
  environment:
    - NODE_ENV=development
    - REACT_APP_USERS_SERVICE_URL=${REACT_APP_USERS_SERVICE_URL}
  depends_on:
    - users-service
  links:
    - users-service

在确保users-service服务正常启动后,在终端中导入环境变量:

$ export REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001

然后重新构建镜像启动新的容器:

$ docker-compose -f docker-compose.yml up --build -d client

新容器启动完成后,运行 React 客户端测试代码:

$ docker-compose -f docker-compose.yml run client npm test

测试通过后,我们可以在本地浏览器中打开http://127.0.0.1:3007访问 React 应用了,当然如果测试没通过的话,我们得根据错误信息去修复我们的测试代码,直到测试通过为止。

更新services/nginx/flask.conf配置:

server {
  listen 80;

  location / {
    proxy_pass http://client:3000;
    proxy_redirect    default;
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
  }

  location /users {
    proxy_pass        http://users-service:5000;
    proxy_redirect    default;
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
  }
}

上面的nginx配置文件做了什么工作?

  • 首先在location块中定义了反向代理规则。
  • 当请求的 URI 与location区域中的 URI 匹配的时候,Nginx将请求转发给了后面的 React 服务(client)或者 Flask 服务(users-service)。

同样的,我们将Nginx服务也定义到docker-compose.yml文件中:

nginx:
  container_name: nginx
  build: ./services/nginx
  restart: always
  ports:
    - 8080:80
  depends_on:
    - users-service
    - client
  links:
    - users-service

然后更新容器:

$ docker-compose -f docker-compose.yml up -d --build

注意我们这里用的主机端口为8080,因为我们本机的80端口已经被占用,在实际线上部署的时候应该用80端口,然后再根据域名进行匹配进行测试。

容器启动完成后,打开浏览器测试下我们的服务,确保下面的两个请求都是正常的:

  • http://127.0.0.1:8080
  • http://127.0.0.1:8080/users

我们可以通过下面的命令查看运行中的容器日志:

$ docker-compose -f docker-compose.yml logs -f

然后我们可以改变下 App 组件的 state 对象:

this.state = {
  users: [],
  username: 'test001',
  email: ''
};

当我们保存的时候可以看到日志中输出了编译成功的信息,而且这个时候浏览器也自动刷新了,并且将 test001 填充在了用户名的输入框中。 change state 当然测试过后,要记得将上面的 state 对象的值更改回去哦~

生产环境

在将 React 应用上到生产环境之前我们需要将应用进行打包构建用于生成一系列的静态文件,我们在services/client目录下面的package.json文件中可以看到有一个build的脚本,该脚本就是用于我们对 React 应用进行打包的,在该目录下面执行命令:

$ npm run build

> client@0.1.0 build /Users/ych/devs/workspace/yidianzhishi/tdd100/flask-microservices-users/services/client
> react-scripts build

Creating an optimized production build...
Compiled successfully.

构建完成后我们可以发现在该目录下面多了一个build的目录,该目录下面打包过后的静态资源文件。我们需要一个静态服务器来访问这些静态资源,执行下面的命令:

$ npm install -g serve
$ serve -s build
   ┌──────────────────────────────────────────────────┐
   │                                                  │
   │   Serving!                                       │
   │                                                  │
   │   - Local:            http://localhost:5000      │
   │   - On Your Network:  http://192.168.31.9:5000   │
   │                                                  │
   │   Copied local address to clipboard!             │
   │                                                  │
   └──────────────────────────────────────────────────┘

我们可以根据上面的提示访问http://localhost:5000,然后我们可以在进行一次相关的测试,确保没有任何错误。

然后在client的根目录下面新增一个用于生产环境的 Dockerfile 文件:(Dockerfile-prod)

# build environment
FROM node:9.4 as builder
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV PATH /usr/src/app/node_modules/.bin:$PATH
ARG REACT_APP_USERS_SERVICE_URL
ARG NODE_ENV
ENV NODE_ENV $NODE_ENV
ENV REACT_APP_USERS_SERVICE_URL $REACT_APP_USERS_SERVICE_URL
COPY package.json /usr/src/app/package.json
RUN npm install --silent
RUN npm install react-scripts@1.1.0 -g --silent
COPY . /usr/src/app
RUN npm run build

# production environment
FROM nginx:1.13.5-alpine
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

我们可以看到上面的Dockerfile文件和之前的不太一样,出现了两个基础镜像,这是Docker提供的多阶段构建功能,详细的可以参考我之前的一篇博文:Docker 的多阶段构建

当构建镜像的时候,参数通过ARG指令传递给Dockerfile,然后就可以将其用做环境变量。npm run build命令将生成静态文件,然后通过Nginx暴露80端口提供服务。

接下来我们不使用Docker Compose来进行一些简单的测试。首先,在services/client目录下面构建镜像,记得使用--build-arg标记来传递合适的参数:

$ docker build -f Dockerfile-prod -t "test" ./ --build-arg NODE_ENV=development --build-arg REACT_APP_USERS_SERVICE_URL=http://127.0.0.1:5001

记得将上面的REACT_APP_USERS_SERVICE_URL替换成你自己的实际的 IP。

上面的构建命令是用services/client下面的Dockerfile-prod文件来构建一个叫 test 的镜像。

镜像构建完成后我们使用上面的 test 镜像来启动一个名为 test01 的容器,将主机的9000端口映射到容器的80端口:

$ docker run --name test01 -d -p 9000:80 test

容器启动成功后,我们可以在浏览器中打开链接http://localhost:9000/来进行相关的测试。 然后我们可以停止容器:

$ docker stop test01
$ docker rm test01

最好,删除上面的镜像:

$ docker rmi test

上面我们对Dockerfile-prod进行了相关的设置和测试,现在我们将该服务添加到docker-compose-prod.yml

client:
  container_name: client
  build:
    context: ./services/client
    dockerfile: Dockerfile-prod
    args:
      - NODE_ENV=production
      - REACT_APP_USERS_SERVICE_URL=${REACT_APP_USERS_SERVICE_URL}
  ports:
    - '3007:80'
  depends_on:
    - users
  links:
    - users

注意这里我们使用的是args,而不是环境变量了。另外,我们的这个 client 服务需要在 nginx 服务启动之前启动,所以我们相应的更新docker-compose-prod.yml

nginx:
  container_name: nginx
  build: ./services/nginx
  restart: always
  ports:
    - 80:80
  depends_on:
    - users
    - client

扫描下面的二维码(或微信搜索iEverything)添加我微信好友(注明python),然后可以加入到我们的python讨论群里面共同学习 qrcode