--- title: Kubernetes 的實務應用 (Part 1?) | Nginx+PHP tags: 學習日誌, Tutorial, 開發文件, K8s, PKS, Demo, PHP, AlfieYFC description: image: https://i.imgur.com/glNdcA3.png robot: noindex, nofollow lang: zh slideOptions: transition: fade theme: dark --- {%hackmd BkVfcTxlQ %} ###### tags: `學習日誌` `Tutorial` `開發文件` `K8s` `PKS` `Demo` `PHP` `Nginx` `AlfieYFC` Kubernetes 的實務應用 (Part 1?) | Nginx+PHP === 2019年初至今,在工作上其實為了客戶做了不少客製化的 Demo 情境,甚至撰寫技術文章。苦於這些文章都是針對客戶情境寫的,有些資訊可能不方便公開透露,不然真的是很想跟大家分享這些實務上有感的 Demo :laughing::laughing: 於是我開始想,辛苦做那麼多卻不能分享出來,幹嘛不把客戶相關的軟體或文字內容拿掉、改掉就好 :no_mouth: 所以就坐下來開始拼貼改寫這些文章啦~~~ 本篇作為這一系列的開端 _~(視情況再決定有沒有Part2)~_,還是先從入門的簡單範例開始吧! Demo 情境說明 === 本篇將開發一個簡單的 [PHP](https://hub.docker.com/_/php) Hello World 容器應用,在頁面被 Client 端(`curl` 或瀏覽器)存取時,會在 Client 端及 Server 端皆產生 Log 訊息。該應用會被分別改寫成三支獨立卻互相雷同的 Web,並且用 [Kubernetes Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) 和一個 [Nginx](https://hub.docker.com/_/nginx) 前端應用,各自實現「服務重導」的功能;Ingress 的實作設計類似 [Kairen 大神這一篇](https://k2r2bai.com/2018/07/19/kubernetes/k8s-external-dns/),跟我一樣喜歡拜神的朋友,可以參訪 Kairen 的部落格喔。 寫得這麼玄一定很多人看不懂,下面給一張圖片應該清楚一些: ![](https://i.imgur.com/NlQAido.png) :::info 另外補充,本篇所有實作都是在 [Pivotal Container Service (PKS)](https://pivotal.io/platform/pivotal-container-service) 環境執行,但是我會在內文較著重於 Kubernetes 原生的功能應用。如果透過本篇你產生出對 PKS 的管理功能有所好奇、疑問之處,歡迎[隨時聯絡我](https://www.facebook.com/alfieyfc)或者[迎棧科技](https://www.inwinstack.com/) (加減替公司工商一下 :joy: ) ::: 應用開發 --- 首先開發三支互相雷同的 Hello World 應用。 第一支命名為 `Happiness`,設定粉紅色背景,並產生 Log `Hello, World of Happiness`。 為了區別 Client 端及 Server 端的 Log,我們在日誌最後分別加上 `@ Client` 和 `@ Server` 文字,輔助辨別及驗證。 開發目錄展開如下: ``` ./ |-------- Happiness/ | |-------- Dockerfile | | | |-------- src/ | |-------- conf/ | | |-------- php.ini | | | |-------- html/ | |-------- index.php | |-------- Sadness/ | |-------- Darkness/ ``` PHP 原始碼如下: ```php= ## Happiness/src/html/index.php <html> <head> <title>Pink Happiness</title> </head> <?php echo "<body style='background-color:pink'>"; ?> <?php $message_server = "Hello, World of Happiness @ Server side!!!"; error_log($message_server); echo '<p>Hello World</p>'; ?> <?php function debug_to_console( $data ) { $output = $data; if ( is_array( $output ) ) $output = implode( ',', $output); echo "<script>console.log( 'Debug Objects: " . $output . "' );</script>"; } $message_client = "Hello, World of Happiness @ Client side!!!"; debug_to_console($message_client); ?> </body> </html> ``` 而另外兩支 Hello World 僅將 Title、log 訊息以及背景顏色更改 - Blue Sadness >> 以下僅列出有修改的原始碼行位 [color=red] > > `<title>Blue Sadness</title>` > `<?php echo "<body style='background-color:blue'>"; ?>` > `$message_server = "Hello, World of Sadness @ Server side!!!"` > `$message_client = "Hello, World of Sadness @ Client side!!!"` - Black Darkness >> 以下僅列出有修改的原始碼行位 [color=red] > > `<title>Black Darkness</title>` > `<?php echo "<body style='background-color:black'>"; ?>` > `$message_server = "Hello, World of Darkness @ Server side!!!"` > `echo '<p style="color=white">Hello World</p>';` > `$message_client = "Hello, World of Darkness @ Client side!!!"` 打包 Docker 映像檔 --- 由於本篇僅為 Demo 用途,因此 Dockerfile 盡可能簡化: ```dockerfile= # Dockerfile FROM php:7.2-apache COPY ./html/index.php /var/www/html/index.php COPY ./conf/php.ini /usr/local/etc/php/php.ini ``` 須注意的是,這個 `php.ini` 檔有特別更改過 `error_log` 欄位,以確保 `index.php` 中的 `error_log($message_server);` 日誌會被產生並顯示在 stderr: > ``` > ; Log errors to specified file. PHP's default behavior is to leave this value empty. > ; http://php.net/error-log > ; Example: > ;error_log = php_errors.log > error_log = /dev/stderr > ``` > [color=orange] 將 `Dockerfile`、`php.ini` 和 `index.php` 放到相應的目錄位置: ![](https://i.imgur.com/TL4hnXh.png) 利用 Docker 指令打包這些 PHP 應用: ```bash docker build -t harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-pink ./Happiness/ docker build -t harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-blue ./Sadness/ docker build -t harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-black ./Darkness/ ``` :::info 上述指令中 `-t` 參數所輸入映像檔的完整名稱,請參考以下規則: > `<registry-fqdn>`/`<project-name>`/`<repo-name>`:`<tag>` ::: ![](https://i.imgur.com/ohbtbjm.png) ![](https://i.imgur.com/gP6ptGa.png) 上傳映像檔 --- 利用 Docker 指令上傳映像檔: ```bash docker login harbor.pks.inwinstack.com docker push harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-pink docker push harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-blue docker push harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-black ``` ![](https://i.imgur.com/BaqoJRv.png) 部署應用 === 應用設定檔(YAML) --- 首先是 `happiness.yaml`~~_~(因為我喜歡先甘後苦再被黑)~_~~,Deployment YAML 編輯如下: ```yaml= # Happiness/happiness.yaml apiVersion: apps/v1 kind: Deployment metadata: name: happiness-deploy spec: replicas: 1 selector: matchLabels: app: happiness template: metadata: labels: app: happiness spec: containers: - name: php image: harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-pink ports: - containerPort: 80 ``` 把 `sadness.yaml` 和 `darkness.yaml` 的 YAML 也一併撰寫,其內容跟上方幾乎一樣,只是改了應用名稱和映像檔 tag 而已。 ```yaml= # Sadness/sadness.yaml apiVersion: apps/v1 kind: Deployment metadata: name: sadness-deploy spec: replicas: 1 selector: matchLabels: app: sadness template: metadata: labels: app: sadness spec: containers: - name: php image: harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-blue ports: - containerPort: 80 ``` ```yaml= # Darkness/darkness.yaml apiVersion: apps/v1 kind: Deployment metadata: name: darkness-deploy spec: replicas: 1 selector: matchLabels: app: darkness template: metadata: labels: app: darkness spec: containers: - name: php image: harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-black ports: - containerPort: 80 ``` 這時你的目錄展開應該長這樣: ![](https://i.imgur.com/fpmrNwE.png) 部署應用 --- 利用 `kubectl apply` 指令將容器應用部署至你的 Kubernetes 叢集: ```bash kubectl apply -f Happiness/happiness.yaml kubectl apply -f Sadness/sadness.yaml kubectl apply -f Darkness/darkness.yaml ``` ![](https://i.imgur.com/2s8Q307.png) 建立服務 --- 任何 Kubernetes 應用皆需要建立 `Service` 來提供對外服務,利用 `kubectl expose` 指令將服務產生: ```bash kubectl expose deploy happiness-deploy --name happiness-svc --type LoadBalancer kubectl expose deploy sadness-deploy --name sadness-svc --type LoadBalancer kubectl expose deploy darkness-deploy --name darkness-svc --type LoadBalancer ``` ![](https://i.imgur.com/snJqeY7.png) 查看應用 --- 利用 `kubectl get` 指令,確認應用服務成功在 K8s 建立: ```bash kubectl get po,svc -o wide ``` ![](https://i.imgur.com/NVdVi9p.png) :::warning 上圖 LoadBalancer type Service 中的 `EXTERNAL-IP` 欄位,獲取自 NSX-T 的 loadbalancer-ip-pool。其中 `100.64.x.x` 為 NSX-T 內部存取溝通轉址使用,而上圖看到的 `192.168.21.x` 則是可以「對外連線」的 IP 位址;這個 IP 位址因應實際環境設定而將不同。 > **這是 PKS + NSX-T on vSphere 特有的機制** > [color=red] ::: 馬上用 Client 端的 Chrome 存取這三個 Service 試試! > 記得打開「開發人員工具」,才看得到 Client 端的 console log: > `Hello, World of ____ @ Client side!!!` ![](https://i.imgur.com/95T6LeS.png) ![](https://i.imgur.com/3zYDuuS.png) ![](https://i.imgur.com/FaeUo6l.png) 回到指令介面,用 `curl` 存取看看: ![](https://i.imgur.com/v9WEA2A.png) 查看日誌 === 利用 `kubectl log` 指令查看各個 Pod 的 log,可以看到 apache2 server 啟動過程的日誌以及剛剛透過 Chrome 和 `curl` 存取的紀錄,也確實在每次存取時產生了 `Hello, World of ____ @ Server side!!!` 的 error_log(下圖==反白內容==)。 ![](https://i.imgur.com/z9YMDT8.png) 使用 `kubectl log` 指令對 `Sadness` 和 `Darkness` 應用查看 Pod 的 log,也應該要得到相同的結果。 Ingress 服務代理(方法一) === Ingress 是 Kubernetes 自帶的服務代理工具,將後端的容器服務 (Kubernetes Service) 以 HTTP 或 HTTPS 協定提供外部存取;想進一步瞭解可以參考[官方文件](https://kubernetes.io/docs/concepts/services-networking/ingress/)。 前面我們利用指令產生了 LoadBalancer 類型的 Service,而往往 LoadBalancer 中的 `EXTERNAL-IP` 數量有限,要想讓其對網際網路提供服務存取,通常也會產生相應的成本考量。因此,在實務上經常見到用 ClusterIP 類型的 Service 作為 Ingress 後端,並且僅以 Ingress 對外提供存取,由 Ingress 重新導向正確的「後端服務」。 ![](https://i.imgur.com/0oYhcHt.png) 為了模擬實務情境,重新建立 ClusterIP 類型的 Service: ```bash kubectl delete svc --all kubectl expose deploy happiness-deploy --name happiness-svc --type ClusterIP kubectl expose deploy sadness-deploy --name sadness-svc --type ClusterIP kubectl expose deploy darkness-deploy --name darkness-svc --type ClusterIP ``` ![](https://i.imgur.com/CdcJq6K.png) 這樣一來,瀏覽器理應無法存取這些服務了 (ClusterIP 是不對外提供連線的)。 ![](https://i.imgur.com/lPxHeHH.png) 建立 Ingress --- Ingress YAML 內容如下: ```yaml= # hello-ingress.yaml apiVersion: extensions/v1beta1 kind: Ingress metadata: name: hello-ingress spec: rules: - host: happiness.pks.inwinstack.com http: paths: - path: / backend: serviceName: happiness-svc servicePort: 80 - host: sadness.pks.inwinstack.com http: paths: - path: / backend: serviceName: sadness-svc servicePort: 80 - host: darkness.pks.inwinstack.com http: paths: - path: / backend: serviceName: darkness-svc servicePort: 80 ``` 利用 `kubectl apply` 指令建立 Ingress,並確認其 `EXTERNAL-IP` 位址: ```bash kubectl apply -f hello-ingress.yaml kubectl get ing kubectl describe ing hello-ingress ``` ![](https://i.imgur.com/9lnwHGm.png) DNS 解析 --- 前面在 Ingress 定義了三個應用的網域名稱 Host,我們利用 `curl` 來驗證看看,使用同一個 IP 位址,是否能透過 Host 欄位存取到相應正確的服務? ```bash curl -H "Host: happiness.pks.inwinstack.com" 192.168.21.2 curl -H "Host: sadness.pks.inwinstack.com" 192.168.21.2 curl -H "Host: darkness.pks.inwinstack.com" 192.168.21.2 ``` ![](https://i.imgur.com/zyiqiOJ.png) 透過驗證我們發現,只要 HTTP GET Header 中的資訊帶有 Host 欄位,並與 Ingress 的 Host 名稱相符,便會被重新導向相呼應的 backend Kubernetes Service。欲使用瀏覽器來實際驗證,則需要在 Client 端的 DNS 解析這些 Host 名稱到 Ingress IP 位址。為了達到此目的,或許很多人會直接編輯 `/etc/hosts` 檔案內容,在 Client 系統直接解析 Hostname。 ![](https://i.imgur.com/OQbGNiq.png) 而今天我想介紹一個很方便的 Chrome extension「[Host Switch Plus](https://chrome.google.com/webstore/detail/host-switch-plus/bopepoejgapmihklfepohbilpkcdoaeo)」。這個工具真的超級方便,直接在 Chrome 上面隨時新增、設定你想暫時解析的網域名稱,隨時用不到了就可以關閉起來。 如下圖所示,利用 Host Switch Plus 解析了這些網域名稱後,在網址列輸入前往,即可存取相應的容器應用了! ![](https://i.imgur.com/K4dLaqy.png) ~~*~偷偷置入我的網站~*~~ ![](https://i.imgur.com/vuUOtpG.png) ![](https://i.imgur.com/RadWgGj.png) ![](https://i.imgur.com/AW1EY9L.png) ![](https://i.imgur.com/Hi3USvV.png) Nginx Pod 服務代理(方法二) === 雖然 Kubernetes Ingress 其實也是在底層用 Nginx 來實作,但是在特定情況下,Dev/Ops 團隊可能會想要用獨立的 Nginx 容器應用來實現服務代理(例如:自定義 access_log 或 error_log 的蒐集),因此本篇另外也實作一個 Nginx Pod 來達到第二種服務代理方式。 ![](https://i.imgur.com/AAPsZPz.png) 建立 Nginx --- ```bash mkdir nginx ``` 建立一個 nginx 目錄,並新增一個檔案 `nginx.conf`: ``` conf= # nginx/nginx.conf user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main ' - [] "" ' ' "" ' '"" ""'; access_log /var/log/nginx/access.log main; server { listen 80; location /happiness/ { proxy_pass http://happiness-svc.hello-demo/; } location /sadness/ { proxy_pass http://sadness-svc.hello-demo/; } location /darkness/ { proxy_pass http://darkness-svc.hello-demo/; } location / { root /usr/share/nginx/html/; } } sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; include /etc/nginx/conf.d/*.conf; } ``` :::info `nginx.conf` 檔案中,我們將 proxy_pass 參數定義為與其 path 相應的 Service 名稱。這利用了 kubedns 的功能來實現,因此如果你的 Kubernetes 叢集沒有實作 kubedns 的話,可以將 proxy_pass 參數改為 ClusterIP 位址: ```conf location /path/ { proxy_pass http://<ClusterIP>; } ``` ::: 打包 Docker 映像檔 --- 由於本篇僅為 Demo 用途,因此 Dockerfile 盡可能簡化: ```dockerfile= # nginx/Dockerfile FROM nginx:1.15.9 COPY ./nginx.conf /etc/nginx/nginx.conf ``` 利用 Docker 指令打包 Nginx 應用: ``` bash docker build -t harbor.pks.inwinstack.com/demo-public/nginx:1.15.9-hello ./nginx/ docker push harbor.pks.inwinstack.com/demo-public/nginx:1.15.9-hello ``` ![](https://i.imgur.com/VMyA9G4.png) 應用設定檔(YAML) --- ```yaml= # nginx/nginx.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy spec: replicas: 1 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: harbor.pks.inwinstack.com/demo-public/nginx:1.15.9-hello ports: - containerPort: 80 ``` 利用 `kubectl apply` 指令將容器應用部署至你的 Kubernetes 叢集,並用 `kubectl expose` 來建立對外提供的服務: ```bash kubectl apply -f nginx/nginx.yaml kubectl expose deploy nginx-deploy --type=LoadBalancer kubectl get svc -o wide ``` ![](https://i.imgur.com/BI6pl5A.png) 存取驗證 --- 先在指令介面用 `curl` 存取看看,會發現我們設置的 `proxy_pass` 確實被重導到其他服務去了(但是 `curl` 看不到重導後的結果): ```bash curl 192.168.21.11 curl 192.168.21.11/happiness ``` ![](https://i.imgur.com/bRqTUch.png) 試著在瀏覽器上存取,Nginx 首頁存取成功! ![](https://i.imgur.com/5Zmvs3O.png) `proxy_pass` 存取後端服務也沒有問題! ![](https://i.imgur.com/eNXkQ93.png) ![](https://i.imgur.com/MsFB3mz.png) ![](https://i.imgur.com/JxTkWvJ.png) --- 結語 === 在此篇中我們開發了一個簡單的 Hello World 網頁應用,部署到 K8s,然後用 LoadBalancer 類型 Service 提供外部存取,並且存取該網頁應用時能夠查看到 Client 端及 Server 端的 Log。 我們進一步地將 Service 改為 ClusterIP 類型,並且利用 Ingress 和 Nginx Pod 兩種方法實現服務代理。而在 Nginx Pod 段落中,我們 expose 的是 LoadBalancer 類型 Service,大家不仿也可以試試將 Nginx 以 ClusterIP 類型產生 Service,再利用 Ingress 代理 Nginx 讓外部存取;如下圖: ![](https://i.imgur.com/9pqJ6yY.png) 事實上服務對外提供存取的方式有很多,而本篇介紹的也僅是入門實作中較常見且易懂的應用架構、方法,希望對大家有所幫助囉! ---