Kubernetes集群中的Nginx配置熱更新方案
Nginx已經是互聯網IT業界一個無敵的存在,作為反向代理、負載均衡、Web服務器等多種角色的扮演者,Nginx在全球各個互聯網公司落地、開花和結果,Ngnix已經成為了支撐全球互聯網應用的一個不可獲取的組成部分。
在我們的平臺中,Nginx同樣被拿來作為服務接入的最前端的反向代理,并且我們的Nginx也是作為一個Service跑在我們的Kubernetes集群中的。Ngnix背后的服務眾多,服務的生生死死都要在Nginx上這些服務路由的配置中有所體現,這就要求部署在Kubernetes集群中的Nginx需要有一個合理的配置熱更新方案。
Nginx自身是支持配置熱更新的,通過nginx -s reload命令可以實現這一點:
# sudo nginx -s reload
# sudo tail -100f /var/log/nginx/error.log
2016/11/18 08:21:03 [notice] 31516#31516: signal process started
這也是諸多nginx熱更新方案的基礎。
隨著Docker容器以及容器集群/云的出現,Nginx也被Dockerize了,Docker中Nginx的配置熱更新方案在 Jason Wilder 的 這篇文章 中有體現,在該方案中,你可以直接使用Jason Wilder開源的 Nginx-proxy 實現容器中Nginx的配置的熱更新。但這個方案并不能直接適用于Kubernetes,而且 作者也并沒有Plan support k8s 。
在Kubernetes集群中部署的Nginx,我其實也找到了一個配置熱更新的方案,這是普元的一份技術資料《 微服務動態路由實現:OpenResty與kubernetes 》中提供的,這個方案通過 OpenResty 與K8s的結合實現了配置熱更新。由于我對OpenResty并不熟悉,并且我個人更希望通過Kubernetes自身的一些Feature來實現這個方案,于是我開始了我自己的探索。
一、需求場景和方案原理
我們要實現的就是:當Kubernetes集群中的Service發生變化時,比如新創建一個Service或刪除了一個Service,這些Service在Nginx反向代理中的路由配置需要同步更新并生效。因此,這個過程的場景大致如下:
- 管理員通過命令或程序通過API操作K8s集群創建或刪除Service;
- 監聽API Server Event的某個程序獲取該Event,并從API Server讀取最新Service數據,重新生成/etc/nginx/conf.d/default.conf;
- /etc/nginx/conf.d/default.conf文件的變動觸發文件變更事件,監聽該事件的腳本調用“nginx -s reload”命令實現Nginx的配置熱更新。
針對這一需求場景,我這里給出一個實現方案,先上圖:

