OpenShift制作NginxLB Operator實戰

Nginx Operator OpenShift

背景

近期需要在OpenShift集群中部署Nginx服務做為負載均衡器,負載集群外部服務,如NTP、DNS、項目App等。因為不同的服務的配置都是不一樣的,不僅后臺服務的IP不一樣,而且使用的協議也不一樣,HTTP/TCP/UDP都有可能,如果按照傳統的方式來實施的話,每一個應用單獨定義Nginx配置,分別部署,每增加一個新的應用被負載都需要做一次復雜的過程,那么有沒有辦法能夠讓這過程變得簡單呢,甚至能夠自動化處理,我們只需要提供最簡單的信息?
下面我們來分析下常用的幾種方法。

打包方案選擇

1. Template

對于OpenShift熟悉的朋友,會馬上想到使用Template模板來實現。
Template模板是OpenShift特有的應用打包方式,它描述了一組對象,同時對這些對象的配置可以進行參數化處理,生成OpenShift 容器平臺創建的對象列表。在模板中可以設置所有在項目中有權限創建的任何資源。

不足之處:

  • OpenShift特有,如果是使用OpenShift容器平臺的話,這個不足可忽略。
  • 無法保證線上資源狀態始終與參數設定的結果一致,如手動增加rc的副本數時,不會自動恢復到與參數設定的副本數。
  • 在創建的時候設置參數,如果在應用運行時對參數動態更新的話,則需要使用腳本命令使用所有的參數,重新生成資源列表。參數需要額外管理,不可靠。
  • 如果應用有創建的順序有依賴,則無法滿足。
  • 無法根據參數的不同對資源進行條件控制。

2. Helm

對于Kubernetes熟悉的朋友,會馬上想到使用Helm來實現。
Helm是Kubernetes生態系統中的一個軟件包管理工具,與Template類似。

不足之處:

  • 需要額外部署Helm客戶端及Tiller。
  • 需要額外管理helm中的charts資源。
  • 無法保證線上資源狀態始終與參數設定的結果一致。
  • 如果應用有創建的順序有依賴,則無法滿足。
  • 參數更新時,需要手動執行helm腳本

3. 創建Ansible playbook

對于熟悉各種自動化工具的運維開發,會想到使用自動化配置管理工具來做,如ansible。
利用ansible的k8s模塊,創建各種資源,而且可以充分發揮ansible強大的控制功能。

不足之處:

  • 需要額外部署Ansible,及對ansible訪問集群的訪問認證。
  • 需要額外管理ansible的playbook文件。
  • 無法保證線上資源狀態始終與參數設定的結果一致。
  • 參數更新時,需要手動執行ansible playbook腳本

4. operator

Operator即為今天的主角,我將給予更加詳細的介紹。
Operator是由coreOS公司(已被RedHat收購)開發的一種打包,部署和管理Kubernetes/OpenShift應用的方法。Kubernetes/OpenShift應用是一個部署在集群上并使用Kubernetes/OpenShift API和kubectl/oc工具進行管理的應用程序。Operator類似于Helm和Template,但是比它們都更加靈活,更加強大,更加方便。
Operator本質上是一個自定義的控制器。它會在集群中運行一個Pod與Kubernetes/OpenShift API Server交互,并通過CRD引入新的資源類型,這些新創建的資源類型與集群上的資源類型如Pod等交互方式是一樣的。同時Operator會監聽自定義的資源類型對象的創建與變化,并開始循環執行,保證應用處于被定義的狀態。
為什么說Operator能夠更好地解決這類問題呢?因為它不僅能夠很好地滿足自定義打包的需求,同時也彌補了以上三種方式的不足。
使用Operator-sdk能夠非常方便地創建自定義的Operator,它支持三種類型:go、ansible、helm。

  • go類型,它的實現更加靈活,可以隨心所欲,擴展性也最強,構建出的operator鏡像也不大,但是它對于編程能力要求高,同時沒有ansible和helm類型拿來即用,可讀性也不及ansible與helm類型。
  • ansible類型,它使用ansible的playbook方式來定義應用的構建與保證應用的狀態,它的實現也很靈活,依賴于ansible的模塊,但是這使得構建出的operator鏡像較大,一般為600多M,因為它包含了ansible應用及默認的各個模塊。
  • helm類型,它使用helm的charts方式來定義應用的構建與保證應用的狀態,它的鏡像一般為200多M,但是它的靈活度不及另外兩種類型。

一般情況下,以上三種方式都能夠滿足要求,建議大家使用自己最熟悉的方式。構建方式并不是我們的約束點,我們最關心的是能夠部署按要求的應用,并保證應用一直處于穩定的狀態。

