Google Code Prettify

2020年4月17日 星期五

K8s: Deployment

在「K8s: ReplicaSet」及「K8s: Service」兩篇,已經有說明了,當 docker hub 上有個 image,怎麼將它部署到 Kubernetes 上,並讓外部可以訪問到它。兩個步驟可以合併成一個步驟如下:
  1. apiVersion: apps/v1
  2. kind: ReplicaSet
  3. metadata:
  4. name: host
  5. spec:
  6. replicas: 3
  7. selector:
  8. matchLabels:
  9. app: host
  10. template:
  11. metadata:
  12. labels:
  13. app: host
  14. spec:
  15. containers:
  16. - name: host
  17. image: twleader/host
  18. ---
  19. apiVersion: v1
  20. kind: Service
  21. metadata:
  22. name: host-svc
  23. spec:
  24. type: LoadBalancer
  25. ports:
  26. - name: http
  27. port: 8080
  28. targetPort: 9080
  29. selector:
  30. app: host
注意看第 18 行,有 --- 將原本兩個 yaml 隔開合併成一個檔,透過這種方式,可以合併多個 yaml 檔,這樣部署可以快一點,但是,對 K8s 來說,這是比較低階的做法,K8s 另外提供了 Deployment 這個更高階部署 rs 的方法,yaml 可以如下:
  1. apiVersion: apps/v1
  2. kind: Deployment
  3. metadata:
  4. name: host
  5. spec:
  6. replicas: 3
  7. template:
  8. metadata:
  9. name: host
  10. labels:
  11. app: host
  12. spec:
  13. containers:
  14. - image: twleader/host:latest
  15. name: host
指令一定要如下,不能忽略後面的 --record !! (原因後面說明)
  1. kubectl create -f xxxx.yaml --record
使用 Deployment 比之前的做法好,這是完全交給 K8s 幫我們決定怎麼部署,除了比較容易外,還可以達到滾動式部署! (Service 還是要另外創建) 假如我們提供的服務不能中斷,換版時希望新版還沒有啟動前,舊版程式要持續提供服務,那麼使用 Deployment,改變第 14 行的版號,讓 K8s 部署新版本,K8s 會先創建新版的 pod 及 ReplicaSet,並等新版的 pod 完全啟動後,再逐步的刪除舊版的 pod。如果要下指令的話,如下:
  1. kubectl set image deployment host host=twleader/host
名稱取的不太好,說明一下,第一個 host 是 deployment 的名稱,接著 host=twleader/host 是指,命名為 host 的 container 要由 twleader/host 這個 image 產生。在滾動更新的過程,如果要知道更新的狀況,可以下這個指令:
  1. kubectl rollout status deployment host
部署好之後,使用 kubectl get po 可以看到舊的 pod 一個一個慢慢消失,使用 kubectl get rs 觀察,則會發現如下,
舊的 ReplicaSet 並沒有被刪除! 這方便當發現新版程式有錯,想要回到舊版時,可以更快速,要怎麼回到舊版呢? 可以使用 undo 如下:
  1. kubectl rollout undo deployment host
事實上,K8s 每次部署都會記錄下每個版本,我們可以回滾到任何一個歷史版本,先看一下 history 指令:
  1. kubectl rollout history deployment host
顯示如下:
之前 create deployment 時有加 --record 的話,這裡的 CHANGE-CAUSE 欄位才會有值,方便我們辨識該版本,我這邊因為都用 create 的方式,如果檔名取不同名稱可以看的出兩個版本的不同,如果是用下指令的,在 image 後面的版號就可以看出版本間的差異,要回到任何一個版本,可以下 undo 並加上 --to-revision 參數:
  1. kubectl rollout undo deployment host --to-revision=2
要特別注意的是,K8s 的版本紀錄是保存在 ReplicaSet 裡,所以舊的 ReplicaSet 不要手動刪除,否則就無法回滾了!

2020年4月16日 星期四

K8s: ConfigMap