簡答說明一下:
- Nginx作為一個Service部署在Kubernetes集群中,可以有多個Pod副本;
- 以一個nginx pod為例,該Pod中包含三個Container,分別是 init container 、nginx container和config-nginx-generator container;
- 三個Container共同掛載且共享一個Pod volume,emptyDir類型即可,無需持久化的存儲卷,三個Container的掛載路徑均為/etc/nginx/conf.d;
- Pod啟動時,init container首先啟動并訪問API Server,獲取Service列表,按照一定條件過濾后(比如通過label的key和Value值),初始創建/etc/nginx/conf.d/default.conf。創建成功后,Container退出;
- nginx container啟動,加載配置,開始提供反向代理服務,并通過inotify工具監視/etc/nginx/conf.d/default.conf文件狀態變化,一般變化,就執行nginx -s reload熱加載最新配置。
- config-nginx-generator container同時也啟動起來,監聽API Server的service變更Event,一旦有Event出現,就重新讀取API Server中的Service list,并重新生成一份新的default.conf,覆蓋old版本 default.conf。
二、環境
由于 Kubernetes 和Docker都在Active Develop的過程中,兩個項目的變動都很快,因此,特定的Feature(比如k8s的init container)、操作和說明在某些版本是好用的,但對另外一些版本卻是不靈光的。這里先把環境確定清楚,避免誤導。
OS:
Ubuntu 14.04.4 LTS Kernel:3.19.0-70-generic #78~14.04.1-Ubuntu SMP Fri Sep 23 17:39:18 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
Docker:
# docker version
Client:
Version: 1.12.2
API version: 1.24
Go version: go1.6.3
Git commit: bb80604
Built: Tue Oct 11 17:00:50 2016
OS/Arch: linux/amd64
Server:
Version: 1.12.2
API version: 1.24
Go version: go1.6.3
Git commit: bb80604
Built: Tue Oct 11 17:00:50 2016
OS/Arch: linux/amd64
Kubernetes集群:1.3.7
私有鏡像倉庫:阿里云鏡像倉庫
三、實現
1、nginx image的創建
nginx image實現了兩個功能,一個自然是nginx自身了,另外一個就是監聽/etc/nginx/conf.d/default.conf文件的變化,并適時調用nginx -s reload更新nginx配置。在kubernetes的源碼目錄kubernetes/examples下有一個例子: https-nginx ,這里面已經為我們實現了一個基于 auto-reload-nginx.sh 的Nginx image Dockerfile,我們稍作改造就可以直接使用了:
//Dockerfile
FROM nginx
MAINTAINER Tony Bai <bigwhite.cn@aliyun.com>
COPY auto-reload-nginx.sh /home/auto-reload-nginx.sh
RUN chmod +x /home/auto-reload-nginx.sh
# install inotify
RUN apt-get update && apt-get install -y inotify-tools
基于該Dockefile構建image:
# docker build -t xxxx/nginx
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
xxxx/nginx latest a1503b1c2b70 42 seconds ago 191.9 MB
官方nginx image基于debian jessie版本構建,apt-get update & install時需要耐心等待一下。
打標簽并推送到我們的阿里云私有鏡像庫:
# docker tag a1503b1c2b70 registry.cn-hangzhou.aliyuncs.com/xxxx/nginx
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
xxxx/nginx latest a1503b1c2b70 12 minutes ago 191.9 MB
registry.cn-hangzhou.aliyuncs.com/xxxx/nginx latest a1503b1c2b70 12 minutes ago 191.9 MB
# docker push registry.cn-hangzhou.aliyuncs.com/xxxx/nginx
2、編寫Pod yaml
由于init container和config-nginx-generator container在真實場景中都是要與Kubernetes的API Server交互,并生成/etc/nginx/conf.d/default.conf,這需要一個實現過程,在這里我們暫不給出兩個Container的具體Dockerfile以及實現功能的實際程序,而是用兩個通用docker image,并通過“手動”方式實現它們各自的功能。因此,我們在這一節中就可以給出Nginx Pod的yaml描述文件了:
//nginx-reload-on-k8s.yaml
apiVersion: v1
kind: Pod
metadata:
name: nginx-reload-on-k8s
annotations:
pod.beta.kubernetes.io/init-containers: '[
{
"name": "nginx-reload-on-k8s-init-1",
"image": "busybox",
"command": ["wget", "-O", "/etc/nginx/conf.d/index1.html", "http://www.baidu.com"],
"volumeMounts": [
{
"name": "conf-volume",
"mountPath": "/etc/nginx/conf.d"
}
]
},
{
"name": "nginx-reload-on-k8s-init-2",
"image": "busybox",
"command": ["wget", "-O", "/etc/nginx/conf.d/index2.html", "http://dict.cn"],
"volumeMounts": [
{
"name": "conf-volume",
"mountPath": "/etc/nginx/conf.d"
}
]
}
]'
spec:
containers:
- name: nginx-config-generator
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: conf-volume
image: registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest
imagePullPolicy: IfNotPresent
command:
- "tail"
- "-f"
- "/var/log/bootstrap.log"
- name: nginx-origin
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: conf-volume
image: registry.cn-hangzhou.aliyuncs.com/xxxx/nginx:latest
imagePullPolicy: IfNotPresent
command: ["/home/auto-reload-nginx.sh"]
ports:
- containerPort: 80
volumes:
- name: conf-volume
emptyDir: {}
Yaml中,我們創建了兩個init container,分別用于從baidu.com和dict.cn抓取主頁,并存儲于/etc/nginx/conf.d的下面備用。nginx-config-generator我們使用image xxxx/test,這就是一個基于ubuntu且安裝了諸多網絡工具的鏡像,用于做目標鏡像調試的;nginx container用的就是上面push到私有鏡像倉庫的那個鏡像,command則是執行/home/auto-reload-nginx.sh這個腳本,從而啟動nginx和通過inotify監控/etc/nginx/conf.d/default.conf文件。
我們來創建這個Pod(注意:只有用kubectl apply命令時,init container才會被創建和執行,如果用kubectl create -f ,那么將忽略init container):
# kubectl apply -f nginx-reload-on-k8s.yaml
pod "nginx-reload-on-k8s" created
# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-reload-on-k8s 2/2 Running 0 41s
通過describe pod/nginx-reload-on-k8s,我們能看到一些Container創建的詳細信息:
# kubectl describe pod/nginx-reload-on-k8s
Name: nginx-reload-on-k8s
Namespace: default
Node: 10.46.181.146/10.46.181.146
Start Time: Thu, 17 Nov 2016 21:39:55 +0800
Labels: <none>
Status: Running
IP: 172.16.57.9
... ...
Events:
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
57s 57s 1 {default-scheduler } Normal Scheduled Successfully assigned nginx-reload-on-k8s to 10.46.181.146
39s 39s 1 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-1} Normal Created Created container with docker id 0e21afb58eee
39s 39s 1 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-1} Normal Started Started container with docker id 0e21afb58eee
56s 38s 2 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-1} Normal Pulling pulling image "busybox"
39s 26s 2 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-1} Normal Pulled Successfully pulled image "busybox"
26s 26s 1 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-2} Normal Created Created container with docker id 85632ff73ea8
26s 26s 1 {kubelet 10.46.181.146} spec.initContainers{nginx-reload-on-k8s-init-2} Normal Started Started container with docker id 85632ff73ea8
25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-config-generator} Normal Pulled Container image "registry.cn-hangzhou.aliyuncs.com/xxxx/test:latest" already present on machine
25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-config-generator} Normal Created Created container with docker id 1ce8c6d8a8af
25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-config-generator} Normal Started Started container with docker id 1ce8c6d8a8af
25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-origin} Normal Pulled Container image "registry.cn-hangzhou.aliyuncs.com/xxxx/nginx:latest" already present on machine
25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-origin} Normal Created Created container with docker id 0c692ec28acd
25s 25s 1 {kubelet 10.46.181.146} spec.containers{nginx-origin} Normal Started Started container with docker id 0c692ec28acd
... ...
可以看到四個container依次被pull and create。
四、測試
現在我們就來測試一下nginx的reload。
之前的兩個init container分別在/etc/nginx/conf.d下創建了index1.html和index2.html,我們就用這兩個文件分別作為配置變更前和變更后的首頁。
注意:這時我們還沒有/etc/nginx/conf.d/default.conf文件,我們在Pod內訪問localhost:80將會得到失敗結果:
# curl localhost:80
curl: (7) Failed to connect to localhost port 80: Connection refused
我們進入nginx-config-generator,創建/etc/nginx/conf.d/default.conf文件,與此同時,通過docker logs -f 監控nginx-origin容器的日志:
//default.conf
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location / {
root /etc/nginx/conf.d;
index index1.html index1.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
我們把/etc/nginx/conf.d/index1.html作為服務站點的首頁了。文件創建完畢后,我們同時就可以從nginx-origin容器的日志能看到如下內容:
At 14:07 on 17/11/16, config file update detected.
2016/11/17 14:07:25 [notice] 20#20: signal process started
我們再從Pod中訪問localhost:80(注意:Pod中的多個container共享network namespace,通過localhost就可以進行互訪):
root@nginx-reload-on-k8s:/etc/nginx# curl localhost:80
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> .... </html>
我們順利得到index1.html的內容,這說明配置實時生效了。
我們再來“觸發”一次配置變更。我們將default.conf中的:
location / {
root /etc/nginx/conf.d;
index index1.html index1.htm;
}
改為:
location / {
root /etc/nginx/conf.d;
index index2.html index2.htm;
}
保存!
從nginx-origin容器日志可以看到如下輸出:
At 14:17 on 17/11/16, config file update detected.
2016/11/17 14:17:46 [notice] 32#32: signal process started
在Pod中再次訪問站點首頁:
# curl localhost:80
<!DOCTYPE HTML>
<html>
<head>
<meta name="renderer" content="webkit"/>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>海詞詞典_在線詞典_在線翻譯_海量正版權威詞典官方網站</title>
... ...
可以看到配置更新成功,首頁換成了dict.cn的首頁。
五、測試
通過上述這些“手動”的觸發和測試,可以看出這個方案是可行的。并且我們可以看出,這個方案是有一些好處的:
- 不需要依賴外部持久化存儲卷;
- 通過k8s api server獲取當前所有 service列表,通過service label來過濾,無需依賴額外的redis server或etcd服務;
剩下的就是具體init container以及config-generator的實現了。這個留給我以及大家后續去完成^_^。
? 2016,bigwhite. 版權所有.
來自:http://tonybai.com/2016/11/17/nginx-config-hot-reloading-approach-for-kubernetes-cluster/