構建分析

1. 資源類型

  • deployment,運行Nginx應用
  • service,運行Nginx service
  • configmap,設置Nginx負載均衡上游及協議類型等配置
  • route,對于HTTP協議可以設置指定的域名
  • NginxLB,添加的CRD資源對象名

2. 參數設置

  • nginx_image, 指定Nginx應用鏡像
  • size,Nginx應用運行的副本數
  • loadbalancers,設定的負載均衡參數配置列表
  • loadbalancers[].protocol,負載均衡網絡協議,支持HTTP/TCP/UDP
  • loadbalancers[].port,負載均衡Nginx監聽的端口
  • loadbalancers[].nodeport,如果負載均衡使用nodeport方式對外提供服務,則可以用該參數指定nodeport端口號
  • loadbalancers[].upstreams,負載均衡上游服務列表
  • loadbalancers[].hostname,對于HTTP協議,可以指定hostname來創建OpenShift Route資源

最終需要實現的NginxLB資源的參數例子為:

apiVersion: fcloudy.com/v1alpha1
kind: NginxLB
metadata:
  name: example-nginxlb
spec:
  nginx_image: "docker.io/xhuaustc/nginx:alpine"
  size: 2
  loadbalancers:
    - protocol: TCP
      port: 53
      nodeport: 32287
      upstreams:
        - 192.168.4.5:53
        - 192.168.5.3:53
    - protocol: HTTP
      port: 80
      upstreams:
        - 192.168.4.5:80
      hostname: xx.nginx.fcloudy.com

以下為NginxLB Operator相關資源的關系

NginxLB Operator資源關系

3. Operator類型

  • 選擇ansible類型,使用它的主要是與集群運維及自動化運維等技術棧統一。

制作Operator

通用步驟與說明可以參考OpenShift 通過Operator SDK制作Operator,本案例的具體操作如下

  1. 新建一個operator項目(type=ansible 資源類型為NginxLB)
$ operator-sdk new nginxlb-operator --api-version=fcloudy.com/v1alpha1 --kind=NginxLB --type=ansible
  1. 在roles/nginxlb/templates中添加模板文件nginx-deployment.yaml.j2、nginx-svc.yaml.j2、nginx-cm.yaml.j2及nginx-route.yaml.j2
    nginx-deployment.yaml.j2
apiVersion: v1
kind: Deployment
metadata:
  labels:
    nginxlb: {{ meta.name }}
    app: {{ meta.name }}
  name: {{ meta.name }}
  namespace: {{ meta.namespace }}
spec:
  replicas: {{ size }}
  selector:
    matchLabels:
      nginxlb: {{ meta.name }}
  template:
    metadata:
      labels:
        nginxlb: {{ meta.name }}
    spec:
      containers:
      - image: "{{ nginx_image | default('docker.io/xhuaustc/nginx:alpine') }}"
        name: nginx
        volumeMounts:
        - mountPath: /etc/nginx/nginx.conf
          name: nginx-config-hgj4i
          subPath: nginx.conf
          readOnly: true
      volumes:
        - configMap:
            defaultMode: 420
            name: nginx
            items:
              - key: nginx.conf
                path: nginx.conf
          name: nginx-config-hgj4i

nginx-svc.yaml.j2

apiVersion: v1
kind: Service
metadata:
  name: {{ meta.name }}-{{ item.port }}-nginx-service
  namespace: {{ meta.namespace }}
spec:
  ports:
    - name: {{ item.protocol | lower }}-{{ item.port | lower }}
      port: {{ item.port }}
{% if item.protocol == 'HTTP' %}
      protocol: TCP
{% else %}
      protocol: {{ item.protocol }}
{% endif %}
{% if item.nodeport is defined %}
      nodePort: {{ item.nodeport}}
{% endif %}
  selector:
    nginxlb: {{ meta.name }}
{% if item.nodeport is defined %}
  type: NodePort
{% else %}
  type: ClusterIP
{% endif %}

nginx-cm.yaml.j2

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx
  namespace: {{ meta.namespace }}