前一篇「K8s: 設定環境變數」是利用 create ReplicaSet 時,由 yaml 檔傳入環境變數,萬一我們的系統有許多個 ReplicaSet 要創建,每個 yaml 都要寫相同的設定,這樣就等於是 hard code 在 yaml 裡,維護相當不容易。

Kubernetes 提供了 ConfigMap 的功能,讓環境變數引用 ConfigMap 裡的值,達到集中管理的目的。怎麼建立 ConfigMap 有許多種方法,這裡舉兩個如下:
  1. kubectl create configmap practicecnfmap --from-literal=jasypt_encryptor_password=secondKey --from-literal=spring_profiles_active=uat
這是最直覺的方式,透過命令建立一個名為 practicecnfmap 的 ConfigMap,裡面有兩個設定 jasypt_encryptor_password 及 spring_profiles_active,它們的值在這裡故意和前一篇不同,這樣才能觀察是否 ConfigMap 有產生作用。萬一 ConfigMap 裡要設定很多參數怎麼辦? 不可能在命令列這樣的輸入,可以選擇先把這些參數以 key=value 的方式存在一個目錄,檔名是 key, 內容是 value,我建立了一個 conf 的目錄,裡面有兩個檔案如下:
檔案裡是值,接著輸入如下指令, 這樣得到的結果和上面的指令是一樣的。
  1. kubectl create configmap practicecnfmap --from-file=./conf
接著要修改 ReplicaSet 的 yaml 檔,如下:
  1. apiVersion: apps/v1
  2. kind: ReplicaSet
  3. metadata:
  4. name: practice-rs
  5. spec:
  6. replicas: 2
  7. selector:
  8. matchLabels:
  9. app: practice-rs
  10. template:
  11. metadata:
  12. labels:
  13. app: practice-rs
  14. spec:
  15. containers:
  16. - name: practice-pod
  17. image: harbor.steven.idv.tw/steven/practice-pod
  18. envFrom:
  19. - configMapRef:
  20. name: practicecnfmap
  21. imagePullSecrets:
  22. - name: steven-harbor
第 18~20 行指出,環境變數由 practicecnfmap 這個 ConfigMap 取得。建立 ConfigMap 後,如果想要修改內容,不需刪除後重新建立,可以用以下指令編輯。
  1. kubectl edit configmap practicecnfmap

2020年4月15日 星期三

K8s: 設定環境變數

  1. spring.profiles.active=${spring_profiles_active}
  2. server.servlet.context-path=/
  3. server.port=8080
  4.  
  5. # encrypt
  6. jasypt.encryptor.bean=encryptorBean
  7. jasypt.encryptor.password=${jasypt_encryptor_password}
  8. jasypt.encryptor.algorithm=PBEWithMD5AndDES
spring boot 的程式,設定多半寫在 application.properties,裡面的值基於資安,或是其它各種理由,可能不會直接寫在檔案裡,而會取自環境變數,如上的 application.properties 有兩個參數來自環境變數,在 K8s 的環境要怎麼傳入環境變數呢?

這裡簡單的寫一支 REST API,會傳回 application.properties 裡的值,看看那兩個取自環境變數的值是否正確,程式如下:
  1. @RestController
  2. @RequestMapping("/info")
  3. @Slf4j
  4. public class InfoController {
  5. @Autowired
  6. private Environment env;
  7. @Autowired
  8. ResourceLoader resourceLoader;
  9. @GetMapping("/app")
  10. public Map app() {
  11. Map all = new HashMap();
  12. Set keys = new HashSet<>();
  13. try {
  14. Resource resource = resourceLoader.getResource("classpath:application.properties");
  15. InputStream is = resource.getInputStream();
  16. Properties prop = new Properties();
  17. prop.load(is);
  18. keys = prop.keySet();
  19. for(Object key:keys) {
  20. all.put((String) key, env.getProperty((String) key));
  21. }
  22. }
  23. catch (IOException e) {
  24. log.error(e.getMessage(), e);
  25. }
  26. return all;
  27. }
  28. }
