一、概述
1、我們說過在helm架構中有這么幾個關鍵組件,helm,tiller server,一般托管運行於k8s之上,helm能夠通過tiller server在目標k8s集群之上部署應用程序,而后,helm對應程序部署來自於helm能訪問到的倉庫中的chart。並且我們可以把chart通過配置生成為 Release,其中config是來自於我們chart中的values.yaml文件,對chart來講其內部沒有太多復雜的地方,無非就是把我們此前所自己編寫的配置清單改寫為可復用格式,所謂的可復用格式就是指的是我把整個可配置清單寫完以后他只能作為特定的部署,比如我們用某一特定的鏡像文件某一特定的deployment控制器或其它控制器將其部署完以后想去部署別的就得回過頭重新去修改這個配置文件或者我們去重新制作一個新版本的配置文件才能使得我們配置被復用,而helm的chart引入了一種機制,他能把我們此前定義的配置清單中的特定的內容配置為模板文件中的模板內容。只不過在這個模板中他們所能調用的屬性或變量值可以來自於不同的位置,有一些是我們helm內建的,來自於chart,或者是來自於部署時的release,也有一些屬性值需要由用戶通過所謂的值文件來額外進行提供。
2、究竟一個chart是由哪些內容組成呢?我們可以在官方文檔中看到其結構(https://docs.helm.sh)
a、Chart.yaml:用來做整個模塊初始化,用來對外表明自己的元數據信息。他是一個yaml格式的文件,里面記錄的是當前chart的版本,名稱,維護者,內部的應用程序版本等元數據信息。
b、LICENSE:許可證
c、README.md:markdown 格式的文件,項目說明
d、requirements.yaml:在一個分層架構或在一個組織復雜的微服務結構中我們部署的應用和應用直接很有可能是存在依賴關系的,比如nmt架構中m應該是被t所依賴的,t應該是被n所依賴的,如果這三個應用我們分別定義成獨立的chart這每個chart都應該去單獨部署,我們應該先去部署mysql的chart然后是tomcat的chart而不是nginx的chart,類似於這種格式部署,這就是依賴關系,因此這個文件就是讓我們去定義當前chart依賴於誰。
e、templates/:目錄,我們所有的模板文件,即我們此前經常去寫的所謂的配置清單,大家知道我們部署一個mysql有可能會用到好幾個清單,第一個清單中定義我們mysql的deployment或stateful這樣的控制器,另外一個清單中我們定義的應該是service,並且我們有可能還會用到pvc等其它資源清單。這些資源清單為了便於被復用他們被改造為能通過屬性值或字段值都被改造為能通過模板編程語言基於模板編程語言基於模板編程語言執行的結果生成配置信息並自動替換在代碼所在處。這個就是我們的templates目錄。但是我們要想自己去寫templates要求我們對Go或json模板語法能有充分的了解才能更加熟練的去改造我們此前所使用的配置清單。
f、values.yaml:模板有了那么模板中的代碼尤其是變量引用的值來自於何處呢?我們說過有三種位置。第一,可以是當前chart自身的信息,比如chart的名稱,chart的版本等等。第二、可以把chart運行為release時的release的信息。第三、也可以是來自於用戶自定義的信息。有些值用戶為了避免用戶在部署時不用必須得給每一個自定義變量都得賦值那么我們可以使用或者我們chart要求其開發人員必須提供一個默認的配置文件以提供其自定義屬性值的默認值。即values.yaml,這個文件主要就是為templates/中的自定義變量的引用設置默認值的。
g、charts/:其作用和requirements.yaml很相像,他里面放置的是每一個被當前chart所依賴到的其它chart的打包格式的文件,一般都是tgz格式的,他們被放置在charts/目錄中,要注意的是只要把一個tgz格式的打包好的chart文件放在這個目錄下那么他將始終被依賴。也就是說無論有沒有requirements.yaml文件他都是能自動定義依賴關系的。這兩項很多時候我們不必要同時提供。但是我們只是在第一個文件中提供有一個好處就是當他分析的時候就會自動去下載被依賴的包放在里面,如果你手動放進來就不必要定義這個文件了。這就是我們chart的組織結構和他的文件路徑組織關系。
h、事實上如果我們自己想自定義一個chart他也有非常簡單的實現方式,我們不必要非得每一個文件都得重頭去創建,helm有一命令能自動幫我們生成基礎目錄結構,甚至是能夠在目錄結構中幫我們生成一個基礎的框架的模板文件,值文件等信息。我只需要在他們的基礎上略加修改就能夠完成我們的目標了。
二、chart文件格式
1、接下來我們說一說Chart.yaml這個文件的基本語法格式,這個文件其實也會被自動生成,生成以后里面要求必須要具備的字段也會被自動生成有如下字段
a、apiVersion:用來標識當前helm 的Chart所屬的helm的api的版本,永遠都是v1就行
b、name:chart的名稱,要與chart的目錄名保持一致
c、version:版本格式定義規范
d、kubeVersion:表示我們對應的chart要依賴於至少運行在哪個版本及以上版本的kubernetes的版本之上
e、description:描述信息
...
2、接下來說一說requirements.yaml格式
a、dependencies:引入一個列表,每一個列表都是一個對象,每一個對象都是定義一個被依賴到的chart,指明chart名稱,chart版本,或chart所在的倉庫的路徑,所以我們自己在把當前的chart打包時分析這個文件的文件格式把每一個所依賴的chart從指定的倉庫中下載至本地,而后再完成打包。其實我們也可以通過模板文件去定義一些條件式的依賴關系。比如你啟用一個功能那么這個包可能就要依賴他,沒有啟動這個功能這個包就不會被依賴。
b、我們可以通過helm dep 這個命令來執行分析依賴關系。他能夠分析這個文件去了解依賴關系,並且下載相應的被依賴的chart到charts/目錄中。因此我們以后部署一個應用把所依賴到的應用都定義在一個包中就能在打包后一個應用被執行時把被依賴的包也自動安裝完成。所以我們使用requirements.yaml這個文件去維護依賴關系是比較易用的。
c、事實上我們如果自行知道依賴關系把相應的chart直接下載好以后扔到chart目錄中他也是可用的,另外有些chart被依賴時他有可能會通過不同的名稱被依賴,因此chart定義依賴關系時也可以使用別名定義。
3、如果有必要的話我們有可能需要自己去開發自己需要的模板文件,並為模板文件中的每一個自定義屬性賦予默認值。那么我們要怎么去管理這些文件呢?接下來就說到templetes和values
4、如果我們想要自定義一個chart我們可以使用helm create 命令,創建后會自動幫你生成一個目錄
[root@k8smaster helm]# helm create myapp Creating myapp [root@k8smaster helm]# tree myapp/ myapp/ ├── charts ├── Chart.yaml ├── templates │?? ├── deployment.yaml │?? ├── _helpers.tpl │?? ├── ingress.yaml │?? ├── NOTES.txt │?? └── service.yaml └── values.yaml 2 directories, 7 files
a、首先我們查看Chart.yaml並修改
[root@k8smaster myapp]# cat Chart.yaml apiVersion: v1 appVersion: "1.0" description: A Helm chart for Kubernetes name: myapp version: 0.1.0 [root@k8smaster myapp]# vim Chart.yaml [root@k8smaster myapp]# cat Chart.yaml apiVersion: v1 appVersion: "1.0" description: A Helm chart for Kubernetes myapp chart name: myapp version: 0.0.1 maintainer: - name: wohaoshuai email: wohaoshuai@qq.com url: http://www.wohaoshuai.com/
b、我們查看templates下deployment.yaml文件
[root@k8smaster templates]# cat deployment.yaml apiVersion: apps/v1beta2 kind: Deployment metadata: name: {{ template "myapp.fullname" . }} labels: app: {{ template "myapp.name" . }} chart: {{ template "myapp.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: app: {{ template "myapp.name" . }} release: {{ .Release.Name }} template: metadata: labels: app: {{ template "myapp.name" . }} release: {{ .Release.Name }} spec: containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: 80 protocol: TCP livenessProbe: httpGet: path: / port: http readinessProbe: httpGet: path: / port: http resources: {{ toYaml .Values.resources | indent 12 }} {{- with .Values.nodeSelector }} nodeSelector: {{ toYaml . | indent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{ toYaml . | indent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{ toYaml . | indent 8 }} {{- end }}
c、我們查看templates下service.yaml和ingress.yaml文件
apiVersion: v1 kind: Service metadata: name: {{ template "myapp.fullname" . }} labels: app: {{ template "myapp.name" . }} chart: {{ template "myapp.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http selector: app: {{ template "myapp.name" . }} release: {{ .Release.Name }}
{{- if .Values.ingress.enabled -}} {{- $fullName := include "myapp.fullname" . -}} {{- $ingressPath := .Values.ingress.path -}} apiVersion: extensions/v1beta1 kind: Ingress metadata: name: {{ $fullName }} labels: app: {{ template "myapp.name" . }} chart: {{ template "myapp.chart" . }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} {{- with .Values.ingress.annotations }} annotations: {{ toYaml . | indent 4 }} {{- end }} spec: {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ . }} http: paths: - path: {{ $ingressPath }} backend: serviceName: {{ $fullName }} servicePort: http {{- end }} {{- end }}
d、上述所有變量值都是在myapp路徑下values.yaml文件中定義的
[root@k8smaster helm]# cat myapp/values.yaml |grep -vE ".*#|^$" image: tag: v1 pullPolicy: IfNotPresent service: type: ClusterIP port: 80 ingress: enabled: false path: / hosts: - chart-example.local tls: [] limits: cpu: 100m memory: 128Mi requests: cpu: 100m memory: 128Mi nodeSelector: {} tolerations: [] affinity: {}
e、接下來做語法檢查
[root@k8smaster helm]# helm lint myapp/ ==> Linting myapp/ [INFO] Chart.yaml: icon is recommended 1 chart(s) linted, no failures
f、接下來就可以給chart打包,打包后就可以上傳到倉庫上分享給其他人使用了。
[root@k8smaster helm]# helm package myapp/ && ls Successfully packaged chart and saved it to: /root/manifests/helm/myapp-0.0.1.tgz myapp myapp-0.0.1.tgz redis redis-9.1.7.tgz tiller-rbac.yaml values.yaml
5、現在對應的這個chart我們能不能使用呢?對helm來講他去搜索chart時將搜索你所指定的各種repo,默認stable,local默認也是存在的,他會去找http://127.0.0.1:8879/charts,如果該端口沒有監聽那么我們在指定目錄下去運行就可以了 ,使用hem serve命令即可,這是一個簡單的由helm自帶的web服務器,用來提供倉庫服務,其實任何nginx或者http一樣的web服務器都可以作為倉庫服務器使用。而且這里他和我們此前所使用的github,dockerhub不一樣的地方在於他不允許我們直接使用命令行上傳我們的chart文件。你要上傳chart要使用其他方式,意味着他的外部服務是只讀的,只允許客戶端獲取而不允許匿名用戶或普通用戶使用http協議上傳其它資源。
a、啟動服務並查看
[root@k8smaster helm]# helm serve Regenerating index. This may take a moment. Now serving you on 127.0.0.1:8879
[root@k8smaster ~]# helm search myapp NAME CHART VERSION APP VERSION DESCRIPTION local/myapp 0.0.1 1.0 A Helm chart for Kubernetes myapp chart
b、在我們helm應用中在我們templates目錄下NOTES.txt文件一定要寫清楚到底要怎么向客戶端提供幫助信息。
c、接下來我們來部署這個myapp
[root@k8smaster helm]# helm install --name myapp3 local/myapp NAME: myapp3 LAST DEPLOYED: Sat Sep 14 09:18:06 2019 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE myapp3-7cf7947c98-28cq8 0/1 ContainerCreating 0 1s myapp3-7cf7947c98-r8b9c 0/1 Pending 0 1s ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp3 ClusterIP 10.107.112.115 <none> 80/TCP 1s ==> v1beta2/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE myapp3 2 2 2 0 1s NOTES: 1. Get the application URL by running these commands: export POD_NAME=$(kubectl get pods --namespace default -l "app=myapp,release=myapp3" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl port-forward $POD_NAME 8080:80 [root@k8smaster helm]# kubectl get pods NAME READY STATUS RESTARTS AGE myapp-6985749785-rlk8g 1/1 Running 1 12d myapp3-7cf7947c98-28cq8 1/1 Running 0 15s myapp3-7cf7947c98-r8b9c 1/1 Running 0 15s
d、獲取當前release狀態
[root@k8smaster helm]# helm status myapp3 LAST DEPLOYED: Sat Sep 14 09:18:06 2019 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp3 ClusterIP 10.107.112.115 <none> 80/TCP 2m ==> v1beta2/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE myapp3 2 2 2 2 2m ==> v1/Pod(related) NAME READY STATUS RESTARTS AGE myapp3-7cf7947c98-28cq8 1/1 Running 0 2m myapp3-7cf7947c98-r8b9c 1/1 Running 0 2m NOTES: 1. Get the application URL by running these commands: export POD_NAME=$(kubectl get pods --namespace default -l "app=myapp,release=myapp3" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl port-forward $POD_NAME 8080:80
e、清楚所有信息
[root@k8smaster helm]# helm delete --purge myapp3 release "myapp3" deleted
6、接下來去說一說如何去使用系統上在官方的chart倉庫中提供其它的chart。我們可以在官方網站上https://hub.kubeapps.com 中搜索elastic關鍵詞可以看到會有很多種版本的倉庫。可以將其添加進來,我們可以通過helm repo add來添加
a、添加 helm repo add http://storage.googleapis.com/kubernetes-charts-incubator
三、EFK
1、EFK是ELK的另一種形式。
a、E:elasticsearch,他是一個標准搜索引擎
b、L:logstash:主要是用於幫助我們生成日志的節點負責收集節點上的日志並轉換格式后注入到elasticsearch中去,注入進去的格式應該是json格式的。他能扮演兩重功用,在收集日志的節點上能扮演成agent收集日志的代理程序,所以在k8s上如果每一個節點都要部署一個並且部署一個就夠了我們可以使用控制器daemonset,生成日志以后應該發送給logstash server,統一整理之后再發送給elasticsearch中去,一般情況下es應該是一個集群,並且logstash agent和server端中間可以加一個消息隊列。logstash server扮演了一個非常重要的作用可以把所有的節點所發來的日志做統一的格式轉換等額外操作以后把日志信息注入到我們的es集群中。所以logstash server有很重要的作用,我們也講過logstash如果去做收集日志的agent這個功能來講就太重量級了,所以我們說過他還可以使用filebeat來替換。所以可以做成ELFK或ELLK。
2、我們接下來說的EFK可不是上面所說的F,用於在節點上工作能夠收集日志信息的組件不光有logstash或filebeat,還有Fluentd,他是一個組織研發的,在k8s集群上運行了很多節點每一個也需要收集日志,並且還有pod,每一個pod中有可能不止一個容器,每一個容器都會生成應用程序日志的,我想去了解這些日志信息,如果我們沒有統一的日志收集平台我們管理日志將會變成什么情形?我們突然之間發現我們某一個pod中的容器崩了,我想知道容器中的日志是什么我們此時的做法是什么呢?可以使用kubectl logs 連入容器內部。所以我們需要實時獲取每一個pod中的每一個容器中的單獨的獲取方式不是一個理想的解決方案。我們使用kubectl logs 盡管能獲取日志但是對於死掉的容器這種做法都是不可行的因此我們非常有必要應該把pod中的容器包括我們的節點運行中所生成的日志提前收集到一個專門的日志存儲中,以免在節點丟失pod down掉或容器down掉時我們想分析日志你還得想辦法去把其硬盤扯下來找日志,這個很多時候幾乎是不可能的。所以我們說在k8s上復雜的雲環境中幾乎必須要用到一個日志收集平台以便於能夠提供一個統一的接口讓我們去查看和分析日志,因此從這個角度來講日志統一收集存儲平台是k8s之上除標准的四大附件之外的另外一個算不上附件的非常重要的基礎組件。所以我們說一個完整意義上的k8s集群我們應該部署像 kube-dns(coredns),ingress-controller,heapster(metrics-server),dashboard這四大附件,EFK是一個日志統一收集工具不是基礎附件但是基本算是一個完整意義上的k8s必須提供的組件
3、因此接下來我們來部署這個組件。在k8s上收集日志的風格有兩種,第一種我們可以外置收集日志,第二種就是在每一個Pod上都能生成日志,每一個pod單獨發送日志給日志存儲,但是這個時候我們需要在每一個pod內額外部署一個seidcar容器,因為一個容器中只運行一個應用程序,你的主容器中運行redis后他就不可能再運行一個日志收集工具和發送工具了,所以這個時候我們只能在Pod中同時運行兩個容器,主容器提供服務,附容器用來收集主容器日志並發送給日志統一收集平台
a、但這樣一來,萬一我們部署一萬個Pod那么就要運行一萬個日志收集容器,雖然我們一個pod包含兩個容器,pod是我們最小粒度單位,但真正意義上來講兩個容器在節點上是需要同時運行的,這樣一來就悲劇了。我們也可以在一個容器上運行兩個進程但是我們一般不建議這樣干,所以最常用的方式我們一般都是基於節點直接部署一個統一的插件,這個節點上的所有容器包括節點自身的日志都由這個插件統一收集以后統一發往日志收集平台,那么這個日志收集平台是應該部署在k8s之上還是之外呢?假如我們使用es存儲日志信息,那么es應該運行在集群之上還是集群之外呢?其實都可以。單獨部署一套和k8s沒關系的集群也可以。因此只有確保k8s自身不會統一所有都出問題時這種情況下才應該把他部署在k8s之上,我們可以把很多應用和k8s這樣的應用組合起來統一工作的,這沒有任何問題。
b、在每一個節點上部署一個fluentd,在fluentd基於節點的ip工作向節點之外的沒有運行在k8s上的es集群發日志也可以,但是以后我們通過fluentd來作為日志收集器,一般來講在k8s上用的時候可以用filebeat,只不過用fluentd的居多。假如我們要使用flu的話我們對於flu來說他是部署為守護進程呢還是部署為k8s之上的pod?
c、首先flu本身是通過我們節點的本地文件系統/var/log來獲取日志的,其實我們節點上的每一個容器的日志都被輸出到 /var/log/containers/下的,也就意味着我們通過/var/log目錄可以獲取到所有日志的。都是節點級別的目錄,說白了就是要節點的文件系統,因此我們把flu運行為節點的守護進程也是理所當然的,但是我們說過如果flu在節點上掛了怎么辦呢?因此如果把flu本身也運行在k8s上運行為daemonset並使用hostpath方式直接把節點的/var/log關聯到相關節點的pod上去就能讓pod直接訪問節點的某特定路徑下的文件中的內容了。因此flu可以部署為daemonset托管在k8s上。flu負責收集日志發送給es,es中的內容讓kibana來展示,kibana我們也可以運行為容器。並且它是無狀態的因此用deployment都可以控制。
d、在 elk中我們通常使用logstash或filebeat去收集節點日志並同一發給logstash server,由logstash server將格式轉換以后再發給es,現在我們提到flu時logstash就沒了么?其實為了讓我們的es能運行在k8s之上,es官方就直接制作好了相關鏡像打包好了相關文件讓其直接能運行在我們k8s上,這個鏡像文件一般而言他可以運行三種格式,首先他把es拆為兩部分,由master和date兩部分組成,master節點負責處理輕量化的查詢請求,data節點負責重量級別的比如索引構建等請求,所以他把兩重任務給隔離開來,master負責接入,data負責處理。因此一般而言我們的es每一個節點就是一個完整的節點,他應該擁有既能負責查詢又能負責構建索引的功能,現在我們把他分成兩層來分開實現,而后這也就意味着master是客戶端接入的唯一入口了。如果master down了data也就沒法工作了。因此一般而言我們有三個節點就可以,主要的目的是為了做冗余,萬一節點宕機了我們服務還能持續進行,當然如果考慮到我們訪問量較大時這三個節點不夠用要橫向擴展他也可以。和data互不干擾的進行擴展。考慮到都要處理數據那么他們幾乎都應該使用存儲卷以便於持久存儲數據。
e、master就算是三個節點我們應該判定一旦出現節點之間集群發生分裂了找不着了我們至少要有兩個節點才能讓他處於正常運行狀態,因此以后就不是我們弄三個節點組成es集群了而是我們做兩個集群,一個是接入的一個是處理數據的。即master集群和data集群。除此之外他還需要有client集群即客戶端,他其實不是真正的client而叫上載,即攝入節點。他幫忙收集任何的日志收集工具由其同一生成特定格式以后再發給我們的master節點,你也可以把它理解為logstash server。
f、考慮到在生產環境中我們使用妥當的方式去部署,我們如果自己去部署的話可能一個Logstash或部署多個flu也就完事了,但使用helm部署時他們開發的這個應用盡可能考慮是在生產環境中使用的,所以他們部署非常完整和完善,也就意味着我們通過helm獲取到的es他就是按照這種格式來組織和部署的。因此有幾個master和data節點就需要幾個存儲卷,因為es的master或data都是有狀態的。因為每一個節點只負責一部分任務,存儲數據也支持一部分,他不是復制狀態的而是分布式的,同樣的data也是分布式的,這么一來也就意味着他的master和后端的data都是有狀態的,因此他們的存儲卷每一個都是獨立,一共有幾個master data加起來就需要有幾個 存儲卷。不過考慮到此處只是測試我們就暫時關掉了。
四、部署和使用EFK
1、部署和使用es
2、部署fluentd
3、部署kibana