data:
  nginx.conf: |
    worker_processes  1;

    error_log  /var/log/nginx/error.log warn;
    pid        /var/run/nginx.pid;


    events {
        worker_connections  1024;
    }
    
    stream{
{% for lb in loadbalancers %}
{% if lb.protocol in ["TCP", "UDP"] %}
      upstream {{meta.name}}-{{lb.protocol}}-{{lb.port}}{
{% for upstream in lb.upstreams %}
          server {{upstream}};
{% endfor %}
      }
      server {
{% if lb.protocol in ["UDP"] %}
      listen {{lb.port}} udp;
{% else %}
        listen {{lb.port}};
{% endif %}
        proxy_pass {{meta.name}}-{{lb.protocol}}-{{lb.port}};
      }
{% endif %}
{% endfor %}
    }

    http {
        include       /etc/nginx/mime.types;
        default_type  application/octet-stream;

        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';

        access_log  /var/log/nginx/access.log  main;

        sendfile        on;
        #tcp_nopush     on;

        keepalive_timeout  65;

        gzip  on;
        
        {% for lb in loadbalancers %}
{% if lb.protocol in ["HTTP"] %}
      upstream {{meta.name}}-{{lb.protocol}}-{{lb.port}}{
{% for upstream in lb.upstreams %}
          server {{upstream}};
{% endfor %}
      }
      server {
        listen {{lb.port}};
        location / {
          proxy_pass http://{{meta.name}}-{{lb.protocol}}-{{lb.port}};
        }
      }
{% endif %}
{% endfor %}
    }

nginx-route.yaml.j2

apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: {{ meta.name }}-{{ item.port }}-nginx-route
  namespace: {{ meta.namespace }}
spec:
  host: "{{ item.hostname }}"
  port:
      targetPort: {{ item.protocol | lower }}-{{ item.port | lower }}
  to:
    kind: Service
    name: {{ meta.name }}-{{ item.port }}-nginx-service
  1. 在roles/nginxlb/tasks/main.yaml中添加執行任務
---
- name: create nginx configmap
  k8s:
    state: present
    definition: "{{ lookup('template', 'nginx-cm.yaml.j2') | from_yaml }}"

- name: create nginx DeploymentConfig
  k8s:
    state: present
    definition: "{{ lookup('template', 'nginx-dc.yaml.j2') | from_yaml }}"

- name: create nginx service
  k8s:
    state: present
    definition: "{{ lookup('template', 'nginx-svc.yaml.j2') | from_yaml }}"
  with_items: "{{ loadbalancers }}"
      
- name: create nginx route
  k8s:
    state: present
    definition: "{{ lookup('template', 'nginx-route.yaml.j2') | from_yaml }}"
  when: item.hostname is defined
  with_items: "{{ loadbalancers }}"   
  1. 構建nginx-lb operator鏡像,并推送到鏡像倉庫
$ operator-sdk build docker.io/xhuaustc/nginxlb-operator:v0.0.1
$ docker push docker.io/xhuaustc/nginxlb-operator:v0.0.1
  1. operator-sdk默認是只能在operator應用所在的namespace下創建資源,如果需要在集群下全局的namespace都能使用NginxLB資源,需要對deploy/operator.yaml作修改。
    • 將WATCH_NAMESPACE值設置為""
    • 更新{{ REPLACE_IMAGE }}為步驟4中構建的鏡像
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginxlb-operator
spec:
  replicas: 1
  selector:
    matchLabels:
      name: nginxlb-operator
  template:
    metadata:
      labels:
        name: nginxlb-operator
    spec:
      serviceAccountName: nginxlb-operator
      containers:
        - name: ansible
          command:
          - /usr/local/bin/ao-logs
          - /tmp/ansible-operator/runner
          - stdout
          # Replace this with the built image name
          image: "docker.io/xhuaustc/nginxlb-operator:v0.0.1"
          imagePullPolicy: "Always"
          volumeMounts:
          - mountPath: /tmp/ansible-operator/runner
            name: runner
            readOnly: true
        - name: operator
          # Replace this with the built image name
          image: "docker.io/xhuaustc/nginxlb-operator:v0.0.1"
          imagePullPolicy: "Always"
          volumeMounts:
          - mountPath: /tmp/ansible-operator/runner
            name: runner
          env:
            - name: WATCH_NAMESPACE
              value: ""
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: OPERATOR_NAME
              value: "nginxlb-operator"
      volumes:
        - name: runner
          emptyDir: {}

  1. 更新deploy/role.yaml與deploy/role_binding.yaml
    • role.yaml與role_binding.yaml中的kind: Role更新為kind: ClusterRole
    • role_binding.yaml中的kind: RoleBinding更新為kind: ClusterRoleBinding
    • 添加額外的權限,如route資源類型的權限等
      role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  creationTimestamp: null
  name: nginxlb-operator
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - services
  - endpoints
  - persistentvolumeclaims
  - events
  - configmaps
  - secrets
  verbs:
  - '*'
- apiGroups:
  - apps
  resources:
  - deployments
  - daemonsets
  - replicasets
  - statefulsets
  verbs:
  - '*'