下面是建立 pod 的 yaml,第 9~13 行就是傳入環境變數的方式。
  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: practice-pod
  5. spec:
  6. containers:
  7. - name: practice-pod
  8. image: steven/practice-pod
  9. env:
  10. - name: spring_profiles_active
  11. value: "test"
  12. - name: jasypt_encryptor_password
  13. value: "mykey"
  14. imagePullSecrets:
  15. - name: steven-harbor
如果我們用 ReplicaSet 啟動兩個 pod,如下:
practice-pod 這個 pod 是用上面的 yaml 產生的,確實可以取得環境變數,但是 practice-rs-grtx7 和 practice-rs-lncdr 這兩個 pod 並無法取得環境變數,因為 rs 每次產生 pod,是從 image 產生,並沒有傳入環境變數,為了解決這個問題,建立 ReplicaSet 的 yaml 要如下 18~22 行:
  1. apiVersion: apps/v1
  2. kind: ReplicaSet
  3. metadata:
  4. name: practice-rs
  5. spec:
  6. replicas: 2
  7. selector:
  8. matchLabels:
  9. app: practice-rs
  10. template:
  11. metadata:
  12. labels:
  13. app: practice-rs
  14. spec:
  15. containers:
  16. - name: practice-pod
  17. image: harbor.steven.idv.tw/steven/practice-pod
  18. env:
  19. - name: spring_profiles_active
  20. value: "test"
  21. - name: jasypt_encryptor_password
  22. value: "mykey"
  23. imagePullSecrets:
  24. - name: steven-harbor
輸入如下指令測試看看: (使用 kubectl get svc 指令,可以看到 external-ip)
  1. curl http://172.16.3.39:9090/info/app
輸出如下結果:
  1. {"jasypt.encryptor.algorithm":"PBEWithMD5AndDES","jasypt.encryptor.bean":"encryptorBean","server.servlet.context-path":"/","server.port":"8080","jasypt.encryptor.password":"mykey","spring.profiles.active":"test"}
可以看到,兩個環境變數都有抓到。

2020年4月14日 星期二

K8s: Ingress

繼續前面「K8s: Service」的例子,建立好服務後,服務會有對內的 cluster IP 及對外的 external IP,這樣對外服務是沒有問題了,但是,一個 Kubernetes 裡會有無數個服務,外部系統要使用這些服務有沒有類似 API Gateway 的功能呢? Kubernetes 提供的解答是 Ingress ! 它就是提供了一些基本功能的 API Gateway。
如上圖,Ingress 會根據網址導向不同的 service,以「K8s: Service」的例子,這裡寫了如下的 yaml 檔來產生 Ingress。
  1. apiVersion: extensions/v1beta1
  2. kind: Ingress
  3. metadata:
  4. name: steven-gw
  5. spec:
  6. rules:
  7. - host: steven-gw.steven.idv.tw
  8. http:
  9. paths:
  10. - path: /host/info
  11. backend:
  12. serviceName: host-http
  13. servicePort: 9090
  1. 第 4 行定義了 Ingress 的名稱
  2. 第 7 行定義當 Ingress 收到這個網址的 request,會將 request 以下面的規則導向服務。
  3. 第 10 行定義,當網址開頭為 /host/info 導向 host-http 服務,port 為 9090,這是上一篇創建服務時,服務提供出來的 port。
執行上面的 yaml 後,用如下指令看一下 Ingress 綁定到那一個 IP:
  1. kubectl get ingresses
應該會出現如下結果:
有了 IP 後,就可以在 DNS 裡設定 steven-gw.steven.idv.tw 指向該 IP,或是在 /etc/hosts 中設定,接下來就可以用如下指令測試看看。
  1. curl http://steven-gw.steven.idv.tw/host/info
