容器化单页面应用中Nginx反向代理与Kubernetes部署

By | 2019年7月27日

在《容器化单页面应用中RESTful API的访问》一文中,我介绍了一个在容器化环境中单页面应用访问后端服务的完整案例。这里我将继续使用这个案例,介绍一下容器化单页面应用部署的另一个场景:将Nginx的职责独立出来。

注:这里单页面应用是值一个包含前端页面、后端服务以及后台数据库的一个完整应用系统,这样符合微服务模式对于服务的定义。不过为了介绍简单,文章案例不使用后台数据库,而是将数据“写死”在后端服务中。

继续回顾一下上篇文章中的案例,我们有两个服务:前端单页面应用(client),以及后端基于ASP.NET Core Web API的RESTful服务(service),案例代码地址是:https://github.com/daxnet/name-list。在这个案例中,前端单页面应用运行在Nginx容器中,这里的Nginx同时还承担了反向代理的角色,用以将前端页面发出的RESTful API请求正确地转发到ASP.NET Core Web API上。

如果整个系统只有这一个单页面应用,那么这么做是简单且合理的;但如果一个系统包含多个单页面应用,或者说一个系统包含一个前端页面与多个后台服务,那么,将Nginx反向代理的职责加到这个前端页面的容器上,明显是不合理的。为什么不合理?因为一个系统有可能不仅仅有基于Web的UI,而且还有可能会有移动客户端,比如Andriod或者iOS的前端,甚至直接暴露API以供外部系统集成。如果运行前端页面的容器还兼职做反向代理的话,这些访问请求都将发送到前端单页面应用的服务器(容器)上,这样就会对前端应用造成压力。

因此,一个更好的做法是,将Nginx的反向代理职责从前端页面所运行的Nginx容器中独立出来。拓扑结构如下图所示:

image

对案例的调整

我们将从以下几个方面对前文所述案例进行配置调整:

  • 简化前端应用的Nginx配置
  • Nginx反向代理容器的创建
  • 调整docker-compose.yml文件

 

简化前端应用的Nginx配置

在之前的案例中,前端应用的Nginx配置中还包含了反向代理的配置,这部分内容现在可以拿掉了,于是,前端应用的Nginx配置就非常简单了,只需要使用默认的静态页面服务配置即可,例如:

events {
    worker_connections 1024;
}

http {

    server {
      listen        80;
      server_name   localhost;

      include  /etc/nginx/mime.types;

      location / {
        root /usr/share/nginx/html;
        index  index.html  index.htm;
      }
    }
}

因此,在docker中完成前端页面的编译之后,将所有的资源复制到/usr/share/nginx/html下即可。前端Dockerfile如下:

FROM nginx AS base
WORKDIR /app
EXPOSE 80

FROM node:10.16.0-alpine AS build
RUN npm install -g @angular/cli@8.0.3
WORKDIR /src
COPY . .
RUN npm install
RUN ng build --prod --output-path /app

FROM base AS final
COPY --from=build /app /usr/share/nginx/html
COPY --from=build /src/nginx.conf /etc/nginx/nginx.conf
CMD ["nginx", "-g", "daemon off;"]

Nginx反向代理容器的创建

下一步就是创建一个Nginx反向代理的容器,基本思路是将反向代理配置到nginx.conf文件中,然后基于Nginx容器镜像,将nginx.conf文件复制到容器中即可。nginx.conf文件内容如下:

events {
    worker_connections 1024;
}

http {

    server {
      listen        80;
      server_name   localhost;

      include  /etc/nginx/mime.types;

      location / {
        root /usr/share/nginx/html;
        index  index.html  index.htm;
      }

      location /app {
          proxy_pass http://namelistcli/;
      }

      location ~ ^/name-service/(.*)$ {
        rewrite ^ $request_uri;
        rewrite ^/name-service/(.*)$ $1 break;
        return 400;
        proxy_pass http://namelistsvc/$1;
      }
    }

    upstream namelistsvc {
      server namelist-service:5000;
    }

    upstream namelistcli {
      server namelist-client:80;
    }
}

上面定义了两个upstream,分别对应应用程序的前端和后端,然后根据不同的路径规则分别将请求路由到不同的服务器上。在Dockerfile中,只需要将该配置文件复制到Nginx的配置路径下即可:

FROM nginx
COPY nginx.conf /etc/nginx/nginx.conf
CMD ["nginx", "-g", "daemon off;"]

调整docker-compose.yml文件