- apiGroups:
  - extensions
  resources:
  - deployments
  - daemonsets
  - replicasets
  - statefulsets
  - deployments/finalizers
  verbs:
  - '*'
- apiGroups:
  - route.openshift.io
  attributeRestrictions: null
  resources:
  - '*'
  verbs:
  - '*'
- apiGroups:
  - monitoring.coreos.com
  resources:
  - servicemonitors
  verbs:
  - get
  - create
- apiGroups:
  - apps
  resourceNames:
  - nginxlb-operator
  resources:
  - deployments/finalizers
  verbs:
  - update
- apiGroups:
  - fcloudy.com
  resources:
  - '*'
  verbs:
  - '*'

role_binding.yaml

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nginxlb-operator
subjects:
- kind: ServiceAccount
  name: nginxlb-operator
  namespace: nginxlb-operator
roleRef:
  kind: ClusterRole
  name: nginxlb-operator
  apiGroup: rbac.authorization.k8s.io

至此完成了NginxLB Operator的制作,制作的結果輸出為:

  1. Operator鏡像:docker.io/xhuaustc/nginxlb-operator:v0.0.1
  2. deploy中的yaml配置文件:
    operator.yaml
    role.yaml
    role_binding.yaml
    service_account.yaml
    crds/fcloudy_v1alpha1_nginxlb_crd.yaml

測試驗證

  1. 創建nginxlb-operator項目
[root@master ~]# oc new-project nginxlb-operator --display=NginxLBOperator
  1. 部署nginxlb-operator
[root@master ~]# oc create -f deploy/crds/fcloudy_v1alpha1_nginxlb_crd.yaml
[root@master ~]# oc create -f deploy/
  1. 查看nginxlb-operator運行狀態
[root@master ~]# oc get pod
NAME                                READY     STATUS    RESTARTS   AGE
nginxlb-operator-85c77c8cdc-c2gpp   2/2       Running   10         1m
  1. 新建NginxLB項目
[root@master ~]# oc new-project nginxlb --display-name=NginxLB
  1. 使用NginxLB創建負載均衡器Nginx應用
[root@master ~]# cat << EOF | oc create -f -
apiVersion: fcloudy.com/v1alpha1
kind: NginxLB
metadata:
  name: example-nginxlb
spec:
  size: 2
  loadbalancers:
  - nodeport: 32289
    port: 8123
    protocol: TCP
    upstreams:
    - 192.168.4.5:123
    - 192.168.5.3:123
  - hostname: xx.nginx.fcloudy.com
    port: 8080
    protocol: HTTP
    upstreams:
    - 192.168.4.5:80
EOF
  1. 查看NginxLB資源狀態
[root@master ~]# oc get all 
NAME                                  READY     STATUS    RESTARTS   AGE
pod/example-nginxlb-6788db776-42rsz   1/1       Running   0          5s
pod/example-nginxlb-6788db776-8cxm9   1/1       Running   0          5s

NAME                                         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/example-nginxlb-8080-nginx-service   ClusterIP   172.30.167.107   <none>        8080/TCP         2s
service/example-nginxlb-8123-nginx-service   NodePort    172.30.108.138   <none>        8123:32289/TCP   3s

NAME                              DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/example-nginxlb   2         2         2            2           5s

NAME                                        DESIRED   CURRENT   READY     AGE
replicaset.apps/example-nginxlb-6788db776   2         2         2         5s

NAME                                                        HOST/PORT              PATH      SERVICES                             PORT        TERMINATION   WILDCARD
route.route.openshift.io/example-nginxlb-8080-nginx-route   xx.nginx.fcloudy.com             example-nginxlb-8080-nginx-service   http-8080                 None
  1. 更新NginxLB example-nginxlb,將size更新為1,只使用一個Nginx應用副本
[root@master ~]# oc patch NginxLB example-nginxlb -p '{"spec":{"size":1}}'  --type=merge
nginxlb.mbcloud.com/example-nginxlb patched
[root@master ~]# oc get pod 
NAME                              READY     STATUS    RESTARTS   AGE
example-nginxlb-6788db776-8cxm9   1/1       Running   0          2m

總結

  • 以上實例只是對一種CRD進行控制與管理,其實一個Operator可以同時管理與控制多個CRD。
  • Operator能夠非常靈活地實現對資源的重新管理及控制,方便對應用生命周期管理。
  • 使用Operator-sdk,我們可以輕松創建自己的Operator。

參考文章
https://www.openshift.com/learn/topics/operators
https://coreos.com/operators/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容