【番外篇】
上面是在 VMware PKS 上測試,PKS 上有預設安裝了 Ingress Controller,所以 ADDRESS 欄會有 IP,如果是自行安裝的 Kubernetes Cluster,因為 K8s 只提供 Ingress API 的定義,實作要再另外安裝,目前比較常見的有三種選擇:
  1. Nginx Kubernetes
  2. F5 BIG-IP Controller
  3. Ingress Kong
底下是 Ingress Kong 的安裝指令。
  1. $ helm repo add kong https://charts.konghq.com
  2. $ helm repo update
  3. $ helm install kong/kong --generate-name --set ingressController.installCRDs=false

2020年4月10日 星期五

安裝 Harbor (Ubuntu)

Docker 官方的 docker registry 是 Docker Hub,以金融業來說,不可能將 docker image 放到公司外部,自建一個私有的 docker registry 就成為必要的需求,Harbor 是許多選項之一,也是目前最流行的選項之一,底下是安裝的步驟。
  • 下載 & 解壓縮
    • git hub 上下載
    • 解壓縮: tar xzvf harbor-offline-installer-v1.10.2.tgz
    • 我的 home 目錄是 /home/steven,解壓縮後檔案在 /home/steven/harbor。
    • cd harbor
  • 設定 hosts
  • 因為我希望 Harbor 的網址是 harbor.steven.idv.tw,所以在 /etc/hosts 中定義了:
    1. 192.168.0.120 harbor.steven.idv.tw
  • 產生憑證
    1. openssl req -newkey rsa:4096 -nodes -sha256 -keyout ca.key -x509 -days 3650 -out ca.crt
    1. openssl req -newkey rsa:4096 -nodes -sha256 -keyout harbor.steven.idv.tw -out harbor.steven.idv.tw
    建立一個名為 extfile.cnf 的網案,內容如下:
    1. subjectAltName = IP:192.168.0.120
    繼續產生憑證:
    1. openssl x509 -req -days 3650 -in harbor.steven.idv.tw -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.cnf -out harbor.steven.idv.tw
    2. openssl req -new -x509 -text -key ca.key -out ca.cert
    接著將產生的三個檔案 copy 到以下目錄 (沒有這個目錄就自己建立),不只 master node,所有 worker node 的該目錄也都要有這些檔案。
    1. sudo cp *.crt *.key *.cert /etc/docker/certs.d/harbor.steven.idv.tw
  • 修改 harbor.yml
    1. hostname: harbor.steven.idv.tw
    2. port: 80
    3. certificate: /etc/docker/certs.d/harbor.steven.idv.tw/ca.crt
    4. private_key: /etc/docker/certs.d/harbor.steven.idv.tw/ca.key
  • 安裝 Harbor
  • 執行 sudo ./install.sh --with-clair,執行過程沒有錯誤的話,打開瀏覽器,輸入 http://harbor.steven.idv.tw 應該可以看到如下畫面。(安裝前請先確定已經有安裝 docker-compose)
    預設的帳號、密碼是 admin / Harbor12345,登入後畫面如下,點選 Administrator > Users,,按【+ NEW USER】新增一個 user 命名為 steven。
    接著在「Projects」中新增一個命名為 home 的 project。
    點選 home 於「Members」將剛剛新建立的 user steven 加入,這樣 steven 才能存取 home。
  • 測試
  • 測試前先修改 /etc/docker/daemon.json,如果沒有這個檔案,自行創建,在裡面加入如下內容:
    1. {"insecure-registries": ["192.168.0.120"]}
    然後重啟 docker
    1. systemctl restart docker
    登入 docker (steven / P@ssw0rd)
    1. docker login harbor.steven.idv.tw
    因為目前 docker 裡有個 image harbor.steven.idv.tw/home/host,如下:
    所以可以用以下指令將 image 推送到 harbor 上。
    1. docker push harbor.steven.idv.tw/home/host
    檢查一下 harbor,確實已經推上去了,如下圖。
  • 啟動 & 關閉
    1. sudo docker-compose up -d
    2. sudo docker-compose down