我们需要相应地调整docker-compose.yml文件,以便能够方便地将这些服务运行起来。docker-compose.yml文件非常简单:

version: '3'
services:
  namelist-service:
    image: daxnet/namelist-service
  
  namelist-client:
    image: daxnet/namelist-client

  namelist-nginx:
    image: daxnet/namelist-nginx
    ports: 
      - 80:80
    links:
      - namelist-service
      - namelist-client

对于namelist-service和namelist-client两个服务,我们没有指定TCP端口,因为这两个服务无需暴露出来,namelist-nginx服务会通过容器链接(links)由docker的DNS来解析这两个服务并在子网内部访问。

下面我们测试一下整个应用程序,使用下面的命令分别编译docker镜像,注意:编译前先进入client或service项目的根目录下:

$ docker build -t daxnet/namelist-client .
$ docker build -t daxnet/namelist-service .

然后,使用docker-compose up命令,启动所有服务,并使用浏览器访问Nginx反向代理服务的/app路径,得到如下结果:

image

目前无需纠结上图中最后一个c415….是什么,它只不过是当前服务端机器的机器名称,在接下来Kubernetes部署阶段,我们会通过实验来验证namelist-service服务在Kubernetes中的伸缩性。

Kubernetes部署

接下来,我们将name-list案例部署到Kubernetes上。在这里,我会使用Minikube来演示。Minikube是一套Kubernetes的最小集群,它只包含一个节点,但对于我们学习和实验来说已经够用。安装Minikube过程也不是特别容易,尤其是在国内的网络环境中,我推荐使用阿里云提供的相关资源以及使用Oracle Virtual Box来作为Minikube的虚拟化环境,这样安装过程最简单。我的Minikube是安装在Ubuntu 18.04的Linux机器上。

首先需要编写Kubernetes的部署描述文件,可以使用Kubernetes官方的Kompose工具,它能够帮助我们很方便地从docker-compose.yml文件生成Kubernetes的部署描述文件。对于name-list而言,我们已经有docker-compose.yml文件了,因此,使用Kompose工具一键生成即可:

$ kompose convert -o k8s.deployment.yaml

这条命令会将所有的部署脚本(包括deployment,service等)输出到同一个yaml文件中,如果不使用-o参数,那么就会分别输出到不同的文件中。但这都不是重点。重点是,我们还需要对生成的yaml文件进行一些修改。

第一个需要修改的地方是,要将namelist-nginx的service类型指定为NodePort,这样我们才可以使用Node IP来访问我们的应用程序。Minikube不支持LoadBalancer类型的service,因此,在访问应用程序之前,我们需要获取Node IP。在上文中我提到,namelist-service和namelist-client无需暴露端口出来,因为Nginx反向代理会将外部请求转发到这两个服务上。然而,由于没有暴露可访问的TCP端口,Kompose并不会对这两个服务产生service的定义,这就需要我们自己添加到所产生的k8s.deployment.yaml文件中,只不过我们不需要指定service的类型,因为我们不需要直接访问它们。

准备好部署文件之后,我们需要使用docker push命令,将namelist的三个docker镜像推送到Docker Hub上。Minikube默认会从Docker Hub上拉取镜像进行部署。这一步我就不多做说明了。

接下来,使用下面的命令将namelist应用部署到Kubernetes上:

$ kubectl apply -f k8s.deployment.yaml

部署完成后,查看deployments、services和pods:

Screenshot from 2019-07-27 20-26-07

然后,使用kubectl cluster-info命令以获得Node IP:

Screenshot from 2019-07-27 20-27-48

在浏览器中使用Node IP和Node Port来访问namelist应用程序:

Screenshot from 2019-07-27 20-29-52

现在,将namelist-service扩展到2个实例:

Screenshot from 2019-07-27 20-33-07

在浏览器中,反复刷新页面,可以看到,页面上显示的机器名在变化,证明Kubernetes将API访问请求重定向到不同的namelist-service服务实例:

Screenshot from 2019-07-27 20-42-08

Screenshot from 2019-07-27 20-42-20

总结

本文介绍了在namelist案例中,将Nginx反向代理职责从前端容器中独立出来的设计与实现,并介绍了Kubernetes部署的基本步骤和注意事项。基于namelist案例还可以继续扩展,比如使用HELM打包Kubernetes应用,今后有机会我会继续介绍。

源代码

本文源代码可以参考:https://github.com/daxnet/name-list/tree/k8s-deployment

Print Friendly, PDF & Email
(总访问量:414;当日访问量:1)

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据