在《
容器化单页面应用中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服务实例:
总结
本文介绍了在namelist案例中,将Nginx反向代理职责从前端容器中独立出来的设计与实现,并介绍了Kubernetes部署的基本步骤和注意事项。基于namelist案例还可以继续扩展,比如使用HELM打包Kubernetes应用,今后有机会我会继续介绍。
源代码
本文源代码可以参考:
https://github.com/daxnet/name-list/tree/k8s-deployment。