容器化单页面应用中Nginx反向代理与Kubernetes部署
在《容器化单页面应用中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容器中独立出来。拓扑结构如下图所示:
对案例的调整
我们将从以下几个方面对前文所述案例进行配置调整:- 简化前端应用的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路径,得到如下结果: 目前无需纠结上图中最后一个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: 然后,使用kubectl cluster-info命令以获得Node IP: 在浏览器中使用Node IP和Node Port来访问namelist应用程序: 现在,将namelist-service扩展到2个实例: 在浏览器中,反复刷新页面,可以看到,页面上显示的机器名在变化,证明Kubernetes将API访问请求重定向到不同的namelist-service服务实例: