[教學] 用Drone, Kubernetes跟Helm,以及RBAC來建置你的CI/CD流程 — Part 3

CI/CD with Drone, Kubernetes and Helm — Part 3

有陣子沒寫文章,翻譯點東西來練習練習。最近公司要導入Drone/K8s/Helm,加上本來公司大多數的解決方案都放在Google Cloud 上,於是開始動手在新的專案做測試。不過這幾個解決方案都算新的,也很難找到適合的說明一次全部到位。剛好看到 這篇 在介紹這個組合,不但救了我幾天的時間,還幫我暖了一下這幾個技術的概念,聯絡了一下作者,決定來翻譯這篇文章。不過應該很難避免加入自己的話啦,英文的部分除非比較常見的翻譯法,否則還是以原文為主。另外原作者列了許多參考連結,在此就沒有再針對那些連結在做翻譯了


介紹

這是這個系列的第三篇,也是最後一篇文章了。在第一篇裡面,我們學會了如何:

  • 用Google Kubenetes Engine 建立一個k8s叢集
  • 部署Tiller並且使用Helm
  • 用Helm以及一個現成的Chart部署一個Drone的實例
  • 在我們的Drone上啟用Https,並且使用cert-manager來處理它

第二篇裡面,我們建立了一個Drone的工作流,其中我們:

  • 使用linter檢查
  • 建立Docker映像
  • 推送到GCR、建立容器的tag

在這篇文章裡面,我們會看到如何建立一個Helm Chart,如何使用我們的Drone Pipeline自動化整個安裝、升級流程。

Helm Chart

建立Chart

Helm的工具提供了很好的整合。我們先從一個dummy的repo開始進行

$ mkir helm
$ cd helm/
$ helm create dummy
Creating dummy

這個流程會從你在的目錄建立一個新的dummy目錄。這個目錄會包含兩個子目錄以及一些檔案:

  • chart/ 一個包含所有現在這個chart所依賴的其他chart的資訊
  • templates/ 一組指示如何組合現有的chart來產生有效的k8s設置檔的內容
  • Charts.yaml 這個Chart的相關資訊
  • values.yaml 這個Chart預設的設置

更多的資訊可以看這份文件的說明

這時候你的repo會長得像這樣

.
├── Dockerfile
├── Gopkg.lock
├── Gopkg.toml
├── helm
│ └── dummy
│ ├── charts
│ ├── Chart.yaml
│ ├── templates
│ │ ├── deployment.yaml
│ │ ├── _helpers.tpl
│ │ ├── ingress.yaml
│ │ ├── NOTES.txt
│ │ └── service.yaml
│ └── values.yaml
├── LICENSE
├── main.go
└── README.md

接下來我們會修改values.yaml來建立這個chart的預設設定,以及新增、修改templates裡面的內容來產生我們k8s的設置。

我們也可以看到Helm已經幫我們建立了一個最小可用的chart版本,裡面也有包含取多有用的工具跟設置。現在你可以看到metadata這個部分裡面通常都是這樣的資訊:

metadata:
name: {{ template "dummy.fullname" . }}
labels:
app: {{ template "dummy.name" . }}
chart: {{ template "dummy.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}

這會確保我們在重複部署我們的應用程式時不會讓我們的資源碰撞或錯亂

Values File

現在打開 dummy/values.yaml:

replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: false
annotations: {}
path: /
hosts:
- chart-example.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}

這些是目前我們的Chart預設的值(也是可被它接受的值)。現在我們會先修改image這個部分的資料,讓我們前面完成的工作可以在這裡繼續被套用:

image:
repository: gcr.io/project-id/dummy
tag: latest
pullPolicy: Always

我們在pullPolicy的部分設成Always,因為我們的latest tag 會一直不斷的變動。這些就是預設的值了,我們接下來會再修改這些資訊來完成特定的需求,後面我們會再提到。

部署文件 Deployment Manifest

我們在前一篇文章建立了一個dummy的專案,裡面包含一個API端點是/health,他永遠回傳200 ok這個訊息,在這裡我們會用這個來當作我們的liveness probe(系統存活探針)跟readiness probe(系統可讀探針)。

Liveness probes 被k8s用來確認你的容器還在執行,並且回傳有效的內容。如果他回傳200以外的結果,k8s會認為這個容器已經壞掉了,並且會因此重新開始部署一個新的pod來取代。Readiness probe, 則是用來確保這個pod已經可以接受外來的請求。如果這個探針沒有收到應有的結果,k8s不會把任何的請求轉送到這個節點。

我們的程式比較陽春,可以在此直接用/health這個路由來當作liveness probe跟readinessprobe。所以我們打開dummy/templates/deployment.yaml並且編輯以下這個部分:

livenessProbe:
httpGet:
path: /health # There
port: http
readinessProbe:
httpGet:
path: /health # And there
port: http

就這樣,我們的部署文件已經可以使用了。我們接下來就執行這個文件,並且看我們的部署文件是不是有正確的被渲染出來。我們先在debug跟dry-run模式裡執行helm,這樣他會輸出所有被渲染後的結果、並且不會實際執行到我們的環境上。同時我們也會在這個發布裡加上-n staging這個參數,我們會把這個假安裝安裝到staging這個命名空間裡

$ helm install --dry-run --debug -n staging --namespace staging dummy/
# Source: dummy/templates/deployment.yaml
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: staging-dummy
labels:
app: dummy
chart: dummy-0.1.0
release: staging
heritage: Tiller
spec:
replicas: 1
selector:
matchLabels:
app: dummy
release: staging
template:
metadata:
labels:
app: dummy
release: staging
spec:
containers:
- name: dummy
image: "gcr.io/project-id/dummy:latest"
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: http

服務文件 Service Manifest

一個k8s服務是一個抽象層,它定義了一個Pod的邏輯集合以及定義一個如何取用他的策略(有時候也會被叫做micro-service微服務)這個Pod的集合能夠被Service取用到,通常是透過Label Selector

所以一個k8s裡的服務,代表一種用選擇器來建立穩定的連結,用以動態取用已經建好的Pod。記得前面我們文件裡的metadata.labels?這就是我們怎麼設定來取用我們的應用程式

所以打開dummy/templates/service.yaml:

apiVersion: v1
kind: Service
metadata:
name: {{ template "dummy.fullname" . }}
labels:
app: {{ template "dummy.name" . }}
chart: {{ template "dummy.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 "dummy.name" . }}
release: {{ .Release.Name }}

有些東西跟前面講得不太一樣,我們的targetPort不同,前面我們用Docker建立的時候,Expose出來的port是8080,怎麼辦呢?我們在這裡就可以把它修改成一個可以從外部傳進來的資訊。

ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
protocol: TCP
name: http

看起來更好了,接著來修改 dummy/values.yaml 檔案:

service:
type: ClusterIP
targetPort: 8080
port: 80

如果你在用ClusterIP這個類型的時候遇到問題,可以把它換成NodePort(註:我實測的時候發現ClusterIP會引來很多問題,所以我建議這邊換成NodePort)。接著我們再試跑一次

$ helm install --dry-run --debug -n staging --namespace staging dummy/
# Source: dummy/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: staging-dummy
labels:
app: dummy
chart: dummy-0.1.0
release: staging
heritage: Tiller
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: dummy
release: staging

Ingress

Helm Charts應該是跟他的k8s平台完全獨立的,我們應該要讓使用者自己決定是否要啟用這個ingress,而且可以修改上面的各種定義。(註:這邊指的是預設使用Google Cloud Load Balancer,但使用者如果不是在GCP上部署,也應該能夠用nginx來處理)

當你使用Helm的時候,有幾種方法可以覆寫預設值,這邊我們可以先建立一個新的文件,如果Helm在這份文件裡面沒有找到他需要的資訊,就會去values.yaml裡面找相關的資訊

這裡我們就先建立一個用在staging環境上的 staging.yml:

ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "gce"
kubernetes.io/ingress.global-static-ip-name: "dummy-staging"
path: /*
hosts:
- staging.dummy.myshost.io

注意:我們必須把path定義成/*而不是/,這是GCLB的設計

這裡我們用第一篇文章裡面提到的建立靜態IP的方式,並且把他指定到我們的文件裡。街ㄓㄜ

$ helm install --dry-run --debug -n staging --namespace staging -f staging.yml dummy/

現在我們有Ingress了!

# Source: dummy/templates/ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: staging-dummy
labels:
app: dummy
chart: dummy-0.1.0
release: staging
heritage: Tiller
annotations:
kubernetes.io/ingress.class: gce
kubernetes.io/ingress.global-static-ip-name: dummy-staging

spec:
rules:
- host: staging.dummy.myshost.io
http:
paths:
- path: /*
backend:
serviceName: staging-dummy
servicePort: http

工作流

Service Account

在我們開始實際用Drone部署我們的staging環境之前,我們要先拿到我們在前面建立的Tiller的服務帳戶的憑證。我們要把這個憑證放在Drone裡面,這樣他才會有權限去處理k8s上的部署

$ kubectl -n kube-system get secrets | grep tiller
tiller-token-xxxx
$ kubectl get secret tiller-token-xxx -n kube-system -o yaml
apiVersion: v1
data:
ca.crt: xxx
namespace: xxx
token: xxx
kind: Secret
metadata:
annotations:
kubernetes.io/service-account.name: tiller
kubernetes.io/service-account.uid: xxxx-xxxx-xxxx-xxxx
creationTimestamp: 2018-05-15T14:51:35Z
name: tiller-token-xxxx
namespace: kube-system
resourceVersion: "860311"
selfLink: /api/v1/namespaces/kube-system/secrets/tiller-token-xxx
uid: xxxx-xxxx-xxx-xxx
type: kubernetes.io/service-account-token

我們需要的是 data.token裡面的資訊。 注意,這邊是Base64的編碼,我們必須把它解碼,Linux上:

echo "前面的那組token" | base64 -d -w 0

在mac環境上:

echo "前面的那組token" | base64 -D -o -

把解碼後的資訊存下來,待會再解釋怎麼使用它。然後,我們來取得我們K8s上的IP

$ kubectl cluster-info
Kubernetes master is running at <your master IP>
...
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

Drone-Helm plugin

接下來我們會用 drone-helm plugin 來自動執行我們的helm命令 這個外掛會需要兩個資訊: api_serverkubernetes_token.

所以我們要把這兩個資訊放到Drone上面去(註:前面有提到Drone的免費版沒有共用Secret,所有的Secret都要在各個不同的Repo上個別設定,想享受就多掏點錢吧)

$ drone secret add --image quay.io/ipedrazas/drone-helm --repository repo/dummy \
--name kubernetes_token --value <the token you base64 decoded earlier>
$ drone secret add --image quay.io/ipedrazas/drone-helm --repository repo/dummy \
--name api_server --value <your master IP>

接下來就是來設定我們的工作流了,接下來的部分包含前面上傳到GCR上的部分以及上的部分以及使用Drone-Helm外掛的部分:

gcr:
image: plugins/gcr
repo: project-id/dummy
tags: latest
secrets: [google_credentials]
when:
event: push
branch: master
  helm_deploy_staging:
image: quay.io/ipedrazas/drone-helm
skip_tls_verify: true
chart: ./helm/dummy
release: "staging"
wait: true
recreate_pods: true
service_account: tiller
secrets: [api_server, kubernetes_token]
values_files: ["helm/staging.yml"]
namespace: staging
when:
event: push
branch: master

這個內容大概也順便解釋了自己在做什麼,不過我們還是在這裡解釋一下:當有一個推送到了master branch,我們會先建置並且推送這個Docker映像檔到GCR上。然後我們會執行drone-helm外掛,裡面有我們的chart的位置(helm/dummy)、我們的發佈的名稱:staging、我們要使用的命名空間staging,我們要使用的服務帳戶名稱:tiller。我們也需要等到所有的資源被建立/重建後才離開(wait:true),因為我們使用了同樣的tag,所以recreate_pods確定我們要重新建立這個節點

就這樣,接下來每次我們推送到master,我們都會更新我們的staging環境了

Production

讀到這裡,你也應該知道helm的好處在哪裡了,現在我們繼續再接再厲,建立一個production環境的資訊,先新建一個檔案叫prod.yml

image:
tag: 1.0.0
pullPolicy: IfNotPresent
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "gce"
kubernetes.io/ingress.global-static-ip-name: "dummy-prod"
path: /*
hosts:
- prod.dummy.myshost.io

就這樣,接下來一樣在.drone.yml裡面新增產品環境的資訊:

tagged_gcr:
image: plugins/gcr
repo: project-id/dummy
tags:
- "${DRONE_TAG##v}"
- "${DRONE_COMMIT_SHA}"
- latest
secrets: [google_credentials]
when:
event: tag
branch: master
  helm_deploy_prod:
image: quay.io/ipedrazas/drone-helm
skip_tls_verify: true
chart: ./helm/dummy
release: "prod"
wait: true
recreate_pods: false
service_account: tiller
secrets: [api_server, kubernetes_token]
values_files: ["helm/prod.yml"]
values: image.tag=${DRONE_TAG##v}
namespace: prod
when:
event: tag
branch: master

完成了,現在你有一個完整的CI/CD流程。當你tag他release的後,他會建立Drone的映像檔、tag他、然後再使用helm把這個映像檔上傳到我們的叢集上。

TLS

當然,我們也會需要處理TLS的問題。不管是staging或是production環境,在這裡我們會像第一篇文章所提到的方式來處理。也就是用cert-manager、ACME Issuer來處理。

憑證文件

這裡我們會用到的方法還是跟前面一樣,我們把憑證文件用template處理。然後我們會修改我們的Ingress文件來處理TLS密鑰。

首先先在dummy/templates建立一個certificate.yaml:

{{- if and .Values.ingress.enabled .Values.tls.enabled -}}
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: {{ template "dummy.fullname" . }}
labels:
app: {{ template "dummy.name" . }}
chart: {{ template "dummy.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
secretName: {{ template "dummy.fullname" . }}-tls
issuerRef:
name: {{ required "A valid .Values.tls.issuer.name entry required!" .Values.tls.issuerName }}
kind: ClusterIssuer
commonName: {{ required "A valid .Values.tls.commonName entry is required!" .Values.tls.commonName }}
dnsNames:
{{- range .Values.tls.dnsNames }}
- {{ . }}
{{- end }}
acme:
config:
- http01:
ingress: {{ template "dummy.fullname" . }}
domains:
{{- range .Values.tls.domains }}
- {{ . }}
{{- end }}
{{- end -}}

現在我們在提供的參數裡需要 tls 這個物件。所以我們也把它加進我們的values.yaml

tls:
apply: false
enabled: false
issuerName:
commonName:
dnsNames: []
domains: []

這裡也一併加了 tls.apply ,我們待會解釋

接下來 staging.yaml的內容也修改成需要的內容

tls:
apply: false
enabled: true
issuerName: letsencrypt
commonName: staging.dummy.myhost.io
dnsNames:
- staging.dummy.myhost.io
domains:
- staging.dummy.myhost.io

Ingress 修改

tls.apply 的功能接下來會出現。這裡會變成兩步動作,首先我們需要部署一次,並且把tls.apply設成false。然後推送到master讓他實際進行上版,這個動作會讓我們的系統部署,並且開始進行憑證的驗證。等到憑證完成之後,我們再來實際把tls.apply 改成true,並且告訴我們的Ingress應該要使用它,這裡就先來修改/templates/ingress.yaml

spec:
{{- if .Values.tls.apply }}
tls:
- hosts:
{{- range .Values.tls.domains }}
- {{ . }}
{{- end }}
secretName: {{ template "dummy.fullname" . }}-tls
{{- end }}
rules:
...

所以我們在這裡設定如果tls.apply是ture的情況,我們才用設定好的密鑰來加密我們的服務

好了,這次真的好了,我們完成了,首先,我們先把tls.apply設成false,推送。等到kubectl describe certificate staging-dummy --namespace=staging 的結果出來,確定憑證發放成功,再把 tls.apply 設成true

等他部署一下~tada,https支援囉

感謝

感謝原作Depado的文章,這系列幫我節省了很多時間。這篇文經過他的同意後翻譯,雖然本人說他也不知道怎麼確定我寫的內容,謝謝收看

系列文連結如下:

第一篇

第二篇

